Процедуры и функции. Основные определения, назначение, отличия


с. 1 с. 2





Процедуры и функции. Основные определения, назначение, отличия
Необходимость использования процедур и функций в первую очередь связана с тем, что при решении задач возникает необходимость проводить вычисления по одним и тем же правилам (алгоритмам) многократно. Например, нахождение корней квадратного уравнения ax2+bx+c=0 при различных значениях a, b и c. Кроме того, практически каждый язык программирования предоставляет возможность организации (создания) библиотек (модулей), которые содержат описания процедур и функций пользователя, к которым в последствии можно будет обратиться из любой программы. Так, например, один единственный раз создав модуль (модуль для Паскаля, библиотека для Си), содержащий ряд математических функций или процедур: вычисление произвольной степени от произвольного числа, решение квадратного уравнения, вычисление произвольного логарифма от произвольного числа и т.д., можно избавиться от написания программного кода по реализации указанных вычислений, всего лишь вызвав соответствующую функцию или процедуру из библиотеки (модуля).

Процедура, как и функция, является подпрограммой, т.е. в ней могут присутствовать свои внутренние переменные, стандартные или определенные ранее пользователем функции (процедуры), и, как правило, входные данные. Как и обычная программа, подпрограмма может возвращать полученный в результате ее работы результат. Таким образом, подпрограмму можно рассматривать как отдельную программу внутри основной программы, имеющей свое имя, свои локальные (внутренние) переменные и входные данные, необходимые для ее работы. В результате выполнения подпрограммы результат, как правило, передается в основную программу для дальнейшей обработки.

Как отмечалось ранее, в качестве подпрограмм выступают процедуры и функции (можно отметить, что в Паскале существует понятие как процедур, так и функций, а в Си — только функций).

Процедура — это подпрограмма, имеющая свое уникальное имя и ряд так называемых формальных параметров, через которые данные передаются из программы в процедуру либо из процедуры в программу. Важным является то, что количество входных и выходных параметров в процедуре не ограничено.

Пример. Составить программу решения квадратного уравнения ax2+bx+c=0 при d0. При этом процесс решения уравнения оформить в виде процедуры.

Program kvadrat;

var y1, y2: real;

procedure Sq(a, b, c: real; var x1, x2: real);

входные параметры выходные параметры

var d: real;

begin

d:=b*b-4*a*c;



x1:=(-b+Sqrt(d))/(2*a); {Sqrt – функция вычисления квадратного

корня}


x2:=(-b-Sqrt(d))/(2*a);

end;


begin

Sq(4.2, -0.5, -1.3, y1, y2); {Вызов подпрограммы Sq}

writeln(‘y1=’,y1:5:3,’ y2=’,y2:5:3);

end.


Будет выведено “y1=0.619 y2=-0.500”

Функция — это также подпрограмма, но основным ее отличием от процедуры является то, что она может возвращать только одно единственное значение. Другими словами, в функции задаются только входные формальные параметры, а в качестве выходного параметра выступает имя самой функции. Например, стандартная функция для вычисления квадратного корня Sqrt(х) имеет только входной параметр х, к которому необходимо применить операцию извлечения корня. Результат операции записывается в ячейку памяти, имя которой Sqrt. Поэтому для просмотра результата или его присваивания какой либо переменной надо записать:

writeln(sqrt(x));

или


y:=sqrt(x);

Из выше сказанного вытекает, что при описании функции должен обязательно присутствовать оператор вида:



имя_функции:=результат

Пример. Написать программу, которая осуществляет ввод 4-х чисел, выбирает наибольшее число между 1-м и 2-м введенными числами и складывает его с наибольшим между 3-м и 4-м введенным числом. Поиск наибольшего числа оформить в виде функции.

program SumMax;

var a, b, c, d, f: real;

function Max(x, y): real;

begin


if x>y then max:=x else max:=y;

end;


begin

read(a, b, c, d);

f:=max(a, b)+max(c, d);

writeln(‘f=’,f);

end.

РЕКУРСИВНЫЕ АЛГОРИТМЫ
1. Введение

Рекурсивным называется объект, частично состоящий или определяемый с помощью самого себя.

Рекурсивные определения в математике представляют собой мощный аппарат. Примером могут служить: натуральные числа, деревья и определенные функции.

1. Натуральные числа:

а) 0 есть натуральное число,

б) число, следующее за натуральным, есть натуральное число.

2. Деревья:

а) 0 есть дерево (его называют "пустым деревом"),

б) если t1 и t2деревья, то построение, содержащее вершину с двумя ниже расположенными деревьями, опять же дерево.

3. Функция “факториал” п! (для неотрицательных целых чисел):

а) 0!,


б) n>0: n! = n*(n-1)!

Мощность рекурсивного определения заключается в том, что оно позволяет с помощью конечного высказывания определить бесконечное множество объектов. Аналогично с помощью конечной рекурсивной программы можно описать бесконечное вычисление, причем программа не будет содержать явных повторений. Однако рекурсивные алгоритмы лучше всего использовать, если в решаемой задаче, вычисляемой функции или структуре обрабатываемых данных рекурсия уже присутствует явно. В общем виде рекурсивную программу Р можно выразить как некоторую композицию Р из множества операторов S (не содержащих Р) и самой Р:



PP [S, P] (1)

Для выражения рекурсивных программ необходимо и достаточно иметь понятие процедуры или функции, поскольку они позволяют дать любому оператору имя, с помощью которого к нему можно обращаться. Если некоторая процедура Р содержит явную ссылку на саму себя, то ее называют прямо рекурсивной, если же Р ссылается на другую процедуру Q, содержащую (прямую или косвенную) ссылку на Р, то Р называют косвенно рекурсивной. Поэтому по тексту программы рекурсивность не всегда явно определима.

Как правило, с процедурой связывают множество локальных объектов, т.е. множество переменных, констант, типов и процедур, которые определены только в этой процедуре и вне ее не существуют или не имеют смысла. При каждой рекурсивной активации такой процедуры порождается новое множество локальных связанных переменных. Хотя они имеют те же самые имена, что и соответствующие элементы локального множества предыдущего "поколения" этой процедуры, их значения отличны от последних, а любые конфликты по именам разрешаются с помощью правил, определяющих область действия идентификаторов: идентификатор всегда относится к самому последнему порожденному множеству переменных. Это же правило справедливо и для параметров процедуры, по определению связанных с самой процедурой.

Подобно операторам цикла, рекурсивные процедуры могут приводить к незаканчивающимся вычислениям, и поэтому этой проблеме следует уделить особое внимание. Очевидно, для того чтобы работа когда-либо завершилась, необходимо, чтобы рекурсивное обращение к Р управлялось некоторым условием В, которое в какой-то момент перестает выполняться. Поэтому более точно схему рекурсивных алгоритмов можно представить в любой из следующих форм:



P  IF B THEN P [S, P], (2)

PP [S, IF B THEN P]. (3)

Основной способ доказательства конечности некоторого повторяющегося процесса таков:



  1. Определяется функция f(х) (х — множество переменных), такая, что из условия f(х)  0 следует истинность условия окончания цикла (с предусловием или постусловием).

  2. Доказывается, что при каждом прохождении цикла f(х) уменьшается.

Аналогично доказывается и окончание рекурсии — показывается, что Р уменьшает f(x), такую, что из f(х)  0 следует В. В частности, наиболее надежный способ обеспечить окончание процедуры — ввести в нее некоторый параметр (значение), назовем его n, и при рекурсивном обращении к Р в качестве параметра задавать n - 1. Если в этом случае в качестве В используется n > 0, то окончание гарантировано. Это опять же выражается двумя схемами:

Р(n)  IF n > 0 THEN Р [S, Р (n - 1)], (4)

Р(р)  Р [S, IF n > 0 THEN Р (n - 1)]. (5)

В практических приложениях важно убедиться, что максимальная глубина рекурсий не только конечна, но и достаточно мала. Причиной является то, что каждая рекурсивная активация процедуры Р требует памяти для размещения ее переменных. Кроме этих локальных переменных нужно еще сохранять текущее "состояние вычислений", чтобы можно было вернуться в него по окончании новой активации Р.


Когда рекурсию использовать не нужно

Рекурсивные алгоритмы особенно подходят для задач, где обрабатываемые данные определяются в терминах рекурсии. Однако это не означает, что такое рекурсивное определение данных гарантирует бесспорность употребления для решения задачи рекурсивного алгоритма.

Программы, в которых следует избегать алгоритмической рекурсии, можно охарактеризовать некоторой схемой, отражающей их строение :

P  IF В THEN S; P, (6)

PS; IF В THEN P. (7)

Такие схемы естественны в ситуациях, где вычисляемые значения определяются с помощью простых рекуррентных отношений. Возьмем хорошо известный пример вычисления факториала



i = 0,1,2,3,4,5,...,

fi = 1, 1, 2, 6, 24,120,... . (8)

Первое из чисел определяется явно — f0 = 1, а последующие определяются рекурсивным образом с помощью предшествующего числа:



fi+1 = (i + 1)* fi. (9)

Такое рекуррентное отношение предполагает рекурсивный алгоритм вычисления n-го факториального числа. Если мы введем две переменные I и F, обозначающие i и fi на i-м уровне рекурсии, то обнаружим, что для перехода к следующему числу последовательности (8) нужно проделать такие вычисления:



I := I+1; F := I * F, (10)

и, подставляя (10) вместо S в (6), получаем рекурсивную программу:



P  IF I < n THEN BEGIN I := I + 1; F := I * F; P END;

I := 0; F := 1; P. (11)

В принятых нами обозначениях первую строчку (11) можно переписать так:

PROCEDURE P(I: INTEGER, VAR F: INTEGER);

BEGIN


IF I < n THEN BEGIN I := I+1; F := I * F; P(I, F) END; (12)

END;


Однако более часто употребляется и полностью эквивалентная ей запись, где вместо процедуры Р используется функция. В этом случае переменная F становится излишней, а роль I явно выполняет параметр функции:

FUNCTION F (I: INTEGER): INTEGER;

BEGIN

IF I > 0 THEN F := I*F(I-1) ELSE F := 1; (13)



END;

Из приведенного примера становится ясно, что рекурсия крайне просто заменяется итерацией. Это проделано в такой программе:

I:=0; F:=1;

WHILE I < n DO BEGIN I:=I+1; F:=I*F END; (14)

В общем случае программы, построенные по схемам (6) и (7), нужно переписывать, руководствуясь схемой:

P  [x:=x0; WHILE B DO S]. (15)

Конечно, существуют и более сложные схемы рекурсий, которые можно и необходимо переводить в итеративную форму.

Из всего выше сказанного следует, что рекурсию необходимо избегать там, где есть очевидное итерационное решение. Однако это не означает, что от рекурсий следует избавляться любой ценой. Существует целый ряд задач, применение рекурсий в которых особенно оправдано.
4. Алгоритмы с возвратом

Особенно интригующая область программирования — задачи так называемого искусственного интеллекта. Здесь мы имеем дело с алгоритмами, ищущими решение не по заданным правилам вычислений, а путем проб и ошибок. Обычно процесс проб и ошибок разделяется на отдельные задачи (прием “Разделяй и властвуй”). Часто эти задачи наиболее естественно выражаются в терминах рекурсии и требуют исследования конечного числа подзадач. В общем виде весь процесс можно оформить как процесс поиска, строящий (и обрезающий) дерево подзадач. Во многих проблемах такое дерево поиска растет очень быстро, рост зависит от параметров задачи и часто бывает экспоненциальным. Соответственно увеличивается и стоимость поиска. Иногда, используя некоторые эвристики, дерево поиска удается сократить и тем самым свести затраты на вычисления к разумным пределам.

Исходя из принципа, что задача искусственного интеллекта разбивается на подзадачи, продемонстрируем применение при этом рекурсии. Начнем с демонстрации основных методов на хорошо известном примере — задаче о ходе коня.

Дана доска размером nn т.е. содержащая n2 полей. Вначале на поле с координатами x0, y0 помещается конь — фигура, перемещающаяся по обычным шахматным правилам. Задача заключается в поиске последовательности ходов (если она существует), при которой конь точно один раз побывает на всех полях доски (обойдет доску), т.е. нужно вычислить n2 - 1 ходов.

Очевидный прием упростить задачу обхода n2 полей — решать более простую, а именно либо выполнить очередной ход, либо доказать, что никакой ход не возможен. Поэтому начнем с определения алгоритма выполнения очередного хода. Первый его вариант выглядит следующим образом:

PROCEDURE TryNextMove;

BEGIN инициализация выбора хода;

REPEAT выбор очередного кандидата из списка ходов;

IF подходит THEN

BEGIN


запись хода;

IF доска не заполнена THEN

BEGIN (24)

TryNextMove;

IF неудача THEN уничтожение предыдущего хода

END


END

UNTIL удача OR кандидатов больше нет

END;

Для более детального описания алгоритма нужно выбрать некоторое представление для данных. Доску, самое простое, можно представить как матрицу, назовем ее h. Введем, кроме того, тип для индексирующих значений:



TYPE index = ARRAY [1..n, 1..n] OF INTEGER; (25)

VAR h: index;

Вследствие того, что мы хотим знать историю продвижения по доске, поля ее будем представлять целыми числами, а не булевскими значениями, позволяющими лишь определять занятость поля. В соответствии с этим можно остановиться на таких соглашениях:

h[x, y] = 0: поле (х, у) еще не посещалось; (26)

h[x, y] = i: поле (х, у) посещалось на i ходу.

Теперь нужно выбрать соответствующие параметры. Они должны определять начальные условия следующего хода и результат (если ход сделан). В первом случае достаточно задавать координаты поля (х, у), откуда следует ход, и число i, указывающее номер хода (для фиксации). Для результата же требуется булевский параметр; если он — "истина", то ход был возможен.

Кроме того, условие доска не заполнена можно переписать как i < n2. Если ввести еще две локальные переменные u и v для возможного хода, определяемого в соответствии с правилами “прыжка” коня, то предикат подходит можно представить как логическую конъюнкцию условий, что новое поле находится в пределах доски (1  un и 1  vn) и еще не посещалось (huv = 0). Фиксация допустимого хода выполняется с помощью присваивания huv := i, а отмена — с помощью huv := 0. Если ввести локальную переменную q1 и использовать ее в качестве параметра-результата при рекурсивных обращениях к этому алгоритму, то q1 можно подставить вместо удача. В соответствии с этим получаем следующую процедуру:

PROCEDURE Try(i: INTEGER; х, у: index; VAR q: BOOLEAN);

VAR u, v: INTEGER; q1: BOOLEAN;

BEGIN инициация выбора хода; (27)

REPEAT <u, v> - координаты следующего хода,

определяемого правилами шахмат;

IF (((1 <= u) AND (u <= n)) AND ((1 <= v) AND (v<= n))) AND (h[u, v] =0) THEN

BEGIN

h[u,v]:= i;



IF i < n*n THEN

BEGIN Try(i+1, u, v, q1);

IF NOT q1 THEN h[u,v]:= 0 ELSE q1:= TRUE;

END;


END;

UNTIL q1 OR других ходов нет;

q:= q1;

END;


Еще один шаг детализации — и получим уже полностью написанную программу. До этого момента программа создавалась совершенно независимо от правил, управляющих движением коня. Уточним и эту частную особенность задачи.


Рис. 8. Восемь возможных ходов коня
Если задана начальная пара координат х, у, то для следующего хода u, v существует восемь возможных кандидатов. На рис. 8 они пронумерованы от 1 до 8. Получать u, v из х, у довольно просто. Достаточно к последним добавлять разности между координатами, хранящиеся либо в массиве разностей, либо в двух массивах, хранящих отдельные разности. Обозначим эти массивы через dx и dy и будем считать, что они соответствующим образом инициализированы. Для нумерации очередного хода-кандидата можно использовать индекс k (подробности показаны в листинге 3). Первый раз к рекурсивной процедуре обращаются с параметрами x0 и y0 — координатами поля, с которого начинается обход. Этому полю должно быть присвоено значение 1, остальные поля маркируются как свободные:

h [х0,у0] := 1; Тгу(2, x0, y0, q);



Листинг 3. Обход поля ходом коня

PROGRAM KnightsTour;

VAR i, j, n, Nsqr: INTEGER;

q: BOOLEAN;

dx, dy: ARRAY [1..8] OF INTEGER;

h: ARRAY [1..8,1..8] OF INTEGER;

PROCEDURE Try(i, x, y: INTEGER; VAR q: BOOLEAN);

VAR k, u, v: INTEGER; q1: BOOLEAN;

BEGIN k:= 0;

REPEAT k:= k+1; q1:= FALSE;

u:= x+dx[k]; v:= y+dy[k];

IF (((1 <= u) AND (u <= n)) AND ((1 <= v) AND (v <= n))) AND (h[u, v] = 0) THEN

BEGIN h[u,v]:= i;

IF i < Nsqr THEN

BEGIN Try(i+1,u,v,q1);

IF NOT q1 THEN h[u,v]:= 0

END

ELSE q1:=TRUE;



END;

UNTIL q1 OR (k = 8);

q:= q1

END;


BEGIN

dx[1] := 2; dx[2] := 1; dx[3] := -1; dx[4] := -2;

dx[5] := -2; dx[6] := -1; dx[7] := 1; dx[8] := 2;

dy[1] := 1; dy[2] := 2; dy[3] := 2; dy[4] := 1;

dy[5] := -1; dy[6] := -2; dy[7] := -2; dy[8] := -1;

Write('Enter N = ');

ReadLn(n);

FOR i := 1 TO n DO

FOR j :=1 TO n DO h[i,j] := 0;

Write('Enter Start Position x0 = ');

ReadLn(i);

Write('Enter Start Position y0 = ');

ReadLn(j);

Nsqr := n*n; h[i,j] := 1; Try(2, i, j, q);

IF q THEN

BEGIN


FOR i := 1 TO n DO

BEGIN


FOR j := 1 TO n DO Write(' ',i,':',j,'->',h[i,j]:2,'; ');

WriteLn;


END;

END


ELSE WriteLn('no path');

ReadLn;


END.

Нельзя упускать еще одну деталь. Переменная huv существует только в том случае, если и u, и v лежат в диапазоне индексов 1…n. Следовательно, выражение в (27), подставленное вместо подходит из (24), осмысленно, только если его четыре первые составляющие истинны. Именно поэтому важно, чтобы составляющая huv = 0 была последней. В табл. 1 приводятся решения для трех исходных позиций: <3,3> и <2,4> при n = 5 и <1,1> при n = 6.


Таблица 1. Три возможных обхода конем

Характерное свойство таких алгоритмов (алгоритмов с возвратами) заключается в том, что в них делаются шаги в направлении общего решения, но все они фиксируются (записываются) таким образом, что позже можно вернуться вспять, отбрасывая тем самым шаги, которые ведут в тупик, а не к общему решению. Такой процесс называется возвратом или откатом (backtracking). Если предположить, что число потенциальных шагов конечно, то из алгоритма (24) можно вывести универсальную схему:

PROCEDURE Try;

BEGIN инициация выбора кандидата;

REPEAT выбор очередного кандидата;

IF подходит THEN BEGIN его запись;

IF решение неполное THEN BEGIN Try; (28)

IF неудача THEN BEGIN стирание записи END

END

END


UNTIL удача OR кандидатов больше нет

END;


В реальных программах могут встречаться и другие варианты, получающиеся из схемы (28). Часто, например, встречается построение с явным параметром для уровня, когда указывается глубина рекурсии. Это приводит к простым условиям окончания работы. Более того, если на каждом шаге число исследуемых путей фиксировано (пусть оно равно m), то можно применять еще одну выведенную схему (к ней надо обращаться с помощью оператора Try(1):

PROCUDURE Try(i : INTEGER);

VAR k: INTEGER;

BEGIN k := 0;

REPEAT k := k+1; выбор k-го кандидата;

IF подходит THEN BEGIN его запись;

IF i < n THEN BEGIN Try(i+1);

IF неудача THEN

BEGIN

стирание записи (29)

END


END

END


UNTIL удача OR (k = m)

END;
5. Задача о восьми ферзях

Задача о восьми ферзях — хорошо известный пример использования методов проб и ошибок и алгоритмов с возвратами. В 1850 г. эту задачу исследовал К.Ф. Гаусс, однако полностью он ее так и не решил, так как для подобных задач характерно отсутствие аналитического решения.

Задача о восьми ферзях формулируется следующим образом: восемь ферзей (королев) нужно расставить на шахматной доске так, чтобы ни один ферзь не угрожал другому. Воспользовавшись схемой (29) как шаблоном, легко получаем грубый вариант решения:

PROCEDURE Try(i: INTEGER);

BEGIN


инициация выбора положения i-гo ферзя;

REPEAT выбор очередного положения;

IF безопасно THEN BEGIN поставить ферзя;

IF i < 8 THEN BEGIN Try(i+1); (30)

IF неудача THEN BEGIN убрать ферзя END

END


END

UNTIL удача OR мест больше нет

END;

Далее необходимо остановиться на каком-либо представлении для данных. Поскольку из шахматных правил известно, что ферзь бьет все фигуры, находящиеся на той же самой вертикали, горизонтали или диагонали, то заключаем, что на каждой вертикали может находиться один и только один ферзь, поэтому при поиске места для i-го ферзя можно ограничить себя лишь i-й вертикалью. Таким образом, параметр i становится индексом вертикали, а процесс выбора возможного местоположения ограничивается восемью допустимыми значениями для индекса горизонтали j.



Остается решить вопрос: как представлять на доске эти восемь ферзей? Очевидно, доску вновь можно было бы представить в виде квадратной матрицы, однако это значительно бы усложнило проверку безопасности поля, что весьма нежелательно, поскольку такая операция выполняется очень часто. Поэтому остановимся на таком представлении данных, которое, насколько это возможно, упростило бы проверку. В этой ситуации лучше всего делать непосредственно доступной именно ту информацию, которая действительно важна и чаще всего используется. В нашем случае это не поля, занятые ферзями, а сведения о том, находится ли уже ферзь на данной горизонтали или диагонали (при этом уже известно, что на каждой k вертикали (1  ki) стоит только один ферзь). Исходя из этого переменные опишем следующим образом:

VAR х: ARRAY [1..8] OF INTEGER;

a: ARRAY [1..8] OF BOOLEAN;

b: ARRAY [b1..b2] OF BOOLEAN; (31)

c: ARRAY [c1..c2] OF BOOLEAN;

где


xi обозначает местоположение ферзя на i-й вертикали;

aj указывает, что на j-й горизонтали ферзя нет;

bk указывает, что на k-й -диагонали ферзя нет;

ck указывает, что на k-й -диагонали ферзя нет.

Выбор границ индексов b1, b2, с1, с2 определяется исходя из способа вычисления индексов для b и с. На -диагонали у всех полей постоянна сумма координат i и j, а на -диагонали постоянна их разность (соответствующие вычисления приведены в листинге 4). В соответствии с таким определением данных оператор поставить ферзя превращается в такие операторы:

x[i] := j; a[j] := FALSE; b[i+j] := FALSE; c[i-j] := FALSE; (32)

а оператор убрать ферзя — в такие:

a[j] := TRUE; b[i+j] := TRUE; c[i-j] := TRUE; (33)

Условие безопасно выполняется, если поле с координатами i, j лежит на горизонтали и вертикали, которые еще не заняты. Следовательно, ему соответствует логическое выражение

a[j] AND b[i+j] AND c[i-j]; (34)

Создание алгоритма закончено (полностью он представлен в листинге 4).



Листинг 4. Расстановка восьми ферзей (одно решение)

Program Queens;

VAR i: INTEGER; q: BOOLEAN;

a: ARRAY [1..8] OF BOOLEAN;

b: ARRAY [2..16] OF BOOLEAN;

c: ARRAY [-7..7] OF BOOLEAN;

x: ARRAY [1..8] OF INTEGER;

PROCEDURE Try(i: INTEGER; VAR q: BOOLEAN);

VAR j: INTEGER;

BEGIN j := 0;

REPEAT j := j+1; q:= FALSE;

IF (a[j] AND b[i+j]) AND c[i-j] THEN BEGIN

x[i] := j; a[j] := FALSE; b[i+j] := FALSE; c[i-j] := FALSE;

IF i < 8 THEN BEGIN Try(i+1, q);

IF NOT q THEN BEGIN

a[j] := TRUE; b[i+j] := TRUE; c[i-j] := TRUE;

END;

END


ELSE q := TRUE;

END;


UNTIL q OR (j = 8)

END;
BEGIN

FOR i := 1 TO 8 DO a[i] := TRUE;

FOR i := 2 TO 16 DO b[i] := TRUE;

FOR i := -7 TO 7 DO c[i] := TRUE;

Try(1,q);

FOR i := 1 TO 8 DO WriteLn(' Queen ',i,': ',x[i]);

ReadLn;


END.


1
























2
























3
























4
























5
























6
























7
























8



























1

2

3

4

5

6

7

8




с. 1 с. 2

скачать файл

Смотрите также: