ЭЛЕКТРОННАЯ БИБЛИОТЕКА КОАПП
Сборники Художественной, Технической, Справочной, Английской, Нормативной, Исторической, и др. литературы.



             ПРОГРАММИРОВАНИЕ: ТЕОРЕМЫ И ЗАДАЧИ

            НЕСКОЛЬКО ЗАМЕЧАНИЙ ВМЕСТО ПРЕДИСЛОВИЯ

Книга   написана  по  материалам  занятий  программированием  со
школьниками математических классов школы N 57.

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

Кто-то однажды сказал, что можно убедить в правильности алгорит-
ма, но не в правильности программы. Одна из целей книги -  попы-
таться продемонстрировать, что это не так.

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

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

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

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

Уровень трудности задач и глав  весьма  различен.  Мы  старались
включить  как простые задачи, которые могут быть полезны для на-
чинающих, так и трудные задачи, которые могут посадить в лужу  и
сильного школьника. (Хоть и редко, но это бывает полезно.)

В качестве языка для записи программ был выбран паскаль  Паскаль
достачно прост и естествен, имеет неплохие реализации (например,
Turbo Pascal 3.0 и 5.0 фирмы Borland) и позволяет записать реше-
ния  всех  рассматриваемых  задач. Возможно, Модула-2 или Оберон
были бы более изящным выбором, но пока что они труднее доступны.

Неудачный опыт писания "популярных" учебников по  программирова-
нию учит: никакого сюсюканья! писать надо так, чтобы потом самим
было не стыдно прочесть.

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

Это не только и не столько учебник для школьника, сколько  спра-
вочник и задачник для преподавателя, готовящегося к занятию.

Об "авторских правах": право формулировать задачу и объяснять её
решение является неотчуждаемым естественным правом всякого,  кто
на это способен. В соответствии с этим текст (в ASCII и TeX-вер-
сиях)  является  свободно  распространяемыми. С ним можно делать
всё, что угодно, и если Вы внесли в него ошибки, не указав,  что
они  принадлежат  Вам, или использовали текст в коммерческих це-
лях, не поделившись прибылью - Бог Вам судья.

Благодарности. Я рад случаю поблагодарить всех, с кем имел честь
сотрудничать, преподавая программирование, особенно тех, кто был
"по другую сторону баррикады".
        Н Е   П О К У П А Й Т Е   Э Т У   К Н И Г У !

                (Предупреждение автора)

     В этой книге ничего не  говорится  об  особенностях  BIOSа,
DOSа, OSа, GEMа и Windows, представляющих основную сложность при
настоящем программировании.

     В ней нет ни слова об объектно-ориентированном программиро-
вании, открывшем новую эпоху в построении дружественных и эффек-
тивных программных систем.

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

     Не рассказано в ней и  о  написании  резидентных  программ,
тонкости взаимодействия которых должен знать каждый.

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

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

     Логическое  программирование,  постепенно вытесняющее уста-
ревший операторный стиль программирования, не затронуто.

     Драматический поворот от баз данных к базам знаний, вызвав-
ший в жизни новую профессию -- инженер знаний -- остался незаме-
ченным автором.

     Проблемы отладки и сопровождения программ,  занимающие,  по
общему  мнению профессионалов, 90% в программировании, игнориру-
ются.

     В  книге  используются  лишь самые элементарные возможности
паскаля. Обширные возможности, предоставляемые современными  ин-
тегрированными программными средами, остаются невостребованными.
(Не  говоря уже о том, что паскаль уже вообще устарел, вытеснен-
ный языком Си.)

     Игрушечные головоломки, которым посвящена книга, никому  не
нужны.  Если  же перед Вами встанет действительно важная задача,
неужели Вы не справитесь с ней сами, без непрошеных  учителей  и
советчиков?

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

     1.1. Задачи без массивов

     1.1.1. Даны две целые переменные a, b.  Составить  фрагмент
программы, после исполнения которого значения переменных поменя-
лись бы местами (новое значение a равно старому значению b и на-
оборот).

     Решение. Введем дополнительную целую переменную t.
        t := a;
        a := b;
        b := t;
Попытка обойтись без дополнительной переменной, написав
        a := b;
        b := a;
не приводит к цели (безвозвратно утрачивается начальное значение
переменной a).

     1.1.2.  Решить  предыдущую  задачу,  не  используя дополни-
тельных переменных (и предполагая, что значениями целых перемен-
ных могут быть произвольные целые числа).

     Решение. (Начальные значения a и b обозначим a0, b0.)
        a := a + b; {a = a0 + b0, b = b0}
        b := a - b; {a = a0 + b0, b = a0}
        a := a - b; {a = b0, b = a0}

     1.1.3.  Дано  целое  число а и натуральное (целое неотрица-
тельное) число n. Вычислить а в степени n. Другими словами,  не-
обходимо  составить  программу,  при исполнении которой значения
переменных а и n не меняются, а значение некоторой другой  пере-
менной  (например, b) становится равным а в степени n. (При этом
разрешается использовать и другие переменные.)

     Решение. Введем целую переменную k, которая меняется от  0
до  n,  причем  поддерживается такое свойство: b = (a в степени
k).

        k := 0; b := 1;
        {b = a в степени k}
        while k <> n do begin
        | k := k + 1;
        | b := b * a;
        end;

Другое решение той же задачи:

        k := n; b := 1;
        {a в степени n = b * (a в степени k)}
        while k <> 0 do begin
        | k := k - 1;
        | b := b * a;
        end;

     1.1.4. Решить предыдущую задачу, если требуется, чтобы чис-
ло действий (выполняемых операторов присваивания)  было  порядка
log n (то есть не превосходило бы C*log n для некоторой констан-
ты C; log n - это степень, в которую нужно возвести 2, чтобы по-
лучить n).

     Решение. Внесем некоторые изменения во второе из предложен-
ных решений предыдущей задачи:

        k := n; b := 1; c:=a;
        {a в степени n = b * (c в степени k)}
        while k <> 0 do begin
        | if k mod 2 = 0 then begin
        | | k:= k div 2;
        | | c:= c*c;
        | end else begin
        | | k := k - 1;
        | | b := b * c;
        | end;
        end;

Каждый второй раз (не реже)  будет  выполняться  первый  вариант
оператора  выбора  (если  k  нечетно, то после вычитания единицы
становится четным), так что за два цикла величина k  уменьшается
по крайней мере вдвое.

     1.1.5.  Даны натуральные числа а, b. Вычислить произведение
а*b, используя в программе лишь операции +, -, =, <>.

     Решение.
        var a, b, c, k : integer;
        k := 0; c := 0;
        {инвариант: c = a * k}
        while k <> b do begin
        | k := k + 1;
        | c := c + a;
        end;
        {c = a * k и k = b, следовательно, c = a * b}

     1.1.6.  Даны  натуральные  числа  а и b. Вычислить их сумму
а+b. Использовать операторы присваивания лишь вида

        <переменная1> := <переменная2>,
        <переменная> := <число>,
        <переменная1> := <переменная2> + 1.

     Решение.
          ...
         {инвариант: c = a + k}
          ...

     1.1.7. Дано натуральное (целое неотрицательное) число  а  и
целое положительное число d. Вычислить частное q и остаток r при
делении а на d, не используя операций div и mod.

     Решение. Согласно определению, a = q * d + r, 0 <= r < d.

        {a >= 0; d > 0}
        r := a; q := 0;
        {инвариант: a = q * d + r, 0 <= r}
        while not (r < d) do begin
        | {r >= d}
        | r := r - d; {r >= 0}
        | q := q + 1;
        end;

     1.1.8.  Дано  натуральное  n,  вычислить n!
        (0!=1, n! = n * (n-1)!).

     1.1.9.   Последовательность  Фибоначчи  определяется  так:
a(0)= 1, a(1) = 1, a(k) = a(k-1) + a(k-2) при k >= 2.  Дано  n,
вычислить a(n).

     1.1.10.  Та же задача, если требуется, чтобы число операций
было пропорционально log n. (Переменные должны быть  целочислен-
ными.)

     Указание.  Пара соседних чисел Фибоначчи получается из пре-
дыдущей умножением на матрицу
            |1 1|
            |1 0|
так что задача сводится к возведению матрицы в  степень  n.  Это
можно сделать за C*log n действий тем же способом, что и для чи-
сел.

     1.1.11. Дано натуральное n, вычислить 1/0!+1/1!+...+1/n!.

     1.1.12.  То  же, если требуется, чтобы количество операций
(выполненных команд присваивания) было бы не более C*n для  не-
которой константы С.
     Решение.  Инвариант:  sum  =  1/1! +...+ 1/k!, last = 1/k!
(важно не вычислять заново каждый раз k!).

     1.1.13.  Даны  два  натуральных числа a и b, не равные нулю
одновременно. Вычислить НОД (a,b) - наибольший общий делитель  а
и b.

     Решение (1 вариант).

        if a > b then begin
        | k := a;
        end else begin
        | k := b;
        end;
        {k = max (a,b)}
        {инвариант: никакое  число, большее k, не является об-
          щим делителем}
        while not (((a mod k)=0) and ((b mod k)=0)) do begin
        | k := k - 1;
        end;
        {k - общий делитель, большие - нет}

       (2  вариант - алгоритм Евклида). Будем считать , что НОД
(0,0) = 0. Тогда НОД (a,b) = НОД (a-b,b)  =  НОД  (a,b-a);  НОД
(a,0) = НОД (0,a) = a для всех a,b>=0.

         m := a; n := b;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0 }
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n;
        | end else begin
        | | n := n - m;
        | end;
        end;
        if m = 0 then begin
        | k := n;
        end else begin
        | k := m;
        end;

     1.1.14. Написать модифицированный вариант алгоритма Евкли-
да,  использующий соотношения НОД (a, b) = НОД (a mod b, b) при
a >= b, НОД (a, b) = НОД (a, b mod a) при b >= a.

     1.1.15. Даны натуральные а и b, не равные 0  одновременно.
Найти d = НОД (a,b) и такие целые x и y, что d = a*x + b*y.

     Решение.  Добавим в алгоритм Евклида переменные p, q, r, s
и впишем в инвариант условия m = p*a + q*b; n = r*a + s*b.

        m:=a; n:=b; p := 1; q := 0; r := 0; s := 1;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0
                    m = p*a + q*b; n = r*a + s*b.}
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n; p := p - r; q := q - s;
        | end else begin
        | | n := n - m; r := r - p; s := s - q;
        | end;
        end;
        if m = 0 then begin
        | k :=n; x := r; y := s;
        end else begin
        | k := m; x := p; y := q;
        end;

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

     1.1.17. (Э.Дейкстра).  Добавим  в алгоритм Евклида дополни-
тельные переменные u, v, z:

         m := a; n := b; u := b; v := a;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0 }
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n; v := v + u;
        | end else begin
        | | n := n - m; u := u + v;
        | end;
        end;
        if m = 0 then begin
        | z:= v;
        end else begin {n=0}
        | z:= u;
        end;

Доказать, что после исполнения алгоритма z равно удвоенному  на-
именьшему общему кратному чисел a, b: z = 2 * НОК (a,b).

     Решение. Заметим, что величина m*u + n*v не меняется в ходе
выполнения  алгоритма. Остается воспользоваться тем, что вначале
она равна 2*a*b и что НОД (a, b) * НОК (a, b) = a*b.

     1.1.18.  Написать  вариант  алгоритма Евклида, использующий
соотношения
        НОД(2*a, 2*b) = 2*НОД(a,b)
        НОД(2*a, b)   =   НОД(a,b) при нечетном b,
не включающий деления с остатком, а использующий лишь деление на
2 и проверку четности. (Число действий должно быть порядка log k
для исходных данных, не превосходящих k.)

     Решение.

  m:= a; n:=b; d:=1;
  {НОД(a,b) = d * НОД(m,n)}
  while not ((m=0) or (n=0)) do begin
  | if (m mod 2 = 0) and (n mod 2 = 0) then begin
  | | d:= d*2; m:= m div 2; n:= n div 2;
  | end else if (m mod 2 = 0) and (n mod 2 = 1) then begin
  | | m:= m div 2;
  | end else if (m mod 2 = 1) and (n mod 2 = 0) then begin
  | | n:= n div 2;
  | end else if (m mod 2=1) and (n mod 2=1) and (m>=n)then begin
  | | m:= m-n;
  | end else if (m mod 2=1) and (n mod 2=1) and (m<=n)then begin
  | | n:= n-m;
  | end;
  end;
  {m=0 => ответ=d*n; n=0 => ответ=d*m}

Оценка числа действий: каждое второе действие делит хотя бы одно
из чисел m и n пополам.

     1.1.19. Дополнить алгоритм предыдущей задачи поиском x и y,
для которых ax+by=НОД(a,b).

     Решение. (Идея сообщена Д.Звонкиным) Прежде всего  заметим,
что  одновременое деление a и b пополам не меняет искомых x и y.
Поэтому можно считать, что с самого начала одно из чисел a  и  b
нечетно. (Это свойство будет сохраняться и далее.)
     Теперь  попытаемся,  как  и  раньше,  хранить  такие  числа
p,q,r,s, что
     m = ap + bq
     n = ar + bs
Проблема в том, что при делении, скажем, m на 2 надо разделить p
и  q  на 2, и они перестанут быть целыми (а станут двоично-раци-
ональными). Двоично-рациональное число естественно хранить в ви-
де пары (числитель, показатель степени двойки в знаменателе).  В
итоге  мы  получаем  d  в  виде комбинации a и b с двоично-раци-
ональными коэффициентами. Иными словами, мы имеем
        (2 в степени i)* d = ax + by
для  некоторых  целых x,y и натурального i. Что делать, если i >
1? Если x и y чётны, то на 2 можно сократить. Если это  не  так,
положение можно исправить преобразованием
        x := x + b
        y := y - a
(оно  не меняет ax+by). Убедимся в этом. Напомним, что мы счита-
ем, что одно из чисел a и b нечётно. Пусть это будет a. Если при
этом y чётно, то и x должно быть чётным (иначе ax+by  будет  не-
чётным). А при нечётном y вычитание из него нёчетного a делает y
чётным.

     1.1.20. Составить программу, печатающую квадраты всех нату-
ральных чисел от 0 до заданного натурального n.

     Решение.

        k:=0;
        writeln (k*k);
        {инвариант: k<=n, напечатаны все
          квадраты до k включительно}
        while not (k=n) do begin
        | k:=k+1;
        | writeln (k*k);
        end;

     1.1.21.  Та же задача, но разрешается использовать из ариф-
метических операций лишь сложение и вычитание, причем общее чис-
ло действий должно быть порядка n.

     Решение.  Введем  переменную k_square (square - квадрат),
связанную с k соотношением k_square = k*k:

        k := 0; k_square := 0;
        writeln (k_square);
        while not (k = n) do begin
        | k := k + 1;
        | {k_square = (k-1) * (k-1) = k*k - 2*k + 1}
        | k_square := k_square + k + k - 1;
        | writeln (k_square);
        end;

     1.1.22. Составить программу, печатающую разложение на прос-
тые множители заданного натурального числа n > 0 (другими слова-
ми, требуется печатать только простые числа и произведение напе-
чатанных  чисел должно быть равно n; если n = 1, печатать ничего
не надо).

     Решение (1 вариант).

        k := n;
        {инвариант:  произведение напечатанных чисел и k равно
         n, напечатаны только простые числа}
        while not (k = 1) do begin
        | l := 2;
        | {инвариант: k не имеет делителей в интервале (1,l)}
        | while k mod l <> 0 do begin
        | | l := l + 1;
        | end;
        | {l - наименьший делитель k, больший 1, следовательно,
        |  простой}
        | writeln (l);
        | k:=k div l;
        end;

     (2 вариант).

         k := n; l := 2;
         {произведение  k и напечатанных чисел равно n; напеча-
          танные числа просты; k не имеет делителей, меньших l}
         while not (k = 1) do begin
         | if k mod l = 0  then begin
         | | {k делится на l и не имеет делителей,
         | |   меньших l, значит, l просто}
         | | k := k div l;
         | | writeln (l);
         | end else begin
         | | { k не делится на l }
         | | l := l + 1;
         | end;
         end;

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

     Решение. Во втором варианте решения вместо l:=l+1 можно на-
писать

                if l*l > k then begin
                | l:=k;
                end else begin
                | l:=l+1;
                end;

     1.1.24. Проверить, является ли заданное натуральное  число
n > 1 простым.

     1.1.25. (Для знакомых с основами алгебры). Дано целое  га-
уссово  число n + mi (принадлежащее Z[i]). (a) Проверить, явля-
ется ли оно простым (в Z[i]); (б) напечатать его разложение  на
простые (в Z[i]) множители.

     1.1.26. Разрешим использовать команды write (i) лишь при i
=  0,1,2,...,9.  Составить программу, печатающую десятичную за-
пись заданного натурального числа n > 0. (Случай n =  0  явился
бы некоторым исключением, так как обычно нули в начале числа не
печатаются, а для n = 0 - печатаются.)

     Решение.

        base:=1;
        {base - степень 10, не превосходящая n}
        while 10 * base <= n do begin
        | base:= base * 10;
        end;
        {base - максимальная степень 10, не превосходящая n}
        k:=n;
        {инвариант: осталось напечатать k с тем же числом
         знаков, что в base; base = 100..00}
        while base <> 1 do begin
        | write(k div base);
        | k:= k mod base;
        | base:= base div 10;
        end;
        {base=1; осталось напечатать однозначное число k}
        write(k);

(Типичная ошибка при решении этой задачи: неправильно  обрабаты-
ваются числа с нулями посередине. Приведенный инвариант допуска-
ет  случай, когда k < base; в этом случае печатание k начинается
со старших нулей.)

     1.1.27. То же самое, но надо напечатать десятичную запись в
обратном порядке. (Для n = 173 надо напечатать 371.)

     Решение.

        k:= n;
        {инвариант: осталось напечатать k в обратном порядке}
        while k <> 0 do begin
        | write (k mod 10);
        | k:= k div 10;
        end;

     1.1.28. Дано натуральное n. Подсчитать  количество  решений
неравенства  x*x + y*y < n в натуральных (неотрицательных целых)
числах, не используя действий с вещественными числами.

     Решение.

        k := 0; s := 0;
        {инвариант: s = количество решений неравенства
          x*x + y*y < n c x < k}
        while k*k < n do begin
        | ...
        | {t = число решений неравенства k*k + y*y < n
        |  (при данном k) }
        | k := k + 1;
        | s := s + t;
        end;
        {k*k >= n, поэтому s = количество всех решений
          неравенства}

     Здесь ... - пока еще не написанный кусок программы, который
будет таким:

        l := 0; t := 0;
        {инвариант: t = число решений
          неравенства k*k + y*y < n c y < l }
        while k*k + l*l < n do begin
        | l := l + 1;
        | t := t + 1;
        end;
        {k*k + l*l >= n,  поэтому  t = число
          всех решений неравенства k*k + y*y < n}

     1.1.29. Та же задача, но количество  операций  должно  быть
порядка (n в степени 1/2). (В предыдущем решении, как можно
подсчитать, порядка n операций.)

     Решение. Нас интересуют точки решетки (с целыми координата-
  *              ми) в первом квадранте, попадающие внутрь круга
  * * *          радиуса  (n  в  степени  1/2). Интересующее нас
  * * * *        множество (назовем его X) состоит из  объедине-
  * * * *        ния  вертикальных  столбцов  убывающей  высоты.
  * * * * *      Идея решения состоит в  том,  чтобы  "двигаться
вдоль  его  границы",  спускаясь  по  верхнему  его краю, как по
лестнице. Координаты движущейся точки  обозначим  <k,l>.  Введем
еще одну переменную s и будем поддерживать истинность такого ус-
ловия:
     <k,l> находится сразу над k-ым столбцом;
     s - число точек в предыдущих столбцах.

     Формально:
l  - минимальное среди тех l >= 0, для которых <k,l> не принад-
    лежит X;
s - число пар натуральных x, y, для которых x < k и <x,y>  при-
    надлежит X.
Обозначим эти условия через (И).

  k := 0; l := 0;
  while "<0,l> принадлежит X" do begin
  | l := l + 1;
  end;
  {k = 0, l - минимальное среди тех l >= 0,
   для которых <k,l> не принадлежит X }
  s := 0;
  {инвариант: И}
  while not (l = 0) do begin
  | s := s + l;
  | {s - число точек в столбцах до k-го включительно}
  | k := k + 1;
  | {точка <k,l> лежит вне X, но,  возможно,  ее  надо сдвинуть
  |    вниз, чтобы восстановить И }
  | while (l <> 0) and ("<k, l-1> не принадлежит X") do begin
  | | l := l - 1;
  | end;
  end;
  {И, l = 0, поэтому k-ый столбец и все следующие пусты, а
    s равно искомому числу}

Оценка числа действий очевидна: сначала мы движемся вверх не бо-
лее  чем  на  (n в степени 1/2) шагов, а затем вниз и вправо - в
каждую сторону не более чем на (n в степени 1/2) шагов.

     1.1.30. Даны натуральные числа n и k, n > 1.  Напечатать  k
десятичных знаков числа 1/n. (При наличии двух десятичных разло-
жений  выбирается то из них, которое не содержит девятки в пери-
оде.) Программа должна использовать только целые переменные.

     Решение. Сдвинув в десятичной записи числа 1/n запятую на k
мест вправо, получим число (10 в степени k)/n. Нам надо  напеча-
тать  его целую часть, т. е. разделить (10 в степени k) на n на-
цело. Стандартный способ требует использования больших по  вели-
чине  чисел, которые могут выйти за границы диапазона представи-
мых чисел. Поэтому мы сделаем иначе (следуя обычному методу "де-
ления уголком") и будем хранить "остаток" r:

  l := 0; r := 1;
  {инв.: напечатано l разрядов 1/n, осталось напечатать
    k - l разрядов дроби r/n}
   while l <> k do begin
   | write ( (10 * r) div n);
   |   r := (10 * r) mod n;
   |   l := l + 1;
   end;

     1.1.31. Дано натуральное число n > 1. Определить длину  пе-
риода десятичной записи дроби 1/n.

     Решение.  Период  дроби  равен периоду в последовательности
остатков (докажите это; в частности, надо доказать,  что  он  не
может  быть  меньше).  Кроме того, в этой последовательности все
периодически повторяющиеся все члены различны, а предпериод име-
ет длину не более n. Поэтому достаточно найти (n+1)-ый член пос-
ледовательности остатков и  затем  минимальное  k,  при  котором
(n+1+k)-ый член совпадает с (n+1)-ым.

  l := 0; r := 1;
  {инвариант: r/n = результат отбрасывания l знаков в 1/n}
  while l <> n+1 do begin
  | r := (10 * r) mod n;
  | l := l + 1;
  end;
  c := r;
  {c = (n+1)-ый член последовательности остатков}
  r := (10 * r) mod n;
  k := 0;
  {r = (n+k+1)-ый член последовательности остатков}
  while r <> c do begin
  | r := (10 * r) mod n;
  | k := k + 1;
  end;

     1.1.32 (Э. Дейкстра). Функция f с натуральными  аргументами
и  значениями определена так: f(0) = 0, f(1) = 1, f (2n) = f(n),
f (2n+1) = f (n) + f (n+1). Составить программу вычисления f (n)
по заданному n, требующую порядка log  n  операций.

     Решение.
  k := n; a := 1; b := 0;
  {инвариант: 0 <= k, f (n) = a * f(k) + b * f (k+1)}
  while k <> 0 do begin
  | if k mod 2 = 0  then begin
  | | l := k div 2;
  | | {k = 2l, f(k) = f(l), f (k+1) = f (2l+1) = f(l) + f(l+1),
  | |  f (n) = a*f(k) + b*f(k+1) = (a+b)*f(l) + b*f(l+1)}
  | | a := a + b; k := l;
  | end else begin
  | | l := k div 2;
  | | {k = 2l + 1, f(k) = f(l) + f(l+1),
  | |  f(k+1) = f(2l+2) = f(l+1),
  | |  f(n) = a*f(k) + b*f(k+1) = a*f(l) + (a+b)*f(l+1)}
  | | b := a + b; k := l;
  | end;
  end;
  {k = 0, f(n) = a * f(0) + b * f(1) = b, что и требовалось}

     1.1.33.  То  же,  если  f(0) = 13, f(1) = 17, а f(2n) =
43 f(n) + 57 f(n+1), f(2n+1) = 91 f(n) + 179 f(n+1) при n>=1.
     Указание.  Хранить  коэффициенты в выражении f(n) через три
соседних числа.

     1.1.34. Даны натуральные числа а и b, причем b >  0.  Найти
частное  и  остаток  при  делении а на b, оперируя лишь с целыми
числами и не используя операции div и mod, за исключением  деле-
ния  на  2  четных  чисел;  число  шагов  не должно превосходить
C1*log(a/b) + C2 для некоторых констант C1, C2.

     Решение.

  b1 := b;
  while b1 <= a do begin
  | b1 := b1 * 2;
  end;
  {b1 > a, b1 = b * (некоторая степень 2)}
  q:=0; r:=a;
  {инвариант: q, r - частное и остаток при делении a на b1,
   b1 = b * (некоторая степень 2)}
  while b1 <> b do begin
  | b1 := b1 div 2 ; q := q * 2;
  | { a = b1 * q + r, 0 <= r, r < 2 * b1}
  | if r >= b1 then begin
  | | r := r - b1;
  | | q := q + 1;
  | end;
  end;
  {q, r - частное и остаток при делении a на b}

     1.2. Массивы.

     В следующих задачах переменные x, y, z предполагаются  опи-
санными  как  array [1..n] of integer (n - некоторое натуральное
число, большее 0), если иное не оговорено явно.

     1.2.1. Заполнить массив x нулями. (Это означает, что  нужно
составить фрагмент программы, после выполнения которого все зна-
чения  x[1]..x[n]  равнялись  бы  нулю, независимо от начального
значения переменной x.)

     Решение.

          i := 0;
          {инвариант: первые i значений x[1]..x[i] равны 0}
          while i <> n do begin
          | i := i + 1;
          | {x[1]..x[i-1] = 0}
          | x[i] := 0;
          end;

     1.2.2. Подсчитать количество нулей в массиве x.  (Составить
фрагмент программы, не меняющий значения x, после исполнения ко-
торого  значение некоторой целой переменной k равнялось бы числу
нулей среди компонент массива x.)

     Решение.
          ...
          {инвариант: k= число нулей среди x[1]...x[i] }
          ...

     1.2.3. Не используя оператора  присваивания  для  массивов,
составить фрагмент программы, эквивалентный оператору x:=y.

     Решение.

  i := 0;
  {инвариант: значение y не изменилось, x[l] = y[l] при l <= i}
  while i <> n do begin
  | i := i + 1;
  | x[i] := y[i];
  end;

     1.2.4. Найти максимум из x[1]..x[n].

     Решение.
          i := 1; max := x[1];
          {инвариант: max = максимум из x[1]..x[i]}
          while i <> n do begin
          | i := i + 1;
          | {max = максимум из x[1]..x[i-1]}
          | if x[i] > max then begin
          | | max := x[i];
          | end;
          end;

     1.2.5.  Дан  массив x: array [1..n] of integer, причём x[1]
<= x[2] <= ... <= x[n]. Найти количество различных  чисел  среди
элементов этого массива.

     Решение. (1 вариант)

  i := 1; k := 1;
  {инвариант: k - количество различных чисел среди x[1]..x[i]}
  while i <> n do begin
  | i := i + 1;
  | if x[i] <> x[i-1] then begin
  | | k := k + 1;
  | end;
  end;

     (2 вариант) Искомое число на 1 больше количества тех  чисел
i из 1..n-1, для которых x[i] <> x[i+1].

  k := 1;
  for i := 1 to n-1 do begin
  | if x[i]<> x[i+1] then begin
  | | k := k + 1;
  | end;
  end;

     1.2.6. (Сообщил А.Л.Брудно.) Прямоугольное поле m на n раз-
бито  на mn квадратных клеток. Некоторые клетки покрашены в чер-
ный цвет. Известно, что все черные клетки могут быть разбиты  на
несколько непересекающихся и не имеющих общих вершин черных пря-
моугольников. Считая, что цвета клеток даны в виде массива типа
        array [1..m] of array [1..n] of boolean;
подсчитать  число  черных  прямоугольников,  о которых шла речь.
Число действий должно быть порядка m*n.

     Решение. Число прямоугольников равно числу их левых верхних
углов. Является ли клетка верхним углом, можно узнать, посмотрев
на ее цвет, а также цвет верхнего  и  левого  соседей.  (Не  за-
будьте, что их может не быть, если клетка с краю.)

     1.2.7. Дан массив x: array [1..n] of integer.  Найти  коли-
чество  различных  чисел  среди  элементов этого массива. (Число
действий должно быть порядка n*n.)

     1.2.8.  Та  же  задача,  если  требуется,  чтобы количество
действий было порядка n* log n. (Указание. Смотри главу о сорти-
ровке.)

     1.2.9. Та же задача, если известно, что все элементы масси-
ва - числа от 1 до k и число действий должно быть порядка n+k.

     1.2.10. Дан массив x [1]..x[n] целых  чисел.  Не  используя
других  массивов, переставить элементы массива в обратном поряд-
ке.

     Решение. Числа x [i] и x [n+1-i] нужно поменять местами для
всех i, для которых i < n + 1 - i, т.е. 2*i < n + 1 <=> 2*i <= n
<=> i <= n div 2:
  for i := 1 to n div 2 do begin
  | ...обменять x [i] и x [n+1-i];
  end;

     1.2.11.  (из  книги  Д.Гриса)  Дан   массив   целых   чисел
x[1]..x[m+n],  рассматриваемый как соединение двух его отрезков:
начала x[1]..x[m] длины m и конца x[m+1]..x[m+n] длины n. Не ис-
пользуя дополнительных массивов,  переставить  начало  и  конец.
(Число действий порядка m+n.)

     Решение. (1 вариант). Перевернем (расположим в обратном по-
рядке) отдельно начало и конец массива, а затем перевернем  весь
массив как единое целое.

     (2 вариант, А.Г.Кушниренко). Рассматривая массив записанным
по кругу, видим, что требуемое действие - поворот круга. Как из-
вестно, поворот есть композиция двух осевых симметрий.

     (3  вариант).  Рассмотрим  более  общую задачу - обмен двух
участков массива x[p+1]..x[q] и x[q+1]..x[s].  Предположим,  что
длина  левого  участка  (назовем  его A) не больше длины правого
(назовем его B). Выделим в B начало той же длины, что и A, назо-
вем его B1, а остаток B2. (Так что B = B1 + B2, если  обозначать
плюсом приписывание массивов друг к другу.) Нам надо из A + B1 +
B2 получить B1 + B2 + A. Меняя местами участки A и B1 - они име-
ют одинаковую длину, и сделать это легко,- получаем B1 + A + B2,
и  осталось  поменять  местами A и B2. Тем самым мы свели дело к
перестановке двух отрзков меньшей длины. Итак,  получаем  такую
схему программы:

  p := 0; q := m; r := m + n;
  {инвариант: осталось переставить x[p+1]..x[q], x[q+1]..x[s]}
  while (p <> q) and (q <> s) do begin
  | {оба участка непусты}
  | if (q - p) <= (s - q) then begin
  | | ..переставить x[p+1]..x[q] и x[q+1]..x[q+(q-p)]
  | | pnew := q; qnew := q + (q - p);
  | | p := pnew; q := qnew;
  | end else begin
  | | ..переставить x[q-(r-q)+1]..x[q] и x[q+1]..x[r]
  | | qnew := q - (r - q); rnew := q;
  | | q := qnew; r := rnew;
  | end;
  end;

Оценка времени работы: на очередном шаге оставшийся для обработ-
ки участок становится короче на длину A; число действий при этом
также пропорционально длине A.

     1.2.12. Коэффициенты многочлена хранятся в массиве a: array
[0..n]  of  integer (n - натуральное число, степень многочлена).
Вычислить значение этого многочлена в точке x (т. е.  a[n]*(x  в
степени n)+...+a[1]*x+a[0]).

     Решение. (Описываемый алгоритм называется схемой Горнера.)

  k := 0; y := a[n];
  {инвариант: 0 <= k <= n,
   y= a[n]*(x в степени k)+...+a[n-1]*(x в степени k-1)+...+
                     + a[n-k]*(x в степени 0)}
  while k<>n do begin
  | k := k + 1;
  | y := y * x + a [n - k];
  end;

     1.2.13. (Для знакомых с основами анализа. Сообщил  А.Г.Куш-
ниренко.)  Дополнить  алгоритм  вычисления значения многочлена в
заданной точке по схеме Горнера вычислением значения его  произ-
водной в той же точке.

     Решение. Добавление нового коэффициента соответствует пере-
ходу от многочлена P(x) к многочлену P(x)*x + c. Его производная
в  точке  x равна P'(x)*x + P(x). (Это решение обладает забавным
свойством: не надо знать заранее степень многочлена. Если требо-
вать выполнения этого условия, да еще просить  вычислять  только
значение производной, не упоминая о самом многочлене, получается
не такая уж простая задача.)

     1.2.14.  В  массивах
  a:array  [0..k] of integer и b: array [0..l] of integer
хранятся коэффициенты двух многочленов степеней k и  l.  Помес-
тить в массив c: array [0..m] of integer коэффициенты их произ-
ведения.  (Числа k, l, m - натуральные, m = k + l; элемент мас-
сива с индексом i содержит коэффициент при x в степени i.)

     Решение.

          for i:=0 to m do begin
          | c[i]:=0;
          end;
          for i:=0 to k do begin
          | for j:=0 to l do begin
          | | c[i+j] := c[i+j] + a[i]*b[j];
          | end;
          end;

     1.2.15. Предложенный выше алгоритм перемножения многочленов
требует порядка n*n действий для перемножения  двух  многочленов
степени n. Придумать более эффективный (для больших n) алгоритм,
которому  достаточно  порядка  (n  в  степени  (log  4)/(log 3))
действий.
     Указание. Представим себе, что надо перемножить два многоч-
лена степени 2k. Их можно представить в виде
        A(x)*x^k + B(x)    и    C(x)*x^k + D(x)
(здесь x^k обозначает x  в степени k). Произведение их равно
       A(x)C(x)*x^{2k}  +  (A(x)D(x)+B(x)C(x))*x^k  + B(x)D(x)
Естественный способ вычисления AC, AD+BC, BD требует четырех ум-
ножений многочленов степени k, однако их количество можно сокра-
тить  до  трех  с  помощью  такой  хитрости:  вычислить AC, BD и
(A+B)(C+D), а затем заметить, что AD+BC=(A+B)(C+D)-AC-BD.

     1.2.16.  Даны  два  возрастающих массива x: array [1..k] of
integer и y: array [1..l] of  integer.  Найти  количество  общих
элементов в этих массивах (т. е. количество тех целых t, для ко-
торых  t = x[i] = y[j] для некоторых i и j). (Число действий по-
рядка k+l.)

     Решение.

  k1:=0; l1:=0; n:=0;
  {инвариант: 0<=k1<=k; 0<=l1<=l; искомый ответ = n + количество
   общих элементов в x[k1+1]...x[k] и y[l1+1]..y[l]}
  while (k1 <> k) and (l1 <> l) do begin
  | if x[k1+1] < y[l1+1] then begin
  | | k1 := k1 + 1;
  | end else if x[k1+1] > y[l1+1] then begin
  | | l1 := l1 + 1;
  | end else begin {x[k1+1] = y[l1+1]}
  | | k1 := k1 + 1;
  | | l1 := l1 + 1;
  | | n := n + 1;
  | end;
  end;
  {k1 = k или l1 = l, поэтому одно из множеств, упомянутых в
   инварианте, пусто, а n равно искомому ответу}

Замечание. В третьей альтернативе достаточно было бы увеличивать
одну из переменных k1, l1; вторая добавлена для симметрии.

     1.2.17.  Решить  предыдущую задачу, если известно лишь, что
x[1] <= ... <= x[k] и y[1] <= ... <= y[l] (возрастание  заменено
неубыванием).

     Решение.  Условие  возрастания  было использовано в третьей
альтернативе выбора: сдвинув k1 и l1 на 1, мы тем самым уменьша-
ли  на  1  количество  общих  элементов   в   x[k1+1]...x[k]   и
x[l1+1]...x[l]. Теперь это придется делать сложнее.

          ...
          end else begin {x[k1+1] = y[l1+1]}
          | t := x [k1+1];
          | while (k1<k) and (x[k1+1]=t) do begin
          | | k1 := k1 + 1;
          | end;
          | while (l1<l) and (x[l1+1]=t) do begin
          | | l1 := l1 + 1;
          | end;
          end;

     Замечание. Эта программа имеет дефект: при проверке условия
                  (l1<l) and (x[l1+1]=t)
(или второго, аналогичного) при ложной первой скобке вторая ока-
жется бессмысленной (индекс выйдет за границы массива) и возник-
нет ошибка. Некоторые версии паскаля, вычисляя (A and B), снача-
ла  вычисляют  A и при ложном A не вычисляют B. (Так ведет себя,
например, система Turbo Pascal, 5.0 - но не 3.0.) Тогда  описан-
ная ошибка не возникнет.
     Но если мы не хотим полагаться на такое свойство  использу-
емой  нами  реализации  паскаля  (не предусмотренное его автором
Н.Виртом), то можно поступить так. Введем  дополнительную  пере-
менную b: boolean и напишем:

  if k1 < k  then b := (x[k1+1]=t)  else  b:=false;
  {b = (k1<k) and (x[k1+1] = t}
  while  b  do  begin
  | k1:=k1+1;
  | if k1 < k then b := (x[k1+1]=t) else b:=false;
  end;

Можно также сделать иначе:

          end else begin {x[k1+1] = y[l1+1]}
          | if k1 + 1 = k then begin
          | | k1 := k1 + 1;
          | | n := n + 1;
          | end else if x[k1+1] = x [k1+2] then begin
          | | k1 := k1 + 1;
          | end else begin
          | | k1 := k1 + 1;
          | | n := n + 1;
          | end;
          end;

Так будет короче, хотя менее симметрично.

     Наконец, можно увеличить размер  массива  в  его  описании,
включив  в  него  фиктивные элементы.

     1.2.18. Даны два неубывающих массива  x:  array  [1..k]  of
integer и y: array [1..l] of integer. Найти число различных эле-
ментов  среди  x[1],...,x[k], y[1],...,y[l]. (Число действий по-
рядка k+l.)

     1.2.19.  Даны два массива x[1] <= ... <= x[k] и y[1] <= ...
<= y[l]. "Соединить" их в массив z[1] <= ... <= z[m] (m  =  k+l;
каждый  элемент  должен  входить в массив z столько раз, сколько
раз он входит в общей сложности в массивы x и y). Число действий
порядка m.

     Решение.

  k1 := 0; l1 := 0;
  {инвариант: ответ получится, если к  z[1]..z[k1+l1]  приписать
   справа соединение массивов x[k1+1]..x[k] и y[l1+1]..y[l]}
  while (k1 <> k) or (l1 <> l) do begin
  | if k1 = k then begin
  | | {l1 < l}
  | | l1 := l1 + 1;
  | | z[k1+l1] := y[l1];
  | end else if l1 = l then begin
  | | {k1 < k}
  | | k1 := k1 + 1;
  | | z[k1+l1] := x[k1];
  | end else if x[k1+1] <= y[l1+1] then begin
  | | k1 := k1 + 1;
  | | z[k1+l1] := x[k1];
  | end else if x[k1+1] >= y[l1+1] then begin
  | | l1 := l1 + 1;
  | | z[k1+l1] := y[l1];
  | end else begin
  | | { такого не бывает }
  | end;
  end;
  {k1 = k, l1 = l, массивы соединены}

Этот  процесс  можно  пояснить  так. Пусть у нас есть две стопки
карточек, отсортированных по алфавиту. Мы соединяем  их  в  одну
стопку,  выбирая каждый раз ту из верхних карточек обеих стопок,
которая идет раньше в алфавитном порядке.

     1.2.20. Даны два массива x[1] <= ... <= x[k] и y[1] <=  ...
<=  y[l].  Найти  их  "пересечение",  т.е. массив z[1] <= ... <=
z[m], содержащий их общие  элементы,  причем  кратность  каждого
элемента в массиве z равняется минимуму из его кратностей в мас-
сивах x и y. Число действий порядка k+l.

     1.2.21. Даны два массива x[1]<=...<=x[k] и  y[1]<=...<=y[l]
и  число q. Найти сумму вида x[i]+y[j], наиболее близкую к числу
q. (Число действий порядка k+l, дополнительная память - фиксиро-
ванное число целых переменных, сами массивы менять не разрешает-
ся.)
     Указание. Надо найти минимальное расстояние между элемента-
ми x[1]<=...<=x[k] и q-y[l]<=..<=q-y[1], что нетрудно сделать  в
ходе их слияния в один (воображаемый) массив.

     1.2.22. (из книги Д.Гриса) Некоторое  число  содержится  в
каждом из трех целочисленных неубывающих массивов x[1] <= ... <=
x[p],  y[1]  <=  ... <= y[q], z[1] <= ... <= z[r]. Найти одно из
таких чисел. Число действий должно быть порядка p + q + r.

     Решение.

  p1:=1; q1=1; r1:=1;
  {инвариант: x[p1]..x[p], y[q1]..y[q], z[r1]..z[r]
   содержат общий элемент }
  while not ((x[p1]=y[q1]) and (y[q1]=z[r1])) do begin
  | if x[p1]<y[q1] then begin
  | | p1:=p1+1;
  | end else if y[q1]<z[r1] then begin
  | | q1:=q1+1;
  | end else if z[r1]<x[p1] then begin
  | | r1:=r1+1;
  | end else begin
  | | { так не бывает }
  | end;
  end;
  {x[p1] = y[q1] = z[r1]}
  writeln (x[p1]);

     1.2.23. Та же задача, только заранее не известно, существу-
ет  ли общий элемент в трех неубывающих массивах и требуется это
выяснить (и найти один из общих элементов, если они есть).

     1.2.24. Элементами  массива  a[1..n]  являются  неубывающие
массивы  [1..m]  целых чисел (a: array [1..n] of array [1..m] of
integer; a[1][1] <= ... <=  a[1][m],  ...,  a[n][1]  <=  ...  <=
a[n][m]). Известно, что существует число, входящее во все масси-
вы  a[i]  (существует  такое  х,  что  для  всякого  i из [1..n]
найдётся j из [1..m], для которого a[i][j]=x). Найти одно из та-
ких чисел х.

     Решение. Введем массив b[1]..b[n], отмечающий начало "оста-
ющейся части" массивов a[1]..a[n].

  for k:=1 to n do begin
  |  b[k]:=1;
  end;
  eq := true;
  for k := 2 to n do begin
  | eq := eq and (a[1][b[1]] = a[k][b[k]]);
  end;
  {инвариант: оставшиеся части  пересекаются,  т.е.  существует
   такое  х,  что для всякого i из [1..n] найдётся j из [1..m],
   не меньшее b[i], для которого a[i][j] =  х;  eq  <=>  первые
   элементы оставшихся частей равны}
  while not eq do begin
  | s := 1; k := 1;
  | {a[s][b[s]] - минимальное среди a[1][b[1]]..a[k][b[k]]}
  | while k <> n do begin
  | | k := k + 1;
  | | if a[k][b[k]] < a[s][b[s]] then begin
  | | | s := k;
  | | end;
  | end;
  | {a[s][b[s]] - минимальное среди a[1][b[1]]..a[n][b[n]]}
  | b [s] := b [s] + 1;
  | for k := 2 to n do begin
  | | eq := eq and (a[1][b[1]] = a[k][b[k]]);
  | end;
  end;
  writeln (a[1][b[1]]);

     1.2.25.  Приведенное  решение предыдущей задачи требует по-
рядка m*n*n действий. Придумать способ с числом действий порядка
m*n.
     Указание.  Придется  пожертвовать симметрией и выбрать одну
из строк за основную. Двигаясь по основной строке,  поддерживаем
такое  соотношение:  во  всех  остальных  строках отмечен макси-
мальный элемент, не  превосходящий  текущего  элемента  основной
строки.

     1.2.26. (Двоичный поиск) Дана  последовательность  x[1]  <=
...  <=  x[n] целых чисел и число a. Выяснить, содержится ли a в
этой последовательности, т. е. существует ли i из 1..n, для  ко-
торого x[i]=a. (Количество действий порядка log n.)

     Решение. (Предполагаем, что n > 0.)

  l := 1; r := n+1;
  {если a есть вообще, то есть и среди x[l]..x[r-1], r > l}
  while r - l <> 1 do begin
  | m := l + (r-l) div 2 ;
  | {l < m < r }
  | if x[m] <= a then begin
  | | l := m;
  | end else begin {x[m] > a}
  | | r := m;
  | end;
  end;
(Обратите внимание, что и в случае x[m] = a инвариант не наруша-
ется.)
     Каждый раз r-l уменьшается примерно вдвое, откуда и вытека-
ет требуемая оценка числа действий.
     Замечание.
l + (r-l) div 2 = (2l + (r-l)) div 2 = (r+l) div 2.

     1.2.27. (Из книги Д.Гриса) Дан массив x:  array  [1..n]  of
array  [1..m]  of  integer,  упорядоченный  по  "строкам"  и  по
"столбцам":
         x[i][j] <= x[i+1][j],
         x[i][j] <= x[i][j+1]
и число a. Требуется выяснить, встречается ли a среди x[i][j].

     Решение. Представляя себе  массив  a  как  матрицу  (прямо-
угольник,  заполненный числами), мы выберем прямоугольник, в ко-
тором только и может содержаться a, и будем его  сужать.  Прямо-
угольник этот будет содержать x[i][j] при 1<=i<=l и k<=j<=m.
                1                     k         m
               -----------------------------------
              1|                     |***********|
               |                     |***********|
               |                     |***********|
              l|                     |***********|
               |---------------------------------|
               |                                 |
              n|                                 |
               -----------------------------------
(допускаются пустые прямоугольники при l = 0 и k = m+1).

  l:=n; k:=1;
  {l>=0, k<=m+1, если a есть, то в описанном прямоугольнике}
  while (l > 0) and (k < m+1) and (x[l][k] <> a) do begin
  | if x[l][k] < a then begin
  | | k := k + 1; {левый столбец не содержит a, удаляем его}
  | end else begin {x[l][k] > a}
  | | l := l - 1; {нижняя строка не содержит a, удаляем ее}
  | end;
  end;
  {x[l][k] = a или прямоугольник пуст }
  answer:= (l > 0) and (k < m+1) ;

     Замечание.  Здесь та же ошибка: x[l][k] может оказаться не-
определенным. (Её исправление предоставляется читателю.)

     1.2.28. (Московская олимпиада по программированию) Дан не-
убывающий массив положительных целых чисел a[1] <= a[2]  <=...<=
a[n].  Найти наименьшее целое положительное число, не представи-
мое в виде суммы нескольких элементов этого массива (каждый эле-
мент массива может быть использован не более одного раза). Число
действий порядка n.

     Решение. Пусть известно, что  числа,  представимые  в  виде
суммы элементов a[1],...,a[k], заполняют отрезок от 1 до некото-
рого N. Если a[k+1] > N+1, то N+1 и будет минимальным числом, не
представимым  в виде суммы элементов массива a[1]..a[n]. Если же
a[k+1] <= N+1, то числа, представимые  в  виде  суммы  элементов
a[1]..a[k+1], заполняют отрезок от 1 до N+a[k+1].

  k := 0; N := 0;
  {инвариант: числа, представимые в виде суммы элементов массива
   a[1]..a[k], заполняют отрезок 1..N}
  while (k <> n) and (a[k+1] <= N+1) do begin
  | N := N + a[k+1];
  | k := k + 1;
  end;
  {(k = n) или (a[k+1] > N+1); в обоих случаях ответ N+1}
  writeln (N+1);

(Снова тот же дефект: в условии цикла при ложном первом  условии
второе не определено.)

     1.2.29.  (Для  знакомых с основами алгебры) В целочисленном
массиве a[1]..a[n] хранится перестановка чисел 1..n  (каждое  из
чисел встречается по одному разу).
     (а) Определить четность перестановки. (И в (а), и в (б) ко-
личество действий порядка n.)
     (б)  Не используя других массивов, заменить перестановку на
обратную (если до работы программы a[i]=j, то после должно  быть
a[j]=i).

     Указание.  (а)  Четность  перестановки  определяется  коли-
чеством циклов. Чтобы отличать уже пройденные циклы, у  их  эле-
ментов можно, например, менять знак. (б) Обращение производим по
циклам.

     1.2.30. Дан массив a[1..n] и число b. Переставить  числа  в
массиве  таким  образом, чтобы слева от некоторой границы стояли
числа, меньшие или равные b, а справа от границы -  большие  или
равные b.

     Решение.

        l:=0; r:=n;
        {инвариант: a[1]..a[l]<=b; a[r+1]..a[n]>=b}
        while l <> r do begin
        | if a[l+1] <= b then begin
        | | l:=l+1;
        | end else if a[r] >=b then begin
        | | r:=r-1;
        | end else begin {a[l+1]>b; a[r]<b}
        | | поменять a[l+1] и  a[r]
        | | l:=l+1; r:+r-1;
        | end;
        end;

     1.2.31. Та же задача, но требуется, чтобы сначала шли  эле-
менты,  меньшие  b, затем равные b, а лишь затем большие b.

     Решение.  Теперь  потребуются  три границы: до первой будут
идти элементы, меньшие b, от первой до второй - равные b,  затем
неизвестно какие до третьей, а после третьей - большие b. (Более
симметричное  решение использовало бы четыре границы, но вряд ли
игра стоит свеч.) В качестве очередного рассматриваемого элемен-
та берем элемент справа от средней границы.

     l:=0; m:=0; r:=n;
     {инвариант: a[1..l]<b; a[l+1..m]=b; a[r+1]..a[n]>b}
     while m <> r do begin
     | if a[m+1]=b then begin
     | | m:=m+1;
     | end else if a[m+1]>b then begin
     | | обменять a[m+1] и a[r]
     | | r:=r-1;
     | end else begin {a[m+1]<b}
     | | обменять a[m+1] и a[l+1]
     | | l:=l+1; m:=m+1;
     end;

     1.2.32.  (вариант  предыдущей  задачи,  названный  в  книге
Дейкстры задачей о голландском флаге) В массиве стоят числа 0, 1
и  2.  Переставить  их  в порядке возрастания, если единственной
разрешенной операцией (помимо чтения) над массивом является  пе-
рестановка двух элементов.

     1.2.33. Дан массив a[1]..a[n]  и  число  m<=n.  Для  каждой
группы  из m стоящих рядом членов (таких групп, очевидно, n-m+1)
вычислить ее сумму. Общее число действий должно быть порядка n.

     Решение.  Переходя  от группы к соседней, мы добавляем один
член, а другой вычитаем.

     1.2.34. Дана квадратная таблица a[1..n][1..n] и число m<=n.
Для каждого квадрата размера m на m  в  этой  таблице  вычислить
сумму  стоящих в нем чисел. Общее число действий должно быть по-
рядка n*n.

     Решение. Сначала для каждого горизонтального прямоугольника
размером n на 1 вычисляем сумму стоящих в нем чисел. (При сдвиге
такого  прямоугольника  по  горизонтали на 1 нужно добавить одно
число и одно вычесть.) Затем,  используя  эти  суммы,  вычисляем
суммы в квадратах. (При сдвиге квадрата по вертикали добавляется
полоска, а другая полоска убавляется.)

     1.3. Индуктивные функции (по А.Г.Кушниренко).

     Пусть M - некоторое множество. Функция f, аргументами кото-
рой являются последовательности элементов множества M, а  значе-
ниями - элементы некоторого множества N, называется индуктивной,
если  ее значение на последовательности x[1]..x[n] можно восста-
новить по ее значению на последовательности  x[1]..x[n-1]  и  по
x[n],  т.  е.  если  существует  функция F из N*M (множество пар
<n,m>, где n - элемент множества N, а m - элемент множества M) в
N, для которой

      f(<x[1],...,x[n]>) = F (f (<x[1],...,x[n-1]>), x[n]).

     Схема алгоритма вычисления индуктивной функции:

  k := 0; f := f0;
  {инвариант: f - значение функции на <x[1],...,x[k]>}
  while  k<> n do begin
  | k := k + 1;
  | f := F (f, x[k]);
  end;

     Здесь f0 - значение функции  на  пустой  последовательности
(последовательности  длины  0). Если функция f определена только
на непустых последовательностях, то первая строка заменяется  на
"k := 1; f := f (<x[1]>);".

     Индуктивные расширения.

     Если функция f не является индуктивной, полезно  искать  ее
индуктивное  расширение  - такую индуктивную функцию g, значения
которой определяют значения f (это значит, что существует  такая
функция  t,  что  f  (<x[1]...x[n]>) = t (g (<x[1]...x[n]>)) при
всех <x[1]...x[n]>). Можно доказать, что среди всех  индуктивных
расширений  существует  минимальное  расширение F (минимальность
означает, что для любого индуктивного расширения  g  значения  F
определяются значениями g).

     1.3.1.  Указать  индуктивные  расширения   для   следующих
функций:
   а)  среднее  арифметическое  последовательности вещественных
чисел;
   б) число элементов последовательности целых чисел, равных ее
максимальному элементу;
   в)  второй по величине элемент последовательности целых чисел
(тот, который будет вторым, если переставить члены в неубывающем
порядке);
   г) максимальное число идущих подряд одинаковых элементов;
   д) максимальная длина монотонного (неубывающего  или  невоз-
растающего)  участка  из  идущих  подряд элементов в последова-
тельности целых чисел;
   е) число групп из единиц, разделенных нулями  (в  последова-
тельности нулей и единиц).

     Решение.

а) <сумма всех членов последовательности; длина>;

б)  <число  элементов,  равных  максимальному;  значение макси-
     мального>;

в) <наибольший элемент последовательности; второй  по  величине
     элемент>;

г) <максимальное число идущих подряд одинаковых элементов; чис-
     ло  идущих  подряд одинаковых элементов в конце последова-
     тельности; последний элемент последовательности>;

д) <максимальная длина монотонного участка; максимальная  длина
      неубывающего  участка  в конце последовательности; макси-
      мальная длина невозрастающего участка в конце  последова-
      тельности; последний член последовательности>;

е) <число групп из единиц, последний член>.

     1.3.2. (Сообщил Д.Варсонофьев.) Даны две последовательности
x[1]..x[n] и y[1]..y[k] целых чисел. Выяснить, является ли  вто-
рая последовательность подпоследовательностью первой, т. е. мож-
но  ли  из первой вычеркнуть некоторые члены так, чтобы осталась
вторая. Число действий порядка n+k.

       Решение.  (1  вариант)  Будем  сводить  задачу  к  задаче
меньшего размера.

  n1:=n;
  k1:=k;
  {инвариант:  искомый ответ <=> возможность из x[1]..x[n1] по-
   лучить y[1]..y[k1] }
  while (n1 > 0) and (k1 > 0) do begin
  | if x[n1] = y[k1] then begin
  | | n1 := n1 - 1;
  | | k1 := k1 - 1;
  | end else begin
  | | n1 := n1 - 1;
  | end;
  end;
  {n1 = 0 или k1 = 0; если k1 = 0, то ответ - да, если k1 <>  0
   (и n1 = 0), то ответ - нет}
  answer := (k1 = 0);

     Мы использовали то, что если x[n1] = y[k1] и y[1]..y[k1] -
подпоследовательность x[1]..x[n1], то y[1]..y[k1-1] - подпосле-
довательность x[1]..x[n1-1].

     (2  вариант)  Функция x[1]..x[n1] |-> (максимальное k1, для
которого y[1]..y[k1] есть подпоследовательность x[1]..x[n1]) ин-
дуктивна.

     1.3.3. Даны две последовательности x[1]..x[n] и  y[1]..y[k]
целых  чисел. Найти максимальную длину последовательности, явля-
ющейся подпоследовательностью обеих  последовательностей.  Коли-
чество операций порядка n*k.

     Решение  (сообщено М.Н.Вайнцвайгом, А.М.Диментманом). Обоз-
начим через  f(n1,k1)  максимальную  длину  общей  подпоследова-
тельности последовательностей x[1]..x[n1] и y[1]..y[k1]. Тогда

   x[n1] <> y[k1] => f(n1,k1) = max (f(n1,k1-1), f(n1-1,k1));
   x[n1] = y[k1]  => f(n1,k1) = max (f(n1,k1-1), f(n1-1,k1),
                              f(n1-1,k1-1)+1 );

(Поскольку  f(n1-1,k1-1)+1  >= f(n1,k1-1), f(n1-1,k1), во втором
случае максимум трех чисел можно заменить на третье из них.)
     Поэтому можно заполнять таблицу значений функции f, имеющую
размер n*k. Можно обойтись и памятью порядка k (или n), если ин-
дуктивно  (по  n1) выписать <f(n1,0), ..., f(n1,k)> (как функция
от n1 этот набор индуктивен).

     1.3.4 (из книги Д.Гриса) Дана последовательность целых  чи-
сел  x[1],...,  x[n].  Найти  максимальную длину ее возрастающей
подпоследовательности (число действий порядка n*log(n)).

     Решение. Искомая функция не индуктивна, но имеет  следующее
индуктивное  расширение: в него входит помимо максимальной длины
возрастающей подпоследовательности (обозначим ее k) также и чис-
ла u[1],...,u[k], где u[i] = (минимальный  из  последних  членов
возрастающих  подпоследовательностей длины i). Очевидно, u[1] <=
... <= u[k]. При добавлении нового члена x значения u и  k  кор-
ректируются.

  n1 := 1; k := 1; u[1] := x[1];
  {инвариант: k и u соответствуют данному выше описанию}
  while n1 <> n do begin
  | n1 := n1 + 1;
  | ...
  | {i - наибольшее из тех чисел отрезка 1..k, для кото-
  |   рых u[i] < x[n1]; если таких нет, то i=0 }
  | if i = k then begin
  | | k := k + 1;
  | | u[k+1] := x[n1];
  | end else begin {i < k, u[i] < x[n1] <= u[i+1] }
  | | u[i+1] := x[n1];
  | end;
  end;

     Фрагмент ... использует идею двоичного поиска; в инвариан-
те условно полагаем u[0] равным минус бесконечности, а  u[k+1]
- плюс бесконечности; наша цель: u[i] < x[n1] <= u[i+1].

  i:=0; j:=k+1;
  {u[i] < x[n1] <= u[j], j > i}
  while (j - i) <> 1 do begin
  | s := i + (j-i) div 2;    {i < s < j}
  | if u[s] >= x[n1] then begin
  | | j := s;
  | end else begin {u[s] < x[n1]}
  | | i := s;
  | end;
  end;
  {u[i] < x[n1] <= u[j], j-i = 1}

     Замечание.  Более  простое  (но не минимальное) индуктивное
расширение получится, если для каждого  i  хранить  максимальную
длину   возрастающей  подпоследовательности,  оканчивающейся  на
x[i]. Это расширение приводит к алгоритму с числом действий  по-
рядка n*n.

     1.3.5.  Какие  изменения  нужно внести в решение предыдущей
задачи, если надо  искать  максимальную  неубывающую  последова-
тельность?
     Глава 2. Порождение комбинаторных объектов.

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

     2.1. Размещения с повторениями.

     2.1.1. Напечатать все последовательности длины k  из  чисел
1..n.

     Решение.  Будем  печатать  их  в лексикографическом порядке
(последовательность a предшествует  последовательности  b,  если
для  некоторого s их начальные отрезки длины s равны, а (s+1)-ый
член  последовательности  a  меньше).  Первой  будет  последова-
тельность  <1, 1, ..., 1>, последней - последовательность <n, n,
..., n>. Будем хранить последнюю напечатанную последовательность
в массиве x[1]...x[k].

        ...x[1]...x[k] положить равным 1
        ...напечатать x
        ...last[1]...last[k] положить равным n
        while x <> last do begin
        | ...x := следующая за x последовательность
        | ...напечатать x
        end;

     Опишем, как можно  перейти  от  x  к  следующей  последова-
тельности.  Согласно определению, у следующей последовательности
первые s членов должны быть такими же, а (s+1)-ый - больше.  Это
возможно, если x[s+1] было меньше n. Среди таких s нужно выбрать
наибольшее  (иначе полученная последовательность не будет непос-
редственно следующей). Соответствующее x[s+1] нужно увеличить на
1. Итак, надо, двигаясь с конца последовательности, найти  самый
правый  член,  меньший  n (он найдется, так как по предположению
x<>last), увеличить его на 1, а идущие  за  ним  члены  положить
равными 1.

        p:=k;
        while not (x[p] < n) do begin
        | p := p-1;
        end;
        {x[p] < n, x[p+1] =...= x[k] = n}
        x[p] := x[p] + 1;
        for i := p+1 to k do begin
        | x[i]:=1;
        end;

     Замечание. Если членами последовательности считать числа не
от  1 до n, а от 0 до n-1, то переход к следующему соответствует
прибавлению 1 в n-ичной системе счисления.

     2.1.2. В предложенном алгоритме используется сравнение двух
массивов x <> last. Устранить его, добавив булевскую  переменную
l и включив в инвариант соотношение l <=> последовательность x -
последняя.

     2.1.3. Напечатать все подмножества множества {1...k}.

     Решение.  Подмножества находятся во взаимно однозначном со-
ответствии с последовательностями нулей и единиц длины k.

     2.1.4. Напечатать все последовательности из k положительных
целых чисел, у которых i-ый член не превосходит i.

     2.2. Перестановки.

     2.2.1. Напечатать все перестановки чисел 1..n (то есть пос-
ледовательности  длины  n, в которые каждое из чисел 1..n входит
по одному разу).

     Решение. Перестановки будем  хранить  в  массиве  x[1],...,
x[n]  и  печатать в лексикографическом порядке. (Первой при этом
будет перестановка <1 2...n>, последней - <n...2 1>.)  Для  сос-
тавления  алгоритма  перехода к следующей перестановке зададимся
вопросом: в каком случае k-ый член перестановки можно увеличить,
не меняя предыдущих? Ответ: если он меньше какого-либо из следу-
ющих членов (членов с номерами больше k). Мы  должны  найти  на-
ибольшее  k,  при  котором  это  так,  т. е. такое k, что x[k] <
x[k+1] > ... > x[n]. После  этого  x[k]  нужно  увеличить  мини-
мальным  возможным способом, т. е. найти среди x[k+1], ..., x[n]
наименьшее число, большее его. Поменяв x[k] с ним, остается рас-
положить числа с номерами k+1, ..., n  так,  чтобы  перестановка
была наименьшей, то есть в возрастающем порядке. Это облегчается
тем, что они уже расположены в убывающем порядке.

     Алгоритм перехода к следующей перестановке.

  {<x[1],...,x[n-1], x[n]> <> <n,...,2, 1>.}
  k:=n-1;
  {последовательность справа от k убывающая: x[k+1] >...> x[n]}
  while x[k] > x[k+1] do begin
  | k:=k-1;
  end;
  {x[k] < x[k+1] > ... > x[n]}
  t:=k+1;
  {t <=n, x[k+1] > ... > x[t] > x[k]}
   while (t < n) and (x[t+1] > x[k]) do begin
   | t:=t+1;
   end;
   {x[k+1] > ... > x[t] > x[k] > x[t+1] > ... > x[n]}
   ... обменять x[k] и x[t]
   {x[k+1] > ... > x[n]}
   ... переставить участок x[k+1] ... x[n] в обратном порядке

Замечание. Программа имеет знакомый  дефект:  если  t  =  n,  то
x[t+1] не определено.

     2.2.2. Модифицировать алгоритм перехода к следующей  перес-
тановке так, чтобы он сам проверял, не является ли данная перес-
тановка последней.

     2.3. Подмножества.

     2.3.1. Перечислить все k-элементные подмножества  множества
{1..n}.

     Решение.  Будем представлять каждое подмножество последова-
тельностью x[1]..x[n] нулей и единиц длины n, в которой ровно  k
единиц. (Другой способ представления разберем позже.) Такие пос-
ледовательности упорядочим лексикографически (см. выше). Очевид-
ный  способ  решения  задачи - перебирать все последовательности
как раньше, а затем отбирать среди них те, у которых k единиц  -
мы отбросим, считая его неэкономичным (число последовательностей
с  k  единицами  может  быть  много меньше числа всех последова-
тельностей). Будем искать такой алгоритм, чтобы  получение  оче-
редной последовательности требовало порядка n действий.
     В каком случае s-ый член  последовательности  можно  увели-
чить,  не  меняя предыдущие? Если x[s] меняется с 0 на 1, то для
сохранения общего числа единиц нужно справа от х[s]  заменить  1
на 0. Таким образом, х[s] - первый справа нуль, за которым стоят
единицы.  Легко  видеть,  что х[s+1] = 1 (иначе х[s] не первый).
Таким образом надо искать наибольшее  s,  для  которого  х[s]=0,
x[s+1]=1;

                  ______________________
               x |________|0|1...1|0...0|
                           s

За х[s+1] могут идти еще несколько единиц, а после них несколько
нулей. Заменив х[s] на 1, надо выбрать идущие за ним члены  так,
чтобы последовательность была бы минимальна с точки зрения наше-
го  порядка,  т. е. чтобы сначала шли нули, а потом единицы. Вот
что получается:

  первая последовательность    0...01...1 (n-k нулей, k единиц)
  последняя последовательность 1...10...0 (k единиц, n-k нулей)

  алгоритм перехода к следующей за х[1]...x[n] последовательнос-
  ти (предполагаем, что она есть):

        s := n - 1;
        while not ((x[s]=0) and (x[s+1]=1)) do begin
        | s := s - 1;
        end;
        {s - член, подлежащий изменению с 0 на 1}
        num:=0;
        for k := s to n do begin
        | num := num + x[k];
        end;
        {num - число единиц на участке x[s]...x[n], число нулей
         равно (длина - число единиц), т. е. (n-s+1) - num}
        x[s]:=1;
        for k := s+1 to n-num+1 do begin
        | x[k] := 0;
        end;
        for k := n-num+2 to n do begin
        | x[k]:=1;
        end;

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

     2.3.2. Перечислить все возрастающие последовательности дли-
ны  k  из  чисел 1..n в лексикографическом порядке. (Пример: при
n=5, k=2 получаем 12 13 14 15 23 24 25 34 35 45.)

     Решение. Минимальной будет последовательность 1, 2, ..., k;
максимальной - (n-k+1),..., (n-1), n. В каком случае  s-ый  член
последовательности можно увеличить? Ответ: если он меньше n-k+s.
После увеличения s-го элемента все следующие должны возрастать с
шагом 1. Получаем такой алгоритм перехода к следующему:

        s:=n;
        while not (x[s] < n-k+s) do begin
        | s:=s-1;
        end;
        {s - элемент, подлежащий увеличению};
        x[s] := x[s]+1;
        for i := s+1 to n do begin
        | x[i] := x[i-1]+1;
        end;

     2.3.3.  Пусть  мы  решили представлять k-элементные подмно-
жества множества {1..n} убывающими последовательностями длины k,
упорядоченными по-прежнему лексикографически. (Пример : 21 31 32
41 42 43 51 52 53 54.) Как выглядит тогда  алгоритм  перехода  к
следующей?

     Ответ. Ищем наибольшее s, для которого х[s]-x[s+1]>1. (Если
такого s нет, полагаем s = 0.) Увеличив x [s+1] на 1, кладем ос-
тальные минимально возможными (x[t] = k+1-t для t>s).

     2.3.4. Решить две предыдущие задачи, заменив  лексикографи-
ческий  порядок  на  обратный  (раньше идут те, которые больше в
лексикографическом порядке).

     2.3.5. Перечислить все вложения (функции, переводящие  раз-
ные  элементы в разные) множества {1..k} в {1..n} (предполагает-
ся, что k <= n). Порождение очередного элемента должно требовать
порядка k действий.

     Указание.  Эта  задача  может  быть  сведена к перечислению
подмножеств и перестановок элементов каждого подмножества.

     2.4. Разбиения.

     2.4.1. Перечислить все разбиения целого положительного чис-
ла  n  на целые положительные слагаемые (разбиения, отличающиеся
лишь порядком слагаемых, считаются за одно). (Пример: n=4,  раз-
биения 1+1+1+1, 2+1+1, 2+2, 3+1, 4.)

     Решение. Договоримся, что (1) в разбиениях слагаемые идут в
невозрастающем порядке, (2) сами разбиения мы перечисляем в лек-
сикографическом  порядке.  Разбиение  храним  в  начале  массива
x[1]...x[n], при этом количество входящих в него чисел обозначим
k. В начале x[1]=...=x[n]=1, k=n, в конце x[1]=n, k=1.
     В  каком  случае  x[s] можно увеличить не меняя предыдущих?
Во-первых, должно быть x[s-1] > x[s] или s  =  1.  Во-вторых,  s
должно  быть не последним элементом (увеличение s надо компенси-
ровать уменьшением следующих). Увеличив s, все следующие элемен-
ты надо взять минимально возможными.

        s := k - 1;
        while not ((s=1) or (x[s-1] > x[s])) do begin
        | s := s-1;
        end;
        {s - подлежащее увеличению слагаемое}
        x [s] := x[s] + 1;
        sum := 0;
        for i := s+1 to k do begin
        | sum := sum + x[i];
        end;
        {sum - сумма членов, стоявших после x[s]}
        for i := 1 to sum-1 do begin
        | x [s+i] := 1;
        end;
        k := s+sum-1;

     2.4.2. Представляя по-прежнему разбиения как невозрастающие
последовательности, перечислить их в порядке, обратном лексиког-
рафическому (для n=4, например, должно получиться 4,  3+1,  2+2,
2+1+1, 1+1+1+1).
     Указание. Уменьшать можно первый справа член, не равный  1;
найдя  его,  уменьшим на 1, а следующие возьмем максимально воз-
можными  (равными ему, пока хватает суммы, а последний - сколько
останется).

     2.4.3. Представляя  разбиения  как  неубывающие  последова-
тельности,  перечислить  их в лексикографическом порядке. Пример
для n=4: 1+1+1+1, 1+1+2, 1+3, 2+2, 4;
     Указание. Последний член увеличить нельзя, а  предпоследний
- можно; если после увеличения на 1 предпоследнего члена за счет
последнего нарушится возрастание, то из двух членов надо сделать
один,  если  нет,  то  последний член надо разбить на слагаемые,
равные предыдущему, и остаток, не меньший его.

     2.4.4.  Представляя  разбиения  как  неубывающие последова-
тельности, перечислить их в порядке, обратном лексикографическо-
му. Пример для n=4: 4, 2+2, 1+3, 1+1+2, 1+1+1+1.
     Указание.  Чтобы элемент x[s] можно было уменьшить, необхо-
димо, чтобы s = 1 или x[s-1] < x[s]. Если x[s] не последний,  то
этого и достаточно. Если он последний, то нужно, чтобы x[s-1] <=
(целая часть (x[s]/2)) или s=1.

     2.5. Коды Грея и аналогичные задачи.

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

     2.5.1.  Перечислить все последовательности длины n из чисел
1..k в таком порядке, чтобы каждая следующая отличалась от  пре-
дыдущей в единственной цифре, причем не более, чем на 1.

     Решение. Рассмотрим прямоугольную доску ширины n  и  высоты
k.  На каждой вертикали будет стоять шашка. Таким образом, поло-
жения шашек соответствуют последовательностям из чисел 1..k дли-
ны n (s-ый член последовательности соответствует высоте шашки на
s-ой горизонтали). На каждой шашке нарисуем  стрелочку,  которая
может быть направлена вверх или вниз. Вначале все шашки поставим
на  нижнюю  горизонталь стрелочкой вверх. Далее двигаем шашки по
такому правилу: найдя самую правую шашку, которую  можно  подви-
нуть  в направлении (нарисованной на ней) стрелки, двигаем ее на
одну клетку в этом направлении, а все стоящие  правее  ее  шашки
(они уперлись в край) разворачиваем кругом.
     Ясно, что на каждом шаге только одна шашка сдвигается, т.е.
один член последовательности меняется на 1. Докажем индукцией по
n,  что проходятся все последовательности из чисел 1...k. Случай
n = 1 очевиден. Пусть n > 1. Все ходы поделим на те, где  двига-
ется  последняя шашка, и те, где двигается не последняя. Во вто-
ром случае последняя шашка стоит у стены, и мы ее  поворачиваем,
так  что  за каждым ходом второго типа следует k-1 ходов первого
типа, за время которых последняя шашка побывает во всех клетках.
Если мы теперь забудем о последней шашке, то движения первых n-1
по предположению индукции пробегают все последовательности длины
n-1 по одному разу; движения же последней шашки из каждой после-
довательности длины n-1 делают k последовательностей длины n.
     В  программе,  помимо последовательности x[1]...x[n], будем
хранить массив d[1]...d[n] из чисел +1 и  -1  (+1  соответствует
стрелке вверх, -1 -стрелке вниз).

Начальное состояние: x[1] =...= x[n] = 1; d[1] =...= d[n] = 1.

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

  {если можно, сделать шаг и положить p := true, если нет,
   положить p := false }
  i := n;
  while (i > 1) and
  | (((d[i]=1) and (x[i]=n)) or ((d[i]=-1) and (x[i]=1)))
  |   do begin
  | i:=i-1;
  end;
  if (d[i]=1 and x[i]=n) or (d[i]=-1 and x[i]=1)
  |    then begin {i=1}
  | p:=false;
  end else begin
  | p:=true;
  | x[i] := x[i] + d[i];
  | for j := i+1 to n do begin
  | | d[j] := - d[j];
  | end;
  end;

     Замечание.  Для последовательностей нулей и единиц возможно
другое решение, использующее двоичную систему. (Именно оно  свя-
зывается обычно с названием "коды Грея".)
     Запишем подряд все числа от 0 до (2 в степени n) - 1 в дво-
ичной системе. Например, для n = 3 напишем:

            000 001 010 011 100 101 110 111

Затем  каждое из чисел подвергнем преобразованию, заменив каждую
цифру, кроме первой, на ее сумму с предыдущей цифрой (по  модулю
2). Иными словами, число

     a[1], a[2],...,a[n]  преобразуем в
     a[1], a[1] + a[2], a[2] + a[3],...,a[n-1] + a[n]

(сумма по модулю 2). Для n=3 получим:

            000 001 011 010  110  111 101 100.

     Легко проверить, что описанное преобразование чисел обрати-
мо (и тем самым дает все  последовательности  по  одному  разу).
Кроме  того,  двоичные  записи соседних чисел отличаются заменой
конца 011...1 на конец 100...0, что  -  после  преобразования  -
приводит к изменению единственной цифры.

     Применение кода Грея. Пусть есть вращающаяся ось, и мы  хо-
тим  поставить датчик угла поворота этой оси. Насадим на ось ба-
рабан, выкрасим половину барабана в черный цвет, половину в  бе-
лый и установим фотоэлемент. На его выходе будет в половине слу-
чаев  0,  а в половине 1 (т. е. мы измеряем угол "с точностью до
180").

     Развертка барабана:
                     0       1
             -> |_|_|_|_|*|*|*|*| <- (склеить бока).

     Сделав рядом другую дорожку из двух черных и белых частей и
поставив  второй фотоэлемент, получаем возможность измерить угол
с точностью до 90 градусов:

                   0   0   1   1
                   0   1   0   1
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|_|_|*|*|

Сделав третью,

                 0 0 0 0 1 1 1 1
                 0 0 1 1 0 0 1 1
                 0 1 0 1 0 1 0 1
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|_|_|*|*|
                |_|*|_|*|_|*|_|*|

мы  измерим угол с точностью до 45 градусов и т.д. Эта идея име-
ет, однако, недостаток: в момент пересечения границ  сразу  нес-
колько  фотоэлементов  меняют  сигнал, и если эти изменения про-
изойдут не одновременно, на какое-то время показания фотоэлемен-
тов будут бессмысленными.  Коды  Грея  позволяют  избежать  этой
опасности.  Сделаем так, чтобы на каждом шаге менялось показание
лишь одного фотоэлемента (в том числе и на последнем, после  це-
лого оборота).

                 0 0 0 0 1 1 1 1
                 0 0 1 1 1 1 0 0
                 0 1 1 0 0 1 1 0
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|*|*|_|_|
                |_|*|*|_|_|*|*|_|

     Написанная нами формула позволяет легко преобразовать  дан-
ные от фотоэлементов в двоичный код угла поворота.

     2.5.2. Напечатать все перестановки чисел  1..n  так,  чтобы
каждая   следующая   получалась   из   предыдущей  перестановкой
(транспозицией) двух соседних чисел. Например, при n = 3  допус-
тим такой порядок: 3.2 1 -> 2 3.1 -> 2.1 3 -> 1 2.3 -> 1.3 2  ->
3 1 2 (между переставляемыми числами вставлены точки).

     Решение. Наряду с множеством перестановок  рассмотрим  мно-
жество  последовательностей y[1]..y[n] целых неотрицательных чи-
сел, у которых y[1] <= 0,..., y[n] <= n-1. В нем столько же эле-
ментов, сколько в множестве всех перестановок, и мы сейчас уста-
новим между ними взаимно однозначное соответствие. Именно,  каж-
дой  перестановке  поставим  в  соответствие  последовательность
y[1]..y[n], где y[i] - количество чисел, меньших i и стоящих ле-
вее i в этой перестановке. Взаимная  однозначность  вытекает  из
такого  замечания. Перестановка чисел 1...n получается из перес-
тановки чисел 1..n-1 добавлением числа n, которое можно вставить
на любое из n мест. При этом к сопоставляемой с  ней  последова-
тельности  добавляется  еще один член, принимающий значения от 0
до n-1, а предыдущие члены не меняются.  При  этом  оказывается,
что  изменение  на единицу одного из членов последовательности y
соответствует перестановке двух соседних чисел, если все  следу-
ющие  числа последовательности y принимают максимально или мини-
мально возможные для них значения. Именно, увеличение y[i] на  1
соответствует  перестановке  числа  i  с  его  правым соседом, а
уменьшение - с левым.
     Теперь вспомним решение задачи о перечислении всех последо-
вательностей, на каждом шаге которого один член меняется на еди-
ницу. Заменив прямоугольную доску доской в форме лестницы (высо-
та i-ой вертикали равна i) и двигая шашки по тем же правилам, мы
перечислим все последовательности y, причем i-ый член будет  ме-
няться,  лишь  если  все  следующие шашки стоят у края. Надо еще
уметь параллельно с изменением  y  корректировать  перестановку.
Очевидный  способ требует отыскания в ней числа i; это можно об-
легчить, если помимо самой перестановки хранить функцию i  |--->
позиция  числа i в перестановке (обратное к перестановке отобра-
жение), и соответствующим образом ее корректировать.  Вот  какая
получается программа:

 program test;
 | const n=...;
 | var
 |   x: array [1..n] of 1..n; {перестановка}
 |   inv_x: array [1..n] of 1..n; {обратная перестановка}
 |   y: array [1..n] of integer; {Y[i] < i}
 |   d: array [1..n] of -1..1; {направления}
 |   b: boolean;
 |
 | procedure print_x;
 | | var i: integer;
 | begin
 | | for i:=1 to n do begin
 | | | write (x[i], ' ');
 | | end;
 | | writeln;
 | end;
 |
 | procedure set_first;{первая перестановка: y[i]=0 при всех i}
 | | var i : integer;
 | begin
 | | for i := 1 to n do begin
 | | | x[i] := n + 1 - i;
 | | | inv_x[i] := n + 1 - i;
 | | | y[i]:=0;
 | | | d[i]:=1;
 | | end;
 | end;
 |
 | procedure move (var done : boolean);
 | | var i, j, pos1, pos2, val1, val2, tmp : integer;
 | begin
 | | i := n;
 | | while (i > 1) and (((d[i]=1) and (y[i]=i-1)) or
 | | |          ((y[i]=-1) and (y[i]=0))) do begin
 | | | i := i-1;
 | | end;
 | | done := (i>1);
 | | {упрощение связано с тем, что первый член нельзя менять}
 | | if done then begin
 | | | y[i] := y[i]+d[i];
 | | | for j := i+1 to n do begin
 | | | | d[j] := -d[j];
 | | | end;
 | | | pos1 := inv_x[i];
 | | | val1 := i;
 | | | pos2 := pos1 + d[i];
 | | | val2 := x[pos2];
 | | | {pos1, pos2 - номера переставляемых элементов;
 | | |   val1, val2 - их значения}
 | | | tmp := x[pos1];
 | | | x[pos1] := x[pos2];
 | | | x[pos2] := tmp;
 | | | tmp := inv_x[val1];
 | | | inv_x[val1] := inv_x[val2];
 | | | inv_x[val2] := tmp;
 | | end;
 | end;
 |
 begin
 | set_first;
 | print_x;
 | b := true;
 | {напечатаны все перестановки до текущей включительно;
 |   если b ложно, то текущая - последняя}
 | while b do begin
 | | move (b);
 | | if b then print_x;
 | end;
 end.

     2.6. Несколько замечаний.

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

     2.6.1. Перечислить все последовательности длины 2n, состав-
ленные из n единиц и n минус единиц, у которых сумма любого  на-
чального  отрезка положительна (т.е. число минус единиц в нем не
превосходит числа единиц).

     Решение. Изображая единицу вектором (1,1), а минус  единицу
вектором  (1,-1), можно сказать, что мы ищем пути из точки (0,0)
в точку (n,0), не опускающиеся ниже оси абсцисс.
     Будем перечислять последовательности  в  лексикографическом
порядке,  считая,  что  -1  предшествует  1.  Первой  последова-
тельностью будет "пила"
        1, -1, 1, -1, ...
а последней - "горка"
        1, 1, 1, ..., 1, -1, -1, ..., -1.
     Как перейти от последовательности к следующей? До некоторо-
го места они должны совпадать, а затем надо заменить  -1  на  1.
Место  замены должно быть расположено как можно правее. Но заме-
нять -1 на 1 можно только в том случае, если справа от нее  есть
единица (которую можно заменить на -1). Заменив -1 на 1, мы при-
ходим  к  такой  задаче:  фиксирован  начальный кусок последова-
тельности, надо найти минимальное продолжение. Ее решение:  надо
приписывать -1, если это не нарушит условия неотрицательности, а
иначе приписывать 1. Получаем такую программу:

    ...
    type array2n = array [1..2n] of integer;
    ...
    procedure get_next (var a: array2n; var last: Boolean);
    | {в a помещается следующая последовательность, если}
    | {она есть (при этом last=false), иначе last:=true}
    | var k, i, sum: integer;
    begin
    | k:=2*n;
    | {инвариант: в a[k+1..2n] только минус единицы}
    | while a[k] = -1 do begin k:=k-1; end;
    | {k - максимальное среди тех, для которых a[k]=1}
    | while (k>0) and (a[k] = 1) do begin k:=k-1; end;
    | {a[k] - самая правая -1, за которой есть 1;
    |  если таких нет, то k=0}
    | if k = 0 then begin
    | | last := true;
    | end else begin
    | | last := false;
    | | i:=0; sum:=0;
    | | {sum = a[1]+...+a[i]}
    | | while i<> k do begin
    | | | i:=i+1; sum:= sum+a[i];
    | | end;
    | | {sum = a[1]+...+a[k]}
    | |  a[k]:= 1; sum:= sum+2;
    | | {вплоть до a[k] все изменено, sum=a[1]+...+a[k]}
    | | while k <> 2*n do begin
    | | | k:=k+1;
    | | | if sum > 0 then begin
    | | | | a[k]:=-1
    | | | end else begin
    | | | | a[k]:=1;
    | | | end;
    | | | sum:= sum+a[k];
    | | end;
    | | {k=n, sum=a[1]+...a[2n]=0}
    | end;
    end;

     2.6.2.  Перечислить все расстановки скобок в произведении n
сомножителей. Порядок сомножителей не меняется, скобки полностью
определяют порядок действий. (Например, для n = 4 есть 5 расста-
новок ((ab)c)d, (a(bc))d, (ab)(cd), a((bc)d), a(b(cd)).)

     Указание. Каждому порядку действий соответствует последова-
тельность команд стекового калькулятора.

     2.6.3.  На окружности задано 2n точек, пронумерованных от 1
до 2n. Перечислить все способы провести n непересекающихся  хорд
с вершинами в этих точках.

     2.6.4. Перечислить все способы разрезать n-угольник на тре-
угольники, проведя n - 2 его диагонали.

     Еще  один класс задач на перечисление всех элементов задан-
ного множества мы  рассмотрим  ниже,  обсуждая  метод  поиска  с
возвратами (backtracking).

     2.7. Подсчет количеств.

     Иногда  можно  найти  количество  объектов  с  тем или иным
свойством, не перечисляя их. Классический пример: C(n,k) - число
всех k-элементных подмножеств n-элементного  множества  -  можно
найти, заполняя таблицу значений функции С по формулам:

    C (n,0) = C (n,n) = 1            (n >= 1)
    C (n,k) = C (n-1,k-1) + C (n-1,k) (n > 1, 0 < k < n);

или по формуле n!/((k!)*(n-k)!). (Первый способ эффективнее, ес-
ли надо вычислить много значений С(n,k).)

    Приведем другие примеры.

     2.7.1 (Число разбиений). (Предлагалась на всесоюзной  олим-
пиаде  по программированию 1988 года.) Пусть P(n) - число разби-
ений целого положительного n на  целые  положительные  слагаемые
(без учета порядка, 1+2 и 2+1 - одно и то же разбиение). При n=0
положим P(n) = 1 (единственное разбиение не содержит слагаемых).
Построить алгоритм вычисления P(n) для заданного n.
     Решение.  Можно  доказать  (это нетривиально) такую формулу
для P(n):

 P(n) = P(n-1)+P(n-2)-P(n-5)-P(n-7)+P(n-12)+P(n-15) +...

(знаки у пар членов чередуются, вычитаемые в  одной  паре  равны
(3*q*q-q)/2 и (3*q*q+q)/2).
     Однако и без ее использования можно придумать способ вычис-
ления  P(n), который существенно эффективнее перебора и подсчета
всех разбиений.
     Обозначим через R(n,k) (при n >= 0, k >= 0) число разбиений
n  на  целые  положительные  слагаемые, не превосходящие k. (При
этом  R(0,k) считаем равным 1 для всех k >= 0.) Очевидно, P(n) =
R(n,n). Все разбиения n на слагаемые, не  превосходящие  k,  ра-
зобьем  на  группы  в  зависимости  от  максимального слагаемого
(обозначим его i). Число R(n,k) равно сумме (по всем i от  1  до
k)  количеств разбиений со слагаемыми не больше k и максимальным
слагаемым, равным i. А разбиения n на слагаемые  не  более  k  с
первым  слагаемым, равным i, по существу представляют собой раз-
биения n - i на слагаемые, не превосходящие i (при i <= k).  Так
что

    R(n,k) = сумма по i от 1 до k чисел R(n-i,i) при k <= n;
    R(n,k) = R(n,n) при k >= n,

что позволяет заполнять таблицу значений функции R.

     2.7.2 (Счастливые билеты). (Задача предлагалась на Всесоюз-
ной олимпиаде по программированию 1989 года). Последовательность
из 2n цифр (каждая цифра от 0 до 9) называется счастливым  биле-
том, если сумма первых n цифр равна сумме последних n цифр. Най-
ти число счастливых последовательностей данной длины.

     Решение. (Сообщено одним из участников олимпиады; к сожале-
нию,  не могу указать фамилию, так как работы проверялись зашиф-
рованными.) Рассмотрим более общую задачу: найти число  последо-
вательностей,  где  разница  между суммой первых n цифр и суммой
последних n цифр равна k (k = -9n,..., 9n). Пусть T(n, k) - чис-
ло таких последовательностей.
     Разобьем  множество  таких  последовательностей на классы в
зависимости от разницы между первой и  последней  цифрами.  Если
эта разница равна t, то разница между суммами групп из оставших-
ся  n-1 цифр равна k-t. Учитывая, что пар цифр с разностью t бы-
вает 10 - (модуль t), получаем формулу
   T(n,k) = сумма по t от -9 до 9 чисел (10-|t|) * T(n-1,  k-t).
(Некоторые слагаемые могут отсутствовать, так как k-t может быть
слишком велико.)
      Глава 3. Обход дерева. Перебор с возвратами.

     3.1. Ферзи, не бьющие друг друга: обход дерева позиций

     В  предыдущей главе мы рассматривали несколько задач одного
и того же типа: "перечислить все элементы  некоторого  множества
A". Схема решения была такова: на множестве A вводился порядок и
описывалась  процедура  перехода  от произвольного элемента мно-
жества A к следующему за ним (в этом порядке).  Такую  схему  не
всегда  удается  реализовать  непосредственно, и в этой главе мы
рассмотрим другой полезный прием перечисления всех элементов не-
которого множества. Его называют "поиск  с  возвратами",  "метод
ветвей  и границ", "backtracking". На наш взгляд наиболее точное
название этого метода - обход дерева.

     3.1.1. Перечислить все способы расстановки n ферзей на шах-
матной доске n на n, при которых они не бьют друг друга.

     Решение. Очевидно, на каждой из n горизонталей должно  сто-
ять  по  ферзю.  Будем  называть k-позицией (для k = 0, 1,...,n)
произвольную расстановку k ферзей на k нижних горизонталях (фер-
зи могут бить друг друга). Нарисуем "дерево позиций": его корнем
будет единственная 0-позиция, а из каждой  k-позиции  выходит  n
стрелок  вверх в (k+1)-позиции. Эти n позиций отличаются положе-
нием ферзя на (k+1)-ой горизонтали. Будем считать, что  располо-
жение  их  на рисунке соответствует положению этого ферзя: левее
та позиция, в которой ферзь расположен левее.

                                        Дерево позиций для
                                           n = 2

Среди позиций этого дерева нам надо отобрать те n-позиции, в ко-
торых ферзи не бьют друг друга. Программа будет "обходить  дере-
во" и искать их. Чтобы не делать лишней работы, заметим вот что:
если  в  какой-то  k-позиции  ферзи  бьют друг друга, то ставить
дальнейших ферзей смысла нет. Поэтому, обнаружив это,  мы  будем
прекращать построение дерева в этом направлении.

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

                                         Дерево допустимых
                                         позиций для n = 3

     Разобьем задачу на две части: (1) обход произвольного дере-
ва и (2) реализацию дерева допустимых позиций.
     Сформулируем задачу обхода произвольного дерева. Будем счи-
тать, что у нас имеется Робот, который в каждый момент находится
в одной из вершин дерева (вершины изображены на рисунке  кружоч-
ками). Он умеет выполнять команды:

                              вверх_налево  (идти по самой левой
                                 из выходящих вверх стрелок)

                              вправо (перейти в соседнюю  справа
                                 вершину)

                              вниз (спуститься вниз на один уро-
                                 вень)

            вверх_налево
            вправо
            вниз

и проверки, соответствующие возможности выполнить каждую из  ко-
манд,   называемые  "есть_сверху",  "есть_справа",  "есть_снизу"
(последняя истинна всюду, кроме корня). Обратите  внимание,  что
команда "вправо" позволяет перейти лишь к "родному брату", но не
к "двоюродному".

                                    Так команда "вправо"
                                    НЕ действует!

     Будем считать, что у Робота есть команда "обработать" и что
его задача - обработать все  листья  (вершины,  из  которых  нет
стрелок вверх, то есть где условие "есть_сверху" ложно). Для на-
шей  шахматной  задачи  команде обработать будет соответствовать
проверка и печать позиции ферзей.

     Доказательство  правильности приводимой далее программы ис-
пользует такие определения. Пусть фиксировано положение Робота в
одной из вершин дерева. Тогда все листья дерева  разбиваются  на
три  категории: над Роботом, левее Робота и правее Робота. (Путь
из корня в лист может проходить через вершину с Роботом,  свора-
чивать  влево,  не доходя до нее и сворачивать вправо, не доходя
до нее.) Через (ОЛ) обозначим условие "обработаны все листья ле-
вее Робота", а через (ОЛН) - условие "обработаны все листья  ле-
вее и над Роботом".

Нам понадобится такая процедура:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОЛ), надо: (ОЛН)}
  begin
  | {инвариант: ОЛ}
  | while есть_сверху do begin
  | | вверх_налево
  | end
  | {ОЛ, Робот в листе}
  | обработать;
  | {ОЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, листья не обработаны
  надо: Робот в корне, листья обработаны

  {ОЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОЛН, есть справа}
  | | вправо;
  | | {ОЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | end;
  end;
  {ОЛН, Робот в корне => все листья обработаны}

Осталось  воспользоваться  следующими  свойствами  команд Робота
(сверху записаны условия, в которых выполняется команда, снизу -
утверждения о результате ее выполнения):

   (1) {ОЛ, не есть_сверху}  (2) {ОЛ}
       обработать                вверх_налево
       {ОЛН}                     {ОЛ}

   (3) {есть_справа, ОЛН}    (4) {не есть_справа, ОЛН}
       вправо                    вниз
       {ОЛ}                      {ОЛН}

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

     3.1.3. Доказать правильность следующей программы обхода де-
рева:

  var state: (WL, WLU);
  state := WL;
  while есть_снизу or (state <> WLU) do begin
  | if (state = WL) and есть_сверху then begin
  | | вверх;
  | end else if (state = WL) and not есть_сверху then begin
  | | обработать; state := WLU;
  | end else if (state = WLU) and есть_справа then begin
  | |  вправо; state := WL;
  | end else begin {state = WLU, not есть_справа, есть_снизу}
  | |  вниз;
  | end;
  end;

     Решение. Инвариант цикла:
        state = WL  => ОЛ
        state = WLU => ОЛН
Доказательство завершения работы: переход из состояния ОЛ в  ОЛН
возможен  только  при  обработке вершины, поэтому если программа
работает бесконечно, то с некоторого момента значение  state  не
меняется, что невозможно.

    3.1.4.  Решить задачу об обходе дерева, если мы хотим, чтобы
обрабатывались все вершины (не только листья).

    Решение. Пусть x - некоторая вершина. Тогда любая вершина  y
относится к одной из четырех категорий. Рассмотрим путь из корня
в y. Он может:
    (а) быть частью пути из корня в x (y ниже x);
    (б) свернуть налево с пути в x (y левее x);
    (в) пройти через x (y над x);
    (г) свернуть направо с пути в x (y правее x);
В  частности,  сама вершина x относится к категории (в). Условия
теперь будут такими:
    (ОНЛ) обработаны все вершины ниже и левее;
    (ОНЛН) обработаны все вершины ниже, левее и над.
Вот как будет выглядеть программа:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОНЛ), надо: (ОНЛН)}
  begin
  | {инвариант: ОНЛ}
  | while есть_сверху do begin
  | | обработать
  | | вверх_налево
  | end
  | {ОНЛ, Робот в листе}
  | обработать;
  | {ОНЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, ничего не обработано
  надо: Робот в корне, все вершины обработаны

  {ОНЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОНЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОНЛН, есть справа}
  | | вправо;
  | | {ОНЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | end;
  end;
  {ОНЛН, Робот в корне => все вершины обработаны}

     3.1.5. Приведенная только что программа обрабатывает верши-
ну до того, как обработан любой из ее потомков. Как изменить ее,
чтобы каждая вершина, не являющаяся листом, обрабатывалась дваж-
ды: один раз до, а другой раз после всех своих потомков? (Листья
по-прежнему обрабатываются по разу.)

    Решение.  Под "обработано ниже и левее" будем понимать "ниже
обработано по разу, слева обработано полностью (листья по  разу,
останые по два)". Под "обработано ниже, левее и над" будем пони-
мать "ниже обработано по разу, левее и над - полностью".

Программа будет такой:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОНЛ), надо: (ОНЛН)}
  begin
  | {инвариант: ОНЛ}
  | while есть_сверху do begin
  | | обработать
  | | вверх_налево
  | end
  | {ОНЛ, Робот в листе}
  | обработать;
  | {ОНЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, ничего не обработано
  надо: Робот в корне, все вершины обработаны

  {ОНЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОНЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОНЛН, есть справа}
  | | вправо;
  | | {ОНЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | | обработать;
  | end;
  end;
  {ОНЛН, Робот в корне => все вершины обработаны полностью}

     3.1.6. Доказать, что число операций в этой программе по по-
рядку равно числу вершин дерева. (Как и в других программах, ко-
торые  отличаются от этой лишь пропуском некоторых команд "обра-
ботать".)
     Указание. Примерно каждое второе  действие  при  исполнении
этой программы - обработка вершины, а каждая вершина обрабатыва-
ется максимум дважды.

     Теперь реализуем операции с деревом позиций. Позицию  будем
представлять  с помощью переменной k: 0..n (число ферзей) и мас-
сива c: array [1..n] of 1..n (c [i] - координаты ферзя  на  i-ой
горизонтали; при i > k значение c [i] роли не играет). Предпола-
гается,  что  все позиции допустимы (если убрать верхнего ферзя,
остальные не бьют друг друга).

  program queens;
  | const n = ...;
  | var
  |   k: 0..n;
  |   c: array [1..n] of 1..n;
  |
  | procedure begin_work; {начать работу}
  | begin
  | | k := 0;
  | end;
  |
  | function danger: boolean; {верхний ферзь под боем}
  | | var b: boolean; i: integer;
  | begin
  | | if k <= 1 then begin
  | | | danger := false;
  | | end else begin
  | | | b := false; i := 1;
  | | | {b <=> верхний ферзь под боем ферзей с номерами < i}
  | | | while i <> k do begin
  | | | | b := b or (c[i]=c[k]) {вертикаль}
  | | | |     or (abs(c[[i]-c[k]))=abs(i-k)); {диагональ}
  | | | | i := i+ 1;
  | | | end;
  | | | danger := b;
  | | end;
  | end;
  |
  | function is_up: boolean {есть_сверху}
  | begin
  | | is_up := (k < n) and not danger;
  | end;
  |
  | function is_right: boolean {есть_справа}
  | begin
  | | is_right := (k > 0) and (c[k] < n);
  | end;
  | {возможна ошибка: при k=0 не определено c[k]}
  |
  | function is_down: boolean {есть_снизу}
  | begin
  | | is_up := (k > 0);
  | end;
  |
  | procedure up; {вверх_налево}
  | begin {k < n}
  | | k := k + 1;
  | | c [k] := 1;
  | end;
  |
  | procedure right; {вправо}
  | begin {k > 0,  c[k] < n}
  | | c [k] := c [k] + 1;
  | end;
  |
  | procedure down; {вниз}
  | begin {k > 0}
  | | k := k - 1;
  | end;
  |
  | procedure work; {обработать}
  | | var i: integer;
  | begin
  | | if (k = n) and not danger then begin
  | | | for i := 1 to n do begin
  | | | | write ('<', i, ',' , c[i], '> ');
  | | | end;
  | | | writeln;
  | | end;
  | end;
  |
  | procedure UW; {вверх_до_упора_и_обработать}
  | begin
  | | while is_up do begin
  | | | up;
  | | end
  | | work;
  | end;
  |
  begin
  | begin_work;
  | UW;
  | while is_down do begin
  | | if is_right then begin
  | | | right;
  | | | UW;
  | | end else begin
  | | | down;
  | | end;
  | end;
  end.

     3.1.7. Приведенная программа тратит довольно много  времени
на  выполнение  проверки  есть_сверху  (проверка,  находится  ли
верхний ферзь под боем, требует числа действий порядка n). Изме-
нить реализацию операций с деревом позиций так,  чтобы  все  три
проверки есть_сверху/справа/снизу и соответствующие команды тре-
бовали  бы  количества действий, ограниченного не зависящей от n
константой.

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

     3.2.  Обход дерева в других задачах.

     3.2.1. Использовать метод обхода дерева для решения  следу-
ющей   задачи:   дан  массив  из  n  целых  положительных  чисел
a[1]..a[n] и число s; требуется узнать, может ли  число  s  быть
представлено  как  сумма  некоторых  из чисел массива a. (Каждое
число можно использовать не более чем по одному разу.)

     Решение. Будем задавать k-позицию последовательностью из  k
булевских  значений,  определяющих,  входят  ли  в  сумму  числа
a[1]..a[k] или не входят. Позиция допустима, если  ее  сумма  не
превосходит s.

     Замечание. По сравнению с полным перебором всех (2 в степе-
ни  n) подмножеств тут есть некоторый выигрыш. Можно также пред-
варительно отсортировать массив a в убывающем порядке,  а  также
считать  недопустимыми  те  позиции, в которых сумма отброшенных
членов больше, чем разность суммы всех  членов  и  s.  Последний
приём  называют  "методом  ветвей  и границ". Но принципиального
улучшения по сравнению с полным перебором тут не получается (эта
задача, как говорят, NP-полна,  см.  подробности  в  книге  Ахо,
Хопкрофта и Ульмана "Построение и анализ вычислительных алгорит-
мов").  Традиционное  название  этой задачи - "задача о рюкзаке"
(рюкзак общей грузоподъемностью s нужно упаковать  под  завязку,
располагая  предметами  веса  a[1]..a[n]).  См.  также в главе 7
(раздел о динамическом программировании)  алгоритм  её  решения,
полиномиальный по n+s.

     3.2.2.  Перечислить все последовательности из n нулей, еди-
ниц и двоек, в которых никакая группа цифр  не  повторяется  два
раза подряд (нет куска вида XX).

     3.2.3.  Аналогичная  задача для последовательностей нулей и
единиц, в которых никакая группа цифр не  повторяется  три  раза
подряд (нет куска вида XXX).

     К этой же категории относятся задачи типа "можно ли сложить
данную фигуру из пентамино" и им подобные. В  них  важно  умелое
сокращение  перебора (вовремя распознать, что имеющееся располо-
жение фигурок уже противоречит требованиям, и по этой ветви  по-
иск не продолжать).
        Глава 4. Сортировка.

     4.1. Квадратичные алгоритмы.

     4.1.1. Пусть a[1],  ...,  a[n]  -  целые  числа.  Требуется
построить  массив  b[1],  ..., b[n], содержащий те же числа, для
которых b[1] <= ... <= b[n].
     Замечание. Среди чисел a[1]...a[n] могут быть равные.  Тре-
буется,  чтобы  каждое целое число входило в b[1]...b[n] столько
же раз, сколько и в a[1]...a[n].

     Решение. Удобно считать, что числа a[1]..a[n] и  b[1]..b[n]
представляют собой начальное и конечное значения массива x. Тре-
бование  "a  и b содержат одни и те же числа" будет заведомо вы-
полнено, если в процессе работы  мы  ограничимся  перестановками
элементов x.
  ...
  k := 0;
  {k наименьших элементов массива x установлены на свои места}
  while k <> n do begin
  | s := k + 1; t := k + 1;
  | {x[s] - наименьший среди x[k+1]...x[t] }
  | while t<>n do begin
  | | t := t + 1;
  | | if x[t] < x[s] then begin
  | | | s := t;
  | | end;
  | end;
  | {x[s] - наименьший среди x[k+1]..x[n] }
  | ... переставить x[s] и x[k+1];
  | k := k + 1;
  end;

     4.1.2.  Дать другое решение задачи сортировки, использующее
инвариант {первые k элементов упорядочены: x[1] <= ... <= x[k]}

     Решение.

  k:=1
  {первые k элементов упорядочены}
  while k <> n do begin
  | {k+1-ый элемент продвигается к началу, пока не займет
  |   надлежащего места }
  | t := k+1;
  | {x[1] <= ... <= x[t-1] и x[t-1], x[t] <= ... <= x[k+1] }
  | while (t > 1) and (x[t] < x[t-1]) do begin
  | | ... поменять x[t-1] и x[t];
  | | t := t - 1;
  | end;
  end;

     Замечание. Дефект программы: при ложном выражении (t  >  1)
проверка x[t] < x[t-1] требует несуществующего значения x[0].
     Оба  предложенных решения требуют числа действий, пропорци-
онального n*n. Существуют более эффективные алгоритмы.

     4.2. Алгоритмы порядка n log n.

     4.2.1. Предложить алгоритм сортировки, число действий кото-
рого  было  бы  порядка  n  log  n,  то  есть не превосходило бы
C*n*log(n) для некоторого C и для всех n.

     Мы предложим два решения.

     Решение 1. (сортировка слиянием).
     Пусть  k  -  положительное  целое  число.  Разобьем  массив
x[1]..x[n]  на  отрезки  длины  k.  (Первый  - x[1]..x[k], затем
x[k+1]..x[2k] и т.д.) Последний отрезок будет неполным,  если  n
не  делится на k. Назовем массив k-упорядоченным, если каждый из
этих отрезков упорядочен. Любой массив 1-упорядочен. Если массив
k-упорядочен и n<=k, то он упорядочен.
     Мы  опишем,  как  преобразовать  k-упорядоченный  массив  в
2k-упорядоченный (из тех же элементов). С помощью этого преобра-
зования алгоритм записывается так:

  k:=1;
  {массив x является k-упорядоченным}
  while k < n do begin
  | .. преобразовать k-упорядоченный массив в 2k-упорядоченный;
  | k := 2 * k;
  end;

     Требуемое  преобразование  состоит в том,что мы многократно
"сливаем" два упорядоченных отрезка длины не  больше  k  в  один
упорядоченный  отрезок. Пусть процедура слияние (p,q,r: integer)
при p <=q <= r сливает отрезки  x[p+1]..x[q]  и  x[q+1]..x[r]  в
упорядоченный  отрезок x[p+1]..x[r] (не затрагивая других частей
массива x).
                  p               q               r
            -------|---------------|---------------|-------
                   | упорядоченный | упорядоченный |
            -------|---------------|---------------|-------
                                  |
                                  |
                                  V
            -------|-------------------------------|-------
                   |     упорядоченный             |
            -------|-------------------------------|-------

Тогда преобразование k-упорядоченного массива в 2k-упорядоченный
осуществляется так:

  t:=0;
  {t кратно 2k или t = n, x[1]..x[t] является
   2k-упорядоченным; остаток массива x не изменился}
  while t + k < n do begin
  | p := t;
  | q := t+k;
  | ...r := min (t+2*k, n); {в паскале нет функции min }
  | слияние (p,q,r);
  | t := r;
  end;

Слияние требует вспомогательного массива для записи  результатов
слияния  -  обозначим его b. Через p0 и q0 обозначим номера пос-
ледних элементов участков, подвергшихся слиянию, s0 -  последний
записанный  в  массив b элемент. На каждом шаге слияния произво-
дится одно из двух действий:

        b[s0+1]:=x[p0+1];
        p0:=p0+1;
        s0:=s0+1;
или
        b[s0+1]:=x[q0+1];
        q0:=q0+1;
        s0:=s0+1;

Первое действие (взятие элемента из первого отрезка) может  про-
изводиться при двух условиях:
    (1) первый отрезок не кончился (p0 < q);
    (2) второй отрезок кончился (q0 = r)  или  не  кончился,  но
элемент в нем не меньше [(q0 < r) и (x[p0+1] <= x[q0+1])].
     Аналогично для второго действия. Итак, получаем

  p0 := p; q0 := q; s0 := p;
  while (p0 <> q) or (q0 <> r) do begin
  | if (p0 < q) and ((q0 = r) or ((q0 < r) and
  | |                (x[p0+1] <= x[q0+1]))) then begin
  | | b [s0+1] := x [p0+1];
  | | p0 := p0+1;
  | | s0 := s0+1;
  | end else begin
  | | {(q0 < r) and ((p0 = q) or ((p0<q) and
  | |   (x[p0+1] >= x[q0+1])))}
  | | b [s0+1] := x [q0+1];
  | | q0 := q0 + 1;
  | | s0 := s0 + 1;
  | end;
  end;

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

     Решение 2 (сортировка деревом).
     Нарисуем "полное двоичное дерево"  -  картинку,  в  которой
снизу один кружок, из него выходят стрелки в два других, из каж-
дого - в два других и так далее:

               .............
                 o  o o  o
                  \/   \/
                   o   o
                    \ /
                     o

     Будем  говорить, что стрелки ведут "от отцов к сыновьям": у
каждого кружка два сына и один отец (если  кружок  не  верхний).
Предположим  для  простоты, что количество подлежащих сортировке
чисел есть степень двойки, и они могут заполнить один  из  рядов
целиком. Запишем их туда. Затем заполним часть дерева под ним по
правилу:
   число в кружке = минимум из чисел в кружках-сыновьях
Тем  самым  в  корне дерева (нижнем кружке) будет записано мини-
мальное число во всем массиве.
     Изымем из сортируемого  массива  минимальный  элемент.  Для
этого  его  надо вначале найти. Это можно сделать, идя от корня:
от отца переходим к тому сыну, где записано то же  число.  Изъяв
минимальный  элемент,  заменим  его  символом  "бесконечность" и
скорректируем более низкие ярусы (для этого  надо  снова  пройти
путь к корню). При этом считаем, что минимум из n и бесконечнос-
ти  равен  n. Тогда в корне появится второй по величине элемент,
мы изымаем его, заменяя бесконечностью и корректируя дерево. Так
постепенно мы изымем все элементы в порядке возрастания, пока  в
корне не останется бесконечность.
     При записи этого алгоритма полезно нумеровать кружочки чис-
лами 1, 2, ...: сыновьями кружка номер n являются кружки  2*n  и
2*n+1. Подробное изложение этого алгоритма мы опустим, поскольку
мы  изложим  более  эффективный  вариант,  не требующий дополни-
тельной памяти, кроме конечного числа переменных (в дополнении к
сортируемому массиву).
     Мы будем записывать сортируемые числа во всех вершинах  де-
рева,  а не только на верхнем уровне. Пусть x[1]..x[n] - массив,
подлежащий сортировке. Вершинами дерева будут числа от 1 до n; о
числе x[i] мы будем говорить как о числе, стоящем в вершине i. В
процессе сортировки количество вершин дерева будет  сокращаться.
Число вершин текущего дерева будем хранить в переменной k. Таким
образом,  в  процессе работы алгоритма массив x[1]..x[n] делится
на две части: в x[1]..x[k] хранятся числа на дереве, а в  x[k+1]
.. x[n] хранится уже отсортированная в порядке возрастания часть
массива - элементы, уже занявшие свое законное место.
     На каждом шаге алгоритм будет изымать максимальный  элемент
дерева и помещать его в отсортированную часть, на освободившееся
в результате сокращения дерева место.
     Договоримся о терминологии. Вершинами дерева считаются чис-
ла от 1 до текущего значения переменной k. У  каждой  вершины  s
могут  быть  сыновья 2s и 2s+1. Если оба этих числа больше k, то
сыновей нет; такая вершина называется листом. Если 2s=k, то вер-
шина s имеет ровно одного сына (2s).
     Для каждого s из 1..k рассмотрим "поддерево" с корнем в  s:
оно  содержит вершину s и всех ее потомков (сыновей, сыновей сы-
новей и т.д. - до тех пор, пока мы не выйдем из  отрезка  1..k).
Вершину  s будем называть регулярной, если стоящее в ней число -
максимальный элемент s-поддерева; s-поддерево  назовем  регуляр-
ным,  если  все  его вершины регулярны. (В частности, любой лист
образует регулярное одноэлементное поддерево.)

     Схема алгоритма такова:

  k:= n
  ... Сделать 1-поддерево регулярным;
  {x[1],..,x[k] <= x[k+1] <= ... <= x[n]; 1-поддерево регулярно,
   в частности, x[1] - максимальный элемент среди x[1]..x[k]}
  while k <> 1 do begin
  | ... обменять местами x[1] и x[k];
  | k := k - 1;
  | {x[1]..x[k-1] <= x[k] <=...<= x[n]; 1-поддерево регу-
  |   лярно везде, кроме, возможно, самого корня }
  | ... восстановить регулярность 1-поддерева всюду
  end;

В качестве вспомогательной процедуры нам  понадобится  процедура
восстановления регулярности s-поддерева в корне. Вот она:

  {s-поддерево регулярно везде, кроме, возможно, корня}
  t := s;
  {s-поддерево регулярно везде, кроме, возможно, вершины t}
  while ((2*t+1 <= k) and (x[2*t+1] > x[t])) or
  |     ((2*t <= k) and (x[2*t] > x[t])) do begin
  | if (2*t+1 <= k) and (x[2*t+1] >= x[2*t]) then begin
  | | ... обменять x[t] и x[2*t+1];
  | | t := 2*t + 1;
  | end else begin
  | | ... обменять x[t] и x[2*t];
  | | t := 2*t;
  | end;
  end;

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

  while наибольшее число не в t, а в одном из сыновей do begin
  | if оно в правом сыне then begin
  | | поменять t с ее правым сыном; t:= правый сын
  | end else begin {наибольшее число - в левом сыне}
  | | поменять t с ее левым сыном; t:= левый сын
  | end
  end

После  обмена  вершина  t  становится регулярной (в нее попадает
максимальное число t-поддерева). Не принявший участия  в  обмене
сын остается регулярным, а принявший участие может и не быть ре-
гулярным. В остальных вершинах s-поддерева не изменились ни чис-
ла,  ни поддеревья их потомков (разве что два элемента поддерева
переставились), так что регуярность не нарушилась.

   Эта же процедура может использоваться для того, чтобы сделать
1-поддерево регулярным на начальной стадии сортировки:

  k := n;
  u := n;
  {все s-поддеревья с s>u регулярны }
  while u<>0 do begin
  | {u-поддерево регулярно везде, кроме разве что корня}
  | ... восстановить регулярность u-поддерева в корне;
  | u:=u-1;
  end;

     Теперь запишем процедуру сортировки на паскале  (предпола-
гая,  что  n  -  константа,  x  имеет тип arr = array [1..n] of
integer).

  procedure sort (var x: arr);
  | var u, k: integer;
  | procedure exchange(i, j: integer);
  | | var tmp: integer;
  | | begin
  | | tmp  := x[i];
  | | x[i] := x[j];
  | | x[j] := tmp;
  | end;
  | procedure restore (s: integer);
  | | var t: integer;
  | | begin
  | | t:=s;
  | | while ((2*t+1 <= k) and (x[2*t+1] > x[t]) ) or
  | | |     ((2*t <= k) and (x[2*t] > x[t])) do begin
  | | | if (2*t+1 <= k) and (x[2*t+1] >= x[2*t]) then begin
  | | | | exchange (t, 2*t+1);
  | | | | t := 2*t+1;
  | | | end else begin
  | | | | exchange (t, 2*t);
  | | | | t := 2*t;
  | | | end;
  | | end;
  | end;
  begin
  | k:=n;
  | u:=n;
  | while u <> 0 do begin
  | | restore (u);
  | | u := u - 1;
  | end;
  | while k <> 1 do begin
  | | exchange (1, k);
  | | k := k - 1;
  | | restore (1);
  | end;
  end;

     Несколько замечаний.

     Метод, использованный при сортировке деревом, бывает полез-
ным в других случах. (См. в главе 6 (о типах данных)  раздел  об
очереди с приоритетами.)

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

     Еще один практически важный алгоритм сортировки таков: что-
бы  отсортировать массив, выберем случайный его элемент b, и ра-
зобъем массив на три части: меньшие b, равные  b  и  большие  b.
(Эта  задача  приведена в главе о массивах.) Теперь осталось от-
сортировать первую и третью части: это делается тем же способом.
Время работы этого алгоритма - случайная величина;  можно  дока-
зать, что в среднем он работает не больше C*n*log n. На практике
- он один из самых быстрых. (Мы еще вернемся к нему, приведя его
рекурсивную и нерекурсивную реализации.)

     Наконец, отметим, что сортировка за время порядка C*n*log n
может быть выполнена с помощью техники сбалансированных деревьев
(см.  главу  12), однако программы тут сложнее и константа C до-
вольно велика.

     4.3. Применения сортировки.

     4.3.1. Найти количество  различных  чисел  среди  элементов
данного массива. Число действий порядка n*log n. (Эта задача уже
была в главе о массивах.)

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

     4.3.2. Дано n отрезков [a[i],  b[i]]  на  прямой  (i=1..n).
Найти максимальное k, для которого существует точка прямой, пок-
рытая k отрезками ("максимальное число слоев"). Число действий -
порядка n*log n.

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

     4.3.3. Дано n точек на плоскости. Указать (n-1)-звенную не-
самопересекающуюся незамкнутую ломаную, проходящую через все эти
точки.  (Соседним  отрезкам  ломаной разрешается лежать на одной
прямой.) Число действий порядка n*log n.

     Решение. Упорядочим точки по  x-координате,  а  при  равных
x-координатах  - по y-координате. В таком порядке и можно прово-
дить ломаную.

     4.3.4. Та же задача, если ломаная должна быть замкнутой.

     Решение. Возьмем самую левую точку (т.е. точку с наименьшей
x-координатой) и проведем из нее лучи во  все  остальные  точки.
Теперь упорядочим эти лучи, а точки на одном луче поместим в по-
рядке увеличения расстояния от начала луча.

     4.3.5. Дано n точек на  плоскости.  Построить  их  выпуклую
оболочку  -  минимальную  выпуклую фигуру, их содержащую. (Форму
выпуклой оболочки примет резиновое колечко, если его натянуть на
гвозди, вбитые в точках.)  Число операций не более n*log n.

    Указание. Упорядочим точки - годится любой из порядков,  ис-
пользованных в двух предыдущих задачах. Затем, рассматривая точ-
ки по очереди, будем строить выпуклую оболочку уже рассмотренных
точек. (Для хранения выпуклой оболочки полезно использовать дек,
см. главу 6 о типах данных.)

     4.4. Нижние оценки для числа сравнений при сортировке.

     Пусть  имеется  n  различных по весу камней и весы, которые
позволяют за одно взвешивание определить, какой из двух  выбран-
ных  нами  камней тяжелее. (В программистских терминах: мы имеем
доступ к функции  тяжелее(i,j:1..n):boolean.)  Надо  упорядочить
камни  по  весу,  сделав  как  можно меньше взвешиваний (вызовов
функции "тяжелее").

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

    4.4.1. Доказать, что сложность любого алгоритма сортировки n
камней не меньше log (n!). (Логарифм берется по основанию 2,  n!
- произведение чисел 1..n.)

     Решение. Пусть имеется алгоритм сложности не более  d.  Для
каждого  из n! возможных расположений камней запротоколируем ре-
зультаты взвешиваний (обращений к функции "тяжелее");  их  можно
записать  в  виде  последовательности  из не более чем d нулей и
единиц. Для  единообразия  дополним  последовательность  нулями,
чтобы ее длина стала равной d. Тем самым у нас имеется n! после-
довательностей  из  d нулей и единиц. Все эти последовательности
разные - иначе наш алгоритм дал бы одинаковые ответы для  разных
порядков  (и один из ответов был бы неправильным). Получаем, что
2 в степени d не меньше n! - что и требовалось доказать.

     Другой способ объяснить то же самое  -  рассмотреть  дерево
вариантов,  возникающее в ходе выполнения алгоритма, и сослаться
на то, что дерево высоты d не может иметь более (2 в степени  d)
листьев.

     Это  рассуждение показывает, что любой алгоритм сортировки,
использующий только сравнения элементов массива и их перестанов-
ки, требует не менее C*n*log n действий, так что наши  алгоритмы
близки  к  оптимальным. Однако алгоритм сортировки, использующий
другие операции, может действовать быстрее. Вот один  из  приме-
ров.

     4.4.2. Имеется массив целых чисел  a[1]..a[n],  причем  все
числа неотрицательны и не превосходят m. Отсортировать этот мас-
сив; число действий порядка m+n.

     Решение.  Для каждого числа от 0 до m подсчитываем, сколько
раз оно встречается в массиве. После этого исходный массив можно
стереть и заполнить заново в порядке возрастания, используя све-
дения о кратности каждого числа.

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

Есть также метод сортировки, в котором последовательно проводится 
ряд  "частичных  сортировок"  по отдельным битам. Начнём с такой
задачи:

     4.4.3. В массиве a[1]..a[n] целых чисел переставить элемен-
ты так, чтобы чётные шли перед нечётными (не меняя взаимный  по-
рядок в каждой из групп).

     Решение.  Сначала  спишем  (во  вспомогательный массив) все
чётные, а потом - все нечётные.

     4.4.4. Имеется массив из n чисел от 0 до (2 в степени k)  -
1, каждое из которых мы будем рассматривать как k-битовое слово.
Используя проверки "i-ый бит равен 0" и "i-ый бит равен 1" вмес-
то сравнений, отсортировать все числа за время порядка n*k.

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

     Аналогичный алгоритм может быть применен для m-ичной систе-
мы  счисления  вместо двоичной. При этом полезна такая вспомога-
тельная задача:

     4.4.5. Даны n чисел и функция f, принимающая (на них)  зна-
чения  1..m.  Требуется переставить числа в таком порядке, чтобы
значения функции f не убывали (сохраняя  притом  порядок  внутри
каждой из групп). Число действий порядка m+n.
     Указание. Завести m списков суммарной длины n (как это сде-
лать,  смотри в главе 6 о типах данных) и помещать в i-ый список
числа, для которых значение функции f равно i.  Вариант:  посчи-
тать  для  всех  i, сколько имеется чисел x c f(x)=i, после чего
легко определить, с какого места нужно начинать размещать  числа
с f(x)=i.

     4.5. Родственные сортировке задачи.

     4.5.1. Какова минимально возможная сложность (число сравне-
ний  в наихудшем случае) алгоритма отыскания самого легкого из n
камней?

     Решение. Очевидный алгоритм  с  инвариантом  "найден  самый
легкий  камень  среди первых i" требует n-1 сравнений. Алгоритма
меньшей сложности нет. Это вытекает из следующего более сильного
утверждения.

     4.5.2. Эксперт хочет докать суду, что данный камень - самый
легкий среди n камней, сделав менее n-1  взвешиваний.  Доказать,
что  это  невозможно.  (Веса камней неизвестны суду, но известны
эксперту.)

     Решение. Изобразим камни точками, а взвешивания  -  линиями
между  ними. Получим граф с n вершинами и менее чем n-1 ребрами.
Такой  граф  несвязен  (добавление  каждого   следующего   ребра
уменьшает число компонент не более чем на 1). Поэтому суд ничего
не  знает  относительно  соотношения весов камней в двух связных
компонентах и может допустить, что самый легкий камень - в любой
из них.

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

     4.5.3. Дано n различных по весу камней и число k (от  1  до
n). Требуется найти k-ый по весу камень,  сделав  не  более  C*n
взвешиваний, где C - некоторая константа, не зависящая от k.

     Замечание.  Сортировка  позволяет  сделать это за C*n*log n
взвешиваний. Указание к этой (трудной) задаче приведено в  главе
про рекурсию.

     Следующая задача имеет неожиданно простое решение.

     4.5.4. Имеется n одинаковых на вид камней, некоторые из ко-
торых на самом деле различны по весу. Имеется  прибор,  позволя-
ющий  по  двум камням определить, одинаковы они или различны (но
не говорящий, какой тяжелее). Известно, что  среди  этих  камней
большинство  (более n/2) одинаковых. Сделав не более n взвешива-
ний, найти хотя бы один камень из этого большинства.

     Предостережение. Если два камня одинаковые, это не гаранти-
рует их принадлежности к большинству.

     Указание. Если найдены два различных камня, то их оба можно
выбросить - хотя бы один из них плохой и  большинство  останется
большинством.

     Решение. Программа просматривает камни по очереди, храня  в
переменной i число просмотренных камней. (Считаем камни пронуме-
рованными от 1 до n.) Помимо этого программа хранит номер "теку-
щего  кандидата"  c  и  его  "кратность"  k. Смысл этих названий
объясняется инвариантом:

   если к непросмотренным камням (с номерами i+1..n)  до-
   бавили бы k копий c-го камня, то наиболее частым среди  (И)
   них был бы такой же камень, что и для исходного массива

Получаем такую программу:

   k:=0; i:=0
   {(И)}
   while i<>n do begin
   | if k=0 then begin
   | | k:=1; c:=i+1; i:=i+1;
   | end else if i+1-ый камень одинаков с c-ым then begin
   | | i:=i+1; k:=k+1;
   | |  {заменяем материальный камень идеальным}
   | end else begin
   | | i:=i+1; k:=k-1;
   | |  {выкидываем один материальный и один идеальный камень}
   | end;
   end;
   искомым является c-ый камень

Замечание.  Поскольку во всех трех вариантах выбора стоит
команда i:=i+1, ее можно вынести наружу.

     Следующая задача не имеет на первый взгляд никакого отноше-
ния к сортировке.

     4.5.5.  Имеется квадратная таблица a[1..n, 1..n]. Известно,
что для некоторого i строка с номером i заполнена одними нулями,
а столбец с номером i - одними единицами (за исключением их  пе-
ресечения на диагонали, где стоит неизвестно что). Найти такое i
(оно, очевидно, единственно). Число действий не превосходит C*n.
(Заметим, что это существенно меньше числа элементов в таблице).

     Указание. Рассмотрите a[i][j] как результат "сравнения" i с
j  и  вспомните, что самый тяжелый из n камней может быть найден
за n сравнений. (Не забудьте, впрочем, что таблица может не быть
"транзитивной".)
     Глава 5. Конечные автоматы в задачах обработки текстов

     5.1. Составные символы, комментарии и т.п.

     5.1.1.  В  тексте  возведение  в степень обозначалось двумя
идущими подряд звездочками. Решено заменить это  обозначение  на
'^'  (так  что,  к  примеру, 'x**y' заменится на 'x^y'). Как это
проще всего сделать? Исходный текст разрешается читать символ за
символом, получающийся текст требуется печатать символ за симво-
лом.

     Решение. В каждый момент программа  находится  в  одном  из
двух состояний: "основное" и "после звездочки"

Состояние    Очередной        Новое       Действие
           входной символ   состояние

основное        *             после          нет
основное     x <> '*'        основное     печатать x
после           *            основное     печатать '^'
после        x <> '*'        основное     печатать *, x

Замечание.  При  этом '***' заменится на '^*' (но не на '*^'). В
условии задачи мы не оговаривали деталей, как это часто делается
- предполагается, что программа "должна действовать разумно".  В
данном  случае,  пожалуй,  самый  простой  способ объяснить, как
программа действует - это описать ее состояния и действия в них.

     5.1.2. Написать программу, удалающую из текста подслова ви-
да 'abc'.

     5.1.3. В паскале комментарии заключаются в фигурные скобки:

                begin {начало цикла}
                i:=i+1; {увеличиваем i на 1}

Написать программу, которая удаляла бы комментарии  и  вставляла
бы  вместо  исключенного  комментария  пробел  (чтобы '1{один}2'
превратилось бы не в '12', а в '1 2').

     Решение. Программа имеет два состояния: "основное" и "внут-
ри комментария".

Состояние    Очередной        Новое       Действие
           входной символ   состояние

основное        {             внутри         нет
основное     x <> '{'        основное     печатать x
внутри          }            основное     печатать пробел
внутри       x <> '}'         внутри         нет

     Замечание. Эта программа не воспринимает вложенные  коммен-
тарии: строка вроде
       '{{комментарий внутри} комментария}'
превратится в
        '  комментария}'
(в  начале  стоят два пробела). Обработка вложенных комментариев
конечным автоматом невозможна (нужно "помнить число скобок" -  а
произвольное натуральное число не помещается в конечную память).

     5.1.4. В паскалевских программах бывают также строки,  зак-
люченные в кавычки. Если фигурная скобка стречается внутри стро-
ки, то она не означает начала или конца комментария. В свою оче-
редь, кавычка в комментарии не означает начала или конца строки.
Как изменить программу, чтобы это учесть?

     Указание. Состояний будет три: основное,  внутри  коммента-
рия, внутри строки.

     5.1.5. Еще одна возможность многих реализаций паскаля - это
комментарии вида

      i:=i+1;     (*   here i is increased by 1  *)

при этом закрывающая скобка должна  соответствовать  открываюшей
(то  есть  { ... *) не разрешается). Как удалять такие коммента-
рии?

     5.2. Ввод чисел

     Пусть  десятичная  запись  числа подается на вход программы
символ за символом. Мы хотим "прочесть" это число  (поместить  в
переменную типа real его значение). Кроме того, надо сообщить об
ошибке, если число записано неверно.

     Более конкретно, представим себе такую ситуацию. Последова-
тельность  символов на входе делится на прочитанную и оставшуюся
части. Мы можем пользоваться функцией Next:char, которая возвра-
щает первый символ оставшей части, а также функцией Move,  кото-
рая перводит забирает первый символ из оставшейся части, перево-
дя его в категорию прочитанных.

        ---------------------|--------------------------
          прочитанная часть  | Next |  ?  |  ?  |  ?  |
        ---------------------|--------------------------

Будем  называть десятичной записью такую последовательность сим-
волов:

  <0 или более пробелов> <1 или более цифр>

а также такую:

  <0 или более пробелов> <1 или более цифр>.<1 или более цифр>

Заметим, что согласно этому  определению  '1.',  '.1',  '1.  1',
'-1.1' не являются десятичными записями. Сформулируем теперь за-
дачу точно:

     5.2.1. Прочесть из входной строки максимальную часть, кото-
рая  может  быть началом десятичной записи. Определить, является
ли эта часть десятичной записью или нет.

     Решение. Запишем программу на паскале (используя  "перечис-
лимый тип" для наглядности записи: переменная state может прини-
мать одно из значений, указанных в скобках).

    var state:
     (Accept, Error, Initial, IntPart, DecPoint, FracPart);

    state := Initial;
    while (state <> Accept) or (state <> Error) do begin
    | if state = Initial then begin
    | | if Next = ' ' then begin
    | | | state := Initial; Move;
    | | end else if Digit(Next) then begin
    | | | state := IntPart; {после начала целой части}
    | | | Move;
    | | end else begin
    | | | state := Error;
    | | end;
    | end else if state = IntPart then begin
    | | if Digit (Next) then begin
    | | | state := IntPart; Move;
    | | end else if Next = '.' then begin
    | | | state := DecPoint; {после десятичной точки}
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if state = DecPoint then begin
    | | if Digit (Next) then begin
    | | | state := FracPart; Move;
    | | end else begin
    | | | state := Error; {должна быть хоть одна цифра}
    | | end;
    | end else if state = FracPart then begin
    | | if Digit (Next) then begin
    | | | state := FracPart; Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if
    | | {такого  быть не может}
    | end;
    end;

Заметьте,  что присваивания state:=Accept и state:=Error не соп-
ровождаются сдвигом (символ, который не может быть частью числа,
не забирается).

     Приведенная программа не запоминает  значение  прочитанного
числа.

     5.2.2. Решить предыдущую задачу с дополнительным требовани-
ем: если прочитанный кусок является десятичной записью, то в пе-
ременную val:real следует поместить ее значение.

     Решение.  При  чтении дробной части используется переменная
step - множитель при следующей десятичной цифре.

    state := Initial; val:= 0;
    while (state <> Accept) or (state <> Error) do begin
    | if state = Initial then begin
    | | if Next = ' ' then begin
    | | | state := Initial; Move;
    | | end else if Digit(Next) then begin
    | | | state := IntPart; {после начала целой части}
    | | | val := DigitValue (Next);
    | | | Move;
    | | end else begin
    | | | state := Error;
    | | end;
    | end else if state = IntPart then begin
    | | if Digit (Next) then begin
    | | | state := IntPart; val := 10*val + DigitVal(Next);
    | | | Move;
    | | end else if Next = '.' then begin
    | | | state := DecPoint; {после десятичной точки}
    | | | step := 0.1;
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if state = DecPoint then begin
    | | if Digit (Next) then begin
    | | | state := FracPart;
    | | | val := val + DigitVal(Next)*step; step := step/10;
    | | | Move;
    | | end else begin
    | | | state := Error; {должна быть хоть одна цифра}
    | | end;
    | end else if state = FracPart then begin
    | | if Digit (Next) then begin
    | | | state := FracPart;
    | | | val := val + DigitVal(Next)*step; step := step/10;
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if
    | | {такого  быть не может}
    | end;
    end;

     5.2.3. Та же задача, если перед  число  может  стоять  знак
"минус" или знак "плюс" (а может ничего не стоять).

     Формат  чисел  в этой задаче обычно иллюстрируют такой кар-
тинкой:

   -----      ---------
---| + |---->-| цифра |-------->--------------------->
 | -----  | | --------- | |                      |
 | -----  | |           | | -----     ---------  |
 |-| - |--| |----<------| |-| . |->---| цифра |--|
 | -----  |                 -----   | --------- |
 |        |                         |-----<-----|
 |--->----|

     5.2.4.  Та же задача, если к тому же после числа может сто-
ять показатель степени десяти, как  в  254E-4  (=0.0254)  или  в
0.123E+9 (=123000000). Нарисуйте соответствующую картинку.

     5.2.5. Что надо изменить в программе  задачи  5.2.2,  чтобы
разрешить пустые целую и дробную части (как в '1.', '.1' или да-
же '.' - последнее число считаем равным нулю)?

     Мы  вернемся  к  конечным автоматам в главе 10 (Сравнение с
образцом).
     Глава 6. Типы данных.

     6.1. Стеки.

     Пусть Т - некоторый тип. Рассмотрим (отсутствующий в паска-
ле)  тип "стек элементов типа Т". Его значениями являются после-
довательности значений типа Т.

     Операции:

Сделать_пустым (var s: стек элементов типа Т).
Добавить (t: T; var s: стек элементов типа Т).
Взять (var t: T; var s: стек элементов типа Т).
Пуст (s: стек элементов типа Т): boolean
Вершина (s: стек элементов типа Т): T

     (Мы пользуемся обозначениями, наполняющими паскаль, хотя  в
паскале типа "стек" нет.) Процедура "Сделать_пустым" делает стек
s  пустым.  Процедура  "Добавить" добавляет t в конец последова-
тельности  s.  Процедура  "Взять"  определена,  если  последова-
тельность  s непуста; она забирает из неё последний элемент, ко-
торый становится значением переменной t. Выражение "Пуст(s)" ис-
тинно, если последовательность s пуста.  Выражение  "Вершина(s)"
определено, если последовательность s непуста, и равно последне-
му элементу последовательности s.
     Мы  покажем,  как моделировать стек в паскале и для чего он
может быть нужен.

     Моделирование ограниченного стека в массиве.

     Будем считать, что количество элементов в стеке не  превос-
ходит  некоторого  числа  n. Тогда стек можно моделировать с по-
мощью двух переменных:
        Содержание: array [1..n] of T;
        Длина: integer;
считая, что в стеке находятся элементы Содержание [1],...,Содер-
жание [длина].

     Чтобы сделать стек пустым, достаточно положить
        Длина := 0

     Добавить элемент t:
         {Длина < n}
         Длина := Длина+1;
         Содержание [Длина] :=t;

     Взять элемент в переменную t:
         t := Содержание [Длина];
         Длина := Длина - 1;

     Стек пуст, если Длина = 0.

     Вершина стека равна Содержание [Длина].

Таким образом, вместо переменной типа стек в программе на паска-
ле можно использовать две переменные Содержание и  Длина.  Можно
также определить тип стек, записав

    const N = ...
    type  stack = record
                    Содержание: array [1..N] of T;
                    Длина: integer;
                  end;

(Мы позволяем себе  использовать  имена  переменных  из  русских
букв,  хотя  обычно  паскаль  этого не любит.) После этого могут
быть - в соответствии с правилами паскаля  -  описаны  процедуры
работы со стеком. Например, можно написать

        procedure Добавить (t: T; var s: stack);
        begin
        | {s.Длина , N}
        | s.Длина := s.Длина + 1;
        | s.Содержание [s.Длина] := t;
        end;

     Использование стека.

     Будем рассматривать последовательности открывающихся и зак-
рывающихся круглых и квадратных скобок ( ) [ ]. Среди всех таких
последовательностей  выделим правильные - те, которые могут быть
получены по таким правилам:

        1) пустая последовательность правильна.
        2) если А и В правильны, то и АВ правильна.
        3) если А правильна, то [A] и (A) правильны.

     Пример. Последовательности (), [[]], [()[]()][]  правильны,
а последовательности ], )(, (], ([)] - нет.

     6.1.1.  Проверить правильность последовательности за время,
не превосходящее константы, умноженной на её длину.  Предполага-
ется, что члены последовательности закодированы числами:
         (   1
         [   2
         )  -1
         ]  -2

     Решение. Пусть a[1]..a[n] - проверяемая последовательность.
Рассмотрим  стек,  элементами  которого  являются  открывающиеся
круглые и квадратные скобки (т. е. 1 и 2).
     Вначале стек делаем пустым. Далее просматриваем члены  пос-
ледовательности  слева  направо.  Встретив  открывающуюся скобку
(круглую или квадратную), помещаем её в стек. Встретив  закрыва-
ющуюся,  проверяем, что вершина в стеке - парная ей скобка; если
это не так, то можно утверждать, что  последовательность  непра-
вильна,  если  скобка  парная, то заберем её (вершину) из стека.
Последовательность правильна,  если  в  конце  стек  оказывается
пуст.
        Сделать_Пустым (s);
        i := 0; Обнаружена_Ошибка := false;
        {прочитано i символов последовательности}
        while (i < n) and not Обнаружена_Ошибка do begin
        | i := i + 1;
        | if (a[i] = 1) or (a[i] = 2) then begin
        | | Добавить (a[i], s);
        | end else begin  {a[i] равно -1 или -2}
        | | if Пуст (s) then begin
        | | | Обнаружена_Ошибка := true;
        | | end else begin
        | | | Взять (t, s);
        | | | Обнаружена ошибка := (t <> - a[i]);
        | | end;
        | end;
        end;
        Правильно := (not Обнаружена_Ошибка) and Пуст (s);

       Убедимся  в  правильности  программы. (1) Если последова-
тельность построена по правилам, то программа даст  ответ  "да".
Это легко доказать индукцией по построению правильной последова-
тельности.  Надо проверить для пустой, для последовательности AB
в предположении, что для A и B уже проверено - и для  последова-
тельностей [A] и (A) - в предположении, что для A уже проверено.
Для  пустой  очевидно.  Для AB действия программы происходят как
для A и кончаются с пустым стеком; затем все происходит как  для
B.  Для  [A]  сначала  помещается  в стек открывающая квадратная
скобка и затем все идет как для A - с той разницей, что в глуби-
не стека лежит лишняя скобка. По  окончании  A  стек  становится
пустым  - если не считать этой скобки - а затем и совсем пустым.
Аналогично для (A).
     (2) Покажем, что если программа завершает работу с  ответом
"да",  то последовательность правильная. Рассуждаем индукцией по
длине последовательности. Проследим за состоянием стека  в  про-
цессе работы программы. Если он в некоторый промежуточный момент
пуст, то последовательность разбивается на две части, для каждой
из  которых  программа дает ответ "да"; остается воспользоваться
предположением индукции и определением правильности. Пусть  стек
все  время  непуст.  Это значит, что положенная в него на первом
шаге скобка будет вынута на последнем шаге. Тем самым, первый  и
последний символы последовательности - это парные скобки, и пос-
ледовательность имеет вид (A) или [A], а работа программы (кроме
первого  и  последнего  шагов) отличается от ее работы на A лишь
наличием лишней скобки на дне стека (раз ее не вынимают, она ни-
как не влияет на работу программы). Снова ссылаемся на предполо-
жение индукции и определение правильности.

     6.1.2. Как упростится программа, если известно, что в  пос-
ледовательности могут быть только круглые скобки?

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

     6.1.3. Реализовать с помощью одного массива два стека, сум-
марное количество элементов в которых ограничено длиной массива;
все  действия со стеками должны выполняться за время, ограничен-
ное константой, не зависящей от длины стеков.

     Решение. Стеки должны расти с концов массива навстречу друг
другу: первый должен занимать места
        Содержание[1] ... Содержание[Длина1],
а второй  -
        Содержание[n] ... Содержание[n - Длина2 + 1]
(вершины обоих стеков записаны последними).

     6.1.4. Реализовать k стеков с элементами типа T, общее  ко-
личество  элементов в которых не превосходит n, с использованием
массивов суммарной длины C*(n+k), затрачивая на каждое  действие
со  стеками (кроме начальных действий, делающих все стеки пусты-
ми) время, ограниченное некоторой константой.

     Решение. Применяемый метод называется "ссылочной реализаци-
ей". Он использует три массива:
        Содержание: array [1..n] of T;
        Следующий: array [1..n] of 0..n;
        Вершина: array [1..k] of 0..n.
     Массив Содержание будем изображать как n ячеек  с  номерами
1..n,  каждая  из которых содержит элемент типа T. Массив Следу-
ющий изобразим в виде стрелок, проведя стрелку из i  в  j,  если
Следующий[i] = j. (Если Следующий[i] = 0, стрелок из i не прово-
дим.) Содержимое s-го стека (s из 1..k)  хранится  так:  вершина
равна Содержание[Вершина[s]], остальные элементы s-го стека мож-
но  найти,  идя  по стрелкам - до тех пор, пока они не кончатся.
При этом (s-ый стек пуст) <=> Вершина[s] = 0.
     Стрелочные траектории, выходящие из Вершина[1], ..., Верши-
на[k] (из тех, которые не равны 0) не должны пересекаться. Поми-
мо них, нам понадобится еще одна стрелочная траектория, содержа-
щая все неиспользуемые в данный момент ячейки. Ее начало мы  бу-
дем  хранить в переменной Свободная (равенство Свободная = 0 оз-
начает, что пустого места не осталось). Вот что получается:

 n=8 | a | p | q | d | s | t | v | w |

 k=2  |  |  |            Свободная

Содержание = <a,p,q,d,s,t,v,w>, Следующий  =  <3,0,6,0,0,2,5,4>
Вершина = <1, 7>, Свободная = 8
Стеки: 1-ый: p t q a (a-вершина); 2-ой: s v (v-вершина).

  procedure Начать_работу; {Делает все стеки пустыми}
  | var i: integer;
  begin
  | for i := 1 to k do begin
  | | Вершина [i]:=0;
  | end;
  | for i := 1 to n-1 do begin
  | | Следующий [i] := i+1;
  | end;
  | Свободная:=1;
  end;

  function  Есть_место: boolean;
  begin
  | Есть Место := (Свободная <> 0);
  end;

  procedure Добавить (t: T; s: integer);
  | {Добавить t к s-му стеку}
  | var i: 1..n;
  begin
  | {Есть_место}
  | i := Свободная;
  | Свободная := Следующий [i];
  | Вершина [s] :=i;
  | Содержание [i] := t;
  | Следующий [i] := Вершина [s];
  end;

  function Пуст (s: integer): boolean; {s-ый стек пуст}
  begin
  | Пуст := (Вершина [s] = 0);
  end;

  procedure Взять (var t: T; s: integer);
  | {взять из s-го стека в t}
  | var i: 1..n;
  | begin
  | {not Пуст (s)}
  | i := Вершина [s];
  | t := Содержание [i];
  | Вершина [s] := Следующий [i];
  | Следующий [i] := Свободная;
  | Свободная := i;
  end;

     6.2. Очереди.

     Значениями типа "очередь элементов типа T", как и для  сте-
ков, являются последовательности значений типа T. Разница состо-
ит  в том, что берутся элементы не с конца, а с начала (а добав-
ляются по-прежнему в конец).

     Операции с очередями.

        Сделать_пустой (var x: очередь элементов типа T);
        Добавить (t: T, var x: очередь элементов типа T);
        Взять (var t: T, var x: очередь элементов типа T);
        Пуста (x: очередь элементов типа T): boolean;
        Очередной (x: очередь элементов типа T): T.

     При выполнении команды "Добавить" указанный элемент  добав-
ляется  в  конец  очереди.  Команда "Взять" выполнима, лишь если
очередь непуста, и  забирает  из  нее  первый  (положенный  туда
раньше  всех)  элемент, помещая его в t. Значением функции "Оче-
редной" (определенной для непустой очереди) является первый эле-
мент очереди.
     Английские названия стеков - Last In First  Out  (последним
вошел  -  первым вышел), а очередей - First In First Out (первым
вошел - первым вышел).

     Реализация очередей в массиве.

     6.2.1. Реализовать операции с очередью  ограниченной  длины
так,  чтобы количество действий для каждой операции было ограни-
чено константой, не зависящей от длины очереди.

     Решение. Будем хранить элементы очереди в соседних  элемен-
тах  массива.  Тогда  очередь  будет прирастать справа и убывать
слева. Поскольку при этом она может дойти до края, свернем  мас-
сив в окружность.
     Введем массив Содержание: array [0..n-1] of T и переменные
         Первый: 0..n-1,
         Длина : 0..n.
При этом элементами очереди будут
         Содержание [Первый], Содержание [Первый + 1],...,
                   Содержание [Первый + Длина - 1],
где  сложение рассматривается по модулю n. (Предупреждение. Если
вместо этого ввести переменные Первый и  Последний,  принимающие
значения  в  вычетах  по  модулю n, то пустая очередь может быть
спутана с очередью из n элементов.)

     Моделирование операций:

     Сделать Пустой:
        Длина := 0;
        Первый := 0;

     Добавить элемент:
        {Длина < n}
        Содержание [(Первый + Длина) mod n] := элемент;
        Длина := Длина + 1;

     Взять элемент;
        {Длина > 0}
        элемент := Содержание [Первый];
        Первый := (Первый + 1) mod n;
        Длина := Длина - 1;

     Пуста = (Длина = 0);

     Очередной = Содержание [Первый];

     6.2.2.  (Сообщил А.Г.Кушниренко) Придумать способ моделиро-
вания очереди с помощью двух стеков (и фиксированного числа  пе-
ременных  типа T). При этом отработка n операций с очередью (на-
чатых, когда очередь была  пуста)  должна  требовать  порядка  n
действий.

     Решение.  Инвариант:  стеки, составленные концами, образуют
очередь. (Перечисляя элементы одного стека вглубь и  затем  эле-
менты  второго  наружу,  мы  перечисляем все элементы очереди от
первого до последнего.) Ясно, что добавление сводится к добавле-
нию к одному из стеков, а проверка пустоты - к проверке  пустоты
обоих стеков. Если мы хотим взять элемент, есть два случая. Если
стек,  где  находится  начало очереди, не пуст, то берем из него
элемент. Если он пуст, то предварительно переписываем в него все
элементы второго стека, меняя порядок (это происходит  само  при
перекладывании  из стека в стек) и сводим дело к первому случаю.
Хотя число действий при этом и не ограничено константой, но тре-
бование задачи выполнено, так как каждый элемент  очереди  может
участвовать в этом процессе не более одного раза.

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

     6.2.4. (Сообщил А.Г.Кушниренко.) Имеется дек элементов типа
T  и конечное число переменных типа T и целого типа. В начальном
состоянии в деке некоторое число элементов. Составить программу,
после исполнения которой в деке остались бы те же самые  элемен-
ты, а их число было бы в одной из целых переменных.

     Указание.  (1) Элементы дека можно циклически переставлять,
забирая с одного конца и помещая в другой. После  этого,  сделав
столько  же  шагов  в обратном направлении, можно вернуть все на
место. (2) Как понять, прошли мы полный круг или не прошли? Если
бы был какой-то элемент, заведомо отсутствующий в деке, то можно
было бы его подсунуть и ждать  вторичного  появления.  Но  таких
элементов нет. Вместо этого можно для данного n выполнить цикли-
ческий  сдвиг  на  n дважды, подсунув разные элементы, и посмот-
реть, появятся ли разные элементы через n шагов.

     Применение очередей.

     6.2.5. Напечатать в  порядке  возрастания  первые  n  нату-
ральных  чисел, в разложение которых на простые множители входят
только числа 2, 3, 5.

       Решение. Введем три очереди x2, x3, x5, в  которых  будем
хранить элементы, которые в 2 (3, 5) раз больше напечатанных, но
еще не напечатанные. Определим процедуру

        procedure напечатать_и_добавить (t: integer);
        begin
        | writeln (t);
        | добавить (2*t, x2);
        | добавить (3*t, x3);
        | добавить (5*t, x5);
        end;

Вот схема программы:

  напечатать_и_добавить (1);
  k := 1; { k - число напечатанных }
  {инвариант:  напечатано  в  порядке  возрастания k минимальных
  членов нужного множества; в очередях элементы, вдвое, втрое  и
  впятеро  большие напечатанных, но не напечатанные, расположен-
  ные в возрастающем порядке}
  while k <> n do begin
  | x := min (очередной (x2), очередной (x3), очередной (x5));
  | напечатать_и_добавить (x);
  | k := k+1;
  | ...взять x из тех очередей, где он был очередным;
  end;

     Пусть инвариант выполняется. Рассмотрим наименьший из нена-
печатанных элементов множества. Тогда он делится нацело на  одно
из чисел 2, 3, 5, и частное также принадлежит множеству. Значит,
оно  напечатано. Значит, x находится в одной из очередей и, сле-
довательно, является в ней первым (меньшие напечатаны, а элемен-
ты очередей не напечатаны). Напечатав x, мы должны его изъять  и
добавить его кратные.
     Длины очередей не превосходят числа напечатанных элементов.

     Следующая задача связана с графами (к которым мы вернёмся в
главе 9).

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

     6.2.6.  Известно, что ориентированный граф связен, т. е. из
любой вершины можно пройти в любую по  ребрам.  Кроме  того,  из
каждой  вершины  выходит столько же ребер, сколько входит. Дока-
зать, что существует замкнутый цикл, проходящий по каждому ребру
ровно один раз. Составить алгоритм отыскания такого цикла.

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

while змея включает не все ребра do begin
| if из головы выходит неиспользованное в змее ребро then begin
| | удлинить змею этим ребром
| end else begin
| | {хвост змеи в той же вершине, что и голова}
| | отрезать конец хвоста и добавить его к голове
| | {"змея откусывает конец хвоста"}
| end;
end;

     Докажем, что мы достигнем цели.

     1)  Идя по змее от хвоста к голове, мы входим в каждую вер-
шину столько же раз, сколько выходим. Так как  в  любую  вершину
входит столько же ребер, сколько выходит, то невозможность выйти
означает, что мы начали движение в этой же точке.
     2) Змея не укорачивается, поэтому либо она  охватывает  все
ребра,  либо, начиная с некоторого момента, будет иметь постоян-
ную длину. Во втором случае змея будет бесконечно "скользить  по
себе".  Это возможно, только если из всех вершин змеи не выходит
неиспользованных ребер. Из связности следует,  что  использованы
все ребра.

     Замечание  по  реализации на паскале. Вершинами графа будем
считать числа 1..n. Для каждой вершины  i  будем  хранить  число
Out[i]  выходящих  из  нее  ребер, а также номера Num[i][1],...,
Num[i][Out[i]] тех вершин, куда  эти  ребра  ведут.  В  процессе
построения  змеи  будет  выбирать  первое свободное ребро. Тогда
достаточно будет хранить для каждой вершины число  выходящих  из
нее  использованных  ребер  -  это  будут ребра, идущие в начале
списка.

     6.2.7. Доказать, что для всякого  n  существует  последова-
тельность  нулей  и  единиц  длины  (2 в степени n) со следующим
свойством: если "свернуть ее в кольцо" и рассмотреть  все  фраг-
менты  длины  n  (их число равно (2 в степени n)), то мы получим
все возможные последовательности нулей и единиц длины n. Постро-
ить алгоритм отыскания такой  последовательности,  требующий  не
более (C в степени n) действий для некоторой константы C.

     Указание. Рассмотрим граф, вершинами которого являются пос-
ледовательности  нулей  и единиц длины (n-1). Будем считать, что
из вершины x ведет ребро в вершину y, если x может быть началом,
а y - концом некоторой последовательности длины n. Тогда из каж-
дой вершины входит и выходит два ребра. Цикл, проходящий по всем
ребрам, и даст требуемую последовательность.

     6.2.8. Реализовать k очередей с ограниченной суммарной дли-
ной  n,  используя  память  порядка  n+k, причем каждая операция
(кроме начальной, делающей все очереди пустыми) должна требовать
ограниченного константой числа действий.

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

        Содержание: array [1..n] of T;
        Следующий: array [1..n] of 0..n;
        Первый: array [1..n] of 0..n;
        Последний: array [1..k] of 0..n;
        Свободная : 0..n;

  procedure Сделать_пустым;
  | var i: integer;
  begin
  | for i := 1 to n-1 do begin
  | | Следующий [i] := i + 1;
  | end;
  | Свободная := 1;
  | for i := 1 to k do begin
  | | Первый [i]:=0;
  | end;
  end;

  function Есть_место : boolean;
  begin
  | Есть_место := Свободная <> 0;
  end;

  function Пуста (номер_очереди: integer): boolean;
  begin
  | Пуста := Первый [номер_очереди] = 0;
  end;

  procedure Взять (var t: T; номер_очереди: integer);
  | var перв: integer;
  begin
  | {not Пуста (номер_очереди)}
  | перв := Первый [номер_очереди];
  | t := Содержание [перв]
  | Первый [номер_очереди] := Следующий [перв];
  | Следующий [перв] := Свободная;
  | Свободная := Перв;
  end;

  procedure Добавить (t: T; номер_очереди: integer);
  | var нов, посл: 1..n;
  begin
  | {Есть_свободное_место }
  | нов := Свободная; Свободная := Следующий [Свободная];
  | {из списка свободного места изъят номер нов}
  | if Пуста (номер_очереди) then begin
  | | Первый [номер_очереди] := нов;
  | | Последний [номер_очереди] := нов;
  | | Следующий [нов] := 0;
  | | Содержание [нов] := t;
  | end else begin
  | | посл := Последний [номер_очереди];
  | | {Следующий [посл] = 0 }
  | | Следующий [посл] := нов;
  | | Следующий [нов] := 0;
  | | Содержание [нов] := t
  | | Последний [номер_очереди] := нов;
  | end;
  end;

  function Очередной (номер_очереди: integer): T;
  begin
  | Очередной := Содержание [Первый [номер_очереди]];
  end;

     6.2.9. Та же задача для деков вместо очередей.

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

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

     6.2.10.  На плоскости задано n точек, пронумерованных слева
направо (а при равных абсциссах - снизу вверх). Составить  прог-
рамму, которая строит многоугольник, являющийся их выпуклой обо-
лочкой, за не более чем C*n действий.

     Решение. Будем присоединять точки к выпуклой оболочке  одна
за  другой.  Легко  показать, что последняя присоединенная точка
будет одной из вершин выпуклой оболочки. Эту  вершину  мы  будем
называть выделенной. Очередная присоединяемая точка видна из вы-
деленной  (почему?). Дополним наш многоугольник, выпустив из вы-
деленной вершины "иглу", ведущую в присоединяемую  точку.  Полу-
чится  вырожденный многоугольник, и остается ликвидировать в нем
"впуклости".

                                               [Рисунок]

     Будем хранить вершины многоугольника в деке в порядке обхо-
да его периметра по часовой стрелке. При этом выделенная вершина
является началом и концом (головой и хвостом) дека.  Присоедине-
ние  "иглы" теперь состоит в добавлении присоединяемой вершины в
голову и в хвост дека.  Устранение  впуклостей  несколько  более
сложно.  Назовем  подхвостом и подподхвостом элементы дека, сто-
ящие за его хвостом. Устранение впуклости у хвоста делается так:

    while по дороге из хвоста в подподхвост  мы поворачиваем
    |                  у подхвоста влево ("впуклость") do begin
    | выкинуть подхвост из дека
    end

Таким же способом устраняется впуклость у головы дека.

    Замечание. Действия с подхвостом и подподхвостом не входят в
определение дека, однако сводятся к небольшому числу манипуляций
с деком (надо забрать три элемента с хвоста, сделать что надо  и
вернуть).

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

     6.3. Множества.

     Пусть  Т - некоторый тип. Существует много способов хранить
(конечные) множества элементов типа Т; выбор между ними  опреде-
ляется типом T и набором требуемых операций.

     Подмножества множества {1..n}.

     6.3.1.  Используя  память,  пропорциональную   n,   хранить
подмножества множества {1..n}.

          Операции              Число действий

        Сделать пустым                C*n
        Проверить принадлежность      C
        Добавить                      C
        Удалить                       С
        Минимальный элемент           C*n
        Проверка пустоты              C*n

     Решение. Храним множество как array [1..n] of boolean.

     6.3.2.  То  же,  но  проверка пустоты должна выполняться за
время C.

       Решение. Храним дополнительно количество элементов.

     6.3.3. То же при следующих ограничениях на число действий:

          Операции             Число действий

        Сделать пустым                C*n
        Проверить принадлежность      C
        Добавить                      C
        Удалить                       C*n
        Минимальный элемент           C
        Проверка пустоты              C

     Решение.  Дополнительно  храним  минимальный  элемент  мно-
жества.

     6.3.4 То же при следующих ограничениях на число действий:

          Операции             Число действий

        Сделать пустым                С*n
        Проверить принадлежность      С
        Добавить                      С*n
        Удалить                       С
        Минимальный элемент           С
        Проверка пустоты              C

       Решение.  Храним минимальный, а для каждого - следующий и
предыдущий по величине.

     Множества целых чисел.

     В следующих задачах величина элементов множества не ограни-
чена, но их количество не превосходит n.

     6.3.5. Память C*n.

          Операции             Число действий

        Сделать пустым                C
        Число элементов               C
        Проверить принадлежность      C*n
        Добавить новый
         (заведомо отсутствующий)     C
        Удалить                       C*n
        Минимальный элемент           C*n
        Взять какой-то элемент        C

     Решение.   Множество   представляем  с  помощью  переменных
a:array [1..n] of integer, k: 0..n; множество содержит k элемен-
тов a[1],...,a[k]; все они различны. По существу мы храним  эле-
менты множества в стеке (без повторений).

     6.3.6. Память C*n.

          Операции             Число действий

        Сделать пустым                C
        Проверить пустоту             C
        Проверить принадлежность      C*(log n)
        Добавить                      С*n
        Удалить                       C*n
        Минимальный элемент           С

     Решение. См. решение предыдущей задачи с дополнительным ус-
ловием a[1] < ... < a[k]. При проверке принадлежности используем
двоичный поиск.

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

     6.3.7.  Используя описанное в предыдущей задаче представле-
ние множеств, найти все вершины ориентированного графа,  доступ-
ные  из  данной по ребрам. (Вершины считаем числами 1..n.) Время
не больше C * (общее число ребер, выходящих  из  доступных  вер-
шин).

     Решение.  (Другое решение смотри в главе о рекурсии.) Пусть
num[i]  -  число  ребер,  выходящих  из   i,   out[i][1],   ...,
out[i][num[i]] - вершины, куда ведут ребра.

  procedure Доступные (i: integer);
  |   {напечатать все вершины, доступные из i, включая i}
  | var  X: подмножество 1..n;
  |      P: подмножество 1..n;
  |      q, v, w: 1..n;
  |      k: integer;
  begin
  | ...сделать X, P пустыми;
  | writeln (i);
  | ...добавить i к X, P;
  | {(1) P = множество напечатанных вершин; P содержит i;
  |  (2) напечатаны только доступные из i вершины;
  |  (3) X - подмножество P;
  |  (4) все напечатанные вершины, из которых выходит
  |      ребро в ненапечатанную вершину, принадлежат X}
  | while X непусто do begin
  | | ...взять какой-нибудь элемент X в v;
  | | for k := 1 to num [v] do begin
  | | | w := out [v][k];
  | | | if w не принадлежит P then begin
  | | | | writeln (w);
  | | | | добавить w в P;
  | | | | добавить w в X
  | | | end;
  | | end;
  | end;
  end;

     Свойство (1) не нарушается, так как печать  происходит  од-
новременно с добавлением в P. Свойства (2): раз v было в X, то v
доступно,  поэтому  w  доступно. Свойство (3) очевидно. Свойство
(4): мы удалили из X элемент v, но все вершины, куда из  v  идут
ребра, перед этим напечатаны.

     Оценка  времени  работы. Заметим, что изъятые из X элементы
больше туда не добавляются, так как они  в  момент  изъятия  (и,
следовательно, всегда позже) принадлежат P, а добавляются только
элементы  не  из P. Поэтому цикл while выполняется не более, чем
по разу, для всех  доступных  вершин,  а  цикл  for  выполняется
столько раз, сколько из вершины выходит ребер.
     Для  X  надо  использовать представление со стеком или оче-
редью (см. выше), для P - булевский массив.

     6.3.8. Решить предыдущую задачу, если требуется, чтобы дос-
тупные вершины печатались в таком порядке: сначала заданная вер-
шина, потом ее соседи, потом соседи соседей (еще  не  напечатан-
ные) и т.д.

     Указание. Так получится, если использовать очередь в приве-
денном выше решении: докажите индукцией по k, что существует мо-
мент, в который напечатаны все вершины на расстоянии  не  больше
k, а в очереди находятся все вершины, удаленные ровно на k.

Более  сложные  способы представления множеств будут разобраны в
главах 11 (Хеширование) и 12 (Деревья).

     6.4. Разные задачи.

     6.4.1. Реализовать структуру данных, которая имеет  все  те
же операции, что массив длины n, а именно

        начать работу
        положить в i-ю ячейку число n
        узнать, что лежит в i-ой ячейке

а также операцию "указать номер минимального элемента" (или  од-
ного  из  минимальных  элементов).  Количество действий для всех
операций  должно  быть не более C*log n, не считая операции "на-
чать работу" (которая требует не более C*n действий).

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

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

     Решение. Следуя алгоритму сортировки деревом (в его оконча-
тельном  варианте),  будем  размещать элементы очереди в массиве
x[1]..x[k],  поддерживая  такое  свойство:  x[i]  старше  (имеет
больший  приоритет)  своих сыновей x[2i] и x[2i+1], если таковые
существуют - и, следовательно, всякий элемент старше  своих  по-
томков. (Сведения о приоритета также хранятся в массиве, так что
мы  имеем  дело  с  массивом пар (элемент, приоритет).) Удаление
элемента с сохранением этого свойства описано в алгоритме сорти-
ровки. Надо еще уметь восстанавливать свойство после  добавления
элемента в конец. Это делается так:

    t:= номер добавленного элемента
    {инвариант: в дереве любой предок приоритетнее потомка,
        если этот потомок - не t}
    while t - не корень и t старше своего отца do begin
    | поменять t с его отцом
    end;

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

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

     7.1. Примеры рекурсивных программ.

При  анализе  рекурсивной  программы  возникает, как обычно, два
вопроса:

        (а) почему программа заканчивает работу?
        (б) почему она работает правильно, если заканчивает
            работу?

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

     7.1.1. Написать рекурсивную процедуру вычисления факториала
целого  положительного  числа  n  (т.е. произведения чисел 1..n,
обозначаемого n!).

     Решение. Используем равенства 1!=1, n!= (n-1)!*n.

        procedure factorial (n: integer; var fact: integer);
        | {положить fact равным факториалу числа y}
        begin
        | if n=1 then begin
        | | fact:=1;
        | end else begin {n>1}
        | | factorial (n-1, fact);
        | | fact:= fact*n;
        | end;
        end;

С использованием процедур-функций можно написать так:

        function factorial (n: integer): integer;
        begin
        | if n=1 then begin
        | | factorial:=1;
        | end else begin {n>1}
        | | factorial:=  factorial (n-1)*n;
        | end;
        end;

Обратите внимание на некоторую двойственность использования име-
ни factorial внутри описания функции: оно обозначает  как  пере-
менную,  так и вызываемую рекурсивно функцию. К счастью, в нашем
случае они различаются  по  скобкам  после  имени,  но  если  бы
функция  была  без  параметров,  то  дело  было бы плохо. (Стан-
дартная, но трудно находимая ошибка возникает, если автор  пола-
гает, что он использует значение переменной, а компилятор в этом
месте видит рекурсивный вызов.)

    7.1.2.  Обычно  факториал определяют и для нуля, считая, что
0!=1. Измените программы соответственно.

    7.1.3. Напишите рекурсивную программу возведения в целую не-
отрицательную степень.

    7.1.4. То же, если требуется, чтобы глубина рекурсии не пре-
восходила C*log n, где n - степень.

    Решение.

        function power (a,n: integer): integer;
        begin
        | if n = 0 then begin
        | | power:= 1;
        | end else if n mod 2 = 0 then begin
        | | power:= power(a*2, n div 2);
        | end else begin
        | | power:= power(a, n-1)*a;
        | end;
        end;

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

        power:= power(a*2, n div 2)
на
        power:= power(a, n div 2)* power(a, n div 2)?

     Решение. Программа останется правильной. Однако она  станет
работать  медленнее. Дело в том, что теперь вызов может породить
два вызова (хотя и одинаковых) вместо одного - и  число  вызовов
быстро  растет  с глубиной рекурсии. Программа по-прежнему имеет
логарифмическую глубину рекурсии, но число шагов  работы  стано-
вится линейным вместо логарифмического.
     Этот недостаток можно устранить, написав
        t:= power(a, n div 2);
        power:= t*t;
или воспользовавшись функцией возведения в квадрат (sqr).

     7.1.6. Используя лишь команды write(x) при x=0..9, написать
рекурсивную программу печати десятичной  записи  целого  положи-
тельного числа n.

     Решение.  Здесь  использование  рекурсии  облегчает   жизнь
(проблема  была в том, что цифры легче получать с конца, а печа-
тать надо с начала).

     procedure print (n:integer); {n>0}
     begin
     | if n<10 then begin
     | | write (n);
     | end else begin
     | | print (n div 10);
     | | write (n mod 10);
     | end;
     end;

     7.1.7. Игра "Ханойские башни" состоит в следующем. Есть три
стержня.  На  первый из них надета пирамидка из n колец (большие
кольца снизу, меньшие сверху). Требуется переместить  кольца  на
другой  стержень. Разрешается перекладывать кольца со стержня на
стержень,  но класть большее кольцо поверх меньшего нельзя. Сос-
тавить программу, указывающую требуемые действия.

     Решение.  Напишем  рекурсивную  процедуру   перемещения   i
верхних  колец  с  m-го стержня на n-ый (предполагается, что ос-
тальные кольца больше по размеру и лежат на стержнях без  движе-
ния).

    procedure move(i,m,n: integer);
    | var s: integer;
    begin
    | if i = 1 then begin
    | | writeln ('сделать ход', m, '->', n);
    | end else begin
    | | s:=6-m-n; {s - третий стержень: сумма номеров равна 6}
    | | move (i-1, m, s);
    | | writeln ('сделать ход', m, '->', n);
    | | move (i-1, s, n);
    | end;
    end;

(Сначала  переносится  пирамидка из i-1 колец на третью палочку.
После этого i-ое кольцо освобождается, и его можно перенести ку-
да следует. Остается положить на него пирамидку.)

     7.2. Рекурсивная обработка деревьев

     Двоичным деревом называется картинка вроде

                   o
                    \
                     o   o
                      \ /
                   o   o
                    \ /
                     o

Нижняя вершина называется корнем. Из каждой вершины  могут  идти
две  линии: влево вверх и вправо вверх. Вершины, куда они ведут,
называются левым и правым сыновьями  исходной  вершины.  Вершина
может  иметь двух сыновей, а может иметь только одного сына (ле-
вого или правого). Она может и вовсе не иметь сыновей, и в  этом
случае называется листом.
     Пусть x - какая-то вершина двоичного дерева. Она сама вмес-
те  с сыновьями, внуками, правнуками и т.д. образует поддерево с
корнем в x - "поддерево потомков x".

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

        l,r: array [1..N] of integer

и левый и правый сын вершины с номером  i  имеют  соответственно
номера  l[i]  и  r[i].  Если вершина с номером i не имеет левого
(или правого) сына, то l[i] (соответственно r[i]) равно  0.  (По
традиции при записи программ мы используем вместо нуля константу
nil, равную нулю.)

     Здесь N - достаточно большое натуральное число (номера всех
вершин  не  превосходят  N). Отметим, что номер вершины никак не
связан с ее положением в дереве и что не все числа  от  1  до  N
обязаны  быть  номерами вершин (и, следовательно, часть данных в
массивах l и r - это мусор).

    7.2.1. Пусть N=7, root=3, массивы l и r таковы:

         i  |   1  2  3  4  5  6  7
       l[i] |   0  0  1  0  6  0  7
       r[i] |   0  0  5  3  2  0  7

Нарисовать соответствующее дерево.

     Ответ:          6   2
                      \ /
                   1   5
                    \ /
                     3

     7.2.2. Написать программу подсчета числа вершин в дереве.

     Решение. Рассмотрим функцию n(x),  равную  числу  вершин  в
поддереве с корнем в вершине номер x. Считаем, что n(nil)=0 (по-
лагая соответствующее поддерево пустым), и не заботимся о значе-
ниях  nil(s)  для чисел s, не являющихся номерами вершин. Рекур-
сивная программа для s такова:

     function n (x:integer):integer;
     begin
     | if x = nil then begin
     | | n:= 0;
     | end else begin
     | | n:= n(l[x]) + n(r[x]) + 1;
     | end;
     end;

(Число вершин в поддереве над вершиной x равно сумме чисел  вер-
шин  над  ее сыновьями плюс она сама.) Глубина рекурсии конечна,
так  как  с  каждым  шагом  высота  соответствующего   поддерева
уменьшается.

     7.2.3. Написать программу подсчета числа листьев в дереве.

     Ответ.

     function n (x:integer):integer;
     begin
     | if x = nil then begin
     | | n:= 0;
     | end else if (l[x]=nil) and (r[x]=nil) then begin {лист}
     | | n:= 1;
     | end;
     | end else begin
     | | n:= n(l[x]) + n(r[x]);
     | end;
     end;

     7.2.4. Написать программу подсчета  высоты  дерева  (корень
имеет высоту 0, его сыновья - высоту 1, внуки - 2 и т.п.; высота
дерева - это максимум высот его вершин).

     Указание.  Рекурсивно  определяется  функция  f(x) = высота
поддерева с корнем в x.

     7.2.5.  Написать  программу, которая по заданному n считает
число всех вершин высоты n (в заданном дереве).

     Вместо подсчета количества вершин того или иного рода можно
просить напечатать список этих вершин (в том или ином порядке).

     7.2.6. Написать программу, которая печатает (по одному  ра-
зу) все вершины дерева.

     Решение.  Процедура  print_subtree(x)  печатает все вершины
поддерева с корнем в x по одному разу; главная программа  содер-
жит вызов print_subtree(root).

     procedure print_subtree (x:integer);
     begin
     | if x = nil then begin
     | | {ничего не делать}
     | end else begin
     | | writeln (x);
     | | print_subtree (l[x]);
     | | print_subtree (r[x]);
     | end;
     end;

Данная программа печатает сначала корень поддерева, затем подде-
рево над левым сыном, а затем над правым. Три строки в else-час-
ти  могут быть переставлены 6 способами, и каждый из этих спосо-
бов дает свой порядок печати вершин.

     7.3. Порождение комбинаторных объектов, перебор

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

     7.3.1. Написать программу, которая печатает по одному  разу
все  последовательности  длины n, составленные из чисел 1..k (их
количество равно k в степени n).

     Решение. Программа будет оперировать с массивом  a[1]..a[n]
и числом t. Рекурсивная процедура generate печатает все последо-
вательности, начинающиеся на a[1]..a[t]; после  ее  окончания  t
имеет то же значение, что и в начале:

     procedure generate;
     | var i,j : integer;
     begin
     | if t = n then begin
     | | for i:=1 to n do begin
     | | | write(a[i]);
     | | end;
     | | writeln;
     | end else begin {t < n}
     | | for j:=1 to k do begin
     | | | t:=t+1;
     | | | a[t]:=j;
     | | | generate;
     | | | t:=t-1;
     | | end;
     | end;
     end;

Основная программа теперь состоит из двух операторов:
     t:=0; generate;

     7.3.2. Написать программу, которая печатала бы все переста-
новки чисел 1..n по одному разу.

     Решение. Программа оперирует с массивом a[1]..a[n], в кото-
ром  хранится  перестановка  чисел  1..n.  Рекурсивная процедура
generate в такой ситуации печатает все перестановки, которые  на
первых  t позициях совпадают с перестановкой a; по выходе из нее
переменные t и a имеют те же значения, что и до входа.  Основная
программа такова:

    for i:=1 to n do begin a[i]:=i; end;
    t:=0;
    generate;

вот описание процедуры:

     procedure generate;
     | var i,j : integer;
     begin
     | if t = n then begin
     | | for i:=1 to n do begin
     | | | write(a[i]);
     | | end;
     | | writeln;
     | end else begin {t < n}
     | | for j:=t+1 to n do begin
     | | | поменять местами a[t+1] и a[j]
     | | | t:=t+1;
     | | | generate;
     | | | t:=t-1;
     | | | поменять местами a[t+1] и a[j]
     | | end;
     | end;
     end;

     7.3.3. Напечатать все возрастающие последовательности длины
k, элементами которых являются натуральные  числа  от  1  до  n.
(Предполагается, что k не превосходит n - иначе таких последова-
тельностей не существует.)

     Решение. Программа оперирует с массивом a[1]..a[k] и  целой
переменной  t. Предполагая, что a[1]..a[t] - возрастающая после-
довательность чисел натуральных чисел из отрезка 1..n, рекурсив-
но определенная процедура generate печатает все ее  возрастающие
продолжения длины k.

     procedure generate;
     | var i: integer;
     begin
     | if t = k then begin
     | | печатать a[1]..a[k]
     | end else begin
     | | t:=t+1;
     | | for i:=a[t-1]+1 to t-k+n do begin
     | | | a[t]:=i;
     | | | generate;
     | | end;
     | | t:=t-1;
     | end;
     end;

     Замечание. Цикл for мог бы иметь верхней границей n (вместо
t-k+n). Наш вариант экономит часть работы,  учитывая  тот  факт,
что  предпоследний  (k-1-ый)  член  не  может  превосходить n-1,
k-2-ой член не может превосходить n-2 и т.п.
     Основная программа теперь выглядит так:

        t:=1;
        for j:=1 to 1-k+n do begin
        | a[1]:=j;
        | generate;
        end;

Можно было бы добавить к массиву a слева еще и a[0]=0,  положить
t=0 и ограничиться единственным вызовом процедуры generate.

     7.3.4.  Перечислить все представления положительного целого
числа n в виде суммы последовательности невозрастающих целых по-
ложительных слагаемых.

     Решение.  Программа  оперирует  с  массивом a[1..n] (макси-
мальное число слагаемых равно n) и с целой переменной t. Предпо-
лагая, что a[1],...,a[t] - невозрастающая последовательность це-
лых чисел, сумма которых не превосходит  n,  процедура  generate
печатает  все  представления  требуемого  вида, продолжающие эту
последовательность. Для экономии вычислений сумма  a[1]+...+a[t]
хранится в специальной переменной s.

     procedure generate;
     | var i: integer;
     begin
     | if s = n then begin
     | | печатать последовательность a[1]..a[t]
     | end else begin
     | | for i:=1 to min(a[t], n-s) do begin
     | | | t:=t+1;
     | | | a[t]:=i;
     | | | s:=s+i;
     | | | generate;
     | | | s:=s-i;
     | | | t:=t-1;
     | | end;
     | end;
     end;

Основная программа при этом может быть такой:

     t:=1;
     for j:=1 to n do begin
     | a[1]:=j
     | s:=j;
     | generate;
     end;

     Замечание.  Можно немного сэконмить, вынеся операции увели-
чения и уменьшения t из цикла, а также не возвращая s каждый раз
к исходному значению (а увеличивая его на 1 и возвращая к исход-
ному значению в конце). Кроме того,  добавив  фиктивный  элемент
a[0]=n, можно упростить основную программу:

     t:=0; s:=0; a[0]:=n; generate;

     7.3.5.  Написать  рекурсивную  программу обхода дерева (ис-
пользуя те же команды и проверки, что и в главе про обход  дере-
ва).

     Решение.  Процедура  обработать_над обрабатывает все листья
над текущей вершиной и заканчивает работу в той же вершине,  что
и начала. Вот ее рекурсивное описание:

     procedure обработать_над;
     begin
     | if есть_сверху then begin
     | | вверх_налево;
     | | обработать_над;
     | | while есть_справа do begin
     | | | вправо;
     | | | обработать_над;
     | | end;
     | | вниз;
     | end else begin
     | | обработать;
     | end;
     end;

     7.4. Другие применения рекурсии

     Топологическая сортировка. Представим  себе  n  чиновников,
каждый  из  которых  выдает справки определенного вида. Мы хотим
получить все эти справки,  соблюдая  ограничения,  установленные
чиновниками.  Ограничения состоят в том, что у каждого чиновника
есть список справок, которые нужно собрать  перед  обращением  к
нему.  Дело  безнадежно,  если  схема  зависимостей  имеет  цикл
(справку  A  нельзя получить без B, B без C,..., Y без Z и Z без
A). Предполагая, что такого цикла нет, требуется составить план,
указывающий один из возможных порядков получения справок.

     Изображая чиновников точками, а  зависимости  -  стрелками,
приходим  к такой формулировке. Имеется n точек, пронумерованных
от 1 до n. Из каждой точки ведет несколько (возможно, 0) стрелок
в другие точки. (Такая картинка называется ориентированным  гра-
фом.)  Циклов нет. Требуется расположить вершины графа (точки) в
таком порядке, чтобы конец любой стрелки предшествовал ее  нача-
лу. Эта задача называется топологической сортировкой.

     7.4.1. Доказать, что это всегда возможно.

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

     7.4.2.  Предположим,  что  ориентированный  граф без циклов
хранится в такой форме: для каждого i от 1 до n в num[i] хранит-
ся число выходящих из i стрелок, в adr[i][1],..., adr[i][num[i]]
- номера вершин, куда эти стрелки ведут. Составить (рекурсивный)
алгоритм, который производит топологическую сортировку не  более
чем за C*(n+m) действий, где m - число ребер графа (стрелок).

     Замечание.  Непосредственная  реализация  приведенного выше
доказательства существования не дает требуемой оценки; ее прихо-
дится немного подправить.

     Решение. Наша программа будет  печатать  номера  вершин.  В
массиве  printed: array[1..n] of boolean мы будем хранить сведе-
ния о том, какие вершины напечатаны (и корректировать их  однов-
ременно  с  печатью  вершины).  Будем говорить, что напечатанная
последовательность вершин корректна, если никакая вершина не на-
печатана дважды и для любого номера i, входящего в эту последос-
тельность,  все вершины, в которые ведут стрелки из i, напечата-
ны, и притом до i.

     procedure add (i: 1..n);
     | {дано: напечатанное корректно;}
     | {надо: напечатанное корректно и включает вершину i}
     begin
     | if printed [i] then begin {вершина i уже напечатана}
     | | {ничего делать не надо}
     | end else begin
     | | {напечатанное корректно}
     | | for j:=1 to num[i] do begin
     | | | add(adr[i][j]);
     | | end;
     | | {напечатанное корректно, все вершины, в которые из
     | |  i ведут стрелки, уже напечатаны - так что можно
     | |  печатать i, не нарушая корректности}
     | |  if not printed[i] then begin
     | |  | writeln(i); printed [i]:= TRUE;
     | |  end;
     | end;
     end;

Основная программа:

     for i:=1 to n do begin
     | printed[i]:= FALSE;
     end;
     for i:=1 to n do begin
     | add(i)
     end;

     7.4.3.  В  приведенной  программе можно выбросить проверку,
заменив
          if not printed[i] then begin
          | writeln(i); printed [i]:= TRUE;
          end;
на
          writeln(i); printed [i]:= TRUE;
Почему? Как изменится спецификация процедуры?

     Решение.  Спецификацию можно выбрать такой:
       дано: напеватанное корректно
       надо: напечатанное корректно и включает вершину i;
             все вновь напечатанные вершины доступны из i.

     7.4.4. Где использован тот факт, что граф не имеет циклов?

     Решение.  Мы опустили доказательство конечности глубины ре-
курсии. Для каждой вершины  рассмотрим  ее  "глубину"  -  макси-
мальную длину пути по стрелкам, из нее выходящего.  Условие  от-
сутствия циклов гарантирует, что эта величина конечна. Из верши-
ны  нулевой глубины стрелок не выходит. Глубина конца стрелки по
крайней мере на 1 меньше, чем глубина начала. При работе  проце-
дуры  add(i)  все рекурсивные вызовы add(j) относятся к вершинам
меньшей глубины.

     Связная  компонента  графа.  Неориентированный граф - набор
точек (вершин), некоторые из которых соединены  линиями  (ребра-
ми). Неориентированный граф можно считать частным случаем ориен-
тированного графа, в котором для каждой стрелки есть обратная.
     Связной компонентой вершины i называется множество всех тех
вершин, в которые можно попасть из i, идя по ребрам графа. (Пос-
кольку  граф неориентированный, отношение "j принадлежит связной
компоненте i" является отношением эквивалентности.)

     7.4.5. Дан неориентированный граф (для каждой вершины  ука-
зано  число  соседей  и массив номеров соседей, как в предыдущей
задаче). Составить алгоритм, который по заданному i печатает все
вершины связной компоненты i по одному разу (и только их). Число
действий не должно превосходить C*(общее число вершин и ребер  в
связной компоненте).

     Решение.  Программа  в  процессе работы будет "закрашивать"
некоторые вершины графа. Незакрашенной частью графа будем  назы-
вать то, что останется, если выбросить все закрашенные вершины и
ведущие в них ребра. Процедура add(i) закрашивает связную компо-
ненту  i в незакрашенном графе (и не делает ничего, если вершина
i уже закрашена).

     procedure  add (i:1..n);
     begin
     | if вершина i закрашена then begin
     | | ничего делать не надо
     | end else begin
     | | закрасить i (напечатать и пометить как закрашенную)
     | | для всех j, соседних с i
     | | | add(j);
     | | end;
     | end;
     end;

Докажем, что эта процедура действует правильно (в предположении,
что рекурсивные вызовы работают правильно). В самом деле,  ниче-
го, кроме связной компоненты незакрашенного графа, она закрасить
не  может. Проверим, что вся она будет закрашена. Пусть k - вер-
шина, доступная из вершины  i  по  пути  i-j-...-k,  проходящему
только по незакрашенным вершинам. Будем рассматривать только пу-
ти,  не  возвращающиеся  снова  в i. Из всех таких путей выберем
путь с наименьшим j (в порядке просмотра соседей в цикле в  про-
цедуре).  Тогда  при  рассмотрении предыдущих соседей ни одна из
вершин j-...-k не будет закрашена (иначе  j  не  было  бы  мини-
мальным) и потому k окажется в связной компоненте незакрашенного
графа к моменту вызова add(j). Что и требовалось.
     Чтобы установить конечность глубины рекурсии, заметим,  что
на каждом уровне рекурсии число незакрашенных вершин уменьшается
хотя бы на 1.
     Оценим число действий. Каждая вершина закрашивается не  бо-
лее  одного раза - при первым вызове add(i) с данным i. Все пос-
ледующие вызовы происходят при закрашивании соседей - количество
таких  вызовов  не  больше числа соседей - и сводятся к проверке
того, что вершина i уже закрашена. Первый  же  вызов  состоит  в
просмотре  всех  соседей  и  рекурсивных вызовах add(j) для всех
них. Таким образом, общее число действий, связанных  с  вершиной
i, не превосходит константы, умноженной на число ее соседей. От-
сюда и вытекает требуемая оценка.

     7.4.6.  Решить ту же задачу для ориентированного графа (на-
печатать все вершины, доступные из данной по стрелкам; граф  мо-
жет содержать циклы).

     Ответ.  Годится  по  существу  та же программа (строку "для
всех соседей" надо заменить на  "для  всех  вершин,  куда  ведут
стрелки").

     Быстрая сортировка Хоара. В заключение приведем рекурсивный
алгоритм сортировки массива, который на практике является  одним
из  самых быстрых. Пусть дан массив a[1]..a[n]. Рекурсивная про-
цедура  sort (l,r:integer) сортирует участок массива с индексами
из полуинтервала (l,r] (т.е. a[l+1]..a[r]),  не  затрагивая  ос-
тального массива.

     procedure sort (l,r: integer);
     begin
     | if (l = r) then begin
     | | ничего делать не надо - участок пуст
     | end else begin
     | | выбрать случайное число s в полуинтервале (l,r]
     | | b := a[s]
     | | переставить элементы сортируемого участка так, чтобы
     | |   сначала шли элементы, меньшие b - участок (l,ll]
     | |   затем элементы, равные b        - участок (ll,rr]
     | |   затем элементы, большие b       - участок (rr,r]
     | | sort (l,ll);
     | | sort (rr,r);
     | end;
     end;

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

     7.4.7. (Для знакомых с основами теории вероятностей). Дока-
зать, что математическое ожидание числа операций при работе это-
го алгоритма не превосходит C*n*log n, причем константа C не за-
висит от сортируемого массива.

     Указание. Пусть T(n) -  максимум  математического  ожидания
числа  операций для всех входов длины n. Из текста процедуры вы-
текает такое неравенство:

     T(n) <= Cn + 1/n [сумма по всем  k+l=(n-1) чисел T(k)+T(l)]

Первый член соответствует распределению  элементов  на  меньшие,
равные  и большие. Второй член - это среднее математическое ожи-
дание для всех вариантов случайного выбора. (Строго говоря, пос-
кольку среди элементов могут быть равные, в правой части  вместо
T(k) и T(l) должны стоять максимумы T(x) по всем x, не превосхо-
дящим  k или l, но это не мешает дальнейшим рассуждениям.) Далее
индукцией по n нужно доказывать оценку T(n)  <=  C'nlog  n.  При
этом   для   вычисления  среднего  значения  x  log  x  по  всем
x=1,..,n-1 нужно интегрировать x lnx по частям как lnx * d(x*x).
При достаточно большом C' член Cn в правой части  перевешивается
за счет интеграла x*x*d(ln x), и индуктивный шаг проходит.

     7.4.8. Имеется массив из n различных целых чисел a[1]..a[n]
и число k. Требуется найти k-ое по величине число в этом  масси-
ве,  сделав  не более C*n действий, где C - некоторая константа,
не зависящая от k.

     Замечание. Сортировка позволяет очевидным  образом  сделать
это  за  C*n*log(n) действий. Очевидный способ: найти наименьший
элемент, затем найти второй, затем третий,..., k-ый требует  по-
рядка  k*n действий, то есть не годится (константа при n зависит
от k).

      Указание.  Изящный  (хотя  практически  и  бесполезный   -
константы слишком велики) способ сделать это таков:
     А. Разобьем наш массив на n/5 групп, в каждой из которых по
5 элементов. Каждую группу упорядочим.
     Б.  Рассмотрим средние элементы всех групп и перепишем их в
массив из n/5 элементов. С помощью  рекурсивного  вызова  найдем
средний по величине элемент этого массива.
     В.  Сравним этот элемент со всеми элементами исходного мас-
сива: они разделятся на большие его и меньшие его (и один равный
ему). Подсчитав количество тех и других, мы узнаем, в  какой  из
этих  частей  должен находится искомый (k-ый) элемент и каков он
там по порядку.
     Г. Применим рекурсивно наш алгоритм к выбранной части.

     Пусть  T(n)  -  максимально  возможное число действий, если
этот способ применять к массивам из не более чем n элементов  (k
может быть каким угодно). Имеем оценку:
     T(n) <= Cn + T(n/5) + T(примерно 0.7n)
Последнее слагаемое объясняется так: при разбиении на части каж-
дая часть содержит не менее 0.3n элементов. В самом деле, если x
-  средний  из средних, то примерно половина всех средних меньше
x. А если в пятерке средний элемент меньше x, то еще два заведо-
мо меньше x. Тем самым по крайней мере 3/5 от половины элементов
меньше x.
    Теперь  по  индукции можно доказать оценку T(n) <= Cn (реша-
ющую роль при этом играет то обстоятельство, что 1/5 + 0.7 < 1).
        Глава 8. Как обойтись без рекурсии.

     Для универсальных языков программирования (каковым является
паскаль)  рекурсия не дает ничего нового: для всякой рекурсивной
программы можно написать эквивалентную программу  без  рекурсии.
Мы  не будем доказывать этого, а продемонстрируем некоторые при-
емы, позволяющие избавиться от рекурсии в конкретных ситуациях.
     Зачем  это  нужно?  Ответ  прагматика мог бы быть таким: во
многих компьютерах (в том числе, к сожалению, и  в  современных,
использующих  так называемые RISC-процессоры), рекурсивные прог-
раммы в несколько раз  медленнее  соответствующих  нерекурсивных
программ.  Еще один возможный ответ: в некоторых языках програм-
мирования рекурсивные программы запрещены. А главное, при удале-
нии рекурсии возникают изящные и поучительные конструкции.

     8.1. Таблица значений (динамическое программирование)

     8.1.1. Следующая рекурсивная процедура вычисляет числа  со-
четаний  (биномиальные коэффициенты). Написать эквивалентную не-
рекурсивную программу.

        function C(n,k: integer):integer;
        | {n,k >=0; k <=n}
        begin
        | if (k = 0) or (k = n) then begin
        | | C:=1;
        | end else begin {0<k<n}
        | | C:= C(n-1,k-1)+C(n-1,k)
        | end;
        end;

Замечание. C(n,k) - число k-элементных подмножеств n-элементного
множества. Соотношение C(n,k) =  C(n-1,k-1)+C(n-1,k)  получится,
если  мы  фиксируем  некоторый элемент n-элементного множества и
отдельно подсчитаем  k-элементные  множества,  включающие  и  не
включающие этот элемент. Таблица значений C(n,k)

                        1
                      1   1
                    1   2   1
                  1   3   3   1
                .................

называется  треугольником  Паскаля  (того  самого). В нем каждый
элемент, кроме крайних единиц, равен сумме двух стоящих над ним.

     Решение. Можно воспользоваться формулой
        C(n,k) = n! / (k! * (n-k)!)
Мы, однако, не будем этого делать, так как хотим продемонстриро-
вать более общие приемы устранения  рекурсии.  Составим  таблицу
значений  функции  C(n,k), заполняя ее для n = 0, 1, 2,..., пока
не дойдем до интересующего нас элемента.

     8.1.2. Что можно сказать о времени работы рекурсивной и не-
рекурсивной версий в предыдущей задаче? Тот же вопрос о памяти.

     Решение. Таблица занимает место порядка n*n, его можно сок-
ратить до n, если заметить, что для вычисления следующей  строки
треугольника  Паскаля  нужна  только  предыдущая. Время работы в
обоих случаях порядка n*n.  Рекурсивная  программа  требует  су-
щественно большего времени: вызов C(n,k) сводится к двум вызовам
для C(n-1,..), те - к четырем вызовам для C(n-2,..) и т.д. Таким
образом, время оказывается экспоненциальным (порядка 2 в степени
n). Используемая рекурсивной версией память пропорциональна n  -
умножаем глубину рекурсии (n) на количество памяти, используемое
одним экземпляром процедуры (константа).

Кардинальный выигрыш во времени при переходе от рекурсивной вер-
сии к нерекурсивной связан с тем, что в рекурсивном варианте од-
ни  и  те  же  вычисления  происходят много раз. Например, вызов
C(5,3) в конечном счете порождает два вызова C(3,2):

                        C(5,3)
                       /     \
                     C(4,2)  C(4,3)
                    /  \     /   \
                 C(3,1) C(3,2)   C(3,3)
                ......................

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

     8.1.2. Порассуждать на ту же тему на примере рекурсивной  и
(простейшей)  нерекурсивной  программ для вычисления чисел Фибо-
наччи, заданных соотношением
        f(1) = f (2) = 1;  f(n) = f(n-1) + f(n-2) для n > 2.

     8.1.3. Дан выпуклый n-угольник (заданный координатами своих
вершин в порядке обхода). Его разрезают на треугольники диагона-
лями, для чего необходимо n-2 диагонали (докажите  индукцией  по
n). Стоимостью разрезания назовем сумму длин всех использованных
диагоналей.   Найти   минимальную  стоимость  разрезания.  Число
действий должно быть ограничено некоторым многочленом от n. (Пе-
ребор не подходит, так как число вариантов не ограничено многоч-
леном.)

     Решение. Будем считать, что вершины пронумерованы от 1 до n
и  идут  по  часовой стрелке. Пусть k, l - номера вершин, причем
l>k. Через A(k,l) обозначим многоугольник, отрезаемый от  нашего
хордой  k--l.  (Эта  хорда разрезает многоугольник на 2, один из
которых включает сторону 1--n; через A(k,l) мы  обозначаем  дру-
гой.)  Исходный многоугольник естественно обозначить A(1,n). При
l=k+1 получается "двуугольник" с совпадающими сторонами.

Через  a(k,l)  обозначим  стоимость  разрезания   многоугольника
A(k,l) диагоналями на треугольники. Напишем рекуррентную формулу
для  a(k,l).  При  l=k+1  получается  двуугольник, и мы полагаем
a(k,l)=0. При l=k+2 получается треугольник, и в этом случае так-
же a(k,l)=0. Пусть l > k+2. Хорда k--l является стороной  много-
угольника  A(k,l)  и,  следовательно,  стороной  одного  из тре-
угольников,  на  которые он разрезан. Противоположной вершиной i
этого треугольника может быть любая из вершин k+1,...,l-1, и ми-
нимальная стоимость разрезания может быть вычислена как

    min {(длина хорды k--i)+(длина хорды i--l)+a(k,i)+a(i,l)}

по всем i=k+1,..., i=l-1. При этом надо учесть,  что  при  i=k+1
хорда k--i - не хорда, а сторона, и ее длину надо считать равной
0 (по стороне разрез не проводится).

     Составив таблицу для a(k,l) и заполняя ее в порядке возрас-
тания числа вершин (равного l-k+2), мы получаем  программу,  ис-
пользующую память порядка n*n и время порядка n*n*n (однократное
применение  рекуррентной  формулы  требует выбора минимума из не
более чем n чисел).

     8.1.4. Матрицей размера m*n называется прямоугольная табли-
ца из m строк и n столбцов, заполненная числами. Матрицу размера
m*n  можно умножить на матрицу размера n*k (ширина левого сомно-
жителя  должна  равняться  высоте правого), и получается матрица
размером m*k. Ценой такого умножения будем считать  произведение
m*n*k (таково число умножений, которые нужно выполнить при стан-
дартном способе умножения - но сейчас это нам не важно). Умноже-
ние матриц ассоциативно, поэтому произведение n матриц можно вы-
числять в разном порядке. Для каждого порядка подсчитаем суммар-
ную цену всех матричных умножений. Найти минимальную цену вычис-
ления произведения, если известны  размеры  всех  матриц.  Число
действий должно быть ограничено многочленом от числа матриц.

     Пример.  Матрицы  размером  2*3, 3*4, 4*5 можно перемножать
двумя способами. В первом цена равна 2*3*4 + 2*4*5 = 24 +  40  =
64, во втором цена равна 3*4*5 + 2*3*5 = 90.

     Решение.  Представим  себе,  что первая матрица написана на
отрезке [0,1], вторая - на отрезке [1,2],..., s-ая - на  отрезке
[s-1,s]. Матрицы на отрезках [i-1,i] и [i,i+1] имеют общий  раз-
мер, позволяющих их перемножить. Обозначим его через d[i]. Таким
образом, исходным данным в задаче является массив d[0]..d[s].
     Через a(i,j) обозначим минимальную цену вычисления произве-
дения  матриц на участке [i,j] (при 0<=i<j<=s). Искомая величина
равна a(0,s). Величины a(i,i+1) равны нулю (матрица одна  и  пе-
ремножать ничего не надо). Рекуррентная формула будет такой:

    a(i,j) = min {a(i,k)+ a(k,j) + d[i]*d[k]*d[j]}

где  минимум берется по всем возможных местам последнего умноже-
ния, то есть по всем k=i+1..j-1. В самом деле, произведение мат-
риц на отрезке [i,k] есть матрица размера d[i]*d[k],  произведе-
ние  матриц  на отрезке [k,j] имеет размер d[k]*d[j], и цена вы-
числения их произведения равна d[i]*d[k]*d[j].

     Замечание. Две последние задачи похожи. Это сходство станет
яснее, если написать  матрицы  -  множители  на  сторонах  1--2,
2--3,..., s-1--s многоугольника, а на каждой хорде i--j написать
произведение всех матриц, стягиваемых этой хордой.

     8.1.5. Железная дорога с односторонним  движением  имеет  n
станций.  Известны цены белетов от i-ой станции до j-ой (при i <
j - в обратную сторонону проезда нет).  Найти  минимальную  сто-
имость  проезда  от начала до конца (с учетом возможной экономии
за счет пересадок).

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

     8.1.6.  Задано конечное множество с бинарной операцией (во-
обще говоря, не коммутативной и даже не ассоциативной).  Имеется
n  элементов  a[1]..a[n]  этого  множества и еще один элемент x.
Проверить,  можно  ли  так  расставить  скобки  в   произведении
a[1]..a[n],  чтобы  в  результате  получился  x.  Число операций
должно не превосходить C*n*n*n для некоторой константы C  (зави-
сищей от числа элементов в выбранном конечном множестве).

     Решение. Заполняем таблицу, в которой для  каждого  участка
a[i]..a[j]  нашего  произведения  хранится список всех возможных
его значений (при разной расстановке скобок).

     По существу этот же прием применяется в полиномиальном  ал-
горитме   проверки   принадлежности   слова  произвольному  кон-
текстно-свободному языку (см. главу 13).

     Следующая задача (задача о рюкзаке) уже упоминалась в главе
3 (Обход дерева).

     8.1.7.  Имеется  n  положительных  целых чисел x[1]..x[n] и
число N. Выяснить, можно ли получить N, складывая  некоторые  из
чисел x[1]..x[n]. Число действий должно быть порядка N*n.
     Указание. После i шагов хранится множество тех чисел на от-
реке   0..N,  которые  предствимы  в  виде  суммы  некоторых  из
x[1]..x[i].

     8.2. Стек отложенных заданий.

     Другой прием устранения рекурсии продемонстрируем на приме-
ре задачи о ханойских башнях.

     8.2.1. Написать нерекурсивную программу для нахождения пос-
ледовательности перемещений дисков в задаче о ханойских башнях.

     Решение. Вспомним рекурсивную программу:

    procedure move(i,m,n: integer);
    | var s: integer;
    begin
    | if i = 1 then begin
    | | writeln ('сделать ход', m, '->', n);
    | end else begin
    | | s:=6-m-n; {s - третий стержень: сумма номеров равна 6}
    | | move (i-1, m, s);
    | | writeln ('сделать ход', m, '->', n);
    | | move (i-1, s, n);
    | end;
    end;

Видно, что задача "переложить i верхних дисков с m-го стержня на
n-ый"  сводится  к трем задачам того же типа: двум задачам с i-1
дисками и к одной задаче с единственным диском. Выполняя эти за-
дачи, важно не позабыть, что еще осталось сделать.

     Для этой цели заведем стек отложенных  заданий,  элементами
которого будут тройки <i,m,n>. Каждая такая тройка интерпретиру-
ется  как  заказ  "переложить i верхних дисков с m-го стержня на
n-ый". Заказы упорядочены в соответствии с требуемым порядком их
выполнения: самый срочный - вершина стека. Получам  такую  прог-
рамму:

    procedure move(i,m,n: integer);
    begin
    | сделать стек заказов пустым
    | положить в стек тройку <i,m,n>
    | {инвариант: осталось выполнить заказы в стеке}
    | while стек непуст do begin
    | | удалить верхний элемент, переложив его в <j,p,q>
    | | if j = 1 then begin
    | | | writeln ('сделать ход', p, '->', q);
    | | end else begin
    | | | s:=6-p-q;
    | | |      {s - третий стержень: сумма номеров равна 6}
    | | | положить в стек тройки <j-1,s,q>, <1,p,q>, <j-1,p,s>
    | | end;
    | end;
    end;

(Заметим,  что  сначала в стек кладется тройка, которую надо вы-
полнять последней.) Стек троек может быть  реализован  как  стри
отдельных  стека.  (Кроме  того, в паскале есть специальный тип,
называемый "запись", который может быть применен.)

     8.2.2. (Сообщил А.К.Звонкин со ссылкой на Анджея  Лисовско-
го.)  Для  задачи  о ханойских башнях есть и другие нерекусивные
алгоритмы. Вот один из них: простаивающим стержнем  (не  тем,  с
которого  переносят, и не тем, на который переносят) должны быть
все стержни по очереди. Другое  правило:  поочередно  перемещать
наименьшее кольцо и не наименьшее кольцо, причем наименьшее - по
кругу.

     8.2.3. Использовать замену рекурсии стеком отложенных зада-
ний в рекурсивной программе печати десятичной записи целого чис-
ла.

     Решение. Цифры добываются с конца и закладываются в стек, а
затем печатаются в обратном порядке.

     8.2.4. Написать  нерекурсивную  программу,  печатающую  все
вершины двоичного дерева.

     Решение. В этом случае стек отложенных заданий будет содер-
жать  заказы двух сортов: заказ напечатать (в свое время) данную
вершину и заказ напечатать все вершины поддерева с данным корнем
(при этом nil считается корнем пустого дерева).  Таким  образом,
элемент стека есть пара: <тип заказа, номер вершины>.
     Вынимая элемент из стека, мы либо сразу исполняем его (если
это  заказ первого типа) либо помещаем в стек три порожденных им
заказа - в одном из шести возможных порядков.

     8.2.5. Что изменится, если требуется  не  печатать  вершины
двоичного дерева, а подсчитать их количество?

     Решение.  Печатание  вершины  следует заменить прибавлением
единицы к счетчику. Другими  словами,  инвариант  таков:  (общее
число  вершин)  = (счетчик) + (сумма чисел вершин в поддеревьях,
корни которых лежат в стеке).

     8.2.6. Для некоторых из шести возможных  порядков  возможны
упрощения, делающие ненужным хранение в стеке элементов двух ви-
дов. Указать некоторые из них.

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

     Замечание. Другую программу печати всех вершин дерева можно
построить на основе программы обхода дерева, разобранной в соот-
ветствующей  главе.  Там  используется команда "вниз". Поскольку
теперешнее представление дерева с помощью массивов l и r не поз-
воляет  найти  предка  заданной вершины, придется хранить список
всех вершин на пути от корня к  текущей  вершине.  Cмотри  также
главу об алгоритмах на графах.

     8.2.7.  Написать  нерекурсивный  вариант  программы быстрой
сортировки. Как обойтись  стеком,  глубина  которого  ограничена
C*log n, где n - число сортируемых элементов?

     Решение.  В  стек кладутся пары <i,j>, интерпретируемые как
отложенные задания на сортировку соответствующих участков масси-
ва. Все эти заказы не пересекаются, поэтому размер стека не  мо-
жет  превысить n. Чтобы ограничиться стеком логарифмической глу-
бины, будем придерживаться такого правила: глубже в  стек  поме-
щать больший из возникающих двух заказов. Пусть  f(n)  -  макси-
мальная  глубина стека, которая может встретиться при сортировке
массива из не более чем n элементов таким способом. Оценим  f(n)
сверху таким способом: после разбиения массива на два участка мы
сначала сортируем более короткий (храня в стеке про запас) более
длинный, при этом глубина стека не больше f(n/2)+1, затем сорти-
руем более длинный, так что

      f(n) <= max (f(n/2)+1, f(n-1)),

откуда очевидной индукцией получаем f(n) = O(log n).

     8.3. Более сложные случаи рекурсии.

     Пусть функция f с натуральными аргументами и значениями оп-
ределена рекурсивно условиями
        f(0) = a,
        f(x) = h(x, f(l(x))),
где a - некоторое число, а h и l -  известные  функции.  Другими
словами,  значение функции f в точке x выражается через значение
f в точке l(x). При этом предполагается, что для любого x в пос-
ледовательности
        x, l(x), l(l(x)),...
рано или поздно встретится 0.
     Если  дополнительно  известно,  что l(x) < x для всех x, то
вычисление f не представляет  труда:  вычисляем  последовательно
f(0), f(1), f(2),...

     8.3.1.  Написать  нерекурсивную  программу вычисления f для
общего случая.

     Решение. Для вычисления f(x) вычисляем последовательность
        l(x), l(l(x)), l(l(l(x))),...
до появления нуля и запоминаем ее, а затем вычисляем значения  f
в точках этой последовательности, идя справа налево.

     Еще более сложный случай из следующей задачи вряд ли встре-
тится  на  практике  (а  если  и встретися, то проще рекурсию не
устранять, а оставить). Но тем не менее: пусть функция f с нату-
ральными аргументами и значениями определяется соотношениями
        f(0) = a,
        f(x) = h(x, f(l(x)), f(r(x))),
где a - некоторое число, а l, r и h - известные функции. Предпо-
лагается, что если взять произвольное число и начать применять к
нему функции l и r в произвольном порядке, то  рано  или  поздно
получится 0.

     8.3.2. Написать нерекурсивную программу вычисления f.

     Решение. Можно было бы сначала построить дерево, у которого
в корне находится x, а в сыновьях вершины i стоят l(i) и r(i)  -
если только i не равно нулю, а затем вычислять значения функции,
идя от листьев к корню. Однако есть и другой способ.

     "Обратной польской записью" (или "постфиксной записью") вы-
ражения  называют  запись,  где знак функции стоит после всех ее
аргументов, а скобки не используются. Вот несколько примеров:

          f(2)                  2 f
          f(g(2))               2 g f
          s(2,t(7))             2 7 t s
          s(2, u(2, s(5,3))     2 2 5 3 s u s

Постфиксная  запись  выражения  позволяет удобно вычислять его с
помощью "стекового калькулятора". Этот калькулятор  имеет  стек,
который  мы  будем представлять себе расположенным горизонтально
(числа вынимаются и кладутся справа). При нажатии на  клавишу  с
числом  это число кладется в стек. При нажатии на функциональную
клавишу соответствующая функция применяется к  нескольким  аргу-
ментам у вершины стека. Например, если в стеке были числа
        2 3 4 5 6
и  нажата  функциональная клавиша s, соотвтетствующая функции от
двух аргументов, то в стеке окажутся числа
        2 3 4 s(5,6)

Перейдем теперь к нашей задаче. В процессе  вычисления  значения
функции  f мы будем работать со стеком чисел, а также с последо-
вательностью чисел и символов "f", "l", "r", "h", которую мы бу-
дем интерпретировать как последовательность  нажатий  кнопок  на
стековом калькуляторе.  Инвариант такой:

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

Пусть нам требуется вычислить значение, к примеру, f(100). Тогда
вначале мы помещаем в стек число 100, а  последовательность  со-
держит  единственный  символ "f". (При этом инвариант соблюдает-
ся.) Далее с последовательностью и стеком выполняются такие пре-
образования:

 старый       старая           новый       новая
 стек      последовательность  стек    последовательность

  X          x P               X x           P
  X x        l P               X l(x)        P
  X x        r P               X r(x)        P
  X x y z    h P               X h(x,y,z)    P
  X 0        f P               X a           P
  X x        f P               X             x x l f x r f h P

Обозначения: x, y, z,.. - числа, X - последовательность чисел, P
- последовательность чисел и символов "f", "l", "r", "h". В пос-
ледней строке предполагается, что m не равно 0. Эта строка соот-
ветствует равенству

        f(x) = h(x, f(l(x)), f(r(x))),

Эти  преобразования выполняются, пока последовательность не ста-
нет пуста. В этот момент в стеке  окажется  единственное  число,
которое и будет ответом.

     Замечание.  Последовательность по существу представляет со-
бой стек отложенных заданий (вершина которого находится слева).
     Глава 9. Разные алгоритмы на графах

     9.1. Кратчайшие пути

     В этом разделе рассматриваются различные варианты одной за-
дач. Пусть имеется n городов, пронумерованных числами от 1 до n.
Для каждой пары городов с номерами i, j в таблице  a[i][j]  хра-
нится  целое число - цена прямого авиабилета из города i в город
j. Считается, что рейсы существуют между любыми городами, a[i,i]
= 0 при всех i, a[i][j] может отличаться от  a[j,i].  Наименьшей
стоимостью проезда из i в j считается минимально возможная сумма
цен  билетов  для маршрутов (в том числе с пересадками), ведущих
из i в j. (Она не превосходит a[i][j], но может быть меньше.)

     В предлагаемых ниже задачах требуется найти наименьшую сто-
имость проезда для некоторых пар городов при тех или иных  огра-
ничениях на массив a и на время работы алгоритма.

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

     Решение. Маршрут длиной больше n всегда содержит цикл,  по-
этому минимум можно искать среди маршрутов длиной не более n,  а
их конечное число.

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

     9.1.2. Найти наименьшую стоимость проезда из 1-го города во
все остальные за время O(n в степени 3).

     Решение. Обозначим через МинСт(1,s,к) наименьшую  стоимость
проезда из 1 в s менее чем с k  пересадками.  Тогда  выполняется
такое соотношение:

   МинСт (1,s,k+1) = наименьшему из чисел МинСт(1,s,k) и
                     МинСт(1,i,k) + a[i][s] (i=1..n)

Как отмечалось выше, искомым ответом является  МинСт(1,i,n)  для
всех i=1..n.

     k:= 1;
     for i := 1 to n do begin x[i] := a[1][i]; end;
     {инвариант: x[i] := МинСт(1,i,k)}
     while k <> n do begin
     | for s := 1 to n do begin
     | | y[s] := x[s];
     | | for i := 1 to n do begin
     | | | if y[s] > x[i]+a[i][s] then begin
     | | | | y[s] := x[i]+a[i][s];
     | | | end;
     | | end
     | | {y[s] = МинСт(1,s,k+1)}
     | | for i := 1 to n do begin x[s] := y[s]; end;
     | end;
     | k := k + 1;
     end;

Приведенный  алгоритм называют алгоритмом динамического програм-
мирования, или алгоритмом Форда - Беллмана.

     9.1.3. Доказать, что программа останется  правильной,  если
не заводить массива y, а производить изменения в самом массиве x
(заменив в программе все вхождения буквы y на x и затем  удалить
ставшие лишними строки).

     Решение. Инвариант будет таков:
     МинСт(1,i,n) <= x[i] <= MинСт(1,i,k)

     Этот алгоритм может быть улучшен в двух  отношениях:  можно
за то же время O(n в степени 3) найти наименьшую стоимость  про-
езда i->j для ВСЕХ пар i,j (а не только с i=1), а  можно  сокра-
тить время работы до O(n в степени 2). Правда, в последнем  слу-
чае нам потребуется, чтобы все цены a[i][j] были неотрицательны.

     9.1.4. Найти наименьшую стоимость проезда i->j для всех i,j
за время O(n в степени 3).

     Решение. Для k = 0..n через А(i,j,k)  обозначим  наименьшую
стоимость маршрута из i в j, если в качестве пересадочных разре-
шено использовать только пункты с номерами не больше k. Тогда

     A(i,j,0) = a[i][j],
а
     A(i,j,k+1) = min (A(i,j,k), A(i,k+1,k)+A(k+1,j,k))

(два  варианта  соответствуют  неиспользованию  и  использованию
пункта k+1 в качестве пересадочного; отметим, что в нем  незачем
бывать более одного раза).
     Этот алгоритм называют алгоритмом Флойда.

     9.1.5.  Известны,  что  все  цены неотрицательны. Найти на-
именьшую стоимость проезда 1->i для всех i=1..n за время  O(n  в
степени 2).

     Решение. В процессе работы алгоритма некоторые города будут
выделенными (в начале - только город 1,  в  конце  -  все).  При
этом:

     для каждого выделенного города i хранится  наименьшая  сто-
имость пути 1->i; при этом известно, что минимум достигается  на
пути, проходящем только через выделенные города;
     для каждого невыделенного города i хранится наименьшая сто-
имость пути 1->i, в котором в качестве промежуточных используют-
ся только выделенные города.

     Множество  выделенных городов расширяется на основании сле-
дующего замечания: если среди всех  невыделенных  городов  взять
тот,  для которого хранимое число минимально, то это число явля-
ется истинной наименьшей стоимостью. В самом  деле,  пусть  есть
более  короткий  путь.  Рассмотрим  первый невыделенный город на
этом пути - уже до него путь длиннее! (Здесь существенна неотри-
цательность цен.)
     Добавив выбранный город к выделенным, мы должны  скорректи-
ровать информацию, хранимую для невыделенных городов.  При  этом
достаточно учесть лишь пути, в которых новый город является пос-
ледним пунктом пересадки, а это легко сделать, так как минималь-
ную стоимость проезда в новый город мы уже знаем.
     При самом бесхитростном способе хранения множества выделен-
ных городов (в булевском векторе)  добавление  одного  города  к
числу выделенных требует времени O(n).
     Этот алгоритм называют алгоритмом Дейкстры.

     Отыскании кратчайшего пути имеет естественную интерпретацию
в терминах матриц. Пусть A - матрица цен одной аваиакомпании,  а
B  -  матрица цен другой. (Мы считаем, что диагональные элементы
матриц равны 0.) Пусть мы хотим лететь с одной пересадкой,  при-
чем  сначала самолетом компании A, а затем - компании B. Сколько
нам придется заплатить, чтобы попасть из города i в город j?

     9.1.6. Доказать, что эта  матрица  вычисляется  по  обычной
формуле  для произведения матриц, только вместо суммы надо брать
минимум, а вместо умножения - сумму.

     9.1.7. Доказать, что таким образом определенное  произведе-
ние матриц ассоциативно.

     9.1.8. Доказать, что задача о кратчайших путях эквивалентна
вычислению "бесконечной степени" матрицы  цен  A:  в  последова-
тельности  A, A*A, A*A*A,... все элементы, начиная с некоторого,
равны искомой матрице стоимостей кратчайших путей. (Если нет от-
рицательных циклов!)

     9.1.9.  Начиная  с  какого элемента можно гарантировать ра-
венство в предыдущей задаче?

     Обычное  (не  модифицированное) умножение матриц тоже может
оказаться полезным, только матрицы  должны  быть  другие.  Пусть
есть не все рейсы (как в следующем разделе), а только некоторые,
a[i,j]  равно  1,  если рейс есть, и 0, если рейса нет. Возведем
матрицу a (обычным образом) в степень k и посмотрим на ее i-j-ый
элемент.

     9.1.10. Чему он равен?

     Ответ. Числу различных способов попасть  из  i  в  j  за  k
рейсов.

     Случай,  когда есть не все рейсы, можно свести к исходному,
введя фиктивные  рейсы  с  бесконечно  большой  (или  достаточно
большой)  стоимостью. Тем не менее возникает такой вопрос. Число
реальных рейсов может быть существенно меньше n*n, поэтому инте-
ресны алгоритмы, которые работают эффективно в  такой  ситуации.
Исходные  данные  естественно  представлять тогда в такой форме:
для каждого города известно число выходящих из него  рейсов,  их
пункты назначения и цены.

     9.1.11.  Доказать,  что алгоритм Дейкстры можно модифициро-
вать так, чтобы для n городов и k маршрутов он требовал не более
C*(n+k log n) операций.
     Указание. Что надо сделать на каждом шаге? Выбрать  невыде-
ленный город с минимальной стоимостью и скорректировать цены для
всех  городов,  в  которые из него есть маршруты. Если бы кто-то
сообщал нам, для какого города стоимость минимальна, то  хватило
бы C*(n+k) действий. А поддержание сведений о том, какой элемент
в  массиве  минимален  (см. задачу 6.4.1 в главе о типах данных)
обходится еще в множитель log n.

     9.2. Связные компоненты, поиск в глубину и ширину

     Наиболее простой случай задачи о кратчайших  путях  -  если
все цены равны 0 или бесконечны. Другими словами, мы интересуем-
ся  возможностью попасть из i в j, но за ценой не постоим (и она
нас не интересует). В других терминах: мы имеем  ориентированный
граф (картинку из точек, некоторые из которых соединены стрелка-
ми) и нас интересуют вершины, доступные из данной.

     Для  этого  случая  задачи о кратчайших путях приведенные в
предыдущем разделе алгоритмы - не наилучшие. В самом деле, более
быстрая  рекурсивная  программа  решения этой задачи приведена в
главе 7 (Рекурсия), а нерекурсивная - в главе 6  (Типы  данных).
Сейчас  нас  интересует  такая задача: не просто перечислить все
вершины, доступные из данной, но перечислить их  в  определенном
порядке. Два популярных случая - поиск в ширину и в глубину.

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

     9.2.1.  Придумать  алгоритм  решения  этой  задачи с числом
действий не более C*(число ребер, выходящих из интересующих  нас
вершин).

     Решение.  Эта  задача  рассматривалась в главе 6 (Типы дан-
ных), 6.3.7 - 6.3.8. Здесь мы приведём подробное решение.  Пусть
num[i]  -  количество  ребер,  выходящих  из  i,  out[i][1],...,
out[i][num[i]] - вершины, куда ведут ребра. Вот программа,  при-
ведённая ранее:

  procedure Доступные (i: integer);
  |   {напечатать все вершины, доступные из i, включая i}
  | var  X: подмножество 1..n;
  |      P: подмножество 1..n;
  |      q, v, w: 1..n;
  |      k: integer;
  begin
  | ...сделать X, P пустыми;
  | writeln (i);
  | ...добавить i к X, P;
  | {(1) P = множество напечатанных вершин; P содержит i;
  |  (2) напечатаны только доступные из i вершины;
  |  (3) X - подмножество P;
  |  (4) все напечатанные вершины, из которых выходит
  |      ребро в ненапечатанную вершину, принадлежат X}
  | while X непусто do begin
  | | ...взять какой-нибудь элемент X в v;
  | | for k := 1 to num [v] do begin
  | | | w := out [v][k];
  | | | if w не принадлежит P then begin
  | | | | writeln (w);
  | | | | добавить w в P;
  | | | | добавить w в X
  | | | end;
  | | end;
  | end;
  end;

Тогда нам было безразлично, какой именно элемент множества X вы-
бирается. Если мы будем считать X очередью (первым пришел - пер-
мым ушел), то эта программа напечатает все вершины, доступные из
i, в порядке возрастания их расстояния  от  i  (числа  ребер  на
кратчайшем пути из i). Докажем это.

     Обозначим  через V(k) множество всех вершин, расстояние ко-
торых от i (в описанном смысле) равно k. Имеет место такое соот-
ношение:

 V(k+1) = (концы ребер с началами в V(k))-V(0)-V(1)-...-V(k)

(знак "-" обозначает вычитание множеств). Докажем, что для любо-
го k=0,1,2... в ходе работы программы будет такой момент  (после
очередной итерации цикла while), когда

     в очереди стоят все элементы V(k) и только они
     напечатаны все элементы V(1),...,V(k)

(Для  k=0  - это состояние перед циклом.) Рассуждая по индукции,
предположим, что в очереди скопились все элементы V(k). Они  бу-
дут  просматривать  в  цикле,  пока не кончатся (поскольку новые
элементы добавляются в конец, они не перемешаются  со  старыми).
Концы  ведущих из них ребер, если они уже не напечатаны, печата-
ются и ставятся в очередь - то есть всё как  в  записанном  выше
соотношении для V(k+1). Так что когда все старые  элементы  кон-
чатся, в очереди будут стоять все элементы V(k+1).

     Поиск в глубину.

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

     Будем предполагать, что для каждой вершины графа  выходящие
из  нее  ребра  упорядочены (напрмиер, пронумерованы). Тем самым
для каждой вершины дерева выходящие из нее ребра также  упорядо-
чены.  Будем обходить дерево так: сначала корень, а потом подде-
ревья (в порядке  ведущих  в  них  ребер).  Такой  обход  дерева
рассматривался  нами в главе о рекурсии. Ему соответствует обход
графа. Если выкинуть из этого обхода повторные посещения уже по-
сещенных вершин, то получится то, что называется "поиск в глуби-
ну".

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

     9.2.2. Написать программу поиска в глубину.
     Указание.  Возьмем  программу обхода дерева (корень - левое
поддерево - правое поддерево) из главы о рекурсии или  из  гравы
об  устранении  рекурсии  и используем ее применительно к обсто-
ятельствам. Главное изменение: не надо посещать вершины  повтор-
но.  Так что если мы попали в уже посещенную вершину, то можно с
ней ничего не делать. (Если путь не минимален  среди  ведущих  в
данную  вершину,  то  и  все  его продолжения не минимальны - их
просматривать не надо).

     Замечание. Существуют две возможности устранения рекурсии в
программе обхода дерева. Можно хранить в стеке корни  подлежащих
посещению  поддеревьев  (как  это делалось в главе об устранении
рекурсии). А можно применять метод из главы об обходе дерева, то
есть реализовать операции  "вверх_налево",  "вправо"  и  "вниз".
Чтобы их реализовать, необходимо хранить в стеке путь из корня к
текущей  вершине. Оба способа - примерно одинаковой сложности, и
в конкретной ситуации любой из них может оказаться  более  удоб-
ным.

     Поиск в глубину лежит в основе многих алгоритмов на графах,
порой в несколько модифицированном виде.

      9.2.3. Неориентированный граф называется двудольным,  если
его  можно  раскрасить в два цвета так, что концы любого ребра -
разного цвета. Составить алгоритм проверки, является ли заданный
граф двудольным (число действий не провосходит C*(число ребер  +
число вершин).

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

     Замечание.  В  этой задаче безразлично, производить поиск в
ширину или в глубину.

     9.2.4. Составить нерекурсивный алгоритм топологической сор-
тировки  ориентированного  графа без циклов. (См. задачу 7.4.2 в
главе о рекурсии.)

     Решение.  Предположим,  что  граф  имеет вершины с номерами
1..n, для каждой вершины i известно число  num[i]  выходящих  из
нее ребер и номера вершин dest[i][1],..., dest[i][num[i]], в ко-
торые эти ребра ведут. Будем условно считать, что ребра перечис-
лены "слева направо": левее то ребро, у которого  номер  меньше.
Нам надо напечатать все вершины в таком порядке, чтобы конец лю-
бого ребра был напечатан перед его началом. Мы предполагаем, что
в графе нет ориентированных циклов - иначе такое невозможно.
      Для начала добавим к графу вершину 0, из которой ребра ве-
дут в вершины 1,...,n. Если ее удастся напечатать с  соблюдением
правил, то тем самым все вершины будут напечатаны.

      Алгоритм  хранит путь, выходящий из нулевой вершины и иду-
щий по ребрам графа. Переменная l отводится для длины этого  пу-
ти.  Путь  образован  вершинами  vert[1],..., vert[l] и ребрами,
имеющими номера edge[1]...edge[l]. Номер edge[s] относится к ну-
мерации ребер, выходящих из вершины vert[s]. Тем самым для  всех
s должны выполняться неравенство
        edge[s] <= num[vert[s]]
и равенство
        vert[s+1] = dest [vert[s]] [edge[s]]
Впрочем,  для  последнего  ребра мы сделаем исключение, разрешив
ему указывать "в пустоту",  т.е.  разрешим
edge[l] равняться num[vert[l]]+1.

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

(И)     вершины  пути, кроме последней (т.е. vert[1]..vert[l])
        не напечатаны, но свернув с пути налево, мы немедленно
        упираемся в напечатанную вершину

Вот что получается:

        l:=1; vert[1]:=0; edge[1]:=1;
        while not( (l=1) and (edge[1]=n+1)) do begin
        | if edge[l]=num[vert[l]]+1 then begin
        | | {путь кончается в пустоте, поэтому все вершины,
        | |     следующие за vert[l], напечатаны - можно
        | |     печатать vert[l]}
        | | writeln (vert[l]);
        | | l:=l-1; edge[l]:=egde[l]+1;
        | end else begin
        | |  {edge[l] <= num[vert[l]], путь кончается в
        | |     вершине}
        | |  lastvert:= dest[vert[l]][edge[l]]; {последняя}
        | |  if lastvert напечатана then begin
        | |  | edge[l]:=edge[l]+1;
        | |  end else begin
        | |  | l:=l+1; vert[l]:=lastvert; edge[l]:=1;
        | |  end;
        | end;
        end;
        {путь сразу же ведет в пустоту, поэтому все вершины
         левее, то есть 1..n, напечатаны}

     9.2.4. Доказать, что если в графе нет циклов, то этот алго-
ритм заканчивает работу.

     Решение. Пусть это не так. Каждая вершина может  печататься
только  один раз, тако что с некоторого момента вершины не печа-
таются. В графе без циклов длина пути ограничена (вершина не мо-
жет входить дважды), поэтому подождав еще,  мы  можем  дождаться
момента,  после  которого  путь не удлиняется. После этого может
разве что увеличиваться edge[l] - но и это не беспредельно.
     Глава 10. Сопоставление с образцом.

     10.1. Простейший пример.

     10.1.1. Имеется последовательность символов x[1]..x[n]. Оп-
ределить, имеются ли в ней идущие друг за другом символы "abcd".
(Другими словами, требуется выяснить, есть ли в слове x[1]..x[n]
подслово "abcd".)

    Решение. Имеется примерно n (если быть точным, n-3) позиций,
на  которых  может находиться искомое подслово в исходном слове.
Для каждой из позиций можно проверить, действительно ли там  оно
находится, сравнив четыре символа. Однако есть более эффективный
способ. Читая слово x[1]..x[n] слева направо, мы ожидаем появле-
ния  буквы  'a'.  Как только она появилась, мы ждем за ней букву
'b', затем 'c', и, наконец, 'd'. Если наши ожидания оправдывают-
ся, то слово "abcd" обнаружено. Если же какая-то из нужных  букв
не  появляется, мы оказываемся у разбитого корыта и начинаем все
сначала.

     Этот простой алгоритм можно описать в разных терминах.  Ис-
пользуя  терминологию  так  называемых конечных автоматов, можно
сказать, что при чтении слова x слева направо мы в каждый момент
находимся в  одном  из  следующих  состояний:  "начальное"  (0),
"сразу после a" (1), "сразу после ab" (2), "сразу после abc" (3)
и  "сразу после abcd" (4). Читая очередную букву, мы переходим в
следующее состояние по правилу

         Текущее         Очередная      Новое
         состояние       буква          состояние
          0                a             1
          0              кроме a         0
          1                b             2
          1                a             1
          1              кроме a,b       0
          2                c             3
          2                a             1
          2              кроме a,c       0
          3                d             4
          3                a             1
          3              кроме a,d       0

Как только мы попадем в состояние 4,  работа заканчивается.

     Соответствующая программа очевидна:
        i:=1; state:=0;
        {i - первая непрочитанная буква, state - состояние}
        while (i<> n+1) and (state <> 4) do begin
          if state = 0 then begin
            if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 1 then begin
            if x[i] = b then begin
              state:= 2;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 2 then begin
            if x[i] = c then begin
              state:= 3;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 3 then begin
            if x[i] = d then begin
              state:= 4;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end;
        end;
        answer := (state = 4);

     Иными  словами, мы в каждый момент храним информацию о том,
какое максимальное начало нашего образца "abcd" является  концом
прочитанной  части.  (Его длина и есть то "состояние", о котором
шла речь.)

     Терминология,  нами используемая, такова. Слово - это любая
последовательность символов из некоторого фиксированного  конеч-
ного множества. Это множество называется алфавитом, его элементы
- буквами. Если отбросить несколько букв с конца слова, останет-
ся  другое  слово, называемое началом первого. Любое слово также
считается своим началом. Конец слова - то, что  останется,  если
отбросить  несколько  первых  букв.  Любое слово считается своим
концом. Подслово - то, что останется, если отбросить буквы  и  с
начала, и с конца. (Другими словами, подслова - это концы начал,
или, что то же, начала концов.)

     В  терминах  индуктивных  функций (см. раздел 1.3) ситуацию
можно описать так: рассмотрим функцию на словах, которая  прини-
мает два значения "истина" и "ложь" и истинна на словах, имеющих
"abcd"  своим подсловом. Эта функция не является индуктивной, но
имеет индуктивное расширение

 x ->длина максимального начала слова abcd, являющегося концом x

     10.2. Повторения в образце - источник проблем.

     10.2.1. Можно ли в предыдущих рассуждениях  заменить  слово
"abcd" на произвольное слово?

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

Вот картинка, поясняющая сказанное:

 x   y   z   a   b   a   b   a   b   c   ....  <- входное слово

             a   b   a   b   c       <-  мы ждали образца здесь

                     a   b   a   b   c  <-  а он оказался здесь

Таким образом, к моменту
                           |
 x   y   z   a   b   a   b |             <- входное слово
                           |
             a   b   a   b | c       <-  мы ждали образца здесь
                           |
                     a   b | a   b   c  <-  а он оказался здесь
                           |
есть два возможных положения образца, каждое из которых подлежит
проверке. Тем не менее по-прежнему  возможен  конечный  автомат,
читающий  входное  слово буква за буквой и переходящий из состо-
яния в состояние в зависимости от прочитанных букв.

     10.2.2. Указать состояния соответствующего автомата и  таб-
лицу  перехода (новое состояние в зависимости от старого и чита-
емой буквы).

     Решение. По-прежнему состояния  будут  соответствовать  на-
ибольшему  началу  образца, являющемуся концом прочитанной части
слова. Их будет шесть: 0,  1  ("a"),  2  ("ab"),  3  ("aba"),  4
("abab"), 5 ("ababc"). Таблица перехода:

         Текущее         Очередная      Новое
         состояние       буква          состояние
          0                a             1 (a)
          0              кроме a         0
          1 (a)            b             2 (ab)
          1 (a)            a             1 (a)
          1 (a)          кроме a,b       0
          2 (ab)           a             3 (aba)
          2 (ab)         кроме a         0
          3 (aba)          b             4 (abab)
          3 (aba)          a             1 (a)
          3 (aba)        кроме a,b       0
          4 (abab)         c             5 (ababc)
          4 (abab)         a             3 (aba)
          4 (abab)       кроме a,c       0

Для проверки посмотрим, к примеру, на вторую снизу строку.  Если
прочитанная  часть  кончалась на "abab", а затем появилась буква
"a", то теперь  прочитанная  часть  кончается  на  "ababa".  На-
ибольшее  начало  образца ("ababc"), которое есть ее конец - это
"aba".

     Философский вопрос: мы говорили, что  трудность  состоит  в
том,  что  есть несколько возможных положений образца, каждое из
которых может оказаться истинным. Им соответствуют несколько на-
чал образца, являющихся концами входного слова. Но конечный  ав-
томат помнит лишь самое длинное из них. Как же остальные?

     Философский ответ. Дело в том, что самое длинное из них оп-
ределяет  все остальные - это его концы, одновременно являющиеся
его началами.

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

     10.3. Вспомогательные утверждения

     Для произвольного слова X рассмотрим все его начала, однов-
ременно  являющиеся его концами, и выберем из них самое длинное.
(Не считая, конечно, самого слова X.) Будем обозначать его n(X).

     Примеры: n(aba)=a, n(abab)=ab, n(ababa)=aba, n(abc) =  пус-
тое слово.

     10.3.1. Доказать, что все слова n(X), n(n(X)), n(n(n(X)))
и т.д. являются началами слова X.

     Решение.  Каждое из них (согласно определению) является на-
чалом предыдущего.

     По той же причине все они являются концами слова X.

     10.3.2. Доказать, что последовательность предыдущей  задачи
обрывается (на пустом слове).

     Решение. Каждое слово короче предыдущего.

     Задача.  Доказать, что любое слово, одновременно являющееся
началом и концом слова X (кроме самого X)  входит  в  последова-
тельность n(X), n(n(X)),...

     Решение. Пусть слово Y есть одновременно начало и конец  X.
Слово  n(X)  - самое длинное из таких слов, так что Y не длиннее
n(X). Оба эти слова являются началами X, поэтому более  короткое
из них является началом более длинного: Y есть начало n(X). Ана-
логично, Y есть конец n(X). Рассуждая по индукции, можно предпо-
лагать, что утверждение задачи верно для всех слов короче  X,  в
частности,  для слова n(X). Так что слово Y, являющееся концом и
началом  n(X), либо равно n(X), либо входит в последовательность
n(n(X)), n(n(n(X))), ..., что и требовалось доказать.

     10.4. Алгоритм Кнута - Морриса - Пратта

     Алгоритм Кнута - Морриса - Пратта (КМП)  получает  на  вход
слово

        X = x[1]x[2]...x[n]

и просматривает его слева направо буква за буквой, заполняя  при
этом массив натуральных чисел l[1]..l[n], так что

      l[i] = длина слова n(x[1]...x[i])

(функция  n  определена в предыдущем пункте). Словами: l[i] есть
длина наибольшего начала слова x[1]..x[i], одновременно являюще-
гося его концом.

     10.4.1.  Какое  отношение  все это имеет к поиску подслова?
Другими словами, как использовать алгоритм КМП  для  определения
того, является ли слово A подсловом слова B?

     Решение.  Применим алгоритм КМП к слову A#B, где # - специ-
альная буква, не встречающаяся ни в A, ни в B. Слово A  является
подсловом слова B тогда и только тогда, когда среди чисел в мас-
сиве l будет число, равное длине слова A.

     10.4.2. Описать алгоритм заполнения таблицы l[1]..l[n].

     Решение.  Предположим, что первые i значений l[1]..l[i] уже
найдены. Мы читаем очередную букву слова (т.е. x[i+1]) и  должны
вычислить l[i+1].

     1                                              i   i+1
    --------------------------------------------------------
    |           уже прочитанная часть X                |   |
    --------------------------------------------------------
    \-----------Z-----------/    \------------Z------------/

Другими словами, нас интересуют начала Z слова x[1]..x[i+1], од-
новременно являющиеся его концами - из них нам надо выбрать  са-
мое длинное. Откуда берутся эти начала? Каждое из них получается
из  некоторого слова Z' приписыванием буквы x[i+1]. Слово Z' яв-
ляется началом и концом слова x[1]..x[i]. Однако не любое слово,
являющееся началом и концом слова x[1]..x[i],  годится  -  надо,
чтобы за ним следовала буква x[i+1].

     Получаем такой рецепт отыскания слова Z. Рассмотрим все на-
чала слова x[1]..x[i], являющиеся одновременно его  концами.  Из
них  выберем  подходящие - те, за которыми идет буква x[i+1]. Из
подходящих выберем самое длинное. Приписав в его  конец  x[i+1],
получим искомое слово Z.

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

    i:=1; l[1]:= 0;
    {таблица l[1]..l[i] заполнена правильно}
    while i <> n do begin
    | len := l[i]
    | {len - длина начала слова x[1]..x[i], которое  является
    |    его концом; все более длинные начала оказались
    |    неподходящими}
    | while (x[len+1] <> x[i+1]) and (len > 0) do begin
    | | {начало оказалось неподходящим, применяем к нему n}
    | | len := l[len];
    | end;
    | {нашли подходящее или убедились в отсутствии}
    | if x[len+1] = x[i+1] do begin
    | | {x[1]..x[len] - самое длинное подходящее начало}
    | | l[i+1] := len+1;
    | end else begin
    | | {подходящих нет}
    | | l[i+1] := 0;
    | end;
    | i := i+1;
    end;

     10.4.3. Доказать, что число действий в  приведенном  только
что алгоритме не превосходит Cn для некоторой константы C.

     Решение. Это не вполне очевидно: обработка каждой очередной
буквы может потребовать многих итераций во внутреннем цикле. Од-
нако каждая такая итерация уменьшает len по крайней мере на 1, и
в этом случае l[i+1] окажется заметно меньше l[i]. С другой сто-
роны, при увеличении i на единицу величина l[i]  может  возрасти
не более чем на 1, так что часто и сильно убывать она не может -
иначе убывание не будет скомпенсировано возрастанием.
     Более точно, можно записать неравенство
    l[i+1] <= l[i] - (число итераций на i-м шаге) + 1
или
    (число итераций на i-м шаге) <= l[i] - l[i+1] + 1
и остается сложить эти неравества по всем i  и  получить  оценку
сверху для общего числа итераций.

     10.4.4.  Будем  использовать этот алгоритм, чтобы выяснить,
является ли слово X длины n подсловом слова Y длины m. (Как  это
делать  с помощью специального разделителя #, описано выше.) При
этом число действий будет не более C*(n+m), и  используемая  па-
мять  тоже. Придумать, как обойтись памятью не более Cn (что мо-
жет быть существенно меньше, если искомый  образец  короткий,  а
слово, в котором его ищут - длинное).

     Решение.  Применяем  алгоритм КМП к слову A#B. При этом вы-
числение значений l[1],...,l[n] проводим для слова X длины  m  и
запоминаем  эти  значения. Дальше мы помним только значение l[i]
для текущего i - кроме него и кроме таблицы l[1]..l[n], нам  для
вычислений ничего не нужно.

     На практике слова X и Y могут не находиться подряд, поэтому
просмотр  слова  X и затем слова Y удобно оформить в виде разных
циклов. Это избавляет также от хлопот с разделителем.

     10.4.5. Написать соответствующий алгоритм (проверяющий, яв-
ляется ли слово X=x[1]..x[n] подсловом слова Y=y[1]..y[m]).

     Решение. Сначала вычисляем таблицу l[1]..l[n]  как  раньше.
Затем пишем такую программу:
     j:=0; len:=0
     {len - длина максимального начала слова X, одновременно
            являющегося концом слова y[1]..j[j]}
     while (len <> n) and (j <> m) do begin
     | while (x[len+1] <> y[j+1]) and (len > 0) do begin
     | | {начало оказалось неподходящим, применяем к нему n}
     | | len := l[len];
     | end;
     | {нашли подходящее или убедились в отсутствии}
     | if x[len+1] = y[j+1] do begin
     | | {x[1]..x[len] - самое длинное подходящее начало}
     | | len := len+1;
     | end else begin
     | | {подходящих нет}
     | | len := 0;
     | end;
     | i := i+1;
     end;
     {если len=n, слово X встретилось; иначе мы дошли до конца
        слова Y, так и не встретив X}

     10.5. Алгоритм Бойера - Мура

     Этот алгоритм делает то, что на первый взгляд  кажется  не-
возможным:  в  типичной  ситуации он читает лишь небольшую часть
всех букв слова, в котором ищется заданный образец. Как так  мо-
жет  быть? Идея проста. Пусть, например, мы ищем образец "abcd".
Посмотрим на четвертую букву слова: если, к примеру,  это  буква
"e",  то  нет  никакой необходимости читать первые три буквы. (В
самом деле, в образце буквы "e" нет, поэтому он  может  начаться
не раньше пятой буквы.)

     Мы приведем самую простой вариант этого алгоритма,  который
не  гарантирует быстрой работы во всех случаях. Пусть x[1]..x[n]
- образец, который надо искать. Для каждого символа s найдем са-
мое правое его вхождение в слово X, то есть  наибольшее  k,  при
котором x[k]=s. Эти сведения будем хранить в массиве pos[s]; ес-
ли  символ  s вовсе не встречается, то нам будет удобно положить
pos[s] = 0 (мы увидим дальше, почему).

     10.5.1. Как заполнить массив pos?

     Решение.
        положить все pos[s] равными 0
        for i:=1 to n do begin
          pos[x[i]]:=i;
        end;

В  процессе поиска мы будем хранить в переменной last номер буквы
в слове, против которой последняя буква образца. Вначале last = m
(длине образца), затем постепенно увеличивается.

     last:=m;
     {все предыдущие положения образца уже проверены}
     while last <= m do begin {слово не кончилось}
     | if x[m] <> y[last] then begin {последние буквы разные}
     | | last := last + (m - pos[y[last]]);
     | | {m - pos[y[last]]  - это минимальный сдвиг образца,
     | |    при котором напротив y[last] встанет такая же
     | |    буква в образце. Если такой буквы нет вообще,
     | |    то сдвигаем на всю длину образца}
     | end else begin
     | | если нынешнее положение подходит, т.е. если
     | | x[1]..x[m] = y[last-m+1]..y[last],
     | | то сообщить о совпадении;
     | | last := last+1;
     | end;
     end;

Знатоки рекомендуют проверку совпадения проводить справа налево,
т.е. начиная с последней буквы образца (в которой совпадение за-
ведомо есть). Можно также немного сэкономить, произведы  вычита-
ние заранее и храня не pos[s], а m-pos[s], т.е. число букв в об-
разце справа от последнего вхождения буквы s.

     Возможны разные модификации этого алгоритма. Например, мож-
но строку last:=last+1 заменить на last:=last+(m-u), где u - ко-
ордината второго справа вхождения буквы x[m]  в образец.

     10.5.2. Как проще всего учесть это в программе?

     Решение. При построении таблицы pos написать
        for i:=1 to n-1 do...
в основной программе вместо last:=last+1 написать
        last:= last+m-pos[y[last]];

     Приведенная нами упрощенный вариант алгоритма Бойера - Мура
в некоторых случаях требует существенно больше n действий (число
действий  порядка  mn),  проигрывая  алгоритму Кнута - Морриса -
Пратта.

     10.5.3. Привести пример ситуации, в которой образец не вхо-
дит в слово, но авлгоритму требуется порядка mn действий,  чтобы
это установить.

     Решение. Пусть образец имеет вид  baaa..aa,  а  само  слово
состоит  только  из  букв a. Тогда на каждом шаге несоответствие
выясняется лишь в последний момент.

     Настоящий (не упрощенный) алгоритм Бойера - Мура гарантиру-
ет, что число действий не првосходит C*(m+n) в худшем случае. Он
использует  идеи,  близкие  к  идеям алгоритма Кнута - Морриса -
Пратта. Представим себе, что мы сравнивали  образец  со  входным
словом, идя справа налево. При этом некоторый кусок Z (являющий-
ся  концом образца) совпал, а затем обнаружилось различие: перед
Z в образце стоит не то, что во входном слове. Что можно сказать
в этот момент о входном слове? В нем обнаружен фрагмент,  равный
Z,  а перед ним стоит не та буква, что в образце. Эта информация
может позволить сдвинуть образец на несколько позиций вправо без
риска пропустить его вхождение. Эти сдаиги следует вычислить за-
ранее для каждого конца Z нашего образца. Как  говорят  знатоки,
все  это  (вычисление  таблицы  сдвигов и использовани ее) можно
уложэить в C*(m+n) действий.

     10.6. Алгоритм Рабина

     Этот алгоритм основан на простой идее. Представим себе, что
в слове длины m мы ищем образем длины n. Вырежем окошечко разме-
ра  n  и будем двигать его по входному слову. Нас интересует, не
совпадает ли слово в окошечке с заданным образцом. Сравнивать по
буквам долго. Вместо этого фиксируем некоторую функцию на словах
длины n. Если значения этой функции на слове в окошечке и на об-
разце различны, то совпадения нет. Только если значения одинако-
вы, нужно проверять совпадение по буквам.

     Что мы выигрываем при таком подходе? Казалось бы, ничего  -
ведь  чтобы  вычислить значение функции на слове в окошечке, все
равно нужно прочесть все буквы этого слова. Так уж лучше их сра-
зу сравнить с образцом. Тем не менее выигрыш возможен, и вот  за
счет  чего.  При  сдвиге окошечка слово не меняется полностью, а
лишь добавляется буква в конце и убирается в начале. Хорошо  бы,
чтобы по этим данным можно было бы легко рассчитать, как меняет-
ся функция.

     10.6.1. Привести пример удобной для вычисления функции.

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

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

     10.6.2. Привести пример семейства удобных функций.

     Решение.  Выберем  некоторое  число  p (желательно простое,
смотри далее) и некоторый вычет x по модулю p. Каждое слово дли-
ны n будем рассматривать как последовательность целых чисел (за-
менив буквы кодами). Эти числа будем рассматривать как коэффици-
енты многочлена степени n-1 и вычислим значение этого многочлена
по модулю p в точке x. Это и будет  одна  из  функций  семейства
(для каждой пары p и x получается, таким образом, своя функция).
Сдвиг  окошка на 1 соответствует вычитанию старшего члена, умно-
жению на x и добавлению свободного члена.
     Следующее соображение говорит в пользу того, что совпадения
не слишком вероятны. Пусть число p фиксировано и к тому же прос-
тое,  а  X  и  Y  -  два различных слова длины n. Тогда им соот-
ветствуют различные многочлены (мы предполагаем, что  коды  всех
букв  различны  - это возможно при p, большем числа букв алфави-
та). Совпадение значений функции означает, что в точке x эти два
различных многочлена совпадают, то есть их разность обращается в
0. Разность есть многочлен степени n-1 и имеет не более n-1 кор-
ней. Таким образом, если n много меньше p, то случайному x  мало
шансов попасть в неудачную точку.

     10.7. Более сложные образцы и автоматы

     Мы можем искать не конкретно слово,  а  подслова  заданного
вида.  Например, можно искать слова вида a?b, где вместо ? может
стоять любая буква (иными словами, нас  интересует  буква  b  на
расстоянии 2 после буквы a).

     10.7.1  Указать  конечный  автомат, проверяющий, есть ли во
входном слове фрагмент вида a?b.

     Решение.  Читая  слово, следует помнить, есть ли буква a на
последнем месте и на предпоследнем - пока  не  встретим  искомый
фрагмент. Получаем такой автомат:

    Старое состояние    Очередная буква   Новое состояние

       00                     a                 01
       00                  не a                 01
       01                     a                 11
       01                  не a                 10
       10                     a                 01
       10                     b                 найдено
       10                не a и не b            00
       11                     a                 11
       11                     b                 найдено
       11                не a и не b            10

     Другой стандартный знак в образце - это звездочка  (*),  на
место  которой может быть подставлено любое слово. Например, об-
разец ab*cd означает, что мы ищем подслово ab, за которым следу-
ет что угодно, а затем (на любом расстоянии) следует cd.

     10.7.2. Указать конечный автомат, проверяющий, есть  ли  во
входном слове образец ab*cd (в описанном только что смысле).

     Решение.

    Старое состояние    Очередная буква   Новое состояние

       нач                    a                 a
       нач                 не a                 нач
        a                     b                 ab
        a                     a                 a
        a                  не a и не b          нач
        ab                    c                 abc
        ab                 не c                 ab
        abc                   d                 найдено
        abc                   c                 abc
        abc                не с и не d          ab

     Еще один вид поиска - это поиск любого из слово  некоторого
списка.

     10.7.3.  Дан  список  слов X[1],...,X[k] и слово Y. Опреде-
лить, входит ли хотя бы одно из слов X[i] в слово Y (как подсло-
во). Количество действий не должно превосходить константы, умно-
женной на суммарную длину всех слов (из списка и того, в котором
происходит поиск).

     Решение. Очевидный способ состоит в том, чтобы каждое слово
из  списка проверять отдельно (с помощью одного из рассмотренных
алгоритмов). Однако при этом мы не укладываемся в заданное число
действий (из-за умножения k на длину слова Y).

     Посмотрим на дело с  другой  стороны.  Каждому  образцу  из
списка соответствует конечный автомат с некоторым множество сос-
тояний. Их можно объединить в один автомат, множеством состояний
которого будет произведение множеств состояний всех тех  автома-
тов.  Это  -  очень  большое  множество.  Однако  на  самом деле
большинство его элементов недоступны  (не  могут  появиться  при
чтении входного слова) и за счет этого получается экономия. При-
мерно эту идею (но в измененном виде) мы и будем использовать.

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

     Склеим  все  образцы в дерево, объединив их совпадающие на-
чальные участки. Например, набору образцов

      {aaa, aab, abab}

соответствует дерево

                       a/ *
           a     a    / b
        * --- * --- * --- *
                \b     a     b
                  \ * --- * --- *

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

Читая  входное слово, мы двигаемся по этому дереву: текущая вер-
шина - это наибольшая (самая правая) из вершин, являющихся  кон-
цом  прочитанной части (=наибольший конец прочитанной части, яв-
ляющийся началом одного из образцов).

Определим функцию n, аргументами и значениями  которой  являются
вершины  дерева. Именно, n(P) = наибольшая вершина дерева, явля-
ющаяся концом P. (Напомним, вершины дерева - это слова.) Нам по-
надобится такое утверждение:

     10.7.4. Пусть P - вершина дерева. Докажите,  что  множество
всех вершин, являющихся концами P, равно {n(P), n(n(P)),...}

     Решение.  См.  доказательство  аналогичного утверждения для
алгоритма Кнута - Морриса - Пратта.

     Теперь ясно, что нужно делать, находясь в вершине P и читая
букву y входного слова. Надо просматривать последовательно  вер-
шины  P, n(P), n(n(P)) и т.д., пока не обнаружится такая, из ко-
торой выходит стрелка с буквой y.  Та  вершина,  в  которую  эта
стрелка ведет, и будет нашим следующим положением.

     Остается  понять,  как  для каждой вершины дерева вычислить
указатель на значение функции n в этой вершине. Это делается как
раньше, при этом значения n для более коротких слов используются
при вычислении очередного значения функции n. Это означает,  что
вершины  дерева  следует  просматривать в порядке возрастания их
длины. Нетрудно понять, что все это можно  уложить  в  требуемое
число действий (хотя константа зависит от числа букв  в  алфави-
те).  Относящиеся  к этому подробности см. в главе об алгоритмах
на графах.

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

     Определение.  Пусть  фикисирован конечный алфавит Г, не со-
держащий  символов  'l', 'e', '(', ')', '*' и '|' (они будут ис-
пользоваться для построения регулярных выражений и не должны пе-
ремешиваться с буквами). Регулярные выражения строятся по  таким
правилам:

     (а) буква алфавита Г - регулярное выражение;
     (б) символы 'l', 'e' - регулярные выражения;
     (в)  если A,B,C,..,E - регулярные выражения, то (ABC...E) -
          регулярное выражение.
     (г)   если   A,B,C,..,E   -   регулярные   выражения,    то
          (A|B|C|...|E) - регулярное выражение.
     (д) если A - регулярное выражение, то A* - регулярное выра-
          жение.

Каждое  регулярное  выражение задает множество слов в алфавите Г
по таким правилам:

     (а) букве соответствует одноэлементное множество, состоящее
         из однобуквенного слова, состоящего из этой буквы;
     (б)  символу  'e' соответствует пустое множество, а символу
         'l' - одноэлементное множество, единственным  элементом
         которого является пустое слово;
     (в) регулярному выражению (ABC...E) соответствует множество
         всех слов, которые можно получить, если к  слову  из  A
         приписать слово из B, затем из C,..., затем из E ("кон-
         катенация" множеств);
     (г)   регулярному   выражению  (A|B|C|...|E)  соответствует
         объединение   множеств,   соответствующих    выражениям
         A,B,C,..,E;
     (д) регулярному выражению A* соответствует "итерация"  мно-
         жества, соответствующего выражению A, то есть множество
         всех  слов,  которые  можно так разрезать на куски, что
         каждый кусок  принадлежит  множеству,  соответствующему
         выражению  A.  (В частности, пустое слово всегда содер-
         жится в A*.)

     Примеры

Выражение               Множество

(a|b)*                  все слова из букв a и b
(aa)*                   все слова из четного числа букв a
(l|a|b|aa|ab|ba|bb)     любое слово из не более чем 2 букв a,b

     10.7.5.   Написать  регулярное  выражение,  которому  соот-
ветствует множество всех слов из букв a и  b,  в  которых  число
букв a четно.

     Решение. Выражение b* задает все слова без a, а выражение
               (b* a b* a b*)
- все слова ровно с двумя буквами  a.  Остается  объединить  эти
множества, а потом применить итерацию:
              ((b* a b* a b*) | b*)*

     10.7.6.  Написать регулярное выражение, которое задает мно-
жество всех слов из букв a,b,c, в  которых  слово  bac  является
подсловом.

     Решение. ((a|b|c)* bac (a|b|c)*)

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

     10.7.7. Какие выражения соответствуют образцам a?b и ab*cd,
рассмотренным  ранее? (В образце '*' используется не в том смыс-
ле, что в регулярных выражениях!) Предполается, что алфавит  со-
держит буквы a,b,c,d,e.

     Решение. ((a|b|c|d|e)* a (a|b|c|d|e) b (a|b|c|d|e)*)  и
              ((a|b|c|d|e)* ab (a|b|c|d|e)* cd (a|b|c|d|e)*).

     10.7.8.  Доказать,  что  для  всякого регулярного выражения
можно  построить  конечный  автомат,  который  распознает  соот-
ветствующее этому выражению множество слов.

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

     Будем двигаться различными способами из Н в К, читая  буквы
по  дороге  (на тех стрелках, где они есть). Каждому пути из Н в
К, таким образом, соответствует некоторое слово. А  источнику  в
целом  соответствует  множество  слов  - тех слов, которые можно
прочесть на путях из Н в К.

     Замечание. Если нарисовать состояния конечного  автомата  в
виде  точек,  а переходы при чтении букв изобразить в виде стре-
лок, то станет ясно, что конечный автомат - это  частный  случай
источника.

     Мы  будем строить конечный автомат по регулярному выражению
в два приема.  Сначала  мы  построим  источник,  которому  соот-
ветствует  то  же  самое множество слов. Затем для произвольного
источника построим автомат, который  проверяет,  принадлежит  ли
слово соответствующему множеству.

     10.7.9.  По регулярному выражению построить источник, зада-
ющий то же множество.

     Решение. Индукция по построению регулярного выражения. Бук-
вам соответствуют графы из одной стрелки. Объединение реализует-
ся так:

               |---------|
          ---->|*Н1   К1*|->---
        /      |---------|      \
      /         |---------|       \
    * --------->|*Н2   К2*|--->-----* К
    Н  \        |---------|        /
         \     |---------|       /
           --->|*Н3   К3*|--->--
               |---------|

Нарисована  картинка  для  объединения  трех  множеств,  прямо-
угольники - это источники, соответствующие им; указаны начальные
и конечные вершины. На новых стрелках (их 6) букв не написано.

     Конкатенации соответствует картинка

       |--------|         |--------|          |--------|
 Н*--->|*Н1  К1*|---->----|*Н2  К2*| ---->----|*Н3  К3*|-->--*К
       |--------|         |--------|          |--------|

     Наконец, итерации соответствует картинка

    Н*--------->----------*----------->----------*К
                        /   \
                      /       \
                      |       |
                      V       ^
                      |       |
                    -------------
                    | *Н1   К1* |
                    -------------

     10.7.10. Дан источник. Построить конечный автомат, проверя-
ющий, принадлежит ли входное слово  множеству,  соответствующему
источнику (т.е. можно ли прочесть это слово, идя из Н в К).

     Решение. Состояниями автомата будут множества вершин источ-
ника. Именно, прочтя некоторое начало X входного слова, мы будем
помнить  множество всех вершин источника, в которые можно пройти
из начальной, прочитав на пути слово X.

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

     10.7.11.  Дан источник. Построить регулярное выражение, за-
дающее то же множество, что и этот источник.

     Решение.  (Сообщено  участниками  просеминара  по  логике.)
Пусть источник имеет вершины 1..k. Будем считать, что  1  -  это
начало,  а  k  - конец. Через D[i,j, s] обозначим множество всех
слов, которые можно прочесть на пути из i в j, если  в  качестве
промежуточных  пунктов  разрешается  использовать только вершины
1,...,s. Согласно определению, источнику соответствует множество
D[1,k,k].
     Индукцией  по s будем доказывать регулярность всех множеств
D[i,j,s] при всех i и j. При  s=0  это  очевидно  (промежуточные
вершины  запрещены, поэтому каждое из множеств состоит только из
букв).
     Из чего состоит множество D[i,j,s+1]? Отметим на  пути  мо-
менты, в которых он заходит в s+1-ую вершину. При этом путь раз-
бивается  на  части, каждая из которых уже не заходит в нее. По-
этому легко сообразить, что

 D[i,j,s+1] = (D[i,j,s]| (D[i,s+1,s] D[s+1,s+1,s]* D[s+1,j,s]))

(вольность записи: мы используем для  операций  над  множествами
обозначения  как  для регулярных выражений). Остается воспользо-
ваться предположением индукции.

     10.7.12. Где еще используется то же самое рассуждение?

     Ответ. В алгоритме Флойда вычисления цены кратчайшего пути,
см. главу 9 (Некоторые алгоритмы на графах).

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

     Решение. Для автоматов переход к отрицанию очевиден.

     Замечание.  На  практике важную роль играет число состояний
автомата. Оказывается, что тут все не так просто,  и  переход  о
источника  к автомату требует экспоненциального роста числа сос-
тояний.  Подробное рассмотрение связанных с этим теоретических и
практических вопросов - дело особое.
     Глава 11. Представление множеств. Хеширование.

     11.1. Хеширование с открытой адресацией

     В предыдущей главе было несколько  представлений  для  мно-
жеств,  элементами которых являются целые числа произвольной ве-
личины. Однако в любом из них хотя бы одна из операций  проверки
принадлежности,  добавления  и удаления элемента требовала коли-
чества действий, пропорционального числу элементов множества. На
практике это бывает слишком много. Существуют способы,  позволя-
ющие  получить для всех трех упомянутых операций оценку C*log n.
Один из таких способов мы рассмотрим в следующей главе.  В  этой
главе мы разберем способ, которые хотя и приводит к C*n действи-
ям  в  худшем  случае,  но  зато "в среднем" требует значительно
меньшего их числа. (Мы не будем уточнять слов "в среднем",  хотя
это и можно сделать.) Этот способ называется хешированием.
     Пусть  нам необходимо представлять множества элементов типа
T, причем число элементов заведомо меньше n.  Выберем  некоторую
функцию h, определенную на значениях типа T и принимающую значе-
ния  0..(n-1).  Было  бы  хорошо, чтобы эта функция принимала на
элементах будущего множества по возможности более  разнообразные
значения.  Худший случай - это когда ее значения на всех элемен-
тах хранимого множества одинаковы. Эту  функцию  будем  называть
хеш-функцией.

     Введем два массива

         val:  array [0..n-1] of T;
         used: array [0..n-1] of boolean;

(мы  позволяем  себе писать n-1 в качестве границы в определении
типа, хотя в паскале это не разрешается). В этих массивах  будут
храниться  элементы  множества: оно равно множеству всех val [i]
для тех i, для которых used [i], причем все эти val [i]  различ-
ны.  По  возможности  мы  будем хранить элемент t на месте h(t),
считая это место "исконным" для элемента t.  Однако  может  слу-
читься  так,  что новый элемент, который мы хотим добавить, пре-
тендует на уже занятое место (для которого used истинно). В этом
случае мы отыщем ближайшее справа свободное место и запишем эле-
мент туда. ("Справа" значит  "в  сторону  увеличения  индексов";
дойдя  до  края,  мы  перескакиваем в начало.) По предположению,
число элементов всегда меньше n, так что пустые места гарантиро-
ванно будут.
     Формально говоря, в любой момент должно  соблюдаться  такое
требование:  для любого элемента множества участок справа от его
исконного места до его фактического места полностью заполнен.
     Благодаря этому проверка принадлежности заданного  элемента
t  осуществляется  легко: встав на h(t), двигаемся направо, пока
не дойдем до пустого места или до элемента t.  В  первом  случае
элемент  t отсутствует в множестве, во втором присутствует. Если
элемент отсутствует, то его можно добавить на  найденное  пустое
место.  Если  присутствует, то можно его удалить (положив used =
false).

     11.1.1. В предыдущем  абзаце  есть  ошибка.  Найдите  ее  и
исправьте.

     Решение.  Дело  в  том, что при удалении требуемое свойство
"отсутствия пустот" может нарушиться. Поэтому будем делать  так.
Создав дыру, будем двигаться направо, пока не натолкнемся на еще
одно  пустое место (тогда на этом можно успокоиться) или на эле-
мент, стоящий не на исконном месте. Во втором случае  посмотрим,
не  нужно  ли этот элемент поставить на место дыры. Если нет, то
продолжаем поиск, если да, то затыкаем им старую дыру. При  этом
образуется новая дыра, с которой делаем все то же самое.

     11.1.2.  Написать программы проверки принадлежности, добав-
ления и удаления.

     Решение.
  function принадлежит (t: T): boolean;
  | var i: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | belong := used [i] and (val [i] = t);
  end;

  procedure добавить (t: T);
  | var i: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | if not used [i] then begin
  | | used [i] := true;
  | | val [i] := t;
  | end;
  end;

  procedure исключить (t: T);
  | var i, gap: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | if used [i] and (val [i] = t) then begin
  | | used [i] := false;
  | | gap := i;
  | | i := (i + 1) mod n;
  | | while used [i] do begin
  | | | if i = h (val[i]) then begin
  | | | | i := (i + 1) mod n;
  | | | end else if dist(h(val[i]),i) < dist(gap,i) then begin
  | | | | i := (i + 1) mod n;
  | | | end else begin
  | | | | used [gap] := true;
  | | | | val [gap] := val [i];
  | | | | used [i] := false;
  | | | | gap := i;
  | | | | i := i + 1;
  | | | end;
  | | end;
  | end;
  end;

     Здесь  dist  (a, b) - измеренное по часовой стрелке (слева
направо) расстояние от a до b, т.е.

     dist (a,b) = (b - a + n) mod n.

(Мы прибавили n, так как функция mod правильно работает  только
при положительном делимом.)

     11.1.3. Существует много вариантов хеширования. Один из них
таков: обнаружив, что исконное место (обозначим его  i)  занято,
будем  искать  свободное  не  среди  i+1, i+2,..., а среди r(i),
r(r(i)), r(r(r(i))),..., где r - некоторое отображение 0..n-1  в
себя. Какие при этом будут трудности?

     Ответ. (1) Не гарантируется, что если пустые места есть, то
мы их найдем. (2) При удалении неясно, как заполнять  дыры.  (На
практике во многих случаях удаление не нужно, так что такой спо-
соб  также  применяется.  Считается,  что удачный подбор r может
предотвратить образование "скоплений" занятых ячеек.)

     11.1.4.  Пусть  для  хранения  множества  всех   правильных
русских  слов  в  программе орфографии используется хеширование.
Что нужно добавить, чтобы к тому же  уметь  находить  английский
перевод любого правильного слова?

     Решение.  Помимо  массива  val,  элементы которого являются
русскими словами, нужен параллельный массив их английских  пере-
водов.

     11.2. Хеширование со списками

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

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

     11.2.1. Пусть хеш-функция принимает значения 1..k. Для каж-
дого  значения хеш-функции рассмотрим список всех элементов мно-
жества с данным значением хеш-функции. Будем хранить эти k спис-
ков с помощью переменных

     Содержание: array [1..n] of T;
     Следующий: array [1..n] of 1..n;
     ПервСвоб: 1..n;
     Вершина: array [1..k] of 1..n;

так же, как мы это делали для k  стеков  ограниченной  суммарной
длины.  Напишите  соответствующие программы. (Теперь с удалением
будет меньше проблем.)

     Решение. Перед началом работы  надо  положить  Вершина[i]=0
для  всех  i=1..k,  и  связать  все  места  в  список свободного
пространства,  положив   ПервСвоб=1   и   Следующий[i]=i+1   для
i=1..n-1, а также Следующий[n]=0.

  function принадлежит (t: T): boolean;
  | var i: integer;
  begin
  | | i := Вершина[h(t)];
  | i := Вершина[h(t)];
  | {осталось искать в списке, начиная с i}
  | while (i <> 0) and (Содержание[i] <> t) do begin
  | | i := Следующий[i];
  | end; {(i=0) or (Содержание [i] = t)}
  | belong := Содержание[i]=t;
  end;

  procedure добавить (t: T);
  | var i: integer;
  begin
  | if not принадлежит(t) then begin
  | | i := ПервСвоб;
  | | {ПервСвоб <> 0 - считаем, что не переполняется}
  | | ПервСвоб := Следующий[ПервСвоб]
  | | Содержание[i]:=t;
  | | Следующий[i]:=Вершина[h(t)];
  | | Вершина[h(t)]:=i;
  | end;
  end;

  procedure исключить (t: T);
  | var i, pred: integer;
  begin
  | i := Вершина[h(t)]; pred := 0;
  | {осталось искать в списке, начиная с i;  pred -
  |    предыдущий. если он есть, и 0, если нет}
  | while (i <> 0) and (Содержание[i] <> t) do begin
  | | pred := i; i := Следующий[i];
  | end; {(i=0) or (Содержание [i] = t)}
  | if Содержание[i]=t then begin
  | | {элемент есть, надо удалить}
  | | if pred = 0 then begin
  | | | {элемент оказался первым в списке}
  | | | Вершина[h(t)] := Следующий[i];
  | | end else begin
  | | | Следующий[pred] := Следующий[i]
  | | end;
  | | {осталось вернуть i  в список свободных}
  | | Следующий[i] :=  ПервСвоб;
  | | ПервСвоб:=i;
  | end;
  end;

     11.2.2.   (Для  знакомых  с  теорией  вероятностей.)  Пусть
хеш-функция с m значениями используется для хранения  множества,
в  котором  в данный момент n элементов. Доказать, что математи-
ческое ожидание числа действий в предыдущей задаче не  превосхо-
дит  С*(1+n/m),  если добавляемый (удаляемый, искомый) элемент t
выбран случайно, причем все значения h(t) имеют  равные  вероят-
ности (равные 1/m).

     Решение.   Если   l(i)  -  длина  списка,  соответствующего
хеш-значению i, то число операцией не превосходит C*(1+l(h(i)));
усредняя, получаем искомый ответ, так как сумма всех l(i)  равна
n.

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

     Пусть H - семейство функций, каждая из  которых  отображает
множество T в множество из n элементов (например, 0..n-1). Гово-
рят, что H - универсальное семейство хеш-функций, если для любых
двух различных значений s и t из множества T вероятность события
"h(s)=h(t)"  для  случайной  функции h из семейства H равна 1/n.
(Другими словами, те функции из H, для которых  h(s)=h(t),  сос-
тавляют 1/n-ую часть всех функций в H.)

     Замечание.  Более сильное требование к семейству H могло бы
состоять в том, чтобы для любых двух различных элементов s  и  t
множества  T  значения h(s) и h(t) случайной функции h являются
независимыми случайными величинами,  равномерно  распределенными
на 0..n-1.

     11.2.3. Пусть t[1]..t[u] - произвольная  последовательность
различных элементов множества T. Рассмотрим количество действий,
происходящих при помещении элементов t[1]..t[u] в множество, хе-
шируемое  с помощью функции h из универсального семейства H. До-
казать, что среднее количество действий (усреднение - по всем  h
из H) не превосходит C*u*(1+u/n).

     Решение. Обозначим через m[i] количество элементов последо-
вательности,   для   которых   хеш-функция   равна   i.   (Числа
m[0]..m[n-1] зависят, конечно,  от  выбора  хеш-функции.)  Коли-
чество действий, которое мы хотим оценить, с точностью до посто-
янного множителя равно сумме квадратов чисел m[0]..m[n-1]. (Если
k  чисел попадают в одну хеш-ячейку, то для этого требуется при-
мерно 1+2+...+k действий.) Эту же сумму квадратов можно записать
как число пар <p,q>, для которых h[t[p]]=h[t[q]]. Последнее  ра-
венство,  если его рассматривать как событие при фиксированных p
и q, имеет вероятность 1/n при p<>q,  поэтому  среднее  значение
соответствующего члена суммы равно 1/n, а для всей суммы получа-
ем оценку порядка u*u/n, а точнее u*u/n + u, если учесть члены с
p=q.

   Оценка  этой  задачи  показывает, что в на каждый добавляемый
элемент  приходится  в среднем C*(1+u/n) операций. В этой оценке
дробь u/n имеет смысл "коэффициента заполнения" хеш-таблицы.

     11.2.4. Доказать аналогичное утверждение  для  произвольной
последовательности  операций добавления, поиска и удаления (а не
только для добавления, как в предыдущей задаче).

     Указание. Будем представлять себе, что в ходе  поиска,  до-
бавления  и удаления элемент проталкивается по списку своих кол-
лег с тем же хеш-значением, пока не найдет своего  двойника  или
не  дойдет  до  конца  списка.  Будем называть i-j-столкновением
столкновение t[i] с t[j]. Общее число  действий  примерно  равно
числу всех столкновений плюс число элементов. При t[i]<>t[j] ве-
роятность i-j-столкновения равна  1/n.  Осталось  проследить  за
столкновениями  между  равными  элементами.  Фиксируем некоторое
значение x из множества T и посмотрим на связанные с ним  опера-
ции.  Они  идут по циклу: добавление - проверки - удаление - до-
бавление - проверки - удаление -  ...  Столкновения  между  ними
происходят  между добавляемым элементом и следующими за ним про-
верками (до удаления включительно), поэтому общее  их  число  не
превосходит числа элементов, равных x.

     Теперь приведем примеры универсальных  семейств.  Очевидно,
для  любых конечных множеств A и B семейство всех функций, отоб-
ражающих A в B, является универсальным.  Однако  этот  пример  с
практической  точки зрения бесполезен: для запоминания случайной
функции из этого семейства нужен массив, число элементов в кото-
ром равно числу элементов в множестве A. (А если мы  можем  себе
позволить  такой массив, то никакого хеширования нам не требует-
ся!)

     Более практичные примеры универсальных семейств могут  быть
построены  с помощью несложных алгебраических конструкций. Через
Z[p] мы обозначаем множество вычетов по простому модулю p,  т.е.
{0,1,...,p-1}; арифметические операции в этом множестве выполня-
ются  по модулю p. Универсальное семейство образуют все линейные
функционалы на Z[p] в степени n со значениями в Z[p]. Более под-
робно,  пусть  a[1],...,a[n]  -  произвольные   элементы   Z[p];
рассмотрим отображение

   h: <x[1]...x[n]> |-> a[1]x{1]+...+a{n]z[n]

Мы получаем семейство из (p в степени n) отображений, параметри-
зованное наборами a[1]...a[n].

     11.2.5. Доказать, что это семейство является универсальным.

     Указание. Пусть x и y - различные точки пространства Z[p] в
степени  n.  Какова  вероятность  того, что случайный функционал
принимает на них одинаковые значения?  Другими  словами,  какова
вероятность  того,  что  он равен нулю на их разности x-y? Ответ
дается таким утверждением: пусть u - ненулевой вектор; тогда все
значения случайного функционала на нем равновероятны.

     В  следующей  задаче  множество B={0,1} рассматривается как
множество вычетов по модулю 2.

     11.2.6. Семейство всех линейных отображений из (B в степени
m) в (B в степени n) является универсальным.

     Родственные идеи неожиданно оказываются полезными в  следу-
ющей ситуации (рассказал Д.Варсонофьев). Пусть мы хотим написать
программу, которая обнаруживала (большинство) опечаток в тексте,
но не хотим хранить список всех правильных словоформ.  Предлага-
ется   поступить  так:  выбрать  некоторое  N  и  набор  функций
f[1],...,f[k], отображающих русские слова в 1..N. В массиве из N
битов положим все биты равными нулю, кроме тех, которые являются
значением какой-то функции набора на какой-то правильной  слово-
форме.  Теперь  приближённый тест на правильность словоформы та-
ков: проверить, что значения всех функций набора на этой  слово-
форме попадают на места, занятые единицами.
     Глава 12. Множества и деревья.

     12.1. Представление множеств с помощью деревьев.

     Полное двоичное дерево. T-деревья.

     Нарисуем точку. Из нее проведем две стрелки (влево вверх  и
вправо вверх) в две другие точки. Из каждой из этих точек прове-
дем по две стрелки и так далее. Полученную картинку (в n-ом слое
будет  (2 в степени (n - 1)) точек) называют полным двоичным де-
ревом. Нижнюю точку называют корнем. У каждой вершины  есть  два
сына  (две  вершины, в которые идут стрелки) - левый и правый. У
всякой вершины, кроме корня, есть единственный отец.
     Пусть выбрано некоторое конечное множество  вершин  полного
двоичного  дерева, содержащее вместе с каждой вершиной и всех ее
предков. Пусть на каждой вершине этого множества написано значе-
ние фиксированного типа T (то есть задано отображение  множества
вершин  в  множество  значений типа T). То, что получится, будем
называть T-деревом. Множество всех T-деревьев обозначим Tree(T).
     Рекурсивное определение. Всякое непустое T-дерево  разбива-
ется на три части: корень (несущий пометку из T), левое и правое
поддеревья  (которые  могут быть и пустыми). Это разбиение уста-
навливает взаимно однозначное соответствие между множеством  не-
пустых T-деревьев и произведением T * Tree (T) * Tree (T). Обоз-
начив через empty пустое дерево, можно написать

     Tree (T) = {empty} + T * Tree (T) * Tree (T).

     Поддеревья. Высота.

     Фиксируем  некоторое T-дерево. Для каждой его вершины x оп-
ределено ее левое поддерево (левый сын вершины x и все  его  по-
томки),  правое поддерево (правый сын вершины x и все его потом-
ки) и поддерево с корнем в x (вершина x и все ее потомки). Левое
и правое поддеревья вершины x могут быть пустыми, а поддерево  с
корнем  в x всегда непусто (содержит по крайней мере x). Высотой
поддерева будем считать максимальную длину цепи  y[1]..y[n]  его
вершин, в которой y [i+1] - сын y [i] для всех i. (Высота пусто-
го дерева равна нулю, высота дерева из одного корня - единице.)

     Упорядоченные T-деревья.

     Пусть  на множестве значений типа T фиксирован порядок. На-
зовем T-дерево упорядоченным, если выполнено такое свойство: для
любой вершины x все пометки в ее левом поддереве меньше  пометки
в x, а все пометки в ее правом поддереве больше пометки в x.

     12.1.1.  Доказать,  что  в упорядоченном дереве все пометки
различны.
     Указание. Индукция по высоте дерева.

     Представление множеств с помощью деревьев.

     Каждое дерево будем считать представлением  множества  всех
пометок  на  его вершинах. При этом одно и то же множество может
иметь различные представления.
     Благодаря упорядоченности каждый элемент легко может "найти
свое место" в дереве: придя в какую-то вершину и сравнив себя  с
тем, кто там находится, элемент решает, идти ему налево или нап-
раво. Начав с корня и двигаясь по этому правилу, он либо обнару-
жит, что такой элемент уже есть, либо найдет место, в котором он
должен  быть. Всюду далее мы предполагаем, что на значениях типа
T задан порядок, и рассматриваем только упорядоченные деревья.

     Хранение деревьев в программе.

     Можно было бы сопоставить вершины полного двоичного  дерева
с  числами  1,  2, 3,... (считая, что левый сын (n) = 2n, правый
сын (n) = 2n + 1) и хранить пометки в массиве val [1...]. Однако
этот способ неэкономен, поскольку  тратится  место  на  хранение
пустых вакансий в полном двоичном дереве.

     Более экономен такой способ. Введем три массива

       val: array [1..n] of T;
       left, right: array [1..n] of 0..n;

(n  -  максимальное  возможное число вершин дерева) и переменную
root: 0..n. Каждая вершина хранимого T-дерева будет иметь  номер
- число от 1 до n. Разные вершины будут иметь разные номера. По-
метка  в  вершине  с номером x равна val [x]. Корень имеет номер
root. Если вершина с номером i имеет сыновей, то их номера равны
left [i] и right [i]. Отсутствующим сыновьям соответствует число
0. Аналогичным образом значение root = 0  соответствует  пустому
дереву.
     Для  хранения  дерева  используется лишь часть массива; для
тех i, которые свободны - т.е. не  являются  номерами  вершин  -
значения  val  [i] безразличны. Нам будет удобно, чтобы все сво-
бодные числа были "связаны в список": первое хранится  в  специ-
альное  переменной  free: 0..n, а следующее за i свободное число
хранится в left [i], так что свободны числа

     free, left [free], left [left[free]],...

Для последнего свободного числа i значение left  [i]  =  0.  Ра-
венство  free = 0 означает, что свободных чисел больше нет. (За-
мечание. Мы использовали для связывания свободных вершин  массив
left, но, конечно, с тем же успехом можно было использовать мас-
сив right.)
     Вместо  значения 0 (обозначающего отсутствие вершины) можно
было бы воспользоваться любым другим числом вне 1..n. Чтобы под-
черкнуть это, будем вместо 0 использовать константу null = 0.

     12.1.2. Составить программу,  определяющую,  содержится  ли
элемент  t:  T  в упорядоченном дереве (хранимом так, как только
что описано).

     Решение.

  if root = null then begin
  | ..не принадлежит
  end else begin
  | x := root;
  | {инвариант: остается проверить наличие t в непустом подде-
  |  реве с корнем x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin {left [x] <> null}
  | | | x := left [x];
  | | end else begin {t > val [x], right [x] <> null}
  | | | x := right [x];
  | | end;
  | end;
  | {либо t = val [x], либо t отсутствует в дереве}
  | ..ответ = (t = val [x])
  end;

     12.1.3. Упростить решение, используя следующий трюк. Расши-
рим область определения массива val, добавив  ячейку  с  номером
null и положим val [null] = t.

     Решение.

  val [null] := t;
  x := root;
  while t <> val [x] do begin
  | if t < val [x] then begin
  | | x := left [x];
  | end else begin
  | | x := right [x];
  | end;
  end;
  ..ответ: (x <> null).

     12.1.4.  Составить  программу  добавления элемента t в мно-
жество, представленное упорядоченным деревом (если элемент t уже
есть, ничего делать не надо).

     Решение. Определим процедуру get_free (var i: integer), да-
ющую свободное (не являющееся номером) число i и соответствующим
образом корректирующую список свободных чисел.

  procedure get_free (var i: integer);
  begin
  | {free <> null}
  | i := free;
  | free := left [free];
  end;

С ее использованием программа приобретает вид:

  if root = null then begin
  | get_free (root);
  | left [root] := null; right [root] := null;
  | val [root] := t;
  end else begin
  | x := root;
  | {инвариант: осталось добавить t к непустому поддереву с
  |  корнем в x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | x := right [x];
  | | end;
  | end;
  | if t <> val [x] then begin {t нет в дереве}
  | | get_free (i);
  | | left [i] := null; right [i] := null;
  | | val [i] := t;
  | | if t < val [x] then begin
  | | | left [x] := i;
  | | end else begin {t > val [x]}
  | | | right [x] := i;
  | | end;
  | end;
  end;

     12.1.5. Составить программу удаления  элемента  t  из  мно-
жества, представленного упорядоченным деревом (если его там нет,
ничего делать не надо).

     Решение.

  if root = null then begin
  | {дерево пусто, ничего делать не надо}
  end else begin
  | x := root;
  | {осталось удалить t из поддерева с корнем в x; поскольку
  |  это может потребовать изменений в отце x, введем
  |  переменные  father: 1..n и direction: (l, r);
  |  поддерживаем такой инвариант: если x не корень, то father
  |  - его отец, а direction равно l или r в зависимости от
  |  того, левым или правым сыном является x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | father := x; direction := l;
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | father := x; direction := r;
  | | | x := right [x];
  | | end;
  | end;
  | {t = val [x] или t нет в дереве}
  | if t = val [x] then begin
  | | ..удаление вершины x  с отцом father и направлением
  | |   direction
  | end;
  end;

Удаление  вершины  x происходит по-разному в разных случаях. При
этом используется процедура

  procedure make_free (i: integer);
  begin
  | left [i] := free;
  | free := i;
  end;

она включает число i в список свободных. Различаются 4 случая  в
зависимости от наличия или отсутствия сыновей у удаляемой верши-
ны.

  if (left [x] = null) and (right [x] = null) then begin
  | {x - лист, т.е. не имеет сыновей}
  | make_free (x);
  | if x = root then begin
  | | root := null;
  | end else if direction = l then begin
  | | left [father] := null;
  | end else begin {direction = r}
  | | right [father] := null;
  | end;
  end else if (left[x]=null) and (right[x] <> null) then begin
  | {x удаляется, а right [x] занимает место x}
  | make_free (x);
  | if x = root then begin
  | | root := right [x];
  | end else if direction = l then begin
  | | left [father] := right [x];
  | end else begin {direction = r}
  | | right [father] := right [x];
  | end;
  end else if (left[x] <> null) and (right[x]=null) then begin
  | ..симметрично
  end else begin {left [x] <> null, right [x] <> null}
  | ..удалить вершину с двумя сыновьями
  end;

Удаление вершины с двумя сыновьями нельзя сделать просто так, но
ее  можно предварительно поменять с вершиной, пометка на которой
является непосредственно следующим (в порядке возрастания)  эле-
ментом за пометкой на x.

    y := right [x];
    father := x; direction := r;
    {теперь father и direction относятся к вершине y}
    while left [y] <> null do begin
    | father := y; direction := r;
    | y := left [y];
    end;
    {val [y] - минимальная из пометок, больших val [x],
     y не имеет левого сына}
    val [x] := val [y];
    ..удалить вершину y (как удалять вершину, у которой нет ле-
      вого сына, мы уже знаем)

     12.1.6. Упростить программу удаления, заметив, что  некото-
рые случаи (например, первые два из четырех) можно объединить.

     12.1.7.  Использовать упорядоченные деревья для представле-
ния функций, область определения которых  -  конечные  множества
значений типа T, а значения имеют некоторый тип U. Операции: вы-
числение  значения  на  данном  аргументе, изменение значения на
данном аргументе, доопределение  функции  на  данном  аргументе,
исключение элемента из области определения функции.

     Решение. Делаем как раньше, добавив еще один массив

         func_val: array [1..n] of U;

если val [x] = t, func_val [x] = u, то значение хранимой функции
на t равно u.

     Оценка количества действий.

     Для  каждой из операций (проверки, добавления и исключения)
количество действий не превосходит  C  *  (высота  дерева).  Для
"ровно подстриженного" дерева (когда все листья на одной высоте)
высота  по порядку величины равна логарифму числа вершин. Однако
для кривобокого дерева все может быть гораздо хуже: в  наихудшем
случае  все  вершины  образуют цепь и высота равна числу вершин.
Так случится, если элементы множества добавляются в возрастающем
или убывающем порядке. Можно доказать, однако, что при  добавле-
нии  элементов "в случайном порядке" средняя высота дерева будет
не больше C * (логарифм числа вершин). Если этой оценки "в сред-
нем" мало, необходимы  дополнительные  действия  по  поддержанию
"сбалансированности" дерева. Об этом см. в следующем пункте.

     12.1.8.  Предположим, что необходимо уметь также отыскивать
k-ый элемент множества (в  порядке  возрастания),  причем  коли-
чество  действий  должно  быть не более C*(высота дерева). Какую
дополнительную информацию надо хранить в вершинах дерева?

     Решение. В каждой вершине будем хранить число всех  ее  по-
томков.  Добавление  и исключение вершины требует коррекции лишь
на пути от корня к этой вершине. В процессе поиска k-ой  вершины
поддерживается  такой  инвариант:  искомая вершина является s-ой
вершиной поддерева с корнем в x (здесь s и x - переменные).)

     12.2. Сбалансированные деревья.

     Дерево называется сбалансированным (или АВЛ-деревом в честь
изобретателей этого метода Г.М.Адельсона-Вельского и  Е.М.Ланди-
са),  если  для любой его вершины высоты левого и правого подде-
ревьев этой вершины отличаются не более чем на 1. (В  частности,
когда одного из сыновей нет, другой - если он есть - обязан быть
листом.)

     12.2.1.  Найти  минимальное  и максимальное возможное коли-
чество вершин в сбалансированном дереве высоты n.

     Решение. Максимальное число вершин равно (2 в степени n)  -
1. Если m (n) - минимальное число вершин, то, как легко видеть,
     m (n + 2) = 1 + m (n) + m (n+1),
откуда
     m (n) = fib (n+1) - 1
(fib(n)  -  n-ое число Фибоначчи, fib(0)=1, fib(1)=1, fib(n+2) =
fib(n) + fib(n+1)).

     12.2.2. Доказать, что сбалансированное дерево с n вершинами
имеет высоту не больше C * (log n) для некоторой константы C, не
зависящей от n.

     Решение. Индукцией по n легко доказать, что fib [n+1] >= (a
в степени n), где a - больший корень квадратного уравнения a*a =
1 + a, то есть a = (sqrt(5)  +  1)/2.  Остается  воспользоваться
предыдущей задачей.

     Вращения.

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

     Пусть вершина a имеет правого сына b. Обозначим через P ле-
вое поддерево вершины a, через Q и R - левое и правое поддеревья
вершины b.

     Упорядоченность дерева требует, чтобы P < a < Q  <  b  <  R
(точнее  следовало бы сказать "любая пометка на P меньше пометки
на a", "пометка на a меньше любой пометки на Q" и  т.д.,  но  мы
позволим  себе  этого не делать). Точно того же требует упорядо-
ченность дерева с корнем b, его левым сыном a, в котором P и Q -
левое и правое поддеревья a, R -  правое  поддерево  b.  Поэтому
первое дерево можно преобразовать во второе, не нарушая упорядо-
ченности.  Такое  преобразование  назовем малым правым вращением
(правым - поскольку существует симметричное, левое, малым - пос-
кольку есть и большое, которое мы сейчас опишем).

     Пусть b - правый сын a, c - левый сын b, P -левое поддерево
a, Q и R -левое и правое поддеревья c, S - правое  поддерево  b.
Тогда P < a < Q < c < R < b < S.

Такой же порядок соответствует дереву с корнем c, имеющим левого
сына a и правого сына b, для которого P и Q - поддеревья вершины
a,  а R и S - поддеревья вершины b. Соответствующее преобразова-
ние будем называть большим правым вращением. (Аналогично опреде-
ляется симметричное ему большое левое вращение.)

     12.2.3. Дано дерево, сбалансированное всюду, кроме корня, в
котором разница высот равна 2 (т.е. левое  и  правое  поддеревья
корня сбалансированы и их высоты отличаются на 2). Доказать, что
оно  может  быть  превращено в сбалансированное одним из четырех
описанных преобразований, причем высота  его  останется  прежней
или уменьшится на 1.

     Решение.  Пусть более низким является, например, левое под-
дерево, и его высота равна k.  Тогда  высота  правого  поддерева
равна k+2. Обозначим корень через a, а его правого сына (он обя-
зательно  есть)  через  b.  Рассмотрим левое и правое поддеревья
вершины b. Одно из них обязательно имеет высоту  k+1,  а  другое
может  иметь  высоту  k или k+1 (меньше k быть не может, так как
поддеревья сбалансированы). Если высота левого  поддерева  равна
k+1,  а  правого  - k, до потребуется большое правое вращение; в
остальных случаях помогает малое.

------------------------------------
------------------------------------
------------------------------------

                                        высота уменьшилась на 1

------------------------------------
------------------------------------
------------------------------------

                                         высота не изменилась

   k-1 или k (в одном из случаев k)

------------------------------------
------------------------------------
------------------------------------
                                        высота уменьшилась на 1

        Три случая балансировки дерева.

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

     Решение. Будем доказывать более общий факт:

     Лемма.  Если в сбалансированном дереве X одно из его подде-
ревьев Y заменили на сбалансированное дерево Z, причем высота  Z
отличается  от  высоты  Y не более чем на 1, то полученное такой
"прививкой" дерево можно превратить в сбалансированное  вращени-
ями  (причем количество вращений не превосходит высоты, на кото-
рой делается прививка).
     Частным случаем прививки является замена пустого  поддерева
на лист или наоборот, так что достаточно доказать эту лемму.
     Доказательство  леммы. Индукция по высоте, на которой дела-
ется прививка. Если она происходит в корне (заменяется все дере-
во целиком), то все очевидно ("привой"  сбалансирован  по  усло-
вию). Пусть заменяется некоторое поддерево, например, левое под-
дерево некоторой вершины x. Возможны два случая.
     (1)  После прививки сбалансированность в вершине x не нару-
шилась (хотя, возможно, нарушилась сбалансированность в  предках
x:  высота поддерева с корнем в x могла измениться). Тогда можно
сослаться на предположение индукции, считая,  что  мы  прививали
целиком поддерево с корнем в x.
     (2) Сбалансированность в x нарушилась. При этом разница вы-
сот  равна 2 (больше она быть не может, так как высота Z отлича-
ется от высоты Y не более чем на 1). Разберем два варианта.
    (2а) Выше правое  (не  заменявшееся)  поддерево  вершины  x.
Пусть высота левого (т.е. Z) равна k, правого - k+2. Высота ста-
рого  левого поддерева вершины x (т.е. Y) была равна k+1. Подде-
рево с корнем x имело в исходном дереве высоту k+3, и эта высота
не изменилась после прививки.
     По предыдущей задаче вращение преобразует поддерево с  кор-
нем в x в сбалансированное поддерево высоты k+2 или k+3. То есть
высота  поддерева с корнем x - в сравнении с его прежней высотой
- не изменилась или уменьшилась на 1, и мы можем воспользоваться
предположением индукции.

      -------------                     ----------------
      -------------                     ----------------
      -------------k                    ----------------k
 2а                                 2б

     (2б) Выше левое поддерево вершины x.  Пусть  высота  левого
(т.е. Z) равна k+2, правого - k. Высота старого левого поддерева
(т.е.  Y) была равна k+1. Поддерево с корнем x в исходном дереве
X имело высоту k+2, после прививки она стала  равна  k+3.  После
подходящего  вращения (см. предыдущую задачу) поддерево с корнем
в x станет сбалансированным, его высота будет равна k+2 или k+3,
так что изменение высоты по сравнению с высотой поддерева с кор-
нем x в дереве X не превосходит 1 и можно сослаться на предполо-
жение индукции.

     12.2.5. Составить программы добавления и  удаления  элемен-
тов,  сохраняющие  сбалансированность.  Число действий не должно
превосходить C*(высота дерева). Разрешается хранить  в  вершинах
дерева дополнительную информацию, необходимую при балансировке.

     Решение. Будем хранить для каждой  вершины  разницу  между
высотой ее правого и левого поддеревьев:

  diff [i] = (высота правого поддерева вершины с номером i) -
             (высота левого поддерева вершины с номером i).

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

          Малое правое вращение

          Большое правое вращение

     (2)  После  преобразований  мы  должны также изменить соот-
ветственно значения в массиве diff. Для этого  достаточно  знать
высоты деревьев P, Q, ... с точностью до константы, поэтому мож-
но предполагать, что одна из высот равна нулю.

     Вот процедуры вращений:

  procedure SR (a:integer); {малое правое вращение с корнем a}
  | var b: 1..n; val_a,val_b: T; h_P,h_Q,h_R: integer;
  begin
  | b := right [a]; {b <> null}
  | val_a := val [a]; val_b := val [b];
  | h_Q := 0; h_R := diff[b]; h_P := (max(h_Q,h_R)+1)-diff[a];
  | val [a] := val_b; val [b] := val_a;
  | right [a] := right [b] {поддерево R}
  | right [b] := left [b] {поддерево Q}
  | left [b] := left [a] {поддерево P}
  | left [a] := b;
  | diff [b] := h_Q - h_P;
  | diff [a] := h_R - (max (h_P, h_Q) + 1);
  end;

  procedure BR (a:integer);{большое правое вращение с корнем a}
  | var b,c: 1..n; val_a,val_b,val_c: T;
  |     h_P,h_Q,h_R,h_S: integer;
  begin
  | b := right [a]; c := left [b]; {b,c <> null}
  | val_a := val [a]; val_b := val [b]; val_c := val [c];
  | h_Q := 0; h_R := diff[c]; h_S := (max(h_Q,h_R)+1)+diff[b];
  | h_P := 1 + max (h_S, h_S-diff[b]) - diff [a];
  | val [a] := val_c; val [c] := val_a;
  | left [b] := right [c] {поддерево R}
  | right [c] := left [c] {поддерево Q}
  | left [c] := left [a] {поддерево P}
  | left [a] := c;
  | diff [b] := h_S - h_R;
  | diff [c] := h_Q - h_P;
  | diff [a] := max (h_S, h_R) - max (h_P, h_Q);
  end;

Левые вращения (большое и малое) записываются симметрично.

     Процедуры  добавления  и  удаления  элементов  пишутся  как
раньше, но только добавление и  удаление  должно  сопровождаться
коррекцией  массива  diff  и восстановлением сбалансированности.
При этом используется процедура с такими свойствами:

   дано:  левое и правое поддеревья вершины с номером a сбалан-
       сированы, в самой вершине разница высот не больше  2,  в
       поддереве с корнем a массив diff заполнен правильно;
   надо:  поддерево с корнем a сбалансировано и массив diff со-
       ответственно изменен, d - изменение его высоты (равно  0
       или -1); в остальной части все осталось как было}

  procedure balance (a: integer; var d: integer);
  begin {-2 <= diff[a] <= 2}
  | if diff [a] = 2 then begin
  | | b := right [a];
  | | if diff [b] = -1 then begin
  | | | BR (a); d := -1;
  | | end else if diff [b] = 0 then begin
  | | | SR (a); d := 0;
  | | end else begin {diff [b] = 1}
  | | | SR (a); d := - 1;
  | | end;
  | end else if diff [a] = -2 then begin
  | | b := left [a];
  | | if diff [b] = 1 then begin
  | | | BL (a); d := -1;
  | | end else if diff [b] = 0 then begin
  | | | SL (a); d := 0;
  | | end else begin {diff [b] = -1}
  | | | SL (a); d := - 1;
  | | end;
  | end else begin {-2 < diff [a] < 2, ничего делать не надо}
  | | d := 0;
  | end;
  end;

     Восстановление  сбалансированности  требует   движения   от
листьев  к  корню, поэтому будем хранить в стеке путь от корня к
рассматриваемой в данный момент вершине. Элементами стека  будут
пары (вершина, направление движения из нее), т.е. значения типа

        record
        | vert: 1..n; {вершина}
        | direction : (l, r); {l - левое, r- правое}
        end;

Программа добавления элемента t теперь выглядит так:

  if root = null then begin
  | get_free (root);
  | left [root] := null; right [root] := null; diff[root] := 0;
  | val [root] := t;
  end else begin
  | x := root; ..сделать стек пустым
  | {инвариант: осталось добавить t к непустому поддереву с
  |  корнем в x; стек содержит путь к x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | ..добавить в стек пару <x, l>
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | ..добавить в стек пару <x, r>
  | | | x := right [x];
  | | end;
  | end;
  | if t <> val [x] then begin {t нет в дереве}
  | | get_free (i); val [i] := t;
  | | left [i] := null; right [i] := null; diff [i] := 0;
  | | if t < val [x] then begin
  | | | ..добавить в стек пару <x, l>
  | | | left [x] := i;
  | | end else begin {t > val [x]}
  | | | ..добавить в стек пару <x, r>
  | | | right [x] := i;
  | | end;
  | | d := 1;
  | | {инвариант: стек содержит путь к изменившемуся поддереву,
  | |  высота  которого увеличилась по сравнению с высотой в
  | |  исходном дереве на d (=0 или 1); это поддерево  сбалан-
  | |  сировано; значения diff для его вершин правильны; в ос-
  | |  тальном дереве  все  осталось  как  было  - в частности,
  | |  значения diff}
  | | while (d <> 0) and ..стек непуст do begin {d = 1}
  | | | ..взять из стека пару в <v, direct>
  | | | if direct = l then begin
  | | | | if diff [v] = 1 then begin
  | | | | | c := 0;
  | | | | end else begin
  | | | | | c := 1;
  | | | | end;
  | | | | diff [v] := diff [v] - 1;
  | | | end else begin {direct = r}
  | | | | if diff [v] = -1 then begin
  | | | | | c := 0;
  | | | | end else begin
  | | | | | c := 1;
  | | | | end;
  | | | | diff [v] := diff [v] + 1;
  | | | end;
  | | | {c = изменение высоты поддерева с корнем в v по сравне-
  | | |  нию с исходным деревом; массив diff содержит правиль-
  | | |  ные значения для этого поддерева; возможно нарушение
  | | |  сбалансированности в v}
  | | | balance (v, d1); d := c + d1;
  | | end;
  | end;
  end;

Легко  проверить, что значение d может быть равно только 0 или 1
(но не -1): если c = 0, то diff [v] = 0 и балансировка не произ-
водится.

     Программа удаления строится аналогично. Ее  основной  фраг-
мент таков:

  {инвариант: стек содержит путь к изменившемуся поддереву,
   высота которого изменилась по сравнению с высотой в
   исходном дереве на d (=0 или -1); это поддерево
   сбалансировано; значения diff для его вершин правильны;
   в остальном дереве все осталось как было -
   в частности, значения diff}
  while (d <> 0) and ..стек непуст do begin
  | {d = -1}
  | ..взять из стека пару в <v, direct>
  | if direct = l then begin
  | | if diff [v] = -1 then begin
  | | | c := -1;
  | | end else begin
  | | | c := 0;
  | | end;
  | | diff [v] := diff [v] + 1;
  | end else begin {direct = r}
  | | if diff [v] = 1 then begin
  | | | c := -1;
  | | end else begin
  | | | c := 0;
  | | end;
  | | diff [v] := diff [v] - 1;
  | end;
  | {c = изменение высоты поддерева с корнем в v по срав-
  |  нению с исходным деревом; массив diff содержит
  |  правильные значения для этого поддерева;
  |  возможно нарушение сбалансированности в v}
  | balance (v, d1);
  | d := c + d1;
  end;

Легко проверить, что значение d может быть равно только 0 или -1
(но  не -2): если c = -1, то diff [v] = 0 и балансировка не про-
изводится.
     Отметим также, что наличие стека делает излишними  перемен-
ные father и direction (их роль теперь играет вершина стека).

     12.2.6. Доказать, что при добавлении элемента
     (а)  второй из трех случаев балансировки (см. рисунок выше)
невозможен;
     (б) полная балансировка требует не  более  одного  вращения
(после чего все дерево становится сбалансированным),
     в  то  время  как  при удалении элемента может понадобиться
много вращений.

     Замечание. Мы старались  записать  программы  добавления  и
удаления  так,  чтобы  они были как можно более похожими друг на
друга. Используя специфику каждой из них,  можно  многое  упрос-
тить.

     Существуют  и другие способы представления множеств, гаран-
тирующие число действий порядка log n на каждую операцию. Опишем
один из них (называемый Б-деревьями).
     До сих пор каждая вершина содержала один элемент  хранимого
множества.  Этот  элемент  служил  границей между левым и правым
поддеревом. Будем теперь хранить в вершине k >= 1 элементов мно-
жества (число k может меняться от вершины к вершине, а также при
добавлении и удалении новых элементов, см. далее). Эти k элемен-
тов служат разделителями для k+1  поддерева.  Пусть  фиксировано
некоторое  число n >= 1. Будем рассматривать деревья, обладающие
такими свойствами:
     (1) Каждая вершина содержит от n до 2n элементов (за исклю-
чением корня, который может содержать любое число элементов от 0
до 2n).
     (2) Вершина с k элементами либо имеет  k+1  сына,  либо  не
имеет сыновей вообще (такие вершины называются листьями).
     (3) Все листья находятся на одной и той же высоте.
     Добавление элемента происходит так. Если лист, в который он
попадает,  неполон  (т.е.  содержит  менее 2n элементов), то нет
проблем. Если он полон, то 2n+1 элемент (все  элементы  листа  и
новый  элемент) разбиваем на два листа по n элементов и разделя-
ющий их серединный элемент. Этот серединный элемент  надо  доба-
вить  в вершину предыдущего уровня. Это возможно, если в ней ме-
нее 2n элементов. Если и она полна, то ее разбивают на две,  вы-
деляют  серединный элемент и т.д. Если в конце концов мы захотим
добавить элемент в корень, а он окажется полным, то корень  рас-
щепляется на две вершины, а высота дерева увеличивается на 1.
     Удаление элемента. Удаление элемента, находящемся не в лис-
те, сводится к удалению непосредственно следующего за ним, кото-
рый находится в листе. Поэтому достаточно научиться удалять эле-
мент  из  листа.  Если лист при этом становится неполным, то его
можно пополнить за счет соседнего листа - если только  и  он  не
имеет  минимально  возможный  размер  n. Если же оба листа имеют
размер n, то на них вместе 2n элементов, вместе с разделителем -
2n+1. После удаления одного элемента остается 2n элементов - как
раз на один лист. Если при этом вершина предыдущего уровня  ста-
новится меньше нормы, процесс повторяется и т.д.

     12.2.7. Реализовать описанную схему хранения множеств, убе-
дившись,  что она также позволяет обойтись C*log(n) действий для
операций включения, исключения и проверки принадлежности.

     12.2.8. Можно определять сбалансированность  дерева  иначе:
требовать, чтобы для каждой вершины ее левое и правое поддеревья
имели не слишком сильно отличающиеся количества вершин. (Преиму-
щество такого определения состоит в том, что при вращениях изме-
няется  сбалансированность  только в одной вершине.) Реализовать
на основе этой  идеи  способ  хранения  множеств,  гарантирующий
оценку  в  C*log(n)  действий для включения, удаления и проверки
принадлежности. (Указание. Он также использует большие  и  малые
вращения.  Подробности см. в книге Рейнгольда, Нивергельта и Део
"Комбинаторные алгоритмы".)
        Н Е   П О К У П А Й Т Е   Э Т У   К Н И Г У !

                (Предупреждение автора)

     В этой книге ничего не  говорится  об  особенностях  BIOSа,
DOSа, OSа, GEMа и Windows, представляющих основную сложность при
настоящем программировании.

     В ней нет ни слова об объектно-ориентированном программиро-
вании, открывшем новую эпоху в построении дружественных и эффек-
тивных программных систем.

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

     Не рассказано в ней и  о  написании  резидентных  программ,
тонкости взаимодействия которых должен знать каждый.

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

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

     Логическое  программирование,  постепенно вытесняющее уста-
ревший операторный стиль программирования, не затронуто.

     Драматический поворот от баз данных к базам знаний, вызвав-
ший в жизни новую профессию -- инженер знаний -- остался незаме-
ченным автором.

     Проблемы отладки и сопровождения программ,  занимающие,  по
общему  мнению профессионалов, 90% в программировании, игнориру-
ются.

     В  книге  используются  лишь самые элементарные возможности
паскаля. Обширные возможности, предоставляемые современными  ин-
тегрированными программными средами, остаются невостребованными.
(Не  говоря уже о том, что паскаль уже вообще устарел, вытеснен-
ный языком Си.)

     Игрушечные головоломки, которым посвящена книга, никому  не
нужны.  Если  же перед Вами встанет действительно важная задача,
неужели Вы не справитесь с ней сами, без непрошеных  учителей  и
советчиков?

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

     1.1. Задачи без массивов

     1.1.1. Даны две целые переменные a, b.  Составить  фрагмент
программы, после исполнения которого значения переменных поменя-
лись бы местами (новое значение a равно старому значению b и на-
оборот).

     Решение. Введем дополнительную целую переменную t.
        t := a;
        a := b;
        b := t;
Попытка обойтись без дополнительной переменной, написав
        a := b;
        b := a;
не приводит к цели (безвозвратно утрачивается начальное значение
переменной a).

     1.1.2.  Решить  предыдущую  задачу,  не  используя дополни-
тельных переменных (и предполагая, что значениями целых перемен-
ных могут быть произвольные целые числа).

     Решение. (Начальные значения a и b обозначим a0, b0.)
        a := a + b; {a = a0 + b0, b = b0}
        b := a - b; {a = a0 + b0, b = a0}
        a := a - b; {a = b0, b = a0}

     1.1.3.  Дано  целое  число а и натуральное (целое неотрица-
тельное) число n. Вычислить а в степени n. Другими словами,  не-
обходимо  составить  программу,  при исполнении которой значения
переменных а и n не меняются, а значение некоторой другой  пере-
менной  (например, b) становится равным а в степени n. (При этом
разрешается использовать и другие переменные.)

     Решение. Введем целую переменную k, которая меняется от  0
до  n,  причем  поддерживается такое свойство: b = (a в степени
k).

        k := 0; b := 1;
        {b = a в степени k}
        while k <> n do begin
        | k := k + 1;
        | b := b * a;
        end;

Другое решение той же задачи:

        k := n; b := 1;
        {a в степени n = b * (a в степени k)}
        while k <> 0 do begin
        | k := k - 1;
        | b := b * a;
        end;

     1.1.4. Решить предыдущую задачу, если требуется, чтобы чис-
ло действий (выполняемых операторов присваивания)  было  порядка
log n (то есть не превосходило бы C*log n для некоторой констан-
ты C; log n - это степень, в которую нужно возвести 2, чтобы по-
лучить n).

     Решение. Внесем некоторые изменения во второе из предложен-
ных решений предыдущей задачи:

        k := n; b := 1; c:=a;
        {a в степени n = b * (c в степени k)}
        while k <> 0 do begin
        | if k mod 2 = 0 then begin
        | | k:= k div 2;
        | | c:= c*c;
        | end else begin
        | | k := k - 1;
        | | b := b * c;
        | end;
        end;

Каждый второй раз (не реже)  будет  выполняться  первый  вариант
оператора  выбора  (если  k  нечетно, то после вычитания единицы
становится четным), так что за два цикла величина k  уменьшается
по крайней мере вдвое.

     1.1.5.  Даны натуральные числа а, b. Вычислить произведение
а*b, используя в программе лишь операции +, -, =, <>.

     Решение.
        var a, b, c, k : integer;
        k := 0; c := 0;
        {инвариант: c = a * k}
        while k <> b do begin
        | k := k + 1;
        | c := c + a;
        end;
        {c = a * k и k = b, следовательно, c = a * b}

     1.1.6.  Даны  натуральные  числа  а и b. Вычислить их сумму
а+b. Использовать операторы присваивания лишь вида

        <переменная1> := <переменная2>,
        <переменная> := <число>,
        <переменная1> := <переменная2> + 1.

     Решение.
          ...
         {инвариант: c = a + k}
          ...

     1.1.7. Дано натуральное (целое неотрицательное) число  а  и
целое положительное число d. Вычислить частное q и остаток r при
делении а на d, не используя операций div и mod.

     Решение. Согласно определению, a = q * d + r, 0 <= r < d.

        {a >= 0; d > 0}
        r := a; q := 0;
        {инвариант: a = q * d + r, 0 <= r}
        while not (r < d) do begin
        | {r >= d}
        | r := r - d; {r >= 0}
        | q := q + 1;
        end;

     1.1.8.  Дано  натуральное  n,  вычислить n!
        (0!=1, n! = n * (n-1)!).

     1.1.9.   Последовательность  Фибоначчи  определяется  так:
a(0)= 1, a(1) = 1, a(k) = a(k-1) + a(k-2) при k >= 2.  Дано  n,
вычислить a(n).

     1.1.10.  Та же задача, если требуется, чтобы число операций
было пропорционально log n. (Переменные должны быть  целочислен-
ными.)

     Указание.  Пара соседних чисел Фибоначчи получается из пре-
дыдущей умножением на матрицу
            |1 1|
            |1 0|
так что задача сводится к возведению матрицы в  степень  n.  Это
можно сделать за C*log n действий тем же способом, что и для чи-
сел.

     1.1.11. Дано натуральное n, вычислить 1/0!+1/1!+...+1/n!.

     1.1.12.  То  же, если требуется, чтобы количество операций
(выполненных команд присваивания) было бы не более C*n для  не-
которой константы С.
     Решение.  Инвариант:  sum  =  1/1! +...+ 1/k!, last = 1/k!
(важно не вычислять заново каждый раз k!).

     1.1.13.  Даны  два  натуральных числа a и b, не равные нулю
одновременно. Вычислить НОД (a,b) - наибольший общий делитель  а
и b.

     Решение (1 вариант).

        if a > b then begin
        | k := a;
        end else begin
        | k := b;
        end;
        {k = max (a,b)}
        {инвариант: никакое  число, большее k, не является об-
          щим делителем}
        while not (((a mod k)=0) and ((b mod k)=0)) do begin
        | k := k - 1;
        end;
        {k - общий делитель, большие - нет}

       (2  вариант - алгоритм Евклида). Будем считать , что НОД
(0,0) = 0. Тогда НОД (a,b) = НОД (a-b,b)  =  НОД  (a,b-a);  НОД
(a,0) = НОД (0,a) = a для всех a,b>=0.

         m := a; n := b;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0 }
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n;
        | end else begin
        | | n := n - m;
        | end;
        end;
        if m = 0 then begin
        | k := n;
        end else begin
        | k := m;
        end;

     1.1.14. Написать модифицированный вариант алгоритма Евкли-
да,  использующий соотношения НОД (a, b) = НОД (a mod b, b) при
a >= b, НОД (a, b) = НОД (a, b mod a) при b >= a.

     1.1.15. Даны натуральные а и b, не равные 0  одновременно.
Найти d = НОД (a,b) и такие целые x и y, что d = a*x + b*y.

     Решение.  Добавим в алгоритм Евклида переменные p, q, r, s
и впишем в инвариант условия m = p*a + q*b; n = r*a + s*b.

        m:=a; n:=b; p := 1; q := 0; r := 0; s := 1;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0
                    m = p*a + q*b; n = r*a + s*b.}
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n; p := p - r; q := q - s;
        | end else begin
        | | n := n - m; r := r - p; s := s - q;
        | end;
        end;
        if m = 0 then begin
        | k :=n; x := r; y := s;
        end else begin
        | k := m; x := p; y := q;
        end;

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

     1.1.17. (Э.Дейкстра).  Добавим  в алгоритм Евклида дополни-
тельные переменные u, v, z:

         m := a; n := b; u := b; v := a;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0 }
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n; v := v + u;
        | end else begin
        | | n := n - m; u := u + v;
        | end;
        end;
        if m = 0 then begin
        | z:= v;
        end else begin {n=0}
        | z:= u;
        end;

Доказать, что после исполнения алгоритма z равно удвоенному  на-
именьшему общему кратному чисел a, b: z = 2 * НОК (a,b).

     Решение. Заметим, что величина m*u + n*v не меняется в ходе
выполнения  алгоритма. Остается воспользоваться тем, что вначале
она равна 2*a*b и что НОД (a, b) * НОК (a, b) = a*b.

     1.1.18.  Написать  вариант  алгоритма Евклида, использующий
соотношения
        НОД(2*a, 2*b) = 2*НОД(a,b)
        НОД(2*a, b)   =   НОД(a,b) при нечетном b,
не включающий деления с остатком, а использующий лишь деление на
2 и проверку четности. (Число действий должно быть порядка log k
для исходных данных, не превосходящих k.)

     Решение.

  m:= a; n:=b; d:=1;
  {НОД(a,b) = d * НОД(m,n)}
  while not ((m=0) or (n=0)) do begin
  | if (m mod 2 = 0) and (n mod 2 = 0) then begin
  | | d:= d*2; m:= m div 2; n:= n div 2;
  | end else if (m mod 2 = 0) and (n mod 2 = 1) then begin
  | | m:= m div 2;
  | end else if (m mod 2 = 1) and (n mod 2 = 0) then begin
  | | n:= n div 2;
  | end else if (m mod 2=1) and (n mod 2=1) and (m>=n)then begin
  | | m:= m-n;
  | end else if (m mod 2=1) and (n mod 2=1) and (m<=n)then begin
  | | n:= n-m;
  | end;
  end;
  {m=0 => ответ=d*n; n=0 => ответ=d*m}

Оценка числа действий: каждое второе действие делит хотя бы одно
из чисел m и n пополам.

     1.1.19. Дополнить алгоритм предыдущей задачи поиском x и y,
для которых ax+by=НОД(a,b).

     Решение. (Идея сообщена Д.Звонкиным) Прежде всего  заметим,
что  одновременое деление a и b пополам не меняет искомых x и y.
Поэтому можно считать, что с самого начала одно из чисел a  и  b
нечетно. (Это свойство будет сохраняться и далее.)
     Теперь  попытаемся,  как  и  раньше,  хранить  такие  числа
p,q,r,s, что
     m = ap + bq
     n = ar + bs
Проблема в том, что при делении, скажем, m на 2 надо разделить p
и  q  на 2, и они перестанут быть целыми (а станут двоично-раци-
ональными). Двоично-рациональное число естественно хранить в ви-
де пары (числитель, показатель степени двойки в знаменателе).  В
итоге  мы  получаем  d  в  виде комбинации a и b с двоично-раци-
ональными коэффициентами. Иными словами, мы имеем
        (2 в степени i)* d = ax + by
для  некоторых  целых x,y и натурального i. Что делать, если i >
1? Если x и y чётны, то на 2 можно сократить. Если это  не  так,
положение можно исправить преобразованием
        x := x + b
        y := y - a
(оно  не меняет ax+by). Убедимся в этом. Напомним, что мы счита-
ем, что одно из чисел a и b нечётно. Пусть это будет a. Если при
этом y чётно, то и x должно быть чётным (иначе ax+by  будет  не-
чётным). А при нечётном y вычитание из него нёчетного a делает y
чётным.

     1.1.20. Составить программу, печатающую квадраты всех нату-
ральных чисел от 0 до заданного натурального n.

     Решение.

        k:=0;
        writeln (k*k);
        {инвариант: k<=n, напечатаны все
          квадраты до k включительно}
        while not (k=n) do begin
        | k:=k+1;
        | writeln (k*k);
        end;

     1.1.21.  Та же задача, но разрешается использовать из ариф-
метических операций лишь сложение и вычитание, причем общее чис-
ло действий должно быть порядка n.

     Решение.  Введем  переменную k_square (square - квадрат),
связанную с k соотношением k_square = k*k:

        k := 0; k_square := 0;
        writeln (k_square);
        while not (k = n) do begin
        | k := k + 1;
        | {k_square = (k-1) * (k-1) = k*k - 2*k + 1}
        | k_square := k_square + k + k - 1;
        | writeln (k_square);
        end;

     1.1.22. Составить программу, печатающую разложение на прос-
тые множители заданного натурального числа n > 0 (другими слова-
ми, требуется печатать только простые числа и произведение напе-
чатанных  чисел должно быть равно n; если n = 1, печатать ничего
не надо).

     Решение (1 вариант).

        k := n;
        {инвариант:  произведение напечатанных чисел и k равно
         n, напечатаны только простые числа}
        while not (k = 1) do begin
        | l := 2;
        | {инвариант: k не имеет делителей в интервале (1,l)}
        | while k mod l <> 0 do begin
        | | l := l + 1;
        | end;
        | {l - наименьший делитель k, больший 1, следовательно,
        |  простой}
        | writeln (l);
        | k:=k div l;
        end;

     (2 вариант).

         k := n; l := 2;
         {произведение  k и напечатанных чисел равно n; напеча-
          танные числа просты; k не имеет делителей, меньших l}
         while not (k = 1) do begin
         | if k mod l = 0  then begin
         | | {k делится на l и не имеет делителей,
         | |   меньших l, значит, l просто}
         | | k := k div l;
         | | writeln (l);
         | end else begin
         | | { k не делится на l }
         | | l := l + 1;
         | end;
         end;

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

     Решение. Во втором варианте решения вместо l:=l+1 можно на-
писать

                if l*l > k then begin
                | l:=k;
                end else begin
                | l:=l+1;
                end;

     1.1.24. Проверить, является ли заданное натуральное  число
n > 1 простым.

     1.1.25. (Для знакомых с основами алгебры). Дано целое  га-
уссово  число n + mi (принадлежащее Z[i]). (a) Проверить, явля-
ется ли оно простым (в Z[i]); (б) напечатать его разложение  на
простые (в Z[i]) множители.

     1.1.26. Разрешим использовать команды write (i) лишь при i
=  0,1,2,...,9.  Составить программу, печатающую десятичную за-
пись заданного натурального числа n > 0. (Случай n =  0  явился
бы некоторым исключением, так как обычно нули в начале числа не
печатаются, а для n = 0 - печатаются.)

     Решение.

        base:=1;
        {base - степень 10, не превосходящая n}
        while 10 * base <= n do begin
        | base:= base * 10;
        end;
        {base - максимальная степень 10, не превосходящая n}
        k:=n;
        {инвариант: осталось напечатать k с тем же числом
         знаков, что в base; base = 100..00}
        while base <> 1 do begin
        | write(k div base);
        | k:= k mod base;
        | base:= base div 10;
        end;
        {base=1; осталось напечатать однозначное число k}
        write(k);

(Типичная ошибка при решении этой задачи: неправильно  обрабаты-
ваются числа с нулями посередине. Приведенный инвариант допуска-
ет  случай, когда k < base; в этом случае печатание k начинается
со старших нулей.)

     1.1.27. То же самое, но надо напечатать десятичную запись в
обратном порядке. (Для n = 173 надо напечатать 371.)

     Решение.

        k:= n;
        {инвариант: осталось напечатать k в обратном порядке}
        while k <> 0 do begin
        | write (k mod 10);
        | k:= k div 10;
        end;

     1.1.28. Дано натуральное n. Подсчитать  количество  решений
неравенства  x*x + y*y < n в натуральных (неотрицательных целых)
числах, не используя действий с вещественными числами.

     Решение.

        k := 0; s := 0;
        {инвариант: s = количество решений неравенства
          x*x + y*y < n c x < k}
        while k*k < n do begin
        | ...
        | {t = число решений неравенства k*k + y*y < n
        |  (при данном k) }
        | k := k + 1;
        | s := s + t;
        end;
        {k*k >= n, поэтому s = количество всех решений
          неравенства}

     Здесь ... - пока еще не написанный кусок программы, который
будет таким:

        l := 0; t := 0;
        {инвариант: t = число решений
          неравенства k*k + y*y < n c y < l }
        while k*k + l*l < n do begin
        | l := l + 1;
        | t := t + 1;
        end;
        {k*k + l*l >= n,  поэтому  t = число
          всех решений неравенства k*k + y*y < n}

     1.1.29. Та же задача, но количество  операций  должно  быть
порядка (n в степени 1/2). (В предыдущем решении, как можно
подсчитать, порядка n операций.)

     Решение. Нас интересуют точки решетки (с целыми координата-
  *              ми) в первом квадранте, попадающие внутрь круга
  * * *          радиуса  (n  в  степени  1/2). Интересующее нас
  * * * *        множество (назовем его X) состоит из  объедине-
  * * * *        ния  вертикальных  столбцов  убывающей  высоты.
  * * * * *      Идея решения состоит в  том,  чтобы  "двигаться
вдоль  его  границы",  спускаясь  по  верхнему  его краю, как по
лестнице. Координаты движущейся точки  обозначим  <k,l>.  Введем
еще одну переменную s и будем поддерживать истинность такого ус-
ловия:
     <k,l> находится сразу над k-ым столбцом;
     s - число точек в предыдущих столбцах.

     Формально:
l  - минимальное среди тех l >= 0, для которых <k,l> не принад-
    лежит X;
s - число пар натуральных x, y, для которых x < k и <x,y>  при-
    надлежит X.
Обозначим эти условия через (И).

  k := 0; l := 0;
  while "<0,l> принадлежит X" do begin
  | l := l + 1;
  end;
  {k = 0, l - минимальное среди тех l >= 0,
   для которых <k,l> не принадлежит X }
  s := 0;
  {инвариант: И}
  while not (l = 0) do begin
  | s := s + l;
  | {s - число точек в столбцах до k-го включительно}
  | k := k + 1;
  | {точка <k,l> лежит вне X, но,  возможно,  ее  надо сдвинуть
  |    вниз, чтобы восстановить И }
  | while (l <> 0) and ("<k, l-1> не принадлежит X") do begin
  | | l := l - 1;
  | end;
  end;
  {И, l = 0, поэтому k-ый столбец и все следующие пусты, а
    s равно искомому числу}

Оценка числа действий очевидна: сначала мы движемся вверх не бо-
лее  чем  на  (n в степени 1/2) шагов, а затем вниз и вправо - в
каждую сторону не более чем на (n в степени 1/2) шагов.

     1.1.30. Даны натуральные числа n и k, n > 1.  Напечатать  k
десятичных знаков числа 1/n. (При наличии двух десятичных разло-
жений  выбирается то из них, которое не содержит девятки в пери-
оде.) Программа должна использовать только целые переменные.

     Решение. Сдвинув в десятичной записи числа 1/n запятую на k
мест вправо, получим число (10 в степени k)/n. Нам надо  напеча-
тать  его целую часть, т. е. разделить (10 в степени k) на n на-
цело. Стандартный способ требует использования больших по  вели-
чине  чисел, которые могут выйти за границы диапазона представи-
мых чисел. Поэтому мы сделаем иначе (следуя обычному методу "де-
ления уголком") и будем хранить "остаток" r:

  l := 0; r := 1;
  {инв.: напечатано l разрядов 1/n, осталось напечатать
    k - l разрядов дроби r/n}
   while l <> k do begin
   | write ( (10 * r) div n);
   |   r := (10 * r) mod n;
   |   l := l + 1;
   end;

     1.1.31. Дано натуральное число n > 1. Определить длину  пе-
риода десятичной записи дроби 1/n.

     Решение.  Период  дроби  равен периоду в последовательности
остатков (докажите это; в частности, надо доказать,  что  он  не
может  быть  меньше).  Кроме того, в этой последовательности все
периодически повторяющиеся все члены различны, а предпериод име-
ет длину не более n. Поэтому достаточно найти (n+1)-ый член пос-
ледовательности остатков и  затем  минимальное  k,  при  котором
(n+1+k)-ый член совпадает с (n+1)-ым.

  l := 0; r := 1;
  {инвариант: r/n = результат отбрасывания l знаков в 1/n}
  while l <> n+1 do begin
  | r := (10 * r) mod n;
  | l := l + 1;
  end;
  c := r;
  {c = (n+1)-ый член последовательности остатков}
  r := (10 * r) mod n;
  k := 0;
  {r = (n+k+1)-ый член последовательности остатков}
  while r <> c do begin
  | r := (10 * r) mod n;
  | k := k + 1;
  end;

     1.1.32 (Э. Дейкстра). Функция f с натуральными  аргументами
и  значениями определена так: f(0) = 0, f(1) = 1, f (2n) = f(n),
f (2n+1) = f (n) + f (n+1). Составить программу вычисления f (n)
по заданному n, требующую порядка log  n  операций.

     Решение.
  k := n; a := 1; b := 0;
  {инвариант: 0 <= k, f (n) = a * f(k) + b * f (k+1)}
  while k <> 0 do begin
  | if k mod 2 = 0  then begin
  | | l := k div 2;
  | | {k = 2l, f(k) = f(l), f (k+1) = f (2l+1) = f(l) + f(l+1),
  | |  f (n) = a*f(k) + b*f(k+1) = (a+b)*f(l) + b*f(l+1)}
  | | a := a + b; k := l;
  | end else begin
  | | l := k div 2;
  | | {k = 2l + 1, f(k) = f(l) + f(l+1),
  | |  f(k+1) = f(2l+2) = f(l+1),
  | |  f(n) = a*f(k) + b*f(k+1) = a*f(l) + (a+b)*f(l+1)}
  | | b := a + b; k := l;
  | end;
  end;
  {k = 0, f(n) = a * f(0) + b * f(1) = b, что и требовалось}

     1.1.33.  То  же,  если  f(0) = 13, f(1) = 17, а f(2n) =
43 f(n) + 57 f(n+1), f(2n+1) = 91 f(n) + 179 f(n+1) при n>=1.
     Указание.  Хранить  коэффициенты в выражении f(n) через три
соседних числа.

     1.1.34. Даны натуральные числа а и b, причем b >  0.  Найти
частное  и  остаток  при  делении а на b, оперируя лишь с целыми
числами и не используя операции div и mod, за исключением  деле-
ния  на  2  четных  чисел;  число  шагов  не должно превосходить
C1*log(a/b) + C2 для некоторых констант C1, C2.

     Решение.

  b1 := b;
  while b1 <= a do begin
  | b1 := b1 * 2;
  end;
  {b1 > a, b1 = b * (некоторая степень 2)}
  q:=0; r:=a;
  {инвариант: q, r - частное и остаток при делении a на b1,
   b1 = b * (некоторая степень 2)}
  while b1 <> b do begin
  | b1 := b1 div 2 ; q := q * 2;
  | { a = b1 * q + r, 0 <= r, r < 2 * b1}
  | if r >= b1 then begin
  | | r := r - b1;
  | | q := q + 1;
  | end;
  end;
  {q, r - частное и остаток при делении a на b}

     1.2. Массивы.

     В следующих задачах переменные x, y, z предполагаются  опи-
санными  как  array [1..n] of integer (n - некоторое натуральное
число, большее 0), если иное не оговорено явно.

     1.2.1. Заполнить массив x нулями. (Это означает, что  нужно
составить фрагмент программы, после выполнения которого все зна-
чения  x[1]..x[n]  равнялись  бы  нулю, независимо от начального
значения переменной x.)

     Решение.

          i := 0;
          {инвариант: первые i значений x[1]..x[i] равны 0}
          while i <> n do begin
          | i := i + 1;
          | {x[1]..x[i-1] = 0}
          | x[i] := 0;
          end;

     1.2.2. Подсчитать количество нулей в массиве x.  (Составить
фрагмент программы, не меняющий значения x, после исполнения ко-
торого  значение некоторой целой переменной k равнялось бы числу
нулей среди компонент массива x.)

     Решение.
          ...
          {инвариант: k= число нулей среди x[1]...x[i] }
          ...

     1.2.3. Не используя оператора  присваивания  для  массивов,
составить фрагмент программы, эквивалентный оператору x:=y.

     Решение.

  i := 0;
  {инвариант: значение y не изменилось, x[l] = y[l] при l <= i}
  while i <> n do begin
  | i := i + 1;
  | x[i] := y[i];
  end;

     1.2.4. Найти максимум из x[1]..x[n].

     Решение.
          i := 1; max := x[1];
          {инвариант: max = максимум из x[1]..x[i]}
          while i <> n do begin
          | i := i + 1;
          | {max = максимум из x[1]..x[i-1]}
          | if x[i] > max then begin
          | | max := x[i];
          | end;
          end;

     1.2.5.  Дан  массив x: array [1..n] of integer, причём x[1]
<= x[2] <= ... <= x[n]. Найти количество различных  чисел  среди
элементов этого массива.

     Решение. (1 вариант)

  i := 1; k := 1;
  {инвариант: k - количество различных чисел среди x[1]..x[i]}
  while i <> n do begin
  | i := i + 1;
  | if x[i] <> x[i-1] then begin
  | | k := k + 1;
  | end;
  end;

     (2 вариант) Искомое число на 1 больше количества тех  чисел
i из 1..n-1, для которых x[i] <> x[i+1].

  k := 1;
  for i := 1 to n-1 do begin
  | if x[i]<> x[i+1] then begin
  | | k := k + 1;
  | end;
  end;

     1.2.6. (Сообщил А.Л.Брудно.) Прямоугольное поле m на n раз-
бито  на mn квадратных клеток. Некоторые клетки покрашены в чер-
ный цвет. Известно, что все черные клетки могут быть разбиты  на
несколько непересекающихся и не имеющих общих вершин черных пря-
моугольников. Считая, что цвета клеток даны в виде массива типа
        array [1..m] of array [1..n] of boolean;
подсчитать  число  черных  прямоугольников,  о которых шла речь.
Число действий должно быть порядка m*n.

     Решение. Число прямоугольников равно числу их левых верхних
углов. Является ли клетка верхним углом, можно узнать, посмотрев
на ее цвет, а также цвет верхнего  и  левого  соседей.  (Не  за-
будьте, что их может не быть, если клетка с краю.)

     1.2.7. Дан массив x: array [1..n] of integer.  Найти  коли-
чество  различных  чисел  среди  элементов этого массива. (Число
действий должно быть порядка n*n.)

     1.2.8.  Та  же  задача,  если  требуется,  чтобы количество
действий было порядка n* log n. (Указание. Смотри главу о сорти-
ровке.)

     1.2.9. Та же задача, если известно, что все элементы масси-
ва - числа от 1 до k и число действий должно быть порядка n+k.

     1.2.10. Дан массив x [1]..x[n] целых  чисел.  Не  используя
других  массивов, переставить элементы массива в обратном поряд-
ке.

     Решение. Числа x [i] и x [n+1-i] нужно поменять местами для
всех i, для которых i < n + 1 - i, т.е. 2*i < n + 1 <=> 2*i <= n
<=> i <= n div 2:
  for i := 1 to n div 2 do begin
  | ...обменять x [i] и x [n+1-i];
  end;

     1.2.11.  (из  книги  Д.Гриса)  Дан   массив   целых   чисел
x[1]..x[m+n],  рассматриваемый как соединение двух его отрезков:
начала x[1]..x[m] длины m и конца x[m+1]..x[m+n] длины n. Не ис-
пользуя дополнительных массивов,  переставить  начало  и  конец.
(Число действий порядка m+n.)

     Решение. (1 вариант). Перевернем (расположим в обратном по-
рядке) отдельно начало и конец массива, а затем перевернем  весь
массив как единое целое.

     (2 вариант, А.Г.Кушниренко). Рассматривая массив записанным
по кругу, видим, что требуемое действие - поворот круга. Как из-
вестно, поворот есть композиция двух осевых симметрий.

     (3  вариант).  Рассмотрим  более  общую задачу - обмен двух
участков массива x[p+1]..x[q] и x[q+1]..x[s].  Предположим,  что
длина  левого  участка  (назовем  его A) не больше длины правого
(назовем его B). Выделим в B начало той же длины, что и A, назо-
вем его B1, а остаток B2. (Так что B = B1 + B2, если  обозначать
плюсом приписывание массивов друг к другу.) Нам надо из A + B1 +
B2 получить B1 + B2 + A. Меняя местами участки A и B1 - они име-
ют одинаковую длину, и сделать это легко,- получаем B1 + A + B2,
и  осталось  поменять  местами A и B2. Тем самым мы свели дело к
перестановке двух отрзков меньшей длины. Итак,  получаем  такую
схему программы:

  p := 0; q := m; r := m + n;
  {инвариант: осталось переставить x[p+1]..x[q], x[q+1]..x[s]}
  while (p <> q) and (q <> s) do begin
  | {оба участка непусты}
  | if (q - p) <= (s - q) then begin
  | | ..переставить x[p+1]..x[q] и x[q+1]..x[q+(q-p)]
  | | pnew := q; qnew := q + (q - p);
  | | p := pnew; q := qnew;
  | end else begin
  | | ..переставить x[q-(r-q)+1]..x[q] и x[q+1]..x[r]
  | | qnew := q - (r - q); rnew := q;
  | | q := qnew; r := rnew;
  | end;
  end;

Оценка времени работы: на очередном шаге оставшийся для обработ-
ки участок становится короче на длину A; число действий при этом
также пропорционально длине A.

     1.2.12. Коэффициенты многочлена хранятся в массиве a: array
[0..n]  of  integer (n - натуральное число, степень многочлена).
Вычислить значение этого многочлена в точке x (т. е.  a[n]*(x  в
степени n)+...+a[1]*x+a[0]).

     Решение. (Описываемый алгоритм называется схемой Горнера.)

  k := 0; y := a[n];
  {инвариант: 0 <= k <= n,
   y= a[n]*(x в степени k)+...+a[n-1]*(x в степени k-1)+...+
                     + a[n-k]*(x в степени 0)}
  while k<>n do begin
  | k := k + 1;
  | y := y * x + a [n - k];
  end;

     1.2.13. (Для знакомых с основами анализа. Сообщил  А.Г.Куш-
ниренко.)  Дополнить  алгоритм  вычисления значения многочлена в
заданной точке по схеме Горнера вычислением значения его  произ-
водной в той же точке.

     Решение. Добавление нового коэффициента соответствует пере-
ходу от многочлена P(x) к многочлену P(x)*x + c. Его производная
в  точке  x равна P'(x)*x + P(x). (Это решение обладает забавным
свойством: не надо знать заранее степень многочлена. Если требо-
вать выполнения этого условия, да еще просить  вычислять  только
значение производной, не упоминая о самом многочлене, получается
не такая уж простая задача.)

     1.2.14.  В  массивах
  a:array  [0..k] of integer и b: array [0..l] of integer
хранятся коэффициенты двух многочленов степеней k и  l.  Помес-
тить в массив c: array [0..m] of integer коэффициенты их произ-
ведения.  (Числа k, l, m - натуральные, m = k + l; элемент мас-
сива с индексом i содержит коэффициент при x в степени i.)

     Решение.

          for i:=0 to m do begin
          | c[i]:=0;
          end;
          for i:=0 to k do begin
          | for j:=0 to l do begin
          | | c[i+j] := c[i+j] + a[i]*b[j];
          | end;
          end;

     1.2.15. Предложенный выше алгоритм перемножения многочленов
требует порядка n*n действий для перемножения  двух  многочленов
степени n. Придумать более эффективный (для больших n) алгоритм,
которому  достаточно  порядка  (n  в  степени  (log  4)/(log 3))
действий.
     Указание. Представим себе, что надо перемножить два многоч-
лена степени 2k. Их можно представить в виде
        A(x)*x^k + B(x)    и    C(x)*x^k + D(x)
(здесь x^k обозначает x  в степени k). Произведение их равно
       A(x)C(x)*x^{2k}  +  (A(x)D(x)+B(x)C(x))*x^k  + B(x)D(x)
Естественный способ вычисления AC, AD+BC, BD требует четырех ум-
ножений многочленов степени k, однако их количество можно сокра-
тить  до  трех  с  помощью  такой  хитрости:  вычислить AC, BD и
(A+B)(C+D), а затем заметить, что AD+BC=(A+B)(C+D)-AC-BD.

     1.2.16.  Даны  два  возрастающих массива x: array [1..k] of
integer и y: array [1..l] of  integer.  Найти  количество  общих
элементов в этих массивах (т. е. количество тех целых t, для ко-
торых  t = x[i] = y[j] для некоторых i и j). (Число действий по-
рядка k+l.)

     Решение.

  k1:=0; l1:=0; n:=0;
  {инвариант: 0<=k1<=k; 0<=l1<=l; искомый ответ = n + количество
   общих элементов в x[k1+1]...x[k] и y[l1+1]..y[l]}
  while (k1 <> k) and (l1 <> l) do begin
  | if x[k1+1] < y[l1+1] then begin
  | | k1 := k1 + 1;
  | end else if x[k1+1] > y[l1+1] then begin
  | | l1 := l1 + 1;
  | end else begin {x[k1+1] = y[l1+1]}
  | | k1 := k1 + 1;
  | | l1 := l1 + 1;
  | | n := n + 1;
  | end;
  end;
  {k1 = k или l1 = l, поэтому одно из множеств, упомянутых в
   инварианте, пусто, а n равно искомому ответу}

Замечание. В третьей альтернативе достаточно было бы увеличивать
одну из переменных k1, l1; вторая добавлена для симметрии.

     1.2.17.  Решить  предыдущую задачу, если известно лишь, что
x[1] <= ... <= x[k] и y[1] <= ... <= y[l] (возрастание  заменено
неубыванием).

     Решение.  Условие  возрастания  было использовано в третьей
альтернативе выбора: сдвинув k1 и l1 на 1, мы тем самым уменьша-
ли  на  1  количество  общих  элементов   в   x[k1+1]...x[k]   и
x[l1+1]...x[l]. Теперь это придется делать сложнее.

          ...
          end else begin {x[k1+1] = y[l1+1]}
          | t := x [k1+1];
          | while (k1<k) and (x[k1+1]=t) do begin
          | | k1 := k1 + 1;
          | end;
          | while (l1<l) and (x[l1+1]=t) do begin
          | | l1 := l1 + 1;
          | end;
          end;

     Замечание. Эта программа имеет дефект: при проверке условия
                  (l1<l) and (x[l1+1]=t)
(или второго, аналогичного) при ложной первой скобке вторая ока-
жется бессмысленной (индекс выйдет за границы массива) и возник-
нет ошибка. Некоторые версии паскаля, вычисляя (A and B), снача-
ла  вычисляют  A и при ложном A не вычисляют B. (Так ведет себя,
например, система Turbo Pascal, 5.0 - но не 3.0.) Тогда  описан-
ная ошибка не возникнет.
     Но если мы не хотим полагаться на такое свойство  использу-
емой  нами  реализации  паскаля  (не предусмотренное его автором
Н.Виртом), то можно поступить так. Введем  дополнительную  пере-
менную b: boolean и напишем:

  if k1 < k  then b := (x[k1+1]=t)  else  b:=false;
  {b = (k1<k) and (x[k1+1] = t}
  while  b  do  begin
  | k1:=k1+1;
  | if k1 < k then b := (x[k1+1]=t) else b:=false;
  end;

Можно также сделать иначе:

          end else begin {x[k1+1] = y[l1+1]}
          | if k1 + 1 = k then begin
          | | k1 := k1 + 1;
          | | n := n + 1;
          | end else if x[k1+1] = x [k1+2] then begin
          | | k1 := k1 + 1;
          | end else begin
          | | k1 := k1 + 1;
          | | n := n + 1;
          | end;
          end;

Так будет короче, хотя менее симметрично.

     Наконец, можно увеличить размер  массива  в  его  описании,
включив  в  него  фиктивные элементы.

     1.2.18. Даны два неубывающих массива  x:  array  [1..k]  of
integer и y: array [1..l] of integer. Найти число различных эле-
ментов  среди  x[1],...,x[k], y[1],...,y[l]. (Число действий по-
рядка k+l.)

     1.2.19.  Даны два массива x[1] <= ... <= x[k] и y[1] <= ...
<= y[l]. "Соединить" их в массив z[1] <= ... <= z[m] (m  =  k+l;
каждый  элемент  должен  входить в массив z столько раз, сколько
раз он входит в общей сложности в массивы x и y). Число действий
порядка m.

     Решение.

  k1 := 0; l1 := 0;
  {инвариант: ответ получится, если к  z[1]..z[k1+l1]  приписать
   справа соединение массивов x[k1+1]..x[k] и y[l1+1]..y[l]}
  while (k1 <> k) or (l1 <> l) do begin
  | if k1 = k then begin
  | | {l1 < l}
  | | l1 := l1 + 1;
  | | z[k1+l1] := y[l1];
  | end else if l1 = l then begin
  | | {k1 < k}
  | | k1 := k1 + 1;
  | | z[k1+l1] := x[k1];
  | end else if x[k1+1] <= y[l1+1] then begin
  | | k1 := k1 + 1;
  | | z[k1+l1] := x[k1];
  | end else if x[k1+1] >= y[l1+1] then begin
  | | l1 := l1 + 1;
  | | z[k1+l1] := y[l1];
  | end else begin
  | | { такого не бывает }
  | end;
  end;
  {k1 = k, l1 = l, массивы соединены}

Этот  процесс  можно  пояснить  так. Пусть у нас есть две стопки
карточек, отсортированных по алфавиту. Мы соединяем  их  в  одну
стопку,  выбирая каждый раз ту из верхних карточек обеих стопок,
которая идет раньше в алфавитном порядке.

     1.2.20. Даны два массива x[1] <= ... <= x[k] и y[1] <=  ...
<=  y[l].  Найти  их  "пересечение",  т.е. массив z[1] <= ... <=
z[m], содержащий их общие  элементы,  причем  кратность  каждого
элемента в массиве z равняется минимуму из его кратностей в мас-
сивах x и y. Число действий порядка k+l.

     1.2.21. Даны два массива x[1]<=...<=x[k] и  y[1]<=...<=y[l]
и  число q. Найти сумму вида x[i]+y[j], наиболее близкую к числу
q. (Число действий порядка k+l, дополнительная память - фиксиро-
ванное число целых переменных, сами массивы менять не разрешает-
ся.)
     Указание. Надо найти минимальное расстояние между элемента-
ми x[1]<=...<=x[k] и q-y[l]<=..<=q-y[1], что нетрудно сделать  в
ходе их слияния в один (воображаемый) массив.

     1.2.22. (из книги Д.Гриса) Некоторое  число  содержится  в
каждом из трех целочисленных неубывающих массивов x[1] <= ... <=
x[p],  y[1]  <=  ... <= y[q], z[1] <= ... <= z[r]. Найти одно из
таких чисел. Число действий должно быть порядка p + q + r.

     Решение.

  p1:=1; q1=1; r1:=1;
  {инвариант: x[p1]..x[p], y[q1]..y[q], z[r1]..z[r]
   содержат общий элемент }
  while not ((x[p1]=y[q1]) and (y[q1]=z[r1])) do begin
  | if x[p1]<y[q1] then begin
  | | p1:=p1+1;
  | end else if y[q1]<z[r1] then begin
  | | q1:=q1+1;
  | end else if z[r1]<x[p1] then begin
  | | r1:=r1+1;
  | end else begin
  | | { так не бывает }
  | end;
  end;
  {x[p1] = y[q1] = z[r1]}
  writeln (x[p1]);

     1.2.23. Та же задача, только заранее не известно, существу-
ет  ли общий элемент в трех неубывающих массивах и требуется это
выяснить (и найти один из общих элементов, если они есть).

     1.2.24. Элементами  массива  a[1..n]  являются  неубывающие
массивы  [1..m]  целых чисел (a: array [1..n] of array [1..m] of
integer; a[1][1] <= ... <=  a[1][m],  ...,  a[n][1]  <=  ...  <=
a[n][m]). Известно, что существует число, входящее во все масси-
вы  a[i]  (существует  такое  х,  что  для  всякого  i из [1..n]
найдётся j из [1..m], для которого a[i][j]=x). Найти одно из та-
ких чисел х.

     Решение. Введем массив b[1]..b[n], отмечающий начало "оста-
ющейся части" массивов a[1]..a[n].

  for k:=1 to n do begin
  |  b[k]:=1;
  end;
  eq := true;
  for k := 2 to n do begin
  | eq := eq and (a[1][b[1]] = a[k][b[k]]);
  end;
  {инвариант: оставшиеся части  пересекаются,  т.е.  существует
   такое  х,  что для всякого i из [1..n] найдётся j из [1..m],
   не меньшее b[i], для которого a[i][j] =  х;  eq  <=>  первые
   элементы оставшихся частей равны}
  while not eq do begin
  | s := 1; k := 1;
  | {a[s][b[s]] - минимальное среди a[1][b[1]]..a[k][b[k]]}
  | while k <> n do begin
  | | k := k + 1;
  | | if a[k][b[k]] < a[s][b[s]] then begin
  | | | s := k;
  | | end;
  | end;
  | {a[s][b[s]] - минимальное среди a[1][b[1]]..a[n][b[n]]}
  | b [s] := b [s] + 1;
  | for k := 2 to n do begin
  | | eq := eq and (a[1][b[1]] = a[k][b[k]]);
  | end;
  end;
  writeln (a[1][b[1]]);

     1.2.25.  Приведенное  решение предыдущей задачи требует по-
рядка m*n*n действий. Придумать способ с числом действий порядка
m*n.
     Указание.  Придется  пожертвовать симметрией и выбрать одну
из строк за основную. Двигаясь по основной строке,  поддерживаем
такое  соотношение:  во  всех  остальных  строках отмечен макси-
мальный элемент, не  превосходящий  текущего  элемента  основной
строки.

     1.2.26. (Двоичный поиск) Дана  последовательность  x[1]  <=
...  <=  x[n] целых чисел и число a. Выяснить, содержится ли a в
этой последовательности, т. е. существует ли i из 1..n, для  ко-
торого x[i]=a. (Количество действий порядка log n.)

     Решение. (Предполагаем, что n > 0.)

  l := 1; r := n+1;
  {если a есть вообще, то есть и среди x[l]..x[r-1], r > l}
  while r - l <> 1 do begin
  | m := l + (r-l) div 2 ;
  | {l < m < r }
  | if x[m] <= a then begin
  | | l := m;
  | end else begin {x[m] > a}
  | | r := m;
  | end;
  end;
(Обратите внимание, что и в случае x[m] = a инвариант не наруша-
ется.)
     Каждый раз r-l уменьшается примерно вдвое, откуда и вытека-
ет требуемая оценка числа действий.
     Замечание.
l + (r-l) div 2 = (2l + (r-l)) div 2 = (r+l) div 2.

     1.2.27. (Из книги Д.Гриса) Дан массив x:  array  [1..n]  of
array  [1..m]  of  integer,  упорядоченный  по  "строкам"  и  по
"столбцам":
         x[i][j] <= x[i+1][j],
         x[i][j] <= x[i][j+1]
и число a. Требуется выяснить, встречается ли a среди x[i][j].

     Решение. Представляя себе  массив  a  как  матрицу  (прямо-
угольник,  заполненный числами), мы выберем прямоугольник, в ко-
тором только и может содержаться a, и будем его  сужать.  Прямо-
угольник этот будет содержать x[i][j] при 1<=i<=l и k<=j<=m.
                1                     k         m
               -----------------------------------
              1|                     |***********|
               |                     |***********|
               |                     |***********|
              l|                     |***********|
               |---------------------------------|
               |                                 |
              n|                                 |
               -----------------------------------
(допускаются пустые прямоугольники при l = 0 и k = m+1).

  l:=n; k:=1;
  {l>=0, k<=m+1, если a есть, то в описанном прямоугольнике}
  while (l > 0) and (k < m+1) and (x[l][k] <> a) do begin
  | if x[l][k] < a then begin
  | | k := k + 1; {левый столбец не содержит a, удаляем его}
  | end else begin {x[l][k] > a}
  | | l := l - 1; {нижняя строка не содержит a, удаляем ее}
  | end;
  end;
  {x[l][k] = a или прямоугольник пуст }
  answer:= (l > 0) and (k < m+1) ;

     Замечание.  Здесь та же ошибка: x[l][k] может оказаться не-
определенным. (Её исправление предоставляется читателю.)

     1.2.28. (Московская олимпиада по программированию) Дан не-
убывающий массив положительных целых чисел a[1] <= a[2]  <=...<=
a[n].  Найти наименьшее целое положительное число, не представи-
мое в виде суммы нескольких элементов этого массива (каждый эле-
мент массива может быть использован не более одного раза). Число
действий порядка n.

     Решение. Пусть известно, что  числа,  представимые  в  виде
суммы элементов a[1],...,a[k], заполняют отрезок от 1 до некото-
рого N. Если a[k+1] > N+1, то N+1 и будет минимальным числом, не
представимым  в виде суммы элементов массива a[1]..a[n]. Если же
a[k+1] <= N+1, то числа, представимые  в  виде  суммы  элементов
a[1]..a[k+1], заполняют отрезок от 1 до N+a[k+1].

  k := 0; N := 0;
  {инвариант: числа, представимые в виде суммы элементов массива
   a[1]..a[k], заполняют отрезок 1..N}
  while (k <> n) and (a[k+1] <= N+1) do begin
  | N := N + a[k+1];
  | k := k + 1;
  end;
  {(k = n) или (a[k+1] > N+1); в обоих случаях ответ N+1}
  writeln (N+1);

(Снова тот же дефект: в условии цикла при ложном первом  условии
второе не определено.)

     1.2.29.  (Для  знакомых с основами алгебры) В целочисленном
массиве a[1]..a[n] хранится перестановка чисел 1..n  (каждое  из
чисел встречается по одному разу).
     (а) Определить четность перестановки. (И в (а), и в (б) ко-
личество действий порядка n.)
     (б)  Не используя других массивов, заменить перестановку на
обратную (если до работы программы a[i]=j, то после должно  быть
a[j]=i).

     Указание.  (а)  Четность  перестановки  определяется  коли-
чеством циклов. Чтобы отличать уже пройденные циклы, у  их  эле-
ментов можно, например, менять знак. (б) Обращение производим по
циклам.

     1.2.30. Дан массив a[1..n] и число b. Переставить  числа  в
массиве  таким  образом, чтобы слева от некоторой границы стояли
числа, меньшие или равные b, а справа от границы -  большие  или
равные b.

     Решение.

        l:=0; r:=n;
        {инвариант: a[1]..a[l]<=b; a[r+1]..a[n]>=b}
        while l <> r do begin
        | if a[l+1] <= b then begin
        | | l:=l+1;
        | end else if a[r] >=b then begin
        | | r:=r-1;
        | end else begin {a[l+1]>b; a[r]<b}
        | | поменять a[l+1] и  a[r]
        | | l:=l+1; r:+r-1;
        | end;
        end;

     1.2.31. Та же задача, но требуется, чтобы сначала шли  эле-
менты,  меньшие  b, затем равные b, а лишь затем большие b.

     Решение.  Теперь  потребуются  три границы: до первой будут
идти элементы, меньшие b, от первой до второй - равные b,  затем
неизвестно какие до третьей, а после третьей - большие b. (Более
симметричное  решение использовало бы четыре границы, но вряд ли
игра стоит свеч.) В качестве очередного рассматриваемого элемен-
та берем элемент справа от средней границы.

     l:=0; m:=0; r:=n;
     {инвариант: a[1..l]<b; a[l+1..m]=b; a[r+1]..a[n]>b}
     while m <> r do begin
     | if a[m+1]=b then begin
     | | m:=m+1;
     | end else if a[m+1]>b then begin
     | | обменять a[m+1] и a[r]
     | | r:=r-1;
     | end else begin {a[m+1]<b}
     | | обменять a[m+1] и a[l+1]
     | | l:=l+1; m:=m+1;
     end;

     1.2.32.  (вариант  предыдущей  задачи,  названный  в  книге
Дейкстры задачей о голландском флаге) В массиве стоят числа 0, 1
и  2.  Переставить  их  в порядке возрастания, если единственной
разрешенной операцией (помимо чтения) над массивом является  пе-
рестановка двух элементов.

     1.2.33. Дан массив a[1]..a[n]  и  число  m<=n.  Для  каждой
группы  из m стоящих рядом членов (таких групп, очевидно, n-m+1)
вычислить ее сумму. Общее число действий должно быть порядка n.

     Решение.  Переходя  от группы к соседней, мы добавляем один
член, а другой вычитаем.

     1.2.34. Дана квадратная таблица a[1..n][1..n] и число m<=n.
Для каждого квадрата размера m на m  в  этой  таблице  вычислить
сумму  стоящих в нем чисел. Общее число действий должно быть по-
рядка n*n.

     Решение. Сначала для каждого горизонтального прямоугольника
размером n на 1 вычисляем сумму стоящих в нем чисел. (При сдвиге
такого  прямоугольника  по  горизонтали на 1 нужно добавить одно
число и одно вычесть.) Затем,  используя  эти  суммы,  вычисляем
суммы в квадратах. (При сдвиге квадрата по вертикали добавляется
полоска, а другая полоска убавляется.)

     1.3. Индуктивные функции (по А.Г.Кушниренко).

     Пусть M - некоторое множество. Функция f, аргументами кото-
рой являются последовательности элементов множества M, а  значе-
ниями - элементы некоторого множества N, называется индуктивной,
если  ее значение на последовательности x[1]..x[n] можно восста-
новить по ее значению на последовательности  x[1]..x[n-1]  и  по
x[n],  т.  е.  если  существует  функция F из N*M (множество пар
<n,m>, где n - элемент множества N, а m - элемент множества M) в
N, для которой

      f(<x[1],...,x[n]>) = F (f (<x[1],...,x[n-1]>), x[n]).

     Схема алгоритма вычисления индуктивной функции:

  k := 0; f := f0;
  {инвариант: f - значение функции на <x[1],...,x[k]>}
  while  k<> n do begin
  | k := k + 1;
  | f := F (f, x[k]);
  end;

     Здесь f0 - значение функции  на  пустой  последовательности
(последовательности  длины  0). Если функция f определена только
на непустых последовательностях, то первая строка заменяется  на
"k := 1; f := f (<x[1]>);".

     Индуктивные расширения.

     Если функция f не является индуктивной, полезно  искать  ее
индуктивное  расширение  - такую индуктивную функцию g, значения
которой определяют значения f (это значит, что существует  такая
функция  t,  что  f  (<x[1]...x[n]>) = t (g (<x[1]...x[n]>)) при
всех <x[1]...x[n]>). Можно доказать, что среди всех  индуктивных
расширений  существует  минимальное  расширение F (минимальность
означает, что для любого индуктивного расширения  g  значения  F
определяются значениями g).

     1.3.1.  Указать  индуктивные  расширения   для   следующих
функций:
   а)  среднее  арифметическое  последовательности вещественных
чисел;
   б) число элементов последовательности целых чисел, равных ее
максимальному элементу;
   в)  второй по величине элемент последовательности целых чисел
(тот, который будет вторым, если переставить члены в неубывающем
порядке);
   г) максимальное число идущих подряд одинаковых элементов;
   д) максимальная длина монотонного (неубывающего  или  невоз-
растающего)  участка  из  идущих  подряд элементов в последова-
тельности целых чисел;
   е) число групп из единиц, разделенных нулями  (в  последова-
тельности нулей и единиц).

     Решение.

а) <сумма всех членов последовательности; длина>;

б)  <число  элементов,  равных  максимальному;  значение макси-
     мального>;

в) <наибольший элемент последовательности; второй  по  величине
     элемент>;

г) <максимальное число идущих подряд одинаковых элементов; чис-
     ло  идущих  подряд одинаковых элементов в конце последова-
     тельности; последний элемент последовательности>;

д) <максимальная длина монотонного участка; максимальная  длина
      неубывающего  участка  в конце последовательности; макси-
      мальная длина невозрастающего участка в конце  последова-
      тельности; последний член последовательности>;

е) <число групп из единиц, последний член>.

     1.3.2. (Сообщил Д.Варсонофьев.) Даны две последовательности
x[1]..x[n] и y[1]..y[k] целых чисел. Выяснить, является ли  вто-
рая последовательность подпоследовательностью первой, т. е. мож-
но  ли  из первой вычеркнуть некоторые члены так, чтобы осталась
вторая. Число действий порядка n+k.

       Решение.  (1  вариант)  Будем  сводить  задачу  к  задаче
меньшего размера.

  n1:=n;
  k1:=k;
  {инвариант:  искомый ответ <=> возможность из x[1]..x[n1] по-
   лучить y[1]..y[k1] }
  while (n1 > 0) and (k1 > 0) do begin
  | if x[n1] = y[k1] then begin
  | | n1 := n1 - 1;
  | | k1 := k1 - 1;
  | end else begin
  | | n1 := n1 - 1;
  | end;
  end;
  {n1 = 0 или k1 = 0; если k1 = 0, то ответ - да, если k1 <>  0
   (и n1 = 0), то ответ - нет}
  answer := (k1 = 0);

     Мы использовали то, что если x[n1] = y[k1] и y[1]..y[k1] -
подпоследовательность x[1]..x[n1], то y[1]..y[k1-1] - подпосле-
довательность x[1]..x[n1-1].

     (2  вариант)  Функция x[1]..x[n1] |-> (максимальное k1, для
которого y[1]..y[k1] есть подпоследовательность x[1]..x[n1]) ин-
дуктивна.

     1.3.3. Даны две последовательности x[1]..x[n] и  y[1]..y[k]
целых  чисел. Найти максимальную длину последовательности, явля-
ющейся подпоследовательностью обеих  последовательностей.  Коли-
чество операций порядка n*k.

     Решение  (сообщено М.Н.Вайнцвайгом, А.М.Диментманом). Обоз-
начим через  f(n1,k1)  максимальную  длину  общей  подпоследова-
тельности последовательностей x[1]..x[n1] и y[1]..y[k1]. Тогда

   x[n1] <> y[k1] => f(n1,k1) = max (f(n1,k1-1), f(n1-1,k1));
   x[n1] = y[k1]  => f(n1,k1) = max (f(n1,k1-1), f(n1-1,k1),
                              f(n1-1,k1-1)+1 );

(Поскольку  f(n1-1,k1-1)+1  >= f(n1,k1-1), f(n1-1,k1), во втором
случае максимум трех чисел можно заменить на третье из них.)
     Поэтому можно заполнять таблицу значений функции f, имеющую
размер n*k. Можно обойтись и памятью порядка k (или n), если ин-
дуктивно  (по  n1) выписать <f(n1,0), ..., f(n1,k)> (как функция
от n1 этот набор индуктивен).

     1.3.4 (из книги Д.Гриса) Дана последовательность целых  чи-
сел  x[1],...,  x[n].  Найти  максимальную длину ее возрастающей
подпоследовательности (число действий порядка n*log(n)).

     Решение. Искомая функция не индуктивна, но имеет  следующее
индуктивное  расширение: в него входит помимо максимальной длины
возрастающей подпоследовательности (обозначим ее k) также и чис-
ла u[1],...,u[k], где u[i] = (минимальный  из  последних  членов
возрастающих  подпоследовательностей длины i). Очевидно, u[1] <=
... <= u[k]. При добавлении нового члена x значения u и  k  кор-
ректируются.

  n1 := 1; k := 1; u[1] := x[1];
  {инвариант: k и u соответствуют данному выше описанию}
  while n1 <> n do begin
  | n1 := n1 + 1;
  | ...
  | {i - наибольшее из тех чисел отрезка 1..k, для кото-
  |   рых u[i] < x[n1]; если таких нет, то i=0 }
  | if i = k then begin
  | | k := k + 1;
  | | u[k+1] := x[n1];
  | end else begin {i < k, u[i] < x[n1] <= u[i+1] }
  | | u[i+1] := x[n1];
  | end;
  end;

     Фрагмент ... использует идею двоичного поиска; в инвариан-
те условно полагаем u[0] равным минус бесконечности, а  u[k+1]
- плюс бесконечности; наша цель: u[i] < x[n1] <= u[i+1].

  i:=0; j:=k+1;
  {u[i] < x[n1] <= u[j], j > i}
  while (j - i) <> 1 do begin
  | s := i + (j-i) div 2;    {i < s < j}
  | if u[s] >= x[n1] then begin
  | | j := s;
  | end else begin {u[s] < x[n1]}
  | | i := s;
  | end;
  end;
  {u[i] < x[n1] <= u[j], j-i = 1}

     Замечание.  Более  простое  (но не минимальное) индуктивное
расширение получится, если для каждого  i  хранить  максимальную
длину   возрастающей  подпоследовательности,  оканчивающейся  на
x[i]. Это расширение приводит к алгоритму с числом действий  по-
рядка n*n.

     1.3.5.  Какие  изменения  нужно внести в решение предыдущей
задачи, если надо  искать  максимальную  неубывающую  последова-
тельность?
     Глава 2. Порождение комбинаторных объектов.

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

     2.1. Размещения с повторениями.

     2.1.1. Напечатать все последовательности длины k  из  чисел
1..n.

     Решение.  Будем  печатать  их  в лексикографическом порядке
(последовательность a предшествует  последовательности  b,  если
для  некоторого s их начальные отрезки длины s равны, а (s+1)-ый
член  последовательности  a  меньше).  Первой  будет  последова-
тельность  <1, 1, ..., 1>, последней - последовательность <n, n,
..., n>. Будем хранить последнюю напечатанную последовательность
в массиве x[1]...x[k].

        ...x[1]...x[k] положить равным 1
        ...напечатать x
        ...last[1]...last[k] положить равным n
        while x <> last do begin
        | ...x := следующая за x последовательность
        | ...напечатать x
        end;

     Опишем, как можно  перейти  от  x  к  следующей  последова-
тельности.  Согласно определению, у следующей последовательности
первые s членов должны быть такими же, а (s+1)-ый - больше.  Это
возможно, если x[s+1] было меньше n. Среди таких s нужно выбрать
наибольшее  (иначе полученная последовательность не будет непос-
редственно следующей). Соответствующее x[s+1] нужно увеличить на
1. Итак, надо, двигаясь с конца последовательности, найти  самый
правый  член,  меньший  n (он найдется, так как по предположению
x<>last), увеличить его на 1, а идущие  за  ним  члены  положить
равными 1.

        p:=k;
        while not (x[p] < n) do begin
        | p := p-1;
        end;
        {x[p] < n, x[p+1] =...= x[k] = n}
        x[p] := x[p] + 1;
        for i := p+1 to k do begin
        | x[i]:=1;
        end;

     Замечание. Если членами последовательности считать числа не
от  1 до n, а от 0 до n-1, то переход к следующему соответствует
прибавлению 1 в n-ичной системе счисления.

     2.1.2. В предложенном алгоритме используется сравнение двух
массивов x <> last. Устранить его, добавив булевскую  переменную
l и включив в инвариант соотношение l <=> последовательность x -
последняя.

     2.1.3. Напечатать все подмножества множества {1...k}.

     Решение.  Подмножества находятся во взаимно однозначном со-
ответствии с последовательностями нулей и единиц длины k.

     2.1.4. Напечатать все последовательности из k положительных
целых чисел, у которых i-ый член не превосходит i.

     2.2. Перестановки.

     2.2.1. Напечатать все перестановки чисел 1..n (то есть пос-
ледовательности  длины  n, в которые каждое из чисел 1..n входит
по одному разу).

     Решение. Перестановки будем  хранить  в  массиве  x[1],...,
x[n]  и  печатать в лексикографическом порядке. (Первой при этом
будет перестановка <1 2...n>, последней - <n...2 1>.)  Для  сос-
тавления  алгоритма  перехода к следующей перестановке зададимся
вопросом: в каком случае k-ый член перестановки можно увеличить,
не меняя предыдущих? Ответ: если он меньше какого-либо из следу-
ющих членов (членов с номерами больше k). Мы  должны  найти  на-
ибольшее  k,  при  котором  это  так,  т. е. такое k, что x[k] <
x[k+1] > ... > x[n]. После  этого  x[k]  нужно  увеличить  мини-
мальным  возможным способом, т. е. найти среди x[k+1], ..., x[n]
наименьшее число, большее его. Поменяв x[k] с ним, остается рас-
положить числа с номерами k+1, ..., n  так,  чтобы  перестановка
была наименьшей, то есть в возрастающем порядке. Это облегчается
тем, что они уже расположены в убывающем порядке.

     Алгоритм перехода к следующей перестановке.

  {<x[1],...,x[n-1], x[n]> <> <n,...,2, 1>.}
  k:=n-1;
  {последовательность справа от k убывающая: x[k+1] >...> x[n]}
  while x[k] > x[k+1] do begin
  | k:=k-1;
  end;
  {x[k] < x[k+1] > ... > x[n]}
  t:=k+1;
  {t <=n, x[k+1] > ... > x[t] > x[k]}
   while (t < n) and (x[t+1] > x[k]) do begin
   | t:=t+1;
   end;
   {x[k+1] > ... > x[t] > x[k] > x[t+1] > ... > x[n]}
   ... обменять x[k] и x[t]
   {x[k+1] > ... > x[n]}
   ... переставить участок x[k+1] ... x[n] в обратном порядке

Замечание. Программа имеет знакомый  дефект:  если  t  =  n,  то
x[t+1] не определено.

     2.2.2. Модифицировать алгоритм перехода к следующей  перес-
тановке так, чтобы он сам проверял, не является ли данная перес-
тановка последней.

     2.3. Подмножества.

     2.3.1. Перечислить все k-элементные подмножества  множества
{1..n}.

     Решение.  Будем представлять каждое подмножество последова-
тельностью x[1]..x[n] нулей и единиц длины n, в которой ровно  k
единиц. (Другой способ представления разберем позже.) Такие пос-
ледовательности упорядочим лексикографически (см. выше). Очевид-
ный  способ  решения  задачи - перебирать все последовательности
как раньше, а затем отбирать среди них те, у которых k единиц  -
мы отбросим, считая его неэкономичным (число последовательностей
с  k  единицами  может  быть  много меньше числа всех последова-
тельностей). Будем искать такой алгоритм, чтобы  получение  оче-
редной последовательности требовало порядка n действий.
     В каком случае s-ый член  последовательности  можно  увели-
чить,  не  меняя предыдущие? Если x[s] меняется с 0 на 1, то для
сохранения общего числа единиц нужно справа от х[s]  заменить  1
на 0. Таким образом, х[s] - первый справа нуль, за которым стоят
единицы.  Легко  видеть,  что х[s+1] = 1 (иначе х[s] не первый).
Таким образом надо искать наибольшее  s,  для  которого  х[s]=0,
x[s+1]=1;

                  ______________________
               x |________|0|1...1|0...0|
                           s

За х[s+1] могут идти еще несколько единиц, а после них несколько
нулей. Заменив х[s] на 1, надо выбрать идущие за ним члены  так,
чтобы последовательность была бы минимальна с точки зрения наше-
го  порядка,  т. е. чтобы сначала шли нули, а потом единицы. Вот
что получается:

  первая последовательность    0...01...1 (n-k нулей, k единиц)
  последняя последовательность 1...10...0 (k единиц, n-k нулей)

  алгоритм перехода к следующей за х[1]...x[n] последовательнос-
  ти (предполагаем, что она есть):

        s := n - 1;
        while not ((x[s]=0) and (x[s+1]=1)) do begin
        | s := s - 1;
        end;
        {s - член, подлежащий изменению с 0 на 1}
        num:=0;
        for k := s to n do begin
        | num := num + x[k];
        end;
        {num - число единиц на участке x[s]...x[n], число нулей
         равно (длина - число единиц), т. е. (n-s+1) - num}
        x[s]:=1;
        for k := s+1 to n-num+1 do begin
        | x[k] := 0;
        end;
        for k := n-num+2 to n do begin
        | x[k]:=1;
        end;

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

     2.3.2. Перечислить все возрастающие последовательности дли-
ны  k  из  чисел 1..n в лексикографическом порядке. (Пример: при
n=5, k=2 получаем 12 13 14 15 23 24 25 34 35 45.)

     Решение. Минимальной будет последовательность 1, 2, ..., k;
максимальной - (n-k+1),..., (n-1), n. В каком случае  s-ый  член
последовательности можно увеличить? Ответ: если он меньше n-k+s.
После увеличения s-го элемента все следующие должны возрастать с
шагом 1. Получаем такой алгоритм перехода к следующему:

        s:=n;
        while not (x[s] < n-k+s) do begin
        | s:=s-1;
        end;
        {s - элемент, подлежащий увеличению};
        x[s] := x[s]+1;
        for i := s+1 to n do begin
        | x[i] := x[i-1]+1;
        end;

     2.3.3.  Пусть  мы  решили представлять k-элементные подмно-
жества множества {1..n} убывающими последовательностями длины k,
упорядоченными по-прежнему лексикографически. (Пример : 21 31 32
41 42 43 51 52 53 54.) Как выглядит тогда  алгоритм  перехода  к
следующей?

     Ответ. Ищем наибольшее s, для которого х[s]-x[s+1]>1. (Если
такого s нет, полагаем s = 0.) Увеличив x [s+1] на 1, кладем ос-
тальные минимально возможными (x[t] = k+1-t для t>s).

     2.3.4. Решить две предыдущие задачи, заменив  лексикографи-
ческий  порядок  на  обратный  (раньше идут те, которые больше в
лексикографическом порядке).

     2.3.5. Перечислить все вложения (функции, переводящие  раз-
ные  элементы в разные) множества {1..k} в {1..n} (предполагает-
ся, что k <= n). Порождение очередного элемента должно требовать
порядка k действий.

     Указание.  Эта  задача  может  быть  сведена к перечислению
подмножеств и перестановок элементов каждого подмножества.

     2.4. Разбиения.

     2.4.1. Перечислить все разбиения целого положительного чис-
ла  n  на целые положительные слагаемые (разбиения, отличающиеся
лишь порядком слагаемых, считаются за одно). (Пример: n=4,  раз-
биения 1+1+1+1, 2+1+1, 2+2, 3+1, 4.)

     Решение. Договоримся, что (1) в разбиениях слагаемые идут в
невозрастающем порядке, (2) сами разбиения мы перечисляем в лек-
сикографическом  порядке.  Разбиение  храним  в  начале  массива
x[1]...x[n], при этом количество входящих в него чисел обозначим
k. В начале x[1]=...=x[n]=1, k=n, в конце x[1]=n, k=1.
     В  каком  случае  x[s] можно увеличить не меняя предыдущих?
Во-первых, должно быть x[s-1] > x[s] или s  =  1.  Во-вторых,  s
должно  быть не последним элементом (увеличение s надо компенси-
ровать уменьшением следующих). Увеличив s, все следующие элемен-
ты надо взять минимально возможными.

        s := k - 1;
        while not ((s=1) or (x[s-1] > x[s])) do begin
        | s := s-1;
        end;
        {s - подлежащее увеличению слагаемое}
        x [s] := x[s] + 1;
        sum := 0;
        for i := s+1 to k do begin
        | sum := sum + x[i];
        end;
        {sum - сумма членов, стоявших после x[s]}
        for i := 1 to sum-1 do begin
        | x [s+i] := 1;
        end;
        k := s+sum-1;

     2.4.2. Представляя по-прежнему разбиения как невозрастающие
последовательности, перечислить их в порядке, обратном лексиког-
рафическому (для n=4, например, должно получиться 4,  3+1,  2+2,
2+1+1, 1+1+1+1).
     Указание. Уменьшать можно первый справа член, не равный  1;
найдя  его,  уменьшим на 1, а следующие возьмем максимально воз-
можными  (равными ему, пока хватает суммы, а последний - сколько
останется).

     2.4.3. Представляя  разбиения  как  неубывающие  последова-
тельности,  перечислить  их в лексикографическом порядке. Пример
для n=4: 1+1+1+1, 1+1+2, 1+3, 2+2, 4;
     Указание. Последний член увеличить нельзя, а  предпоследний
- можно; если после увеличения на 1 предпоследнего члена за счет
последнего нарушится возрастание, то из двух членов надо сделать
один,  если  нет,  то  последний член надо разбить на слагаемые,
равные предыдущему, и остаток, не меньший его.

     2.4.4.  Представляя  разбиения  как  неубывающие последова-
тельности, перечислить их в порядке, обратном лексикографическо-
му. Пример для n=4: 4, 2+2, 1+3, 1+1+2, 1+1+1+1.
     Указание.  Чтобы элемент x[s] можно было уменьшить, необхо-
димо, чтобы s = 1 или x[s-1] < x[s]. Если x[s] не последний,  то
этого и достаточно. Если он последний, то нужно, чтобы x[s-1] <=
(целая часть (x[s]/2)) или s=1.

     2.5. Коды Грея и аналогичные задачи.

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

     2.5.1.  Перечислить все последовательности длины n из чисел
1..k в таком порядке, чтобы каждая следующая отличалась от  пре-
дыдущей в единственной цифре, причем не более, чем на 1.

     Решение. Рассмотрим прямоугольную доску ширины n  и  высоты
k.  На каждой вертикали будет стоять шашка. Таким образом, поло-
жения шашек соответствуют последовательностям из чисел 1..k дли-
ны n (s-ый член последовательности соответствует высоте шашки на
s-ой горизонтали). На каждой шашке нарисуем  стрелочку,  которая
может быть направлена вверх или вниз. Вначале все шашки поставим
на  нижнюю  горизонталь стрелочкой вверх. Далее двигаем шашки по
такому правилу: найдя самую правую шашку, которую  можно  подви-
нуть  в направлении (нарисованной на ней) стрелки, двигаем ее на
одну клетку в этом направлении, а все стоящие  правее  ее  шашки
(они уперлись в край) разворачиваем кругом.
     Ясно, что на каждом шаге только одна шашка сдвигается, т.е.
один член последовательности меняется на 1. Докажем индукцией по
n,  что проходятся все последовательности из чисел 1...k. Случай
n = 1 очевиден. Пусть n > 1. Все ходы поделим на те, где  двига-
ется  последняя шашка, и те, где двигается не последняя. Во вто-
ром случае последняя шашка стоит у стены, и мы ее  поворачиваем,
так  что  за каждым ходом второго типа следует k-1 ходов первого
типа, за время которых последняя шашка побывает во всех клетках.
Если мы теперь забудем о последней шашке, то движения первых n-1
по предположению индукции пробегают все последовательности длины
n-1 по одному разу; движения же последней шашки из каждой после-
довательности длины n-1 делают k последовательностей длины n.
     В  программе,  помимо последовательности x[1]...x[n], будем
хранить массив d[1]...d[n] из чисел +1 и  -1  (+1  соответствует
стрелке вверх, -1 -стрелке вниз).

Начальное состояние: x[1] =...= x[n] = 1; d[1] =...= d[n] = 1.

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

  {если можно, сделать шаг и положить p := true, если нет,
   положить p := false }
  i := n;
  while (i > 1) and
  | (((d[i]=1) and (x[i]=n)) or ((d[i]=-1) and (x[i]=1)))
  |   do begin
  | i:=i-1;
  end;
  if (d[i]=1 and x[i]=n) or (d[i]=-1 and x[i]=1)
  |    then begin {i=1}
  | p:=false;
  end else begin
  | p:=true;
  | x[i] := x[i] + d[i];
  | for j := i+1 to n do begin
  | | d[j] := - d[j];
  | end;
  end;

     Замечание.  Для последовательностей нулей и единиц возможно
другое решение, использующее двоичную систему. (Именно оно  свя-
зывается обычно с названием "коды Грея".)
     Запишем подряд все числа от 0 до (2 в степени n) - 1 в дво-
ичной системе. Например, для n = 3 напишем:

            000 001 010 011 100 101 110 111

Затем  каждое из чисел подвергнем преобразованию, заменив каждую
цифру, кроме первой, на ее сумму с предыдущей цифрой (по  модулю
2). Иными словами, число

     a[1], a[2],...,a[n]  преобразуем в
     a[1], a[1] + a[2], a[2] + a[3],...,a[n-1] + a[n]

(сумма по модулю 2). Для n=3 получим:

            000 001 011 010  110  111 101 100.

     Легко проверить, что описанное преобразование чисел обрати-
мо (и тем самым дает все  последовательности  по  одному  разу).
Кроме  того,  двоичные  записи соседних чисел отличаются заменой
конца 011...1 на конец 100...0, что  -  после  преобразования  -
приводит к изменению единственной цифры.

     Применение кода Грея. Пусть есть вращающаяся ось, и мы  хо-
тим  поставить датчик угла поворота этой оси. Насадим на ось ба-
рабан, выкрасим половину барабана в черный цвет, половину в  бе-
лый и установим фотоэлемент. На его выходе будет в половине слу-
чаев  0,  а в половине 1 (т. е. мы измеряем угол "с точностью до
180").

     Развертка барабана:
                     0       1
             -> |_|_|_|_|*|*|*|*| <- (склеить бока).

     Сделав рядом другую дорожку из двух черных и белых частей и
поставив  второй фотоэлемент, получаем возможность измерить угол
с точностью до 90 градусов:

                   0   0   1   1
                   0   1   0   1
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|_|_|*|*|

Сделав третью,

                 0 0 0 0 1 1 1 1
                 0 0 1 1 0 0 1 1
                 0 1 0 1 0 1 0 1
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|_|_|*|*|
                |_|*|_|*|_|*|_|*|

мы  измерим угол с точностью до 45 градусов и т.д. Эта идея име-
ет, однако, недостаток: в момент пересечения границ  сразу  нес-
колько  фотоэлементов  меняют  сигнал, и если эти изменения про-
изойдут не одновременно, на какое-то время показания фотоэлемен-
тов будут бессмысленными.  Коды  Грея  позволяют  избежать  этой
опасности.  Сделаем так, чтобы на каждом шаге менялось показание
лишь одного фотоэлемента (в том числе и на последнем, после  це-
лого оборота).

                 0 0 0 0 1 1 1 1
                 0 0 1 1 1 1 0 0
                 0 1 1 0 0 1 1 0
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|*|*|_|_|
                |_|*|*|_|_|*|*|_|

     Написанная нами формула позволяет легко преобразовать  дан-
ные от фотоэлементов в двоичный код угла поворота.

     2.5.2. Напечатать все перестановки чисел  1..n  так,  чтобы
каждая   следующая   получалась   из   предыдущей  перестановкой
(транспозицией) двух соседних чисел. Например, при n = 3  допус-
тим такой порядок: 3.2 1 -> 2 3.1 -> 2.1 3 -> 1 2.3 -> 1.3 2  ->
3 1 2 (между переставляемыми числами вставлены точки).

     Решение. Наряду с множеством перестановок  рассмотрим  мно-
жество  последовательностей y[1]..y[n] целых неотрицательных чи-
сел, у которых y[1] <= 0,..., y[n] <= n-1. В нем столько же эле-
ментов, сколько в множестве всех перестановок, и мы сейчас уста-
новим между ними взаимно однозначное соответствие. Именно,  каж-
дой  перестановке  поставим  в  соответствие  последовательность
y[1]..y[n], где y[i] - количество чисел, меньших i и стоящих ле-
вее i в этой перестановке. Взаимная  однозначность  вытекает  из
такого  замечания. Перестановка чисел 1...n получается из перес-
тановки чисел 1..n-1 добавлением числа n, которое можно вставить
на любое из n мест. При этом к сопоставляемой с  ней  последова-
тельности  добавляется  еще один член, принимающий значения от 0
до n-1, а предыдущие члены не меняются.  При  этом  оказывается,
что  изменение  на единицу одного из членов последовательности y
соответствует перестановке двух соседних чисел, если все  следу-
ющие  числа последовательности y принимают максимально или мини-
мально возможные для них значения. Именно, увеличение y[i] на  1
соответствует  перестановке  числа  i  с  его  правым соседом, а
уменьшение - с левым.
     Теперь вспомним решение задачи о перечислении всех последо-
вательностей, на каждом шаге которого один член меняется на еди-
ницу. Заменив прямоугольную доску доской в форме лестницы (высо-
та i-ой вертикали равна i) и двигая шашки по тем же правилам, мы
перечислим все последовательности y, причем i-ый член будет  ме-
няться,  лишь  если  все  следующие шашки стоят у края. Надо еще
уметь параллельно с изменением  y  корректировать  перестановку.
Очевидный  способ требует отыскания в ней числа i; это можно об-
легчить, если помимо самой перестановки хранить функцию i  |--->
позиция  числа i в перестановке (обратное к перестановке отобра-
жение), и соответствующим образом ее корректировать.  Вот  какая
получается программа:

 program test;
 | const n=...;
 | var
 |   x: array [1..n] of 1..n; {перестановка}
 |   inv_x: array [1..n] of 1..n; {обратная перестановка}
 |   y: array [1..n] of integer; {Y[i] < i}
 |   d: array [1..n] of -1..1; {направления}
 |   b: boolean;
 |
 | procedure print_x;
 | | var i: integer;
 | begin
 | | for i:=1 to n do begin
 | | | write (x[i], ' ');
 | | end;
 | | writeln;
 | end;
 |
 | procedure set_first;{первая перестановка: y[i]=0 при всех i}
 | | var i : integer;
 | begin
 | | for i := 1 to n do begin
 | | | x[i] := n + 1 - i;
 | | | inv_x[i] := n + 1 - i;
 | | | y[i]:=0;
 | | | d[i]:=1;
 | | end;
 | end;
 |
 | procedure move (var done : boolean);
 | | var i, j, pos1, pos2, val1, val2, tmp : integer;
 | begin
 | | i := n;
 | | while (i > 1) and (((d[i]=1) and (y[i]=i-1)) or
 | | |          ((y[i]=-1) and (y[i]=0))) do begin
 | | | i := i-1;
 | | end;
 | | done := (i>1);
 | | {упрощение связано с тем, что первый член нельзя менять}
 | | if done then begin
 | | | y[i] := y[i]+d[i];
 | | | for j := i+1 to n do begin
 | | | | d[j] := -d[j];
 | | | end;
 | | | pos1 := inv_x[i];
 | | | val1 := i;
 | | | pos2 := pos1 + d[i];
 | | | val2 := x[pos2];
 | | | {pos1, pos2 - номера переставляемых элементов;
 | | |   val1, val2 - их значения}
 | | | tmp := x[pos1];
 | | | x[pos1] := x[pos2];
 | | | x[pos2] := tmp;
 | | | tmp := inv_x[val1];
 | | | inv_x[val1] := inv_x[val2];
 | | | inv_x[val2] := tmp;
 | | end;
 | end;
 |
 begin
 | set_first;
 | print_x;
 | b := true;
 | {напечатаны все перестановки до текущей включительно;
 |   если b ложно, то текущая - последняя}
 | while b do begin
 | | move (b);
 | | if b then print_x;
 | end;
 end.

     2.6. Несколько замечаний.

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

     2.6.1. Перечислить все последовательности длины 2n, состав-
ленные из n единиц и n минус единиц, у которых сумма любого  на-
чального  отрезка положительна (т.е. число минус единиц в нем не
превосходит числа единиц).

     Решение. Изображая единицу вектором (1,1), а минус  единицу
вектором  (1,-1), можно сказать, что мы ищем пути из точки (0,0)
в точку (n,0), не опускающиеся ниже оси абсцисс.
     Будем перечислять последовательности  в  лексикографическом
порядке,  считая,  что  -1  предшествует  1.  Первой  последова-
тельностью будет "пила"
        1, -1, 1, -1, ...
а последней - "горка"
        1, 1, 1, ..., 1, -1, -1, ..., -1.
     Как перейти от последовательности к следующей? До некоторо-
го места они должны совпадать, а затем надо заменить  -1  на  1.
Место  замены должно быть расположено как можно правее. Но заме-
нять -1 на 1 можно только в том случае, если справа от нее  есть
единица (которую можно заменить на -1). Заменив -1 на 1, мы при-
ходим  к  такой  задаче:  фиксирован  начальный кусок последова-
тельности, надо найти минимальное продолжение. Ее решение:  надо
приписывать -1, если это не нарушит условия неотрицательности, а
иначе приписывать 1. Получаем такую программу:

    ...
    type array2n = array [1..2n] of integer;
    ...
    procedure get_next (var a: array2n; var last: Boolean);
    | {в a помещается следующая последовательность, если}
    | {она есть (при этом last=false), иначе last:=true}
    | var k, i, sum: integer;
    begin
    | k:=2*n;
    | {инвариант: в a[k+1..2n] только минус единицы}
    | while a[k] = -1 do begin k:=k-1; end;
    | {k - максимальное среди тех, для которых a[k]=1}
    | while (k>0) and (a[k] = 1) do begin k:=k-1; end;
    | {a[k] - самая правая -1, за которой есть 1;
    |  если таких нет, то k=0}
    | if k = 0 then begin
    | | last := true;
    | end else begin
    | | last := false;
    | | i:=0; sum:=0;
    | | {sum = a[1]+...+a[i]}
    | | while i<> k do begin
    | | | i:=i+1; sum:= sum+a[i];
    | | end;
    | | {sum = a[1]+...+a[k]}
    | |  a[k]:= 1; sum:= sum+2;
    | | {вплоть до a[k] все изменено, sum=a[1]+...+a[k]}
    | | while k <> 2*n do begin
    | | | k:=k+1;
    | | | if sum > 0 then begin
    | | | | a[k]:=-1
    | | | end else begin
    | | | | a[k]:=1;
    | | | end;
    | | | sum:= sum+a[k];
    | | end;
    | | {k=n, sum=a[1]+...a[2n]=0}
    | end;
    end;

     2.6.2.  Перечислить все расстановки скобок в произведении n
сомножителей. Порядок сомножителей не меняется, скобки полностью
определяют порядок действий. (Например, для n = 4 есть 5 расста-
новок ((ab)c)d, (a(bc))d, (ab)(cd), a((bc)d), a(b(cd)).)

     Указание. Каждому порядку действий соответствует последова-
тельность команд стекового калькулятора.

     2.6.3.  На окружности задано 2n точек, пронумерованных от 1
до 2n. Перечислить все способы провести n непересекающихся  хорд
с вершинами в этих точках.

     2.6.4. Перечислить все способы разрезать n-угольник на тре-
угольники, проведя n - 2 его диагонали.

     Еще  один класс задач на перечисление всех элементов задан-
ного множества мы  рассмотрим  ниже,  обсуждая  метод  поиска  с
возвратами (backtracking).

     2.7. Подсчет количеств.

     Иногда  можно  найти  количество  объектов  с  тем или иным
свойством, не перечисляя их. Классический пример: C(n,k) - число
всех k-элементных подмножеств n-элементного  множества  -  можно
найти, заполняя таблицу значений функции С по формулам:

    C (n,0) = C (n,n) = 1            (n >= 1)
    C (n,k) = C (n-1,k-1) + C (n-1,k) (n > 1, 0 < k < n);

или по формуле n!/((k!)*(n-k)!). (Первый способ эффективнее, ес-
ли надо вычислить много значений С(n,k).)

    Приведем другие примеры.

     2.7.1 (Число разбиений). (Предлагалась на всесоюзной  олим-
пиаде  по программированию 1988 года.) Пусть P(n) - число разби-
ений целого положительного n на  целые  положительные  слагаемые
(без учета порядка, 1+2 и 2+1 - одно и то же разбиение). При n=0
положим P(n) = 1 (единственное разбиение не содержит слагаемых).
Построить алгоритм вычисления P(n) для заданного n.
     Решение.  Можно  доказать  (это нетривиально) такую формулу
для P(n):

 P(n) = P(n-1)+P(n-2)-P(n-5)-P(n-7)+P(n-12)+P(n-15) +...

(знаки у пар членов чередуются, вычитаемые в  одной  паре  равны
(3*q*q-q)/2 и (3*q*q+q)/2).
     Однако и без ее использования можно придумать способ вычис-
ления  P(n), который существенно эффективнее перебора и подсчета
всех разбиений.
     Обозначим через R(n,k) (при n >= 0, k >= 0) число разбиений
n  на  целые  положительные  слагаемые, не превосходящие k. (При
этом  R(0,k) считаем равным 1 для всех k >= 0.) Очевидно, P(n) =
R(n,n). Все разбиения n на слагаемые, не  превосходящие  k,  ра-
зобьем  на  группы  в  зависимости  от  максимального слагаемого
(обозначим его i). Число R(n,k) равно сумме (по всем i от  1  до
k)  количеств разбиений со слагаемыми не больше k и максимальным
слагаемым, равным i. А разбиения n на слагаемые  не  более  k  с
первым  слагаемым, равным i, по существу представляют собой раз-
биения n - i на слагаемые, не превосходящие i (при i <= k).  Так
что

    R(n,k) = сумма по i от 1 до k чисел R(n-i,i) при k <= n;
    R(n,k) = R(n,n) при k >= n,

что позволяет заполнять таблицу значений функции R.

     2.7.2 (Счастливые билеты). (Задача предлагалась на Всесоюз-
ной олимпиаде по программированию 1989 года). Последовательность
из 2n цифр (каждая цифра от 0 до 9) называется счастливым  биле-
том, если сумма первых n цифр равна сумме последних n цифр. Най-
ти число счастливых последовательностей данной длины.

     Решение. (Сообщено одним из участников олимпиады; к сожале-
нию,  не могу указать фамилию, так как работы проверялись зашиф-
рованными.) Рассмотрим более общую задачу: найти число  последо-
вательностей,  где  разница  между суммой первых n цифр и суммой
последних n цифр равна k (k = -9n,..., 9n). Пусть T(n, k) - чис-
ло таких последовательностей.
     Разобьем  множество  таких  последовательностей на классы в
зависимости от разницы между первой и  последней  цифрами.  Если
эта разница равна t, то разница между суммами групп из оставших-
ся  n-1 цифр равна k-t. Учитывая, что пар цифр с разностью t бы-
вает 10 - (модуль t), получаем формулу
   T(n,k) = сумма по t от -9 до 9 чисел (10-|t|) * T(n-1,  k-t).
(Некоторые слагаемые могут отсутствовать, так как k-t может быть
слишком велико.)
      Глава 3. Обход дерева. Перебор с возвратами.

     3.1. Ферзи, не бьющие друг друга: обход дерева позиций

     В  предыдущей главе мы рассматривали несколько задач одного
и того же типа: "перечислить все элементы  некоторого  множества
A". Схема решения была такова: на множестве A вводился порядок и
описывалась  процедура  перехода  от произвольного элемента мно-
жества A к следующему за ним (в этом порядке).  Такую  схему  не
всегда  удается  реализовать  непосредственно, и в этой главе мы
рассмотрим другой полезный прием перечисления всех элементов не-
которого множества. Его называют "поиск  с  возвратами",  "метод
ветвей  и границ", "backtracking". На наш взгляд наиболее точное
название этого метода - обход дерева.

     3.1.1. Перечислить все способы расстановки n ферзей на шах-
матной доске n на n, при которых они не бьют друг друга.

     Решение. Очевидно, на каждой из n горизонталей должно  сто-
ять  по  ферзю.  Будем  называть k-позицией (для k = 0, 1,...,n)
произвольную расстановку k ферзей на k нижних горизонталях (фер-
зи могут бить друг друга). Нарисуем "дерево позиций": его корнем
будет единственная 0-позиция, а из каждой  k-позиции  выходит  n
стрелок  вверх в (k+1)-позиции. Эти n позиций отличаются положе-
нием ферзя на (k+1)-ой горизонтали. Будем считать, что  располо-
жение  их  на рисунке соответствует положению этого ферзя: левее
та позиция, в которой ферзь расположен левее.

                                        Дерево позиций для
                                           n = 2

Среди позиций этого дерева нам надо отобрать те n-позиции, в ко-
торых ферзи не бьют друг друга. Программа будет "обходить  дере-
во" и искать их. Чтобы не делать лишней работы, заметим вот что:
если  в  какой-то  k-позиции  ферзи  бьют друг друга, то ставить
дальнейших ферзей смысла нет. Поэтому, обнаружив это,  мы  будем
прекращать построение дерева в этом направлении.

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

                                         Дерево допустимых
                                         позиций для n = 3

     Разобьем задачу на две части: (1) обход произвольного дере-
ва и (2) реализацию дерева допустимых позиций.
     Сформулируем задачу обхода произвольного дерева. Будем счи-
тать, что у нас имеется Робот, который в каждый момент находится
в одной из вершин дерева (вершины изображены на рисунке  кружоч-
ками). Он умеет выполнять команды:

                              вверх_налево  (идти по самой левой
                                 из выходящих вверх стрелок)

                              вправо (перейти в соседнюю  справа
                                 вершину)

                              вниз (спуститься вниз на один уро-
                                 вень)

            вверх_налево
            вправо
            вниз

и проверки, соответствующие возможности выполнить каждую из  ко-
манд,   называемые  "есть_сверху",  "есть_справа",  "есть_снизу"
(последняя истинна всюду, кроме корня). Обратите  внимание,  что
команда "вправо" позволяет перейти лишь к "родному брату", но не
к "двоюродному".

                                    Так команда "вправо"
                                    НЕ действует!

     Будем считать, что у Робота есть команда "обработать" и что
его задача - обработать все  листья  (вершины,  из  которых  нет
стрелок вверх, то есть где условие "есть_сверху" ложно). Для на-
шей  шахматной  задачи  команде обработать будет соответствовать
проверка и печать позиции ферзей.

     Доказательство  правильности приводимой далее программы ис-
пользует такие определения. Пусть фиксировано положение Робота в
одной из вершин дерева. Тогда все листья дерева  разбиваются  на
три  категории: над Роботом, левее Робота и правее Робота. (Путь
из корня в лист может проходить через вершину с Роботом,  свора-
чивать  влево,  не доходя до нее и сворачивать вправо, не доходя
до нее.) Через (ОЛ) обозначим условие "обработаны все листья ле-
вее Робота", а через (ОЛН) - условие "обработаны все листья  ле-
вее и над Роботом".

Нам понадобится такая процедура:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОЛ), надо: (ОЛН)}
  begin
  | {инвариант: ОЛ}
  | while есть_сверху do begin
  | | вверх_налево
  | end
  | {ОЛ, Робот в листе}
  | обработать;
  | {ОЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, листья не обработаны
  надо: Робот в корне, листья обработаны

  {ОЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОЛН, есть справа}
  | | вправо;
  | | {ОЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | end;
  end;
  {ОЛН, Робот в корне => все листья обработаны}

Осталось  воспользоваться  следующими  свойствами  команд Робота
(сверху записаны условия, в которых выполняется команда, снизу -
утверждения о результате ее выполнения):

   (1) {ОЛ, не есть_сверху}  (2) {ОЛ}
       обработать                вверх_налево
       {ОЛН}                     {ОЛ}

   (3) {есть_справа, ОЛН}    (4) {не есть_справа, ОЛН}
       вправо                    вниз
       {ОЛ}                      {ОЛН}

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

     3.1.3. Доказать правильность следующей программы обхода де-
рева:

  var state: (WL, WLU);
  state := WL;
  while есть_снизу or (state <> WLU) do begin
  | if (state = WL) and есть_сверху then begin
  | | вверх;
  | end else if (state = WL) and not есть_сверху then begin
  | | обработать; state := WLU;
  | end else if (state = WLU) and есть_справа then begin
  | |  вправо; state := WL;
  | end else begin {state = WLU, not есть_справа, есть_снизу}
  | |  вниз;
  | end;
  end;

     Решение. Инвариант цикла:
        state = WL  => ОЛ
        state = WLU => ОЛН
Доказательство завершения работы: переход из состояния ОЛ в  ОЛН
возможен  только  при  обработке вершины, поэтому если программа
работает бесконечно, то с некоторого момента значение  state  не
меняется, что невозможно.

    3.1.4.  Решить задачу об обходе дерева, если мы хотим, чтобы
обрабатывались все вершины (не только листья).

    Решение. Пусть x - некоторая вершина. Тогда любая вершина  y
относится к одной из четырех категорий. Рассмотрим путь из корня
в y. Он может:
    (а) быть частью пути из корня в x (y ниже x);
    (б) свернуть налево с пути в x (y левее x);
    (в) пройти через x (y над x);
    (г) свернуть направо с пути в x (y правее x);
В  частности,  сама вершина x относится к категории (в). Условия
теперь будут такими:
    (ОНЛ) обработаны все вершины ниже и левее;
    (ОНЛН) обработаны все вершины ниже, левее и над.
Вот как будет выглядеть программа:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОНЛ), надо: (ОНЛН)}
  begin
  | {инвариант: ОНЛ}
  | while есть_сверху do begin
  | | обработать
  | | вверх_налево
  | end
  | {ОНЛ, Робот в листе}
  | обработать;
  | {ОНЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, ничего не обработано
  надо: Робот в корне, все вершины обработаны

  {ОНЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОНЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОНЛН, есть справа}
  | | вправо;
  | | {ОНЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | end;
  end;
  {ОНЛН, Робот в корне => все вершины обработаны}

     3.1.5. Приведенная только что программа обрабатывает верши-
ну до того, как обработан любой из ее потомков. Как изменить ее,
чтобы каждая вершина, не являющаяся листом, обрабатывалась дваж-
ды: один раз до, а другой раз после всех своих потомков? (Листья
по-прежнему обрабатываются по разу.)

    Решение.  Под "обработано ниже и левее" будем понимать "ниже
обработано по разу, слева обработано полностью (листья по  разу,
останые по два)". Под "обработано ниже, левее и над" будем пони-
мать "ниже обработано по разу, левее и над - полностью".

Программа будет такой:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОНЛ), надо: (ОНЛН)}
  begin
  | {инвариант: ОНЛ}
  | while есть_сверху do begin
  | | обработать
  | | вверх_налево
  | end
  | {ОНЛ, Робот в листе}
  | обработать;
  | {ОНЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, ничего не обработано
  надо: Робот в корне, все вершины обработаны

  {ОНЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОНЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОНЛН, есть справа}
  | | вправо;
  | | {ОНЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | | обработать;
  | end;
  end;
  {ОНЛН, Робот в корне => все вершины обработаны полностью}

     3.1.6. Доказать, что число операций в этой программе по по-
рядку равно числу вершин дерева. (Как и в других программах, ко-
торые  отличаются от этой лишь пропуском некоторых команд "обра-
ботать".)
     Указание. Примерно каждое второе  действие  при  исполнении
этой программы - обработка вершины, а каждая вершина обрабатыва-
ется максимум дважды.

     Теперь реализуем операции с деревом позиций. Позицию  будем
представлять  с помощью переменной k: 0..n (число ферзей) и мас-
сива c: array [1..n] of 1..n (c [i] - координаты ферзя  на  i-ой
горизонтали; при i > k значение c [i] роли не играет). Предпола-
гается,  что  все позиции допустимы (если убрать верхнего ферзя,
остальные не бьют друг друга).

  program queens;
  | const n = ...;
  | var
  |   k: 0..n;
  |   c: array [1..n] of 1..n;
  |
  | procedure begin_work; {начать работу}
  | begin
  | | k := 0;
  | end;
  |
  | function danger: boolean; {верхний ферзь под боем}
  | | var b: boolean; i: integer;
  | begin
  | | if k <= 1 then begin
  | | | danger := false;
  | | end else begin
  | | | b := false; i := 1;
  | | | {b <=> верхний ферзь под боем ферзей с номерами < i}
  | | | while i <> k do begin
  | | | | b := b or (c[i]=c[k]) {вертикаль}
  | | | |     or (abs(c[[i]-c[k]))=abs(i-k)); {диагональ}
  | | | | i := i+ 1;
  | | | end;
  | | | danger := b;
  | | end;
  | end;
  |
  | function is_up: boolean {есть_сверху}
  | begin
  | | is_up := (k < n) and not danger;
  | end;
  |
  | function is_right: boolean {есть_справа}
  | begin
  | | is_right := (k > 0) and (c[k] < n);
  | end;
  | {возможна ошибка: при k=0 не определено c[k]}
  |
  | function is_down: boolean {есть_снизу}
  | begin
  | | is_up := (k > 0);
  | end;
  |
  | procedure up; {вверх_налево}
  | begin {k < n}
  | | k := k + 1;
  | | c [k] := 1;
  | end;
  |
  | procedure right; {вправо}
  | begin {k > 0,  c[k] < n}
  | | c [k] := c [k] + 1;
  | end;
  |
  | procedure down; {вниз}
  | begin {k > 0}
  | | k := k - 1;
  | end;
  |
  | procedure work; {обработать}
  | | var i: integer;
  | begin
  | | if (k = n) and not danger then begin
  | | | for i := 1 to n do begin
  | | | | write ('<', i, ',' , c[i], '> ');
  | | | end;
  | | | writeln;
  | | end;
  | end;
  |
  | procedure UW; {вверх_до_упора_и_обработать}
  | begin
  | | while is_up do begin
  | | | up;
  | | end
  | | work;
  | end;
  |
  begin
  | begin_work;
  | UW;
  | while is_down do begin
  | | if is_right then begin
  | | | right;
  | | | UW;
  | | end else begin
  | | | down;
  | | end;
  | end;
  end.

     3.1.7. Приведенная программа тратит довольно много  времени
на  выполнение  проверки  есть_сверху  (проверка,  находится  ли
верхний ферзь под боем, требует числа действий порядка n). Изме-
нить реализацию операций с деревом позиций так,  чтобы  все  три
проверки есть_сверху/справа/снизу и соответствующие команды тре-
бовали  бы  количества действий, ограниченного не зависящей от n
константой.

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

     3.2.  Обход дерева в других задачах.

     3.2.1. Использовать метод обхода дерева для решения  следу-
ющей   задачи:   дан  массив  из  n  целых  положительных  чисел
a[1]..a[n] и число s; требуется узнать, может ли  число  s  быть
представлено  как  сумма  некоторых  из чисел массива a. (Каждое
число можно использовать не более чем по одному разу.)

     Решение. Будем задавать k-позицию последовательностью из  k
булевских  значений,  определяющих,  входят  ли  в  сумму  числа
a[1]..a[k] или не входят. Позиция допустима, если  ее  сумма  не
превосходит s.

     Замечание. По сравнению с полным перебором всех (2 в степе-
ни  n) подмножеств тут есть некоторый выигрыш. Можно также пред-
варительно отсортировать массив a в убывающем порядке,  а  также
считать  недопустимыми  те  позиции, в которых сумма отброшенных
членов больше, чем разность суммы всех  членов  и  s.  Последний
приём  называют  "методом  ветвей  и границ". Но принципиального
улучшения по сравнению с полным перебором тут не получается (эта
задача, как говорят, NP-полна,  см.  подробности  в  книге  Ахо,
Хопкрофта и Ульмана "Построение и анализ вычислительных алгорит-
мов").  Традиционное  название  этой задачи - "задача о рюкзаке"
(рюкзак общей грузоподъемностью s нужно упаковать  под  завязку,
располагая  предметами  веса  a[1]..a[n]).  См.  также в главе 7
(раздел о динамическом программировании)  алгоритм  её  решения,
полиномиальный по n+s.

     3.2.2.  Перечислить все последовательности из n нулей, еди-
ниц и двоек, в которых никакая группа цифр  не  повторяется  два
раза подряд (нет куска вида XX).

     3.2.3.  Аналогичная  задача для последовательностей нулей и
единиц, в которых никакая группа цифр не  повторяется  три  раза
подряд (нет куска вида XXX).

     К этой же категории относятся задачи типа "можно ли сложить
данную фигуру из пентамино" и им подобные. В  них  важно  умелое
сокращение  перебора (вовремя распознать, что имеющееся располо-
жение фигурок уже противоречит требованиям, и по этой ветви  по-
иск не продолжать).
        Глава 4. Сортировка.

     4.1. Квадратичные алгоритмы.

     4.1.1. Пусть a[1],  ...,  a[n]  -  целые  числа.  Требуется
построить  массив  b[1],  ..., b[n], содержащий те же числа, для
которых b[1] <= ... <= b[n].
     Замечание. Среди чисел a[1]...a[n] могут быть равные.  Тре-
буется,  чтобы  каждое целое число входило в b[1]...b[n] столько
же раз, сколько и в a[1]...a[n].

     Решение. Удобно считать, что числа a[1]..a[n] и  b[1]..b[n]
представляют собой начальное и конечное значения массива x. Тре-
бование  "a  и b содержат одни и те же числа" будет заведомо вы-
полнено, если в процессе работы  мы  ограничимся  перестановками
элементов x.
  ...
  k := 0;
  {k наименьших элементов массива x установлены на свои места}
  while k <> n do begin
  | s := k + 1; t := k + 1;
  | {x[s] - наименьший среди x[k+1]...x[t] }
  | while t<>n do begin
  | | t := t + 1;
  | | if x[t] < x[s] then begin
  | | | s := t;
  | | end;
  | end;
  | {x[s] - наименьший среди x[k+1]..x[n] }
  | ... переставить x[s] и x[k+1];
  | k := k + 1;
  end;

     4.1.2.  Дать другое решение задачи сортировки, использующее
инвариант {первые k элементов упорядочены: x[1] <= ... <= x[k]}

     Решение.

  k:=1
  {первые k элементов упорядочены}
  while k <> n do begin
  | {k+1-ый элемент продвигается к началу, пока не займет
  |   надлежащего места }
  | t := k+1;
  | {x[1] <= ... <= x[t-1] и x[t-1], x[t] <= ... <= x[k+1] }
  | while (t > 1) and (x[t] < x[t-1]) do begin
  | | ... поменять x[t-1] и x[t];
  | | t := t - 1;
  | end;
  end;

     Замечание. Дефект программы: при ложном выражении (t  >  1)
проверка x[t] < x[t-1] требует несуществующего значения x[0].
     Оба  предложенных решения требуют числа действий, пропорци-
онального n*n. Существуют более эффективные алгоритмы.

     4.2. Алгоритмы порядка n log n.

     4.2.1. Предложить алгоритм сортировки, число действий кото-
рого  было  бы  порядка  n  log  n,  то  есть не превосходило бы
C*n*log(n) для некоторого C и для всех n.

     Мы предложим два решения.

     Решение 1. (сортировка слиянием).
     Пусть  k  -  положительное  целое  число.  Разобьем  массив
x[1]..x[n]  на  отрезки  длины  k.  (Первый  - x[1]..x[k], затем
x[k+1]..x[2k] и т.д.) Последний отрезок будет неполным,  если  n
не  делится на k. Назовем массив k-упорядоченным, если каждый из
этих отрезков упорядочен. Любой массив 1-упорядочен. Если массив
k-упорядочен и n<=k, то он упорядочен.
     Мы  опишем,  как  преобразовать  k-упорядоченный  массив  в
2k-упорядоченный (из тех же элементов). С помощью этого преобра-
зования алгоритм записывается так:

  k:=1;
  {массив x является k-упорядоченным}
  while k < n do begin
  | .. преобразовать k-упорядоченный массив в 2k-упорядоченный;
  | k := 2 * k;
  end;

     Требуемое  преобразование  состоит в том,что мы многократно
"сливаем" два упорядоченных отрезка длины не  больше  k  в  один
упорядоченный  отрезок. Пусть процедура слияние (p,q,r: integer)
при p <=q <= r сливает отрезки  x[p+1]..x[q]  и  x[q+1]..x[r]  в
упорядоченный  отрезок x[p+1]..x[r] (не затрагивая других частей
массива x).
                  p               q               r
            -------|---------------|---------------|-------
                   | упорядоченный | упорядоченный |
            -------|---------------|---------------|-------
                                  |
                                  |
                                  V
            -------|-------------------------------|-------
                   |     упорядоченный             |
            -------|-------------------------------|-------

Тогда преобразование k-упорядоченного массива в 2k-упорядоченный
осуществляется так:

  t:=0;
  {t кратно 2k или t = n, x[1]..x[t] является
   2k-упорядоченным; остаток массива x не изменился}
  while t + k < n do begin
  | p := t;
  | q := t+k;
  | ...r := min (t+2*k, n); {в паскале нет функции min }
  | слияние (p,q,r);
  | t := r;
  end;

Слияние требует вспомогательного массива для записи  результатов
слияния  -  обозначим его b. Через p0 и q0 обозначим номера пос-
ледних элементов участков, подвергшихся слиянию, s0 -  последний
записанный  в  массив b элемент. На каждом шаге слияния произво-
дится одно из двух действий:

        b[s0+1]:=x[p0+1];
        p0:=p0+1;
        s0:=s0+1;
или
        b[s0+1]:=x[q0+1];
        q0:=q0+1;
        s0:=s0+1;

Первое действие (взятие элемента из первого отрезка) может  про-
изводиться при двух условиях:
    (1) первый отрезок не кончился (p0 < q);
    (2) второй отрезок кончился (q0 = r)  или  не  кончился,  но
элемент в нем не меньше [(q0 < r) и (x[p0+1] <= x[q0+1])].
     Аналогично для второго действия. Итак, получаем

  p0 := p; q0 := q; s0 := p;
  while (p0 <> q) or (q0 <> r) do begin
  | if (p0 < q) and ((q0 = r) or ((q0 < r) and
  | |                (x[p0+1] <= x[q0+1]))) then begin
  | | b [s0+1] := x [p0+1];
  | | p0 := p0+1;
  | | s0 := s0+1;
  | end else begin
  | | {(q0 < r) and ((p0 = q) or ((p0<q) and
  | |   (x[p0+1] >= x[q0+1])))}
  | | b [s0+1] := x [q0+1];
  | | q0 := q0 + 1;
  | | s0 := s0 + 1;
  | end;
  end;

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

     Решение 2 (сортировка деревом).
     Нарисуем "полное двоичное дерево"  -  картинку,  в  которой
снизу один кружок, из него выходят стрелки в два других, из каж-
дого - в два других и так далее:

               .............
                 o  o o  o
                  \/   \/
                   o   o
                    \ /
                     o

     Будем  говорить, что стрелки ведут "от отцов к сыновьям": у
каждого кружка два сына и один отец (если  кружок  не  верхний).
Предположим  для  простоты, что количество подлежащих сортировке
чисел есть степень двойки, и они могут заполнить один  из  рядов
целиком. Запишем их туда. Затем заполним часть дерева под ним по
правилу:
   число в кружке = минимум из чисел в кружках-сыновьях
Тем  самым  в  корне дерева (нижнем кружке) будет записано мини-
мальное число во всем массиве.
     Изымем из сортируемого  массива  минимальный  элемент.  Для
этого  его  надо вначале найти. Это можно сделать, идя от корня:
от отца переходим к тому сыну, где записано то же  число.  Изъяв
минимальный  элемент,  заменим  его  символом  "бесконечность" и
скорректируем более низкие ярусы (для этого  надо  снова  пройти
путь к корню). При этом считаем, что минимум из n и бесконечнос-
ти  равен  n. Тогда в корне появится второй по величине элемент,
мы изымаем его, заменяя бесконечностью и корректируя дерево. Так
постепенно мы изымем все элементы в порядке возрастания, пока  в
корне не останется бесконечность.
     При записи этого алгоритма полезно нумеровать кружочки чис-
лами 1, 2, ...: сыновьями кружка номер n являются кружки  2*n  и
2*n+1. Подробное изложение этого алгоритма мы опустим, поскольку
мы  изложим  более  эффективный  вариант,  не требующий дополни-
тельной памяти, кроме конечного числа переменных (в дополнении к
сортируемому массиву).
     Мы будем записывать сортируемые числа во всех вершинах  де-
рева,  а не только на верхнем уровне. Пусть x[1]..x[n] - массив,
подлежащий сортировке. Вершинами дерева будут числа от 1 до n; о
числе x[i] мы будем говорить как о числе, стоящем в вершине i. В
процессе сортировки количество вершин дерева будет  сокращаться.
Число вершин текущего дерева будем хранить в переменной k. Таким
образом,  в  процессе работы алгоритма массив x[1]..x[n] делится
на две части: в x[1]..x[k] хранятся числа на дереве, а в  x[k+1]
.. x[n] хранится уже отсортированная в порядке возрастания часть
массива - элементы, уже занявшие свое законное место.
     На каждом шаге алгоритм будет изымать максимальный  элемент
дерева и помещать его в отсортированную часть, на освободившееся
в результате сокращения дерева место.
     Договоримся о терминологии. Вершинами дерева считаются чис-
ла от 1 до текущего значения переменной k. У  каждой  вершины  s
могут  быть  сыновья 2s и 2s+1. Если оба этих числа больше k, то
сыновей нет; такая вершина называется листом. Если 2s=k, то вер-
шина s имеет ровно одного сына (2s).
     Для каждого s из 1..k рассмотрим "поддерево" с корнем в  s:
оно  содержит вершину s и всех ее потомков (сыновей, сыновей сы-
новей и т.д. - до тех пор, пока мы не выйдем из  отрезка  1..k).
Вершину  s будем называть регулярной, если стоящее в ней число -
максимальный элемент s-поддерева; s-поддерево  назовем  регуляр-
ным,  если  все  его вершины регулярны. (В частности, любой лист
образует регулярное одноэлементное поддерево.)

     Схема алгоритма такова:

  k:= n
  ... Сделать 1-поддерево регулярным;
  {x[1],..,x[k] <= x[k+1] <= ... <= x[n]; 1-поддерево регулярно,
   в частности, x[1] - максимальный элемент среди x[1]..x[k]}
  while k <> 1 do begin
  | ... обменять местами x[1] и x[k];
  | k := k - 1;
  | {x[1]..x[k-1] <= x[k] <=...<= x[n]; 1-поддерево регу-
  |   лярно везде, кроме, возможно, самого корня }
  | ... восстановить регулярность 1-поддерева всюду
  end;

В качестве вспомогательной процедуры нам  понадобится  процедура
восстановления регулярности s-поддерева в корне. Вот она:

  {s-поддерево регулярно везде, кроме, возможно, корня}
  t := s;
  {s-поддерево регулярно везде, кроме, возможно, вершины t}
  while ((2*t+1 <= k) and (x[2*t+1] > x[t])) or
  |     ((2*t <= k) and (x[2*t] > x[t])) do begin
  | if (2*t+1 <= k) and (x[2*t+1] >= x[2*t]) then begin
  | | ... обменять x[t] и x[2*t+1];
  | | t := 2*t + 1;
  | end else begin
  | | ... обменять x[t] и x[2*t];
  | | t := 2*t;
  | end;
  end;

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

  while наибольшее число не в t, а в одном из сыновей do begin
  | if оно в правом сыне then begin
  | | поменять t с ее правым сыном; t:= правый сын
  | end else begin {наибольшее число - в левом сыне}
  | | поменять t с ее левым сыном; t:= левый сын
  | end
  end

После  обмена  вершина  t  становится регулярной (в нее попадает
максимальное число t-поддерева). Не принявший участия  в  обмене
сын остается регулярным, а принявший участие может и не быть ре-
гулярным. В остальных вершинах s-поддерева не изменились ни чис-
ла,  ни поддеревья их потомков (разве что два элемента поддерева
переставились), так что регуярность не нарушилась.

   Эта же процедура может использоваться для того, чтобы сделать
1-поддерево регулярным на начальной стадии сортировки:

  k := n;
  u := n;
  {все s-поддеревья с s>u регулярны }
  while u<>0 do begin
  | {u-поддерево регулярно везде, кроме разве что корня}
  | ... восстановить регулярность u-поддерева в корне;
  | u:=u-1;
  end;

     Теперь запишем процедуру сортировки на паскале  (предпола-
гая,  что  n  -  константа,  x  имеет тип arr = array [1..n] of
integer).

  procedure sort (var x: arr);
  | var u, k: integer;
  | procedure exchange(i, j: integer);
  | | var tmp: integer;
  | | begin
  | | tmp  := x[i];
  | | x[i] := x[j];
  | | x[j] := tmp;
  | end;
  | procedure restore (s: integer);
  | | var t: integer;
  | | begin
  | | t:=s;
  | | while ((2*t+1 <= k) and (x[2*t+1] > x[t]) ) or
  | | |     ((2*t <= k) and (x[2*t] > x[t])) do begin
  | | | if (2*t+1 <= k) and (x[2*t+1] >= x[2*t]) then begin
  | | | | exchange (t, 2*t+1);
  | | | | t := 2*t+1;
  | | | end else begin
  | | | | exchange (t, 2*t);
  | | | | t := 2*t;
  | | | end;
  | | end;
  | end;
  begin
  | k:=n;
  | u:=n;
  | while u <> 0 do begin
  | | restore (u);
  | | u := u - 1;
  | end;
  | while k <> 1 do begin
  | | exchange (1, k);
  | | k := k - 1;
  | | restore (1);
  | end;
  end;

     Несколько замечаний.

     Метод, использованный при сортировке деревом, бывает полез-
ным в других случах. (См. в главе 6 (о типах данных)  раздел  об
очереди с приоритетами.)

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

     Еще один практически важный алгоритм сортировки таков: что-
бы  отсортировать массив, выберем случайный его элемент b, и ра-
зобъем массив на три части: меньшие b, равные  b  и  большие  b.
(Эта  задача  приведена в главе о массивах.) Теперь осталось от-
сортировать первую и третью части: это делается тем же способом.
Время работы этого алгоритма - случайная величина;  можно  дока-
зать, что в среднем он работает не больше C*n*log n. На практике
- он один из самых быстрых. (Мы еще вернемся к нему, приведя его
рекурсивную и нерекурсивную реализации.)

     Наконец, отметим, что сортировка за время порядка C*n*log n
может быть выполнена с помощью техники сбалансированных деревьев
(см.  главу  12), однако программы тут сложнее и константа C до-
вольно велика.

     4.3. Применения сортировки.

     4.3.1. Найти количество  различных  чисел  среди  элементов
данного массива. Число действий порядка n*log n. (Эта задача уже
была в главе о массивах.)

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

     4.3.2. Дано n отрезков [a[i],  b[i]]  на  прямой  (i=1..n).
Найти максимальное k, для которого существует точка прямой, пок-
рытая k отрезками ("максимальное число слоев"). Число действий -
порядка n*log n.

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

     4.3.3. Дано n точек на плоскости. Указать (n-1)-звенную не-
самопересекающуюся незамкнутую ломаную, проходящую через все эти
точки.  (Соседним  отрезкам  ломаной разрешается лежать на одной
прямой.) Число действий порядка n*log n.

     Решение. Упорядочим точки по  x-координате,  а  при  равных
x-координатах  - по y-координате. В таком порядке и можно прово-
дить ломаную.

     4.3.4. Та же задача, если ломаная должна быть замкнутой.

     Решение. Возьмем самую левую точку (т.е. точку с наименьшей
x-координатой) и проведем из нее лучи во  все  остальные  точки.
Теперь упорядочим эти лучи, а точки на одном луче поместим в по-
рядке увеличения расстояния от начала луча.

     4.3.5. Дано n точек на  плоскости.  Построить  их  выпуклую
оболочку  -  минимальную  выпуклую фигуру, их содержащую. (Форму
выпуклой оболочки примет резиновое колечко, если его натянуть на
гвозди, вбитые в точках.)  Число операций не более n*log n.

    Указание. Упорядочим точки - годится любой из порядков,  ис-
пользованных в двух предыдущих задачах. Затем, рассматривая точ-
ки по очереди, будем строить выпуклую оболочку уже рассмотренных
точек. (Для хранения выпуклой оболочки полезно использовать дек,
см. главу 6 о типах данных.)

     4.4. Нижние оценки для числа сравнений при сортировке.

     Пусть  имеется  n  различных по весу камней и весы, которые
позволяют за одно взвешивание определить, какой из двух  выбран-
ных  нами  камней тяжелее. (В программистских терминах: мы имеем
доступ к функции  тяжелее(i,j:1..n):boolean.)  Надо  упорядочить
камни  по  весу,  сделав  как  можно меньше взвешиваний (вызовов
функции "тяжелее").

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

    4.4.1. Доказать, что сложность любого алгоритма сортировки n
камней не меньше log (n!). (Логарифм берется по основанию 2,  n!
- произведение чисел 1..n.)

     Решение. Пусть имеется алгоритм сложности не более  d.  Для
каждого  из n! возможных расположений камней запротоколируем ре-
зультаты взвешиваний (обращений к функции "тяжелее");  их  можно
записать  в  виде  последовательности  из не более чем d нулей и
единиц. Для  единообразия  дополним  последовательность  нулями,
чтобы ее длина стала равной d. Тем самым у нас имеется n! после-
довательностей  из  d нулей и единиц. Все эти последовательности
разные - иначе наш алгоритм дал бы одинаковые ответы для  разных
порядков  (и один из ответов был бы неправильным). Получаем, что
2 в степени d не меньше n! - что и требовалось доказать.

     Другой способ объяснить то же самое  -  рассмотреть  дерево
вариантов,  возникающее в ходе выполнения алгоритма, и сослаться
на то, что дерево высоты d не может иметь более (2 в степени  d)
листьев.

     Это  рассуждение показывает, что любой алгоритм сортировки,
использующий только сравнения элементов массива и их перестанов-
ки, требует не менее C*n*log n действий, так что наши  алгоритмы
близки  к  оптимальным. Однако алгоритм сортировки, использующий
другие операции, может действовать быстрее. Вот один  из  приме-
ров.

     4.4.2. Имеется массив целых чисел  a[1]..a[n],  причем  все
числа неотрицательны и не превосходят m. Отсортировать этот мас-
сив; число действий порядка m+n.

     Решение.  Для каждого числа от 0 до m подсчитываем, сколько
раз оно встречается в массиве. После этого исходный массив можно
стереть и заполнить заново в порядке возрастания, используя све-
дения о кратности каждого числа.

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

Есть также метод сортировки, в котором последовательно проводится 
ряд  "частичных  сортировок"  по отдельным битам. Начнём с такой
задачи:

     4.4.3. В массиве a[1]..a[n] целых чисел переставить элемен-
ты так, чтобы чётные шли перед нечётными (не меняя взаимный  по-
рядок в каждой из групп).

     Решение.  Сначала  спишем  (во  вспомогательный массив) все
чётные, а потом - все нечётные.

     4.4.4. Имеется массив из n чисел от 0 до (2 в степени k)  -
1, каждое из которых мы будем рассматривать как k-битовое слово.
Используя проверки "i-ый бит равен 0" и "i-ый бит равен 1" вмес-
то сравнений, отсортировать все числа за время порядка n*k.

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

     Аналогичный алгоритм может быть применен для m-ичной систе-
мы  счисления  вместо двоичной. При этом полезна такая вспомога-
тельная задача:

     4.4.5. Даны n чисел и функция f, принимающая (на них)  зна-
чения  1..m.  Требуется переставить числа в таком порядке, чтобы
значения функции f не убывали (сохраняя  притом  порядок  внутри
каждой из групп). Число действий порядка m+n.
     Указание. Завести m списков суммарной длины n (как это сде-
лать,  смотри в главе 6 о типах данных) и помещать в i-ый список
числа, для которых значение функции f равно i.  Вариант:  посчи-
тать  для  всех  i, сколько имеется чисел x c f(x)=i, после чего
легко определить, с какого места нужно начинать размещать  числа
с f(x)=i.

     4.5. Родственные сортировке задачи.

     4.5.1. Какова минимально возможная сложность (число сравне-
ний  в наихудшем случае) алгоритма отыскания самого легкого из n
камней?

     Решение. Очевидный алгоритм  с  инвариантом  "найден  самый
легкий  камень  среди первых i" требует n-1 сравнений. Алгоритма
меньшей сложности нет. Это вытекает из следующего более сильного
утверждения.

     4.5.2. Эксперт хочет докать суду, что данный камень - самый
легкий среди n камней, сделав менее n-1  взвешиваний.  Доказать,
что  это  невозможно.  (Веса камней неизвестны суду, но известны
эксперту.)

     Решение. Изобразим камни точками, а взвешивания  -  линиями
между  ними. Получим граф с n вершинами и менее чем n-1 ребрами.
Такой  граф  несвязен  (добавление  каждого   следующего   ребра
уменьшает число компонент не более чем на 1). Поэтому суд ничего
не  знает  относительно  соотношения весов камней в двух связных
компонентах и может допустить, что самый легкий камень - в любой
из них.

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

     4.5.3. Дано n различных по весу камней и число k (от  1  до
n). Требуется найти k-ый по весу камень,  сделав  не  более  C*n
взвешиваний, где C - некоторая константа, не зависящая от k.

     Замечание.  Сортировка  позволяет  сделать это за C*n*log n
взвешиваний. Указание к этой (трудной) задаче приведено в  главе
про рекурсию.

     Следующая задача имеет неожиданно простое решение.

     4.5.4. Имеется n одинаковых на вид камней, некоторые из ко-
торых на самом деле различны по весу. Имеется  прибор,  позволя-
ющий  по  двум камням определить, одинаковы они или различны (но
не говорящий, какой тяжелее). Известно, что  среди  этих  камней
большинство  (более n/2) одинаковых. Сделав не более n взвешива-
ний, найти хотя бы один камень из этого большинства.

     Предостережение. Если два камня одинаковые, это не гаранти-
рует их принадлежности к большинству.

     Указание. Если найдены два различных камня, то их оба можно
выбросить - хотя бы один из них плохой и  большинство  останется
большинством.

     Решение. Программа просматривает камни по очереди, храня  в
переменной i число просмотренных камней. (Считаем камни пронуме-
рованными от 1 до n.) Помимо этого программа хранит номер "теку-
щего  кандидата"  c  и  его  "кратность"  k. Смысл этих названий
объясняется инвариантом:

   если к непросмотренным камням (с номерами i+1..n)  до-
   бавили бы k копий c-го камня, то наиболее частым среди  (И)
   них был бы такой же камень, что и для исходного массива

Получаем такую программу:

   k:=0; i:=0
   {(И)}
   while i<>n do begin
   | if k=0 then begin
   | | k:=1; c:=i+1; i:=i+1;
   | end else if i+1-ый камень одинаков с c-ым then begin
   | | i:=i+1; k:=k+1;
   | |  {заменяем материальный камень идеальным}
   | end else begin
   | | i:=i+1; k:=k-1;
   | |  {выкидываем один материальный и один идеальный камень}
   | end;
   end;
   искомым является c-ый камень

Замечание.  Поскольку во всех трех вариантах выбора стоит
команда i:=i+1, ее можно вынести наружу.

     Следующая задача не имеет на первый взгляд никакого отноше-
ния к сортировке.

     4.5.5.  Имеется квадратная таблица a[1..n, 1..n]. Известно,
что для некоторого i строка с номером i заполнена одними нулями,
а столбец с номером i - одними единицами (за исключением их  пе-
ресечения на диагонали, где стоит неизвестно что). Найти такое i
(оно, очевидно, единственно). Число действий не превосходит C*n.
(Заметим, что это существенно меньше числа элементов в таблице).

     Указание. Рассмотрите a[i][j] как результат "сравнения" i с
j  и  вспомните, что самый тяжелый из n камней может быть найден
за n сравнений. (Не забудьте, впрочем, что таблица может не быть
"транзитивной".)
     Глава 5. Конечные автоматы в задачах обработки текстов

     5.1. Составные символы, комментарии и т.п.

     5.1.1.  В  тексте  возведение  в степень обозначалось двумя
идущими подряд звездочками. Решено заменить это  обозначение  на
'^'  (так  что,  к  примеру, 'x**y' заменится на 'x^y'). Как это
проще всего сделать? Исходный текст разрешается читать символ за
символом, получающийся текст требуется печатать символ за симво-
лом.

     Решение. В каждый момент программа  находится  в  одном  из
двух состояний: "основное" и "после звездочки"

Состояние    Очередной        Новое       Действие
           входной символ   состояние

основное        *             после          нет
основное     x <> '*'        основное     печатать x
после           *            основное     печатать '^'
после        x <> '*'        основное     печатать *, x

Замечание.  При  этом '***' заменится на '^*' (но не на '*^'). В
условии задачи мы не оговаривали деталей, как это часто делается
- предполагается, что программа "должна действовать разумно".  В
данном  случае,  пожалуй,  самый  простой  способ объяснить, как
программа действует - это описать ее состояния и действия в них.

     5.1.2. Написать программу, удалающую из текста подслова ви-
да 'abc'.

     5.1.3. В паскале комментарии заключаются в фигурные скобки:

                begin {начало цикла}
                i:=i+1; {увеличиваем i на 1}

Написать программу, которая удаляла бы комментарии  и  вставляла
бы  вместо  исключенного  комментария  пробел  (чтобы '1{один}2'
превратилось бы не в '12', а в '1 2').

     Решение. Программа имеет два состояния: "основное" и "внут-
ри комментария".

Состояние    Очередной        Новое       Действие
           входной символ   состояние

основное        {             внутри         нет
основное     x <> '{'        основное     печатать x
внутри          }            основное     печатать пробел
внутри       x <> '}'         внутри         нет

     Замечание. Эта программа не воспринимает вложенные  коммен-
тарии: строка вроде
       '{{комментарий внутри} комментария}'
превратится в
        '  комментария}'
(в  начале  стоят два пробела). Обработка вложенных комментариев
конечным автоматом невозможна (нужно "помнить число скобок" -  а
произвольное натуральное число не помещается в конечную память).

     5.1.4. В паскалевских программах бывают также строки,  зак-
люченные в кавычки. Если фигурная скобка стречается внутри стро-
ки, то она не означает начала или конца комментария. В свою оче-
редь, кавычка в комментарии не означает начала или конца строки.
Как изменить программу, чтобы это учесть?

     Указание. Состояний будет три: основное,  внутри  коммента-
рия, внутри строки.

     5.1.5. Еще одна возможность многих реализаций паскаля - это
комментарии вида

      i:=i+1;     (*   here i is increased by 1  *)

при этом закрывающая скобка должна  соответствовать  открываюшей
(то  есть  { ... *) не разрешается). Как удалять такие коммента-
рии?

     5.2. Ввод чисел

     Пусть  десятичная  запись  числа подается на вход программы
символ за символом. Мы хотим "прочесть" это число  (поместить  в
переменную типа real его значение). Кроме того, надо сообщить об
ошибке, если число записано неверно.

     Более конкретно, представим себе такую ситуацию. Последова-
тельность  символов на входе делится на прочитанную и оставшуюся
части. Мы можем пользоваться функцией Next:char, которая возвра-
щает первый символ оставшей части, а также функцией Move,  кото-
рая перводит забирает первый символ из оставшейся части, перево-
дя его в категорию прочитанных.

        ---------------------|--------------------------
          прочитанная часть  | Next |  ?  |  ?  |  ?  |
        ---------------------|--------------------------

Будем  называть десятичной записью такую последовательность сим-
волов:

  <0 или более пробелов> <1 или более цифр>

а также такую:

  <0 или более пробелов> <1 или более цифр>.<1 или более цифр>

Заметим, что согласно этому  определению  '1.',  '.1',  '1.  1',
'-1.1' не являются десятичными записями. Сформулируем теперь за-
дачу точно:

     5.2.1. Прочесть из входной строки максимальную часть, кото-
рая  может  быть началом десятичной записи. Определить, является
ли эта часть десятичной записью или нет.

     Решение. Запишем программу на паскале (используя  "перечис-
лимый тип" для наглядности записи: переменная state может прини-
мать одно из значений, указанных в скобках).

    var state:
     (Accept, Error, Initial, IntPart, DecPoint, FracPart);

    state := Initial;
    while (state <> Accept) or (state <> Error) do begin
    | if state = Initial then begin
    | | if Next = ' ' then begin
    | | | state := Initial; Move;
    | | end else if Digit(Next) then begin
    | | | state := IntPart; {после начала целой части}
    | | | Move;
    | | end else begin
    | | | state := Error;
    | | end;
    | end else if state = IntPart then begin
    | | if Digit (Next) then begin
    | | | state := IntPart; Move;
    | | end else if Next = '.' then begin
    | | | state := DecPoint; {после десятичной точки}
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if state = DecPoint then begin
    | | if Digit (Next) then begin
    | | | state := FracPart; Move;
    | | end else begin
    | | | state := Error; {должна быть хоть одна цифра}
    | | end;
    | end else if state = FracPart then begin
    | | if Digit (Next) then begin
    | | | state := FracPart; Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if
    | | {такого  быть не может}
    | end;
    end;

Заметьте,  что присваивания state:=Accept и state:=Error не соп-
ровождаются сдвигом (символ, который не может быть частью числа,
не забирается).

     Приведенная программа не запоминает  значение  прочитанного
числа.

     5.2.2. Решить предыдущую задачу с дополнительным требовани-
ем: если прочитанный кусок является десятичной записью, то в пе-
ременную val:real следует поместить ее значение.

     Решение.  При  чтении дробной части используется переменная
step - множитель при следующей десятичной цифре.

    state := Initial; val:= 0;
    while (state <> Accept) or (state <> Error) do begin
    | if state = Initial then begin
    | | if Next = ' ' then begin
    | | | state := Initial; Move;
    | | end else if Digit(Next) then begin
    | | | state := IntPart; {после начала целой части}
    | | | val := DigitValue (Next);
    | | | Move;
    | | end else begin
    | | | state := Error;
    | | end;
    | end else if state = IntPart then begin
    | | if Digit (Next) then begin
    | | | state := IntPart; val := 10*val + DigitVal(Next);
    | | | Move;
    | | end else if Next = '.' then begin
    | | | state := DecPoint; {после десятичной точки}
    | | | step := 0.1;
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if state = DecPoint then begin
    | | if Digit (Next) then begin
    | | | state := FracPart;
    | | | val := val + DigitVal(Next)*step; step := step/10;
    | | | Move;
    | | end else begin
    | | | state := Error; {должна быть хоть одна цифра}
    | | end;
    | end else if state = FracPart then begin
    | | if Digit (Next) then begin
    | | | state := FracPart;
    | | | val := val + DigitVal(Next)*step; step := step/10;
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if
    | | {такого  быть не может}
    | end;
    end;

     5.2.3. Та же задача, если перед  число  может  стоять  знак
"минус" или знак "плюс" (а может ничего не стоять).

     Формат  чисел  в этой задаче обычно иллюстрируют такой кар-
тинкой:

   -----      ---------
---| + |---->-| цифра |-------->--------------------->
 | -----  | | --------- | |                      |
 | -----  | |           | | -----     ---------  |
 |-| - |--| |----<------| |-| . |->---| цифра |--|
 | -----  |                 -----   | --------- |
 |        |                         |-----<-----|
 |--->----|

     5.2.4.  Та же задача, если к тому же после числа может сто-
ять показатель степени десяти, как  в  254E-4  (=0.0254)  или  в
0.123E+9 (=123000000). Нарисуйте соответствующую картинку.

     5.2.5. Что надо изменить в программе  задачи  5.2.2,  чтобы
разрешить пустые целую и дробную части (как в '1.', '.1' или да-
же '.' - последнее число считаем равным нулю)?

     Мы  вернемся  к  конечным автоматам в главе 10 (Сравнение с
образцом).
     Глава 6. Типы данных.

     6.1. Стеки.

     Пусть Т - некоторый тип. Рассмотрим (отсутствующий в паска-
ле)  тип "стек элементов типа Т". Его значениями являются после-
довательности значений типа Т.

     Операции:

Сделать_пустым (var s: стек элементов типа Т).
Добавить (t: T; var s: стек элементов типа Т).
Взять (var t: T; var s: стек элементов типа Т).
Пуст (s: стек элементов типа Т): boolean
Вершина (s: стек элементов типа Т): T

     (Мы пользуемся обозначениями, наполняющими паскаль, хотя  в
паскале типа "стек" нет.) Процедура "Сделать_пустым" делает стек
s  пустым.  Процедура  "Добавить" добавляет t в конец последова-
тельности  s.  Процедура  "Взять"  определена,  если  последова-
тельность  s непуста; она забирает из неё последний элемент, ко-
торый становится значением переменной t. Выражение "Пуст(s)" ис-
тинно, если последовательность s пуста.  Выражение  "Вершина(s)"
определено, если последовательность s непуста, и равно последне-
му элементу последовательности s.
     Мы  покажем,  как моделировать стек в паскале и для чего он
может быть нужен.

     Моделирование ограниченного стека в массиве.

     Будем считать, что количество элементов в стеке не  превос-
ходит  некоторого  числа  n. Тогда стек можно моделировать с по-
мощью двух переменных:
        Содержание: array [1..n] of T;
        Длина: integer;
считая, что в стеке находятся элементы Содержание [1],...,Содер-
жание [длина].

     Чтобы сделать стек пустым, достаточно положить
        Длина := 0

     Добавить элемент t:
         {Длина < n}
         Длина := Длина+1;
         Содержание [Длина] :=t;

     Взять элемент в переменную t:
         t := Содержание [Длина];
         Длина := Длина - 1;

     Стек пуст, если Длина = 0.

     Вершина стека равна Содержание [Длина].

Таким образом, вместо переменной типа стек в программе на паска-
ле можно использовать две переменные Содержание и  Длина.  Можно
также определить тип стек, записав

    const N = ...
    type  stack = record
                    Содержание: array [1..N] of T;
                    Длина: integer;
                  end;

(Мы позволяем себе  использовать  имена  переменных  из  русских
букв,  хотя  обычно  паскаль  этого не любит.) После этого могут
быть - в соответствии с правилами паскаля  -  описаны  процедуры
работы со стеком. Например, можно написать

        procedure Добавить (t: T; var s: stack);
        begin
        | {s.Длина , N}
        | s.Длина := s.Длина + 1;
        | s.Содержание [s.Длина] := t;
        end;

     Использование стека.

     Будем рассматривать последовательности открывающихся и зак-
рывающихся круглых и квадратных скобок ( ) [ ]. Среди всех таких
последовательностей  выделим правильные - те, которые могут быть
получены по таким правилам:

        1) пустая последовательность правильна.
        2) если А и В правильны, то и АВ правильна.
        3) если А правильна, то [A] и (A) правильны.

     Пример. Последовательности (), [[]], [()[]()][]  правильны,
а последовательности ], )(, (], ([)] - нет.

     6.1.1.  Проверить правильность последовательности за время,
не превосходящее константы, умноженной на её длину.  Предполага-
ется, что члены последовательности закодированы числами:
         (   1
         [   2
         )  -1
         ]  -2

     Решение. Пусть a[1]..a[n] - проверяемая последовательность.
Рассмотрим  стек,  элементами  которого  являются  открывающиеся
круглые и квадратные скобки (т. е. 1 и 2).
     Вначале стек делаем пустым. Далее просматриваем члены  пос-
ледовательности  слева  направо.  Встретив  открывающуюся скобку
(круглую или квадратную), помещаем её в стек. Встретив  закрыва-
ющуюся,  проверяем, что вершина в стеке - парная ей скобка; если
это не так, то можно утверждать, что  последовательность  непра-
вильна,  если  скобка  парная, то заберем её (вершину) из стека.
Последовательность правильна,  если  в  конце  стек  оказывается
пуст.
        Сделать_Пустым (s);
        i := 0; Обнаружена_Ошибка := false;
        {прочитано i символов последовательности}
        while (i < n) and not Обнаружена_Ошибка do begin
        | i := i + 1;
        | if (a[i] = 1) or (a[i] = 2) then begin
        | | Добавить (a[i], s);
        | end else begin  {a[i] равно -1 или -2}
        | | if Пуст (s) then begin
        | | | Обнаружена_Ошибка := true;
        | | end else begin
        | | | Взять (t, s);
        | | | Обнаружена ошибка := (t <> - a[i]);
        | | end;
        | end;
        end;
        Правильно := (not Обнаружена_Ошибка) and Пуст (s);

       Убедимся  в  правильности  программы. (1) Если последова-
тельность построена по правилам, то программа даст  ответ  "да".
Это легко доказать индукцией по построению правильной последова-
тельности.  Надо проверить для пустой, для последовательности AB
в предположении, что для A и B уже проверено - и для  последова-
тельностей [A] и (A) - в предположении, что для A уже проверено.
Для  пустой  очевидно.  Для AB действия программы происходят как
для A и кончаются с пустым стеком; затем все происходит как  для
B.  Для  [A]  сначала  помещается  в стек открывающая квадратная
скобка и затем все идет как для A - с той разницей, что в глуби-
не стека лежит лишняя скобка. По  окончании  A  стек  становится
пустым  - если не считать этой скобки - а затем и совсем пустым.
Аналогично для (A).
     (2) Покажем, что если программа завершает работу с  ответом
"да",  то последовательность правильная. Рассуждаем индукцией по
длине последовательности. Проследим за состоянием стека  в  про-
цессе работы программы. Если он в некоторый промежуточный момент
пуст, то последовательность разбивается на две части, для каждой
из  которых  программа дает ответ "да"; остается воспользоваться
предположением индукции и определением правильности. Пусть  стек
все  время  непуст.  Это значит, что положенная в него на первом
шаге скобка будет вынута на последнем шаге. Тем самым, первый  и
последний символы последовательности - это парные скобки, и пос-
ледовательность имеет вид (A) или [A], а работа программы (кроме
первого  и  последнего  шагов) отличается от ее работы на A лишь
наличием лишней скобки на дне стека (раз ее не вынимают, она ни-
как не влияет на работу программы). Снова ссылаемся на предполо-
жение индукции и определение правильности.

     6.1.2. Как упростится программа, если известно, что в  пос-
ледовательности могут быть только круглые скобки?

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

     6.1.3. Реализовать с помощью одного массива два стека, сум-
марное количество элементов в которых ограничено длиной массива;
все  действия со стеками должны выполняться за время, ограничен-
ное константой, не зависящей от длины стеков.

     Решение. Стеки должны расти с концов массива навстречу друг
другу: первый должен занимать места
        Содержание[1] ... Содержание[Длина1],
а второй  -
        Содержание[n] ... Содержание[n - Длина2 + 1]
(вершины обоих стеков записаны последними).

     6.1.4. Реализовать k стеков с элементами типа T, общее  ко-
личество  элементов в которых не превосходит n, с использованием
массивов суммарной длины C*(n+k), затрачивая на каждое  действие
со  стеками (кроме начальных действий, делающих все стеки пусты-
ми) время, ограниченное некоторой константой.

     Решение. Применяемый метод называется "ссылочной реализаци-
ей". Он использует три массива:
        Содержание: array [1..n] of T;
        Следующий: array [1..n] of 0..n;
        Вершина: array [1..k] of 0..n.
     Массив Содержание будем изображать как n ячеек  с  номерами
1..n,  каждая  из которых содержит элемент типа T. Массив Следу-
ющий изобразим в виде стрелок, проведя стрелку из i  в  j,  если
Следующий[i] = j. (Если Следующий[i] = 0, стрелок из i не прово-
дим.) Содержимое s-го стека (s из 1..k)  хранится  так:  вершина
равна Содержание[Вершина[s]], остальные элементы s-го стека мож-
но  найти,  идя  по стрелкам - до тех пор, пока они не кончатся.
При этом (s-ый стек пуст) <=> Вершина[s] = 0.
     Стрелочные траектории, выходящие из Вершина[1], ..., Верши-
на[k] (из тех, которые не равны 0) не должны пересекаться. Поми-
мо них, нам понадобится еще одна стрелочная траектория, содержа-
щая все неиспользуемые в данный момент ячейки. Ее начало мы  бу-
дем  хранить в переменной Свободная (равенство Свободная = 0 оз-
начает, что пустого места не осталось). Вот что получается:

 n=8 | a | p | q | d | s | t | v | w |

 k=2  |  |  |            Свободная

Содержание = <a,p,q,d,s,t,v,w>, Следующий  =  <3,0,6,0,0,2,5,4>
Вершина = <1, 7>, Свободная = 8
Стеки: 1-ый: p t q a (a-вершина); 2-ой: s v (v-вершина).

  procedure Начать_работу; {Делает все стеки пустыми}
  | var i: integer;
  begin
  | for i := 1 to k do begin
  | | Вершина [i]:=0;
  | end;
  | for i := 1 to n-1 do begin
  | | Следующий [i] := i+1;
  | end;
  | Свободная:=1;
  end;

  function  Есть_место: boolean;
  begin
  | Есть Место := (Свободная <> 0);
  end;

  procedure Добавить (t: T; s: integer);
  | {Добавить t к s-му стеку}
  | var i: 1..n;
  begin
  | {Есть_место}
  | i := Свободная;
  | Свободная := Следующий [i];
  | Вершина [s] :=i;
  | Содержание [i] := t;
  | Следующий [i] := Вершина [s];
  end;

  function Пуст (s: integer): boolean; {s-ый стек пуст}
  begin
  | Пуст := (Вершина [s] = 0);
  end;

  procedure Взять (var t: T; s: integer);
  | {взять из s-го стека в t}
  | var i: 1..n;
  | begin
  | {not Пуст (s)}
  | i := Вершина [s];
  | t := Содержание [i];
  | Вершина [s] := Следующий [i];
  | Следующий [i] := Свободная;
  | Свободная := i;
  end;

     6.2. Очереди.

     Значениями типа "очередь элементов типа T", как и для  сте-
ков, являются последовательности значений типа T. Разница состо-
ит  в том, что берутся элементы не с конца, а с начала (а добав-
ляются по-прежнему в конец).

     Операции с очередями.

        Сделать_пустой (var x: очередь элементов типа T);
        Добавить (t: T, var x: очередь элементов типа T);
        Взять (var t: T, var x: очередь элементов типа T);
        Пуста (x: очередь элементов типа T): boolean;
        Очередной (x: очередь элементов типа T): T.

     При выполнении команды "Добавить" указанный элемент  добав-
ляется  в  конец  очереди.  Команда "Взять" выполнима, лишь если
очередь непуста, и  забирает  из  нее  первый  (положенный  туда
раньше  всех)  элемент, помещая его в t. Значением функции "Оче-
редной" (определенной для непустой очереди) является первый эле-
мент очереди.
     Английские названия стеков - Last In First  Out  (последним
вошел  -  первым вышел), а очередей - First In First Out (первым
вошел - первым вышел).

     Реализация очередей в массиве.

     6.2.1. Реализовать операции с очередью  ограниченной  длины
так,  чтобы количество действий для каждой операции было ограни-
чено константой, не зависящей от длины очереди.

     Решение. Будем хранить элементы очереди в соседних  элемен-
тах  массива.  Тогда  очередь  будет прирастать справа и убывать
слева. Поскольку при этом она может дойти до края, свернем  мас-
сив в окружность.
     Введем массив Содержание: array [0..n-1] of T и переменные
         Первый: 0..n-1,
         Длина : 0..n.
При этом элементами очереди будут
         Содержание [Первый], Содержание [Первый + 1],...,
                   Содержание [Первый + Длина - 1],
где  сложение рассматривается по модулю n. (Предупреждение. Если
вместо этого ввести переменные Первый и  Последний,  принимающие
значения  в  вычетах  по  модулю n, то пустая очередь может быть
спутана с очередью из n элементов.)

     Моделирование операций:

     Сделать Пустой:
        Длина := 0;
        Первый := 0;

     Добавить элемент:
        {Длина < n}
        Содержание [(Первый + Длина) mod n] := элемент;
        Длина := Длина + 1;

     Взять элемент;
        {Длина > 0}
        элемент := Содержание [Первый];
        Первый := (Первый + 1) mod n;
        Длина := Длина - 1;

     Пуста = (Длина = 0);

     Очередной = Содержание [Первый];

     6.2.2.  (Сообщил А.Г.Кушниренко) Придумать способ моделиро-
вания очереди с помощью двух стеков (и фиксированного числа  пе-
ременных  типа T). При этом отработка n операций с очередью (на-
чатых, когда очередь была  пуста)  должна  требовать  порядка  n
действий.

     Решение.  Инвариант:  стеки, составленные концами, образуют
очередь. (Перечисляя элементы одного стека вглубь и  затем  эле-
менты  второго  наружу,  мы  перечисляем все элементы очереди от
первого до последнего.) Ясно, что добавление сводится к добавле-
нию к одному из стеков, а проверка пустоты - к проверке  пустоты
обоих стеков. Если мы хотим взять элемент, есть два случая. Если
стек,  где  находится  начало очереди, не пуст, то берем из него
элемент. Если он пуст, то предварительно переписываем в него все
элементы второго стека, меняя порядок (это происходит  само  при
перекладывании  из стека в стек) и сводим дело к первому случаю.
Хотя число действий при этом и не ограничено константой, но тре-
бование задачи выполнено, так как каждый элемент  очереди  может
участвовать в этом процессе не более одного раза.

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

     6.2.4. (Сообщил А.Г.Кушниренко.) Имеется дек элементов типа
T  и конечное число переменных типа T и целого типа. В начальном
состоянии в деке некоторое число элементов. Составить программу,
после исполнения которой в деке остались бы те же самые  элемен-
ты, а их число было бы в одной из целых переменных.

     Указание.  (1) Элементы дека можно циклически переставлять,
забирая с одного конца и помещая в другой. После  этого,  сделав
столько  же  шагов  в обратном направлении, можно вернуть все на
место. (2) Как понять, прошли мы полный круг или не прошли? Если
бы был какой-то элемент, заведомо отсутствующий в деке, то можно
было бы его подсунуть и ждать  вторичного  появления.  Но  таких
элементов нет. Вместо этого можно для данного n выполнить цикли-
ческий  сдвиг  на  n дважды, подсунув разные элементы, и посмот-
реть, появятся ли разные элементы через n шагов.

     Применение очередей.

     6.2.5. Напечатать в  порядке  возрастания  первые  n  нату-
ральных  чисел, в разложение которых на простые множители входят
только числа 2, 3, 5.

       Решение. Введем три очереди x2, x3, x5, в  которых  будем
хранить элементы, которые в 2 (3, 5) раз больше напечатанных, но
еще не напечатанные. Определим процедуру

        procedure напечатать_и_добавить (t: integer);
        begin
        | writeln (t);
        | добавить (2*t, x2);
        | добавить (3*t, x3);
        | добавить (5*t, x5);
        end;

Вот схема программы:

  напечатать_и_добавить (1);
  k := 1; { k - число напечатанных }
  {инвариант:  напечатано  в  порядке  возрастания k минимальных
  членов нужного множества; в очередях элементы, вдвое, втрое  и
  впятеро  большие напечатанных, но не напечатанные, расположен-
  ные в возрастающем порядке}
  while k <> n do begin
  | x := min (очередной (x2), очередной (x3), очередной (x5));
  | напечатать_и_добавить (x);
  | k := k+1;
  | ...взять x из тех очередей, где он был очередным;
  end;

     Пусть инвариант выполняется. Рассмотрим наименьший из нена-
печатанных элементов множества. Тогда он делится нацело на  одно
из чисел 2, 3, 5, и частное также принадлежит множеству. Значит,
оно  напечатано. Значит, x находится в одной из очередей и, сле-
довательно, является в ней первым (меньшие напечатаны, а элемен-
ты очередей не напечатаны). Напечатав x, мы должны его изъять  и
добавить его кратные.
     Длины очередей не превосходят числа напечатанных элементов.

     Следующая задача связана с графами (к которым мы вернёмся в
главе 9).

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

     6.2.6.  Известно, что ориентированный граф связен, т. е. из
любой вершины можно пройти в любую по  ребрам.  Кроме  того,  из
каждой  вершины  выходит столько же ребер, сколько входит. Дока-
зать, что существует замкнутый цикл, проходящий по каждому ребру
ровно один раз. Составить алгоритм отыскания такого цикла.

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

while змея включает не все ребра do begin
| if из головы выходит неиспользованное в змее ребро then begin
| | удлинить змею этим ребром
| end else begin
| | {хвост змеи в той же вершине, что и голова}
| | отрезать конец хвоста и добавить его к голове
| | {"змея откусывает конец хвоста"}
| end;
end;

     Докажем, что мы достигнем цели.

     1)  Идя по змее от хвоста к голове, мы входим в каждую вер-
шину столько же раз, сколько выходим. Так как  в  любую  вершину
входит столько же ребер, сколько выходит, то невозможность выйти
означает, что мы начали движение в этой же точке.
     2) Змея не укорачивается, поэтому либо она  охватывает  все
ребра,  либо, начиная с некоторого момента, будет иметь постоян-
ную длину. Во втором случае змея будет бесконечно "скользить  по
себе".  Это возможно, только если из всех вершин змеи не выходит
неиспользованных ребер. Из связности следует,  что  использованы
все ребра.

     Замечание  по  реализации на паскале. Вершинами графа будем
считать числа 1..n. Для каждой вершины  i  будем  хранить  число
Out[i]  выходящих  из  нее  ребер, а также номера Num[i][1],...,
Num[i][Out[i]] тех вершин, куда  эти  ребра  ведут.  В  процессе
построения  змеи  будет  выбирать  первое свободное ребро. Тогда
достаточно будет хранить для каждой вершины число  выходящих  из
нее  использованных  ребер  -  это  будут ребра, идущие в начале
списка.

     6.2.7. Доказать, что для всякого  n  существует  последова-
тельность  нулей  и  единиц  длины  (2 в степени n) со следующим
свойством: если "свернуть ее в кольцо" и рассмотреть  все  фраг-
менты  длины  n  (их число равно (2 в степени n)), то мы получим
все возможные последовательности нулей и единиц длины n. Постро-
ить алгоритм отыскания такой  последовательности,  требующий  не
более (C в степени n) действий для некоторой константы C.

     Указание. Рассмотрим граф, вершинами которого являются пос-
ледовательности  нулей  и единиц длины (n-1). Будем считать, что
из вершины x ведет ребро в вершину y, если x может быть началом,
а y - концом некоторой последовательности длины n. Тогда из каж-
дой вершины входит и выходит два ребра. Цикл, проходящий по всем
ребрам, и даст требуемую последовательность.

     6.2.8. Реализовать k очередей с ограниченной суммарной дли-
ной  n,  используя  память  порядка  n+k, причем каждая операция
(кроме начальной, делающей все очереди пустыми) должна требовать
ограниченного константой числа действий.

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

        Содержание: array [1..n] of T;
        Следующий: array [1..n] of 0..n;
        Первый: array [1..n] of 0..n;
        Последний: array [1..k] of 0..n;
        Свободная : 0..n;

  procedure Сделать_пустым;
  | var i: integer;
  begin
  | for i := 1 to n-1 do begin
  | | Следующий [i] := i + 1;
  | end;
  | Свободная := 1;
  | for i := 1 to k do begin
  | | Первый [i]:=0;
  | end;
  end;

  function Есть_место : boolean;
  begin
  | Есть_место := Свободная <> 0;
  end;

  function Пуста (номер_очереди: integer): boolean;
  begin
  | Пуста := Первый [номер_очереди] = 0;
  end;

  procedure Взять (var t: T; номер_очереди: integer);
  | var перв: integer;
  begin
  | {not Пуста (номер_очереди)}
  | перв := Первый [номер_очереди];
  | t := Содержание [перв]
  | Первый [номер_очереди] := Следующий [перв];
  | Следующий [перв] := Свободная;
  | Свободная := Перв;
  end;

  procedure Добавить (t: T; номер_очереди: integer);
  | var нов, посл: 1..n;
  begin
  | {Есть_свободное_место }
  | нов := Свободная; Свободная := Следующий [Свободная];
  | {из списка свободного места изъят номер нов}
  | if Пуста (номер_очереди) then begin
  | | Первый [номер_очереди] := нов;
  | | Последний [номер_очереди] := нов;
  | | Следующий [нов] := 0;
  | | Содержание [нов] := t;
  | end else begin
  | | посл := Последний [номер_очереди];
  | | {Следующий [посл] = 0 }
  | | Следующий [посл] := нов;
  | | Следующий [нов] := 0;
  | | Содержание [нов] := t
  | | Последний [номер_очереди] := нов;
  | end;
  end;

  function Очередной (номер_очереди: integer): T;
  begin
  | Очередной := Содержание [Первый [номер_очереди]];
  end;

     6.2.9. Та же задача для деков вместо очередей.

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

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

     6.2.10.  На плоскости задано n точек, пронумерованных слева
направо (а при равных абсциссах - снизу вверх). Составить  прог-
рамму, которая строит многоугольник, являющийся их выпуклой обо-
лочкой, за не более чем C*n действий.

     Решение. Будем присоединять точки к выпуклой оболочке  одна
за  другой.  Легко  показать, что последняя присоединенная точка
будет одной из вершин выпуклой оболочки. Эту  вершину  мы  будем
называть выделенной. Очередная присоединяемая точка видна из вы-
деленной  (почему?). Дополним наш многоугольник, выпустив из вы-
деленной вершины "иглу", ведущую в присоединяемую  точку.  Полу-
чится  вырожденный многоугольник, и остается ликвидировать в нем
"впуклости".

                                               [Рисунок]

     Будем хранить вершины многоугольника в деке в порядке обхо-
да его периметра по часовой стрелке. При этом выделенная вершина
является началом и концом (головой и хвостом) дека.  Присоедине-
ние  "иглы" теперь состоит в добавлении присоединяемой вершины в
голову и в хвост дека.  Устранение  впуклостей  несколько  более
сложно.  Назовем  подхвостом и подподхвостом элементы дека, сто-
ящие за его хвостом. Устранение впуклости у хвоста делается так:

    while по дороге из хвоста в подподхвост  мы поворачиваем
    |                  у подхвоста влево ("впуклость") do begin
    | выкинуть подхвост из дека
    end

Таким же способом устраняется впуклость у головы дека.

    Замечание. Действия с подхвостом и подподхвостом не входят в
определение дека, однако сводятся к небольшому числу манипуляций
с деком (надо забрать три элемента с хвоста, сделать что надо  и
вернуть).

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

     6.3. Множества.

     Пусть  Т - некоторый тип. Существует много способов хранить
(конечные) множества элементов типа Т; выбор между ними  опреде-
ляется типом T и набором требуемых операций.

     Подмножества множества {1..n}.

     6.3.1.  Используя  память,  пропорциональную   n,   хранить
подмножества множества {1..n}.

          Операции              Число действий

        Сделать пустым                C*n
        Проверить принадлежность      C
        Добавить                      C
        Удалить                       С
        Минимальный элемент           C*n
        Проверка пустоты              C*n

     Решение. Храним множество как array [1..n] of boolean.

     6.3.2.  То  же,  но  проверка пустоты должна выполняться за
время C.

       Решение. Храним дополнительно количество элементов.

     6.3.3. То же при следующих ограничениях на число действий:

          Операции             Число действий

        Сделать пустым                C*n
        Проверить принадлежность      C
        Добавить                      C
        Удалить                       C*n
        Минимальный элемент           C
        Проверка пустоты              C

     Решение.  Дополнительно  храним  минимальный  элемент  мно-
жества.

     6.3.4 То же при следующих ограничениях на число действий:

          Операции             Число действий

        Сделать пустым                С*n
        Проверить принадлежность      С
        Добавить                      С*n
        Удалить                       С
        Минимальный элемент           С
        Проверка пустоты              C

       Решение.  Храним минимальный, а для каждого - следующий и
предыдущий по величине.

     Множества целых чисел.

     В следующих задачах величина элементов множества не ограни-
чена, но их количество не превосходит n.

     6.3.5. Память C*n.

          Операции             Число действий

        Сделать пустым                C
        Число элементов               C
        Проверить принадлежность      C*n
        Добавить новый
         (заведомо отсутствующий)     C
        Удалить                       C*n
        Минимальный элемент           C*n
        Взять какой-то элемент        C

     Решение.   Множество   представляем  с  помощью  переменных
a:array [1..n] of integer, k: 0..n; множество содержит k элемен-
тов a[1],...,a[k]; все они различны. По существу мы храним  эле-
менты множества в стеке (без повторений).

     6.3.6. Память C*n.

          Операции             Число действий

        Сделать пустым                C
        Проверить пустоту             C
        Проверить принадлежность      C*(log n)
        Добавить                      С*n
        Удалить                       C*n
        Минимальный элемент           С

     Решение. См. решение предыдущей задачи с дополнительным ус-
ловием a[1] < ... < a[k]. При проверке принадлежности используем
двоичный поиск.

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

     6.3.7.  Используя описанное в предыдущей задаче представле-
ние множеств, найти все вершины ориентированного графа,  доступ-
ные  из  данной по ребрам. (Вершины считаем числами 1..n.) Время
не больше C * (общее число ребер, выходящих  из  доступных  вер-
шин).

     Решение.  (Другое решение смотри в главе о рекурсии.) Пусть
num[i]  -  число  ребер,  выходящих  из   i,   out[i][1],   ...,
out[i][num[i]] - вершины, куда ведут ребра.

  procedure Доступные (i: integer);
  |   {напечатать все вершины, доступные из i, включая i}
  | var  X: подмножество 1..n;
  |      P: подмножество 1..n;
  |      q, v, w: 1..n;
  |      k: integer;
  begin
  | ...сделать X, P пустыми;
  | writeln (i);
  | ...добавить i к X, P;
  | {(1) P = множество напечатанных вершин; P содержит i;
  |  (2) напечатаны только доступные из i вершины;
  |  (3) X - подмножество P;
  |  (4) все напечатанные вершины, из которых выходит
  |      ребро в ненапечатанную вершину, принадлежат X}
  | while X непусто do begin
  | | ...взять какой-нибудь элемент X в v;
  | | for k := 1 to num [v] do begin
  | | | w := out [v][k];
  | | | if w не принадлежит P then begin
  | | | | writeln (w);
  | | | | добавить w в P;
  | | | | добавить w в X
  | | | end;
  | | end;
  | end;
  end;

     Свойство (1) не нарушается, так как печать  происходит  од-
новременно с добавлением в P. Свойства (2): раз v было в X, то v
доступно,  поэтому  w  доступно. Свойство (3) очевидно. Свойство
(4): мы удалили из X элемент v, но все вершины, куда из  v  идут
ребра, перед этим напечатаны.

     Оценка  времени  работы. Заметим, что изъятые из X элементы
больше туда не добавляются, так как они  в  момент  изъятия  (и,
следовательно, всегда позже) принадлежат P, а добавляются только
элементы  не  из P. Поэтому цикл while выполняется не более, чем
по разу, для всех  доступных  вершин,  а  цикл  for  выполняется
столько раз, сколько из вершины выходит ребер.
     Для  X  надо  использовать представление со стеком или оче-
редью (см. выше), для P - булевский массив.

     6.3.8. Решить предыдущую задачу, если требуется, чтобы дос-
тупные вершины печатались в таком порядке: сначала заданная вер-
шина, потом ее соседи, потом соседи соседей (еще  не  напечатан-
ные) и т.д.

     Указание. Так получится, если использовать очередь в приве-
денном выше решении: докажите индукцией по k, что существует мо-
мент, в который напечатаны все вершины на расстоянии  не  больше
k, а в очереди находятся все вершины, удаленные ровно на k.

Более  сложные  способы представления множеств будут разобраны в
главах 11 (Хеширование) и 12 (Деревья).

     6.4. Разные задачи.

     6.4.1. Реализовать структуру данных, которая имеет  все  те
же операции, что массив длины n, а именно

        начать работу
        положить в i-ю ячейку число n
        узнать, что лежит в i-ой ячейке

а также операцию "указать номер минимального элемента" (или  од-
ного  из  минимальных  элементов).  Количество действий для всех
операций  должно  быть не более C*log n, не считая операции "на-
чать работу" (которая требует не более C*n действий).

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

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

     Решение. Следуя алгоритму сортировки деревом (в его оконча-
тельном  варианте),  будем  размещать элементы очереди в массиве
x[1]..x[k],  поддерживая  такое  свойство:  x[i]  старше  (имеет
больший  приоритет)  своих сыновей x[2i] и x[2i+1], если таковые
существуют - и, следовательно, всякий элемент старше  своих  по-
томков. (Сведения о приоритета также хранятся в массиве, так что
мы  имеем  дело  с  массивом пар (элемент, приоритет).) Удаление
элемента с сохранением этого свойства описано в алгоритме сорти-
ровки. Надо еще уметь восстанавливать свойство после  добавления
элемента в конец. Это делается так:

    t:= номер добавленного элемента
    {инвариант: в дереве любой предок приоритетнее потомка,
        если этот потомок - не t}
    while t - не корень и t старше своего отца do begin
    | поменять t с его отцом
    end;

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

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

     7.1. Примеры рекурсивных программ.

При  анализе  рекурсивной  программы  возникает, как обычно, два
вопроса:

        (а) почему программа заканчивает работу?
        (б) почему она работает правильно, если заканчивает
            работу?

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

     7.1.1. Написать рекурсивную процедуру вычисления факториала
целого  положительного  числа  n  (т.е. произведения чисел 1..n,
обозначаемого n!).

     Решение. Используем равенства 1!=1, n!= (n-1)!*n.

        procedure factorial (n: integer; var fact: integer);
        | {положить fact равным факториалу числа y}
        begin
        | if n=1 then begin
        | | fact:=1;
        | end else begin {n>1}
        | | factorial (n-1, fact);
        | | fact:= fact*n;
        | end;
        end;

С использованием процедур-функций можно написать так:

        function factorial (n: integer): integer;
        begin
        | if n=1 then begin
        | | factorial:=1;
        | end else begin {n>1}
        | | factorial:=  factorial (n-1)*n;
        | end;
        end;

Обратите внимание на некоторую двойственность использования име-
ни factorial внутри описания функции: оно обозначает  как  пере-
менную,  так и вызываемую рекурсивно функцию. К счастью, в нашем
случае они различаются  по  скобкам  после  имени,  но  если  бы
функция  была  без  параметров,  то  дело  было бы плохо. (Стан-
дартная, но трудно находимая ошибка возникает, если автор  пола-
гает, что он использует значение переменной, а компилятор в этом
месте видит рекурсивный вызов.)

    7.1.2.  Обычно  факториал определяют и для нуля, считая, что
0!=1. Измените программы соответственно.

    7.1.3. Напишите рекурсивную программу возведения в целую не-
отрицательную степень.

    7.1.4. То же, если требуется, чтобы глубина рекурсии не пре-
восходила C*log n, где n - степень.

    Решение.

        function power (a,n: integer): integer;
        begin
        | if n = 0 then begin
        | | power:= 1;
        | end else if n mod 2 = 0 then begin
        | | power:= power(a*2, n div 2);
        | end else begin
        | | power:= power(a, n-1)*a;
        | end;
        end;

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

        power:= power(a*2, n div 2)
на
        power:= power(a, n div 2)* power(a, n div 2)?

     Решение. Программа останется правильной. Однако она  станет
работать  медленнее. Дело в том, что теперь вызов может породить
два вызова (хотя и одинаковых) вместо одного - и  число  вызовов
быстро  растет  с глубиной рекурсии. Программа по-прежнему имеет
логарифмическую глубину рекурсии, но число шагов  работы  стано-
вится линейным вместо логарифмического.
     Этот недостаток можно устранить, написав
        t:= power(a, n div 2);
        power:= t*t;
или воспользовавшись функцией возведения в квадрат (sqr).

     7.1.6. Используя лишь команды write(x) при x=0..9, написать
рекурсивную программу печати десятичной  записи  целого  положи-
тельного числа n.

     Решение.  Здесь  использование  рекурсии  облегчает   жизнь
(проблема  была в том, что цифры легче получать с конца, а печа-
тать надо с начала).

     procedure print (n:integer); {n>0}
     begin
     | if n<10 then begin
     | | write (n);
     | end else begin
     | | print (n div 10);
     | | write (n mod 10);
     | end;
     end;

     7.1.7. Игра "Ханойские башни" состоит в следующем. Есть три
стержня.  На  первый из них надета пирамидка из n колец (большие
кольца снизу, меньшие сверху). Требуется переместить  кольца  на
другой  стержень. Разрешается перекладывать кольца со стержня на
стержень,  но класть большее кольцо поверх меньшего нельзя. Сос-
тавить программу, указывающую требуемые действия.

     Решение.  Напишем  рекурсивную  процедуру   перемещения   i
верхних  колец  с  m-го стержня на n-ый (предполагается, что ос-
тальные кольца больше по размеру и лежат на стержнях без  движе-
ния).

    procedure move(i,m,n: integer);
    | var s: integer;
    begin
    | if i = 1 then begin
    | | writeln ('сделать ход', m, '->', n);
    | end else begin
    | | s:=6-m-n; {s - третий стержень: сумма номеров равна 6}
    | | move (i-1, m, s);
    | | writeln ('сделать ход', m, '->', n);
    | | move (i-1, s, n);
    | end;
    end;

(Сначала  переносится  пирамидка из i-1 колец на третью палочку.
После этого i-ое кольцо освобождается, и его можно перенести ку-
да следует. Остается положить на него пирамидку.)

     7.2. Рекурсивная обработка деревьев

     Двоичным деревом называется картинка вроде

                   o
                    \
                     o   o
                      \ /
                   o   o
                    \ /
                     o

Нижняя вершина называется корнем. Из каждой вершины  могут  идти
две  линии: влево вверх и вправо вверх. Вершины, куда они ведут,
называются левым и правым сыновьями  исходной  вершины.  Вершина
может  иметь двух сыновей, а может иметь только одного сына (ле-
вого или правого). Она может и вовсе не иметь сыновей, и в  этом
случае называется листом.
     Пусть x - какая-то вершина двоичного дерева. Она сама вмес-
те  с сыновьями, внуками, правнуками и т.д. образует поддерево с
корнем в x - "поддерево потомков x".

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

        l,r: array [1..N] of integer

и левый и правый сын вершины с номером  i  имеют  соответственно
номера  l[i]  и  r[i].  Если вершина с номером i не имеет левого
(или правого) сына, то l[i] (соответственно r[i]) равно  0.  (По
традиции при записи программ мы используем вместо нуля константу
nil, равную нулю.)

     Здесь N - достаточно большое натуральное число (номера всех
вершин  не  превосходят  N). Отметим, что номер вершины никак не
связан с ее положением в дереве и что не все числа  от  1  до  N
обязаны  быть  номерами вершин (и, следовательно, часть данных в
массивах l и r - это мусор).

    7.2.1. Пусть N=7, root=3, массивы l и r таковы:

         i  |   1  2  3  4  5  6  7
       l[i] |   0  0  1  0  6  0  7
       r[i] |   0  0  5  3  2  0  7

Нарисовать соответствующее дерево.

     Ответ:          6   2
                      \ /
                   1   5
                    \ /
                     3

     7.2.2. Написать программу подсчета числа вершин в дереве.

     Решение. Рассмотрим функцию n(x),  равную  числу  вершин  в
поддереве с корнем в вершине номер x. Считаем, что n(nil)=0 (по-
лагая соответствующее поддерево пустым), и не заботимся о значе-
ниях  nil(s)  для чисел s, не являющихся номерами вершин. Рекур-
сивная программа для s такова:

     function n (x:integer):integer;
     begin
     | if x = nil then begin
     | | n:= 0;
     | end else begin
     | | n:= n(l[x]) + n(r[x]) + 1;
     | end;
     end;

(Число вершин в поддереве над вершиной x равно сумме чисел  вер-
шин  над  ее сыновьями плюс она сама.) Глубина рекурсии конечна,
так  как  с  каждым  шагом  высота  соответствующего   поддерева
уменьшается.

     7.2.3. Написать программу подсчета числа листьев в дереве.

     Ответ.

     function n (x:integer):integer;
     begin
     | if x = nil then begin
     | | n:= 0;
     | end else if (l[x]=nil) and (r[x]=nil) then begin {лист}
     | | n:= 1;
     | end;
     | end else begin
     | | n:= n(l[x]) + n(r[x]);
     | end;
     end;

     7.2.4. Написать программу подсчета  высоты  дерева  (корень
имеет высоту 0, его сыновья - высоту 1, внуки - 2 и т.п.; высота
дерева - это максимум высот его вершин).

     Указание.  Рекурсивно  определяется  функция  f(x) = высота
поддерева с корнем в x.

     7.2.5.  Написать  программу, которая по заданному n считает
число всех вершин высоты n (в заданном дереве).

     Вместо подсчета количества вершин того или иного рода можно
просить напечатать список этих вершин (в том или ином порядке).

     7.2.6. Написать программу, которая печатает (по одному  ра-
зу) все вершины дерева.

     Решение.  Процедура  print_subtree(x)  печатает все вершины
поддерева с корнем в x по одному разу; главная программа  содер-
жит вызов print_subtree(root).

     procedure print_subtree (x:integer);
     begin
     | if x = nil then begin
     | | {ничего не делать}
     | end else begin
     | | writeln (x);
     | | print_subtree (l[x]);
     | | print_subtree (r[x]);
     | end;
     end;

Данная программа печатает сначала корень поддерева, затем подде-
рево над левым сыном, а затем над правым. Три строки в else-час-
ти  могут быть переставлены 6 способами, и каждый из этих спосо-
бов дает свой порядок печати вершин.

     7.3. Порождение комбинаторных объектов, перебор

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

     7.3.1. Написать программу, которая печатает по одному  разу
все  последовательности  длины n, составленные из чисел 1..k (их
количество равно k в степени n).

     Решение. Программа будет оперировать с массивом  a[1]..a[n]
и числом t. Рекурсивная процедура generate печатает все последо-
вательности, начинающиеся на a[1]..a[t]; после  ее  окончания  t
имеет то же значение, что и в начале:

     procedure generate;
     | var i,j : integer;
     begin
     | if t = n then begin
     | | for i:=1 to n do begin
     | | | write(a[i]);
     | | end;
     | | writeln;
     | end else begin {t < n}
     | | for j:=1 to k do begin
     | | | t:=t+1;
     | | | a[t]:=j;
     | | | generate;
     | | | t:=t-1;
     | | end;
     | end;
     end;

Основная программа теперь состоит из двух операторов:
     t:=0; generate;

     7.3.2. Написать программу, которая печатала бы все переста-
новки чисел 1..n по одному разу.

     Решение. Программа оперирует с массивом a[1]..a[n], в кото-
ром  хранится  перестановка  чисел  1..n.  Рекурсивная процедура
generate в такой ситуации печатает все перестановки, которые  на
первых  t позициях совпадают с перестановкой a; по выходе из нее
переменные t и a имеют те же значения, что и до входа.  Основная
программа такова:

    for i:=1 to n do begin a[i]:=i; end;
    t:=0;
    generate;

вот описание процедуры:

     procedure generate;
     | var i,j : integer;
     begin
     | if t = n then begin
     | | for i:=1 to n do begin
     | | | write(a[i]);
     | | end;
     | | writeln;
     | end else begin {t < n}
     | | for j:=t+1 to n do begin
     | | | поменять местами a[t+1] и a[j]
     | | | t:=t+1;
     | | | generate;
     | | | t:=t-1;
     | | | поменять местами a[t+1] и a[j]
     | | end;
     | end;
     end;

     7.3.3. Напечатать все возрастающие последовательности длины
k, элементами которых являются натуральные  числа  от  1  до  n.
(Предполагается, что k не превосходит n - иначе таких последова-
тельностей не существует.)

     Решение. Программа оперирует с массивом a[1]..a[k] и  целой
переменной  t. Предполагая, что a[1]..a[t] - возрастающая после-
довательность чисел натуральных чисел из отрезка 1..n, рекурсив-
но определенная процедура generate печатает все ее  возрастающие
продолжения длины k.

     procedure generate;
     | var i: integer;
     begin
     | if t = k then begin
     | | печатать a[1]..a[k]
     | end else begin
     | | t:=t+1;
     | | for i:=a[t-1]+1 to t-k+n do begin
     | | | a[t]:=i;
     | | | generate;
     | | end;
     | | t:=t-1;
     | end;
     end;

     Замечание. Цикл for мог бы иметь верхней границей n (вместо
t-k+n). Наш вариант экономит часть работы,  учитывая  тот  факт,
что  предпоследний  (k-1-ый)  член  не  может  превосходить n-1,
k-2-ой член не может превосходить n-2 и т.п.
     Основная программа теперь выглядит так:

        t:=1;
        for j:=1 to 1-k+n do begin
        | a[1]:=j;
        | generate;
        end;

Можно было бы добавить к массиву a слева еще и a[0]=0,  положить
t=0 и ограничиться единственным вызовом процедуры generate.

     7.3.4.  Перечислить все представления положительного целого
числа n в виде суммы последовательности невозрастающих целых по-
ложительных слагаемых.

     Решение.  Программа  оперирует  с  массивом a[1..n] (макси-
мальное число слагаемых равно n) и с целой переменной t. Предпо-
лагая, что a[1],...,a[t] - невозрастающая последовательность це-
лых чисел, сумма которых не превосходит  n,  процедура  generate
печатает  все  представления  требуемого  вида, продолжающие эту
последовательность. Для экономии вычислений сумма  a[1]+...+a[t]
хранится в специальной переменной s.

     procedure generate;
     | var i: integer;
     begin
     | if s = n then begin
     | | печатать последовательность a[1]..a[t]
     | end else begin
     | | for i:=1 to min(a[t], n-s) do begin
     | | | t:=t+1;
     | | | a[t]:=i;
     | | | s:=s+i;
     | | | generate;
     | | | s:=s-i;
     | | | t:=t-1;
     | | end;
     | end;
     end;

Основная программа при этом может быть такой:

     t:=1;
     for j:=1 to n do begin
     | a[1]:=j
     | s:=j;
     | generate;
     end;

     Замечание.  Можно немного сэконмить, вынеся операции увели-
чения и уменьшения t из цикла, а также не возвращая s каждый раз
к исходному значению (а увеличивая его на 1 и возвращая к исход-
ному значению в конце). Кроме того,  добавив  фиктивный  элемент
a[0]=n, можно упростить основную программу:

     t:=0; s:=0; a[0]:=n; generate;

     7.3.5.  Написать  рекурсивную  программу обхода дерева (ис-
пользуя те же команды и проверки, что и в главе про обход  дере-
ва).

     Решение.  Процедура  обработать_над обрабатывает все листья
над текущей вершиной и заканчивает работу в той же вершине,  что
и начала. Вот ее рекурсивное описание:

     procedure обработать_над;
     begin
     | if есть_сверху then begin
     | | вверх_налево;
     | | обработать_над;
     | | while есть_справа do begin
     | | | вправо;
     | | | обработать_над;
     | | end;
     | | вниз;
     | end else begin
     | | обработать;
     | end;
     end;

     7.4. Другие применения рекурсии

     Топологическая сортировка. Представим  себе  n  чиновников,
каждый  из  которых  выдает справки определенного вида. Мы хотим
получить все эти справки,  соблюдая  ограничения,  установленные
чиновниками.  Ограничения состоят в том, что у каждого чиновника
есть список справок, которые нужно собрать  перед  обращением  к
нему.  Дело  безнадежно,  если  схема  зависимостей  имеет  цикл
(справку  A  нельзя получить без B, B без C,..., Y без Z и Z без
A). Предполагая, что такого цикла нет, требуется составить план,
указывающий один из возможных порядков получения справок.

     Изображая чиновников точками, а  зависимости  -  стрелками,
приходим  к такой формулировке. Имеется n точек, пронумерованных
от 1 до n. Из каждой точки ведет несколько (возможно, 0) стрелок
в другие точки. (Такая картинка называется ориентированным  гра-
фом.)  Циклов нет. Требуется расположить вершины графа (точки) в
таком порядке, чтобы конец любой стрелки предшествовал ее  нача-
лу. Эта задача называется топологической сортировкой.

     7.4.1. Доказать, что это всегда возможно.

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

     7.4.2.  Предположим,  что  ориентированный  граф без циклов
хранится в такой форме: для каждого i от 1 до n в num[i] хранит-
ся число выходящих из i стрелок, в adr[i][1],..., adr[i][num[i]]
- номера вершин, куда эти стрелки ведут. Составить (рекурсивный)
алгоритм, который производит топологическую сортировку не  более
чем за C*(n+m) действий, где m - число ребер графа (стрелок).

     Замечание.  Непосредственная  реализация  приведенного выше
доказательства существования не дает требуемой оценки; ее прихо-
дится немного подправить.

     Решение. Наша программа будет  печатать  номера  вершин.  В
массиве  printed: array[1..n] of boolean мы будем хранить сведе-
ния о том, какие вершины напечатаны (и корректировать их  однов-
ременно  с  печатью  вершины).  Будем говорить, что напечатанная
последовательность вершин корректна, если никакая вершина не на-
печатана дважды и для любого номера i, входящего в эту последос-
тельность,  все вершины, в которые ведут стрелки из i, напечата-
ны, и притом до i.

     procedure add (i: 1..n);
     | {дано: напечатанное корректно;}
     | {надо: напечатанное корректно и включает вершину i}
     begin
     | if printed [i] then begin {вершина i уже напечатана}
     | | {ничего делать не надо}
     | end else begin
     | | {напечатанное корректно}
     | | for j:=1 to num[i] do begin
     | | | add(adr[i][j]);
     | | end;
     | | {напечатанное корректно, все вершины, в которые из
     | |  i ведут стрелки, уже напечатаны - так что можно
     | |  печатать i, не нарушая корректности}
     | |  if not printed[i] then begin
     | |  | writeln(i); printed [i]:= TRUE;
     | |  end;
     | end;
     end;

Основная программа:

     for i:=1 to n do begin
     | printed[i]:= FALSE;
     end;
     for i:=1 to n do begin
     | add(i)
     end;

     7.4.3.  В  приведенной  программе можно выбросить проверку,
заменив
          if not printed[i] then begin
          | writeln(i); printed [i]:= TRUE;
          end;
на
          writeln(i); printed [i]:= TRUE;
Почему? Как изменится спецификация процедуры?

     Решение.  Спецификацию можно выбрать такой:
       дано: напеватанное корректно
       надо: напечатанное корректно и включает вершину i;
             все вновь напечатанные вершины доступны из i.

     7.4.4. Где использован тот факт, что граф не имеет циклов?

     Решение.  Мы опустили доказательство конечности глубины ре-
курсии. Для каждой вершины  рассмотрим  ее  "глубину"  -  макси-
мальную длину пути по стрелкам, из нее выходящего.  Условие  от-
сутствия циклов гарантирует, что эта величина конечна. Из верши-
ны  нулевой глубины стрелок не выходит. Глубина конца стрелки по
крайней мере на 1 меньше, чем глубина начала. При работе  проце-
дуры  add(i)  все рекурсивные вызовы add(j) относятся к вершинам
меньшей глубины.

     Связная  компонента  графа.  Неориентированный граф - набор
точек (вершин), некоторые из которых соединены  линиями  (ребра-
ми). Неориентированный граф можно считать частным случаем ориен-
тированного графа, в котором для каждой стрелки есть обратная.
     Связной компонентой вершины i называется множество всех тех
вершин, в которые можно попасть из i, идя по ребрам графа. (Пос-
кольку  граф неориентированный, отношение "j принадлежит связной
компоненте i" является отношением эквивалентности.)

     7.4.5. Дан неориентированный граф (для каждой вершины  ука-
зано  число  соседей  и массив номеров соседей, как в предыдущей
задаче). Составить алгоритм, который по заданному i печатает все
вершины связной компоненты i по одному разу (и только их). Число
действий не должно превосходить C*(общее число вершин и ребер  в
связной компоненте).

     Решение.  Программа  в  процессе работы будет "закрашивать"
некоторые вершины графа. Незакрашенной частью графа будем  назы-
вать то, что останется, если выбросить все закрашенные вершины и
ведущие в них ребра. Процедура add(i) закрашивает связную компо-
ненту  i в незакрашенном графе (и не делает ничего, если вершина
i уже закрашена).

     procedure  add (i:1..n);
     begin
     | if вершина i закрашена then begin
     | | ничего делать не надо
     | end else begin
     | | закрасить i (напечатать и пометить как закрашенную)
     | | для всех j, соседних с i
     | | | add(j);
     | | end;
     | end;
     end;

Докажем, что эта процедура действует правильно (в предположении,
что рекурсивные вызовы работают правильно). В самом деле,  ниче-
го, кроме связной компоненты незакрашенного графа, она закрасить
не  может. Проверим, что вся она будет закрашена. Пусть k - вер-
шина, доступная из вершины  i  по  пути  i-j-...-k,  проходящему
только по незакрашенным вершинам. Будем рассматривать только пу-
ти,  не  возвращающиеся  снова  в i. Из всех таких путей выберем
путь с наименьшим j (в порядке просмотра соседей в цикле в  про-
цедуре).  Тогда  при  рассмотрении предыдущих соседей ни одна из
вершин j-...-k не будет закрашена (иначе  j  не  было  бы  мини-
мальным) и потому k окажется в связной компоненте незакрашенного
графа к моменту вызова add(j). Что и требовалось.
     Чтобы установить конечность глубины рекурсии, заметим,  что
на каждом уровне рекурсии число незакрашенных вершин уменьшается
хотя бы на 1.
     Оценим число действий. Каждая вершина закрашивается не  бо-
лее  одного раза - при первым вызове add(i) с данным i. Все пос-
ледующие вызовы происходят при закрашивании соседей - количество
таких  вызовов  не  больше числа соседей - и сводятся к проверке
того, что вершина i уже закрашена. Первый  же  вызов  состоит  в
просмотре  всех  соседей  и  рекурсивных вызовах add(j) для всех
них. Таким образом, общее число действий, связанных  с  вершиной
i, не превосходит константы, умноженной на число ее соседей. От-
сюда и вытекает требуемая оценка.

     7.4.6.  Решить ту же задачу для ориентированного графа (на-
печатать все вершины, доступные из данной по стрелкам; граф  мо-
жет содержать циклы).

     Ответ.  Годится  по  существу  та же программа (строку "для
всех соседей" надо заменить на  "для  всех  вершин,  куда  ведут
стрелки").

     Быстрая сортировка Хоара. В заключение приведем рекурсивный
алгоритм сортировки массива, который на практике является  одним
из  самых быстрых. Пусть дан массив a[1]..a[n]. Рекурсивная про-
цедура  sort (l,r:integer) сортирует участок массива с индексами
из полуинтервала (l,r] (т.е. a[l+1]..a[r]),  не  затрагивая  ос-
тального массива.

     procedure sort (l,r: integer);
     begin
     | if (l = r) then begin
     | | ничего делать не надо - участок пуст
     | end else begin
     | | выбрать случайное число s в полуинтервале (l,r]
     | | b := a[s]
     | | переставить элементы сортируемого участка так, чтобы
     | |   сначала шли элементы, меньшие b - участок (l,ll]
     | |   затем элементы, равные b        - участок (ll,rr]
     | |   затем элементы, большие b       - участок (rr,r]
     | | sort (l,ll);
     | | sort (rr,r);
     | end;
     end;

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

     7.4.7. (Для знакомых с основами теории вероятностей). Дока-
зать, что математическое ожидание числа операций при работе это-
го алгоритма не превосходит C*n*log n, причем константа C не за-
висит от сортируемого массива.

     Указание. Пусть T(n) -  максимум  математического  ожидания
числа  операций для всех входов длины n. Из текста процедуры вы-
текает такое неравенство:

     T(n) <= Cn + 1/n [сумма по всем  k+l=(n-1) чисел T(k)+T(l)]

Первый член соответствует распределению  элементов  на  меньшие,
равные  и большие. Второй член - это среднее математическое ожи-
дание для всех вариантов случайного выбора. (Строго говоря, пос-
кольку среди элементов могут быть равные, в правой части  вместо
T(k) и T(l) должны стоять максимумы T(x) по всем x, не превосхо-
дящим  k или l, но это не мешает дальнейшим рассуждениям.) Далее
индукцией по n нужно доказывать оценку T(n)  <=  C'nlog  n.  При
этом   для   вычисления  среднего  значения  x  log  x  по  всем
x=1,..,n-1 нужно интегрировать x lnx по частям как lnx * d(x*x).
При достаточно большом C' член Cn в правой части  перевешивается
за счет интеграла x*x*d(ln x), и индуктивный шаг проходит.

     7.4.8. Имеется массив из n различных целых чисел a[1]..a[n]
и число k. Требуется найти k-ое по величине число в этом  масси-
ве,  сделав  не более C*n действий, где C - некоторая константа,
не зависящая от k.

     Замечание. Сортировка позволяет очевидным  образом  сделать
это  за  C*n*log(n) действий. Очевидный способ: найти наименьший
элемент, затем найти второй, затем третий,..., k-ый требует  по-
рядка  k*n действий, то есть не годится (константа при n зависит
от k).

      Указание.  Изящный  (хотя  практически  и  бесполезный   -
константы слишком велики) способ сделать это таков:
     А. Разобьем наш массив на n/5 групп, в каждой из которых по
5 элементов. Каждую группу упорядочим.
     Б.  Рассмотрим средние элементы всех групп и перепишем их в
массив из n/5 элементов. С помощью  рекурсивного  вызова  найдем
средний по величине элемент этого массива.
     В.  Сравним этот элемент со всеми элементами исходного мас-
сива: они разделятся на большие его и меньшие его (и один равный
ему). Подсчитав количество тех и других, мы узнаем, в  какой  из
этих  частей  должен находится искомый (k-ый) элемент и каков он
там по порядку.
     Г. Применим рекурсивно наш алгоритм к выбранной части.

     Пусть  T(n)  -  максимально  возможное число действий, если
этот способ применять к массивам из не более чем n элементов  (k
может быть каким угодно). Имеем оценку:
     T(n) <= Cn + T(n/5) + T(примерно 0.7n)
Последнее слагаемое объясняется так: при разбиении на части каж-
дая часть содержит не менее 0.3n элементов. В самом деле, если x
-  средний  из средних, то примерно половина всех средних меньше
x. А если в пятерке средний элемент меньше x, то еще два заведо-
мо меньше x. Тем самым по крайней мере 3/5 от половины элементов
меньше x.
    Теперь  по  индукции можно доказать оценку T(n) <= Cn (реша-
ющую роль при этом играет то обстоятельство, что 1/5 + 0.7 < 1).
        Глава 8. Как обойтись без рекурсии.

     Для универсальных языков программирования (каковым является
паскаль)  рекурсия не дает ничего нового: для всякой рекурсивной
программы можно написать эквивалентную программу  без  рекурсии.
Мы  не будем доказывать этого, а продемонстрируем некоторые при-
емы, позволяющие избавиться от рекурсии в конкретных ситуациях.
     Зачем  это  нужно?  Ответ  прагматика мог бы быть таким: во
многих компьютерах (в том числе, к сожалению, и  в  современных,
использующих  так называемые RISC-процессоры), рекурсивные прог-
раммы в несколько раз  медленнее  соответствующих  нерекурсивных
программ.  Еще один возможный ответ: в некоторых языках програм-
мирования рекурсивные программы запрещены. А главное, при удале-
нии рекурсии возникают изящные и поучительные конструкции.

     8.1. Таблица значений (динамическое программирование)

     8.1.1. Следующая рекурсивная процедура вычисляет числа  со-
четаний  (биномиальные коэффициенты). Написать эквивалентную не-
рекурсивную программу.

        function C(n,k: integer):integer;
        | {n,k >=0; k <=n}
        begin
        | if (k = 0) or (k = n) then begin
        | | C:=1;
        | end else begin {0<k<n}
        | | C:= C(n-1,k-1)+C(n-1,k)
        | end;
        end;

Замечание. C(n,k) - число k-элементных подмножеств n-элементного
множества. Соотношение C(n,k) =  C(n-1,k-1)+C(n-1,k)  получится,
если  мы  фиксируем  некоторый элемент n-элементного множества и
отдельно подсчитаем  k-элементные  множества,  включающие  и  не
включающие этот элемент. Таблица значений C(n,k)

                        1
                      1   1
                    1   2   1
                  1   3   3   1
                .................

называется  треугольником  Паскаля  (того  самого). В нем каждый
элемент, кроме крайних единиц, равен сумме двух стоящих над ним.

     Решение. Можно воспользоваться формулой
        C(n,k) = n! / (k! * (n-k)!)
Мы, однако, не будем этого делать, так как хотим продемонстриро-
вать более общие приемы устранения  рекурсии.  Составим  таблицу
значений  функции  C(n,k), заполняя ее для n = 0, 1, 2,..., пока
не дойдем до интересующего нас элемента.

     8.1.2. Что можно сказать о времени работы рекурсивной и не-
рекурсивной версий в предыдущей задаче? Тот же вопрос о памяти.

     Решение. Таблица занимает место порядка n*n, его можно сок-
ратить до n, если заметить, что для вычисления следующей  строки
треугольника  Паскаля  нужна  только  предыдущая. Время работы в
обоих случаях порядка n*n.  Рекурсивная  программа  требует  су-
щественно большего времени: вызов C(n,k) сводится к двум вызовам
для C(n-1,..), те - к четырем вызовам для C(n-2,..) и т.д. Таким
образом, время оказывается экспоненциальным (порядка 2 в степени
n). Используемая рекурсивной версией память пропорциональна n  -
умножаем глубину рекурсии (n) на количество памяти, используемое
одним экземпляром процедуры (константа).

Кардинальный выигрыш во времени при переходе от рекурсивной вер-
сии к нерекурсивной связан с тем, что в рекурсивном варианте од-
ни  и  те  же  вычисления  происходят много раз. Например, вызов
C(5,3) в конечном счете порождает два вызова C(3,2):

                        C(5,3)
                       /     \
                     C(4,2)  C(4,3)
                    /  \     /   \
                 C(3,1) C(3,2)   C(3,3)
                ......................

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

     8.1.2. Порассуждать на ту же тему на примере рекурсивной  и
(простейшей)  нерекурсивной  программ для вычисления чисел Фибо-
наччи, заданных соотношением
        f(1) = f (2) = 1;  f(n) = f(n-1) + f(n-2) для n > 2.

     8.1.3. Дан выпуклый n-угольник (заданный координатами своих
вершин в порядке обхода). Его разрезают на треугольники диагона-
лями, для чего необходимо n-2 диагонали (докажите  индукцией  по
n). Стоимостью разрезания назовем сумму длин всех использованных
диагоналей.   Найти   минимальную  стоимость  разрезания.  Число
действий должно быть ограничено некоторым многочленом от n. (Пе-
ребор не подходит, так как число вариантов не ограничено многоч-
леном.)

     Решение. Будем считать, что вершины пронумерованы от 1 до n
и  идут  по  часовой стрелке. Пусть k, l - номера вершин, причем
l>k. Через A(k,l) обозначим многоугольник, отрезаемый от  нашего
хордой  k--l.  (Эта  хорда разрезает многоугольник на 2, один из
которых включает сторону 1--n; через A(k,l) мы  обозначаем  дру-
гой.)  Исходный многоугольник естественно обозначить A(1,n). При
l=k+1 получается "двуугольник" с совпадающими сторонами.

Через  a(k,l)  обозначим  стоимость  разрезания   многоугольника
A(k,l) диагоналями на треугольники. Напишем рекуррентную формулу
для  a(k,l).  При  l=k+1  получается  двуугольник, и мы полагаем
a(k,l)=0. При l=k+2 получается треугольник, и в этом случае так-
же a(k,l)=0. Пусть l > k+2. Хорда k--l является стороной  много-
угольника  A(k,l)  и,  следовательно,  стороной  одного  из тре-
угольников,  на  которые он разрезан. Противоположной вершиной i
этого треугольника может быть любая из вершин k+1,...,l-1, и ми-
нимальная стоимость разрезания может быть вычислена как

    min {(длина хорды k--i)+(длина хорды i--l)+a(k,i)+a(i,l)}

по всем i=k+1,..., i=l-1. При этом надо учесть,  что  при  i=k+1
хорда k--i - не хорда, а сторона, и ее длину надо считать равной
0 (по стороне разрез не проводится).

     Составив таблицу для a(k,l) и заполняя ее в порядке возрас-
тания числа вершин (равного l-k+2), мы получаем  программу,  ис-
пользующую память порядка n*n и время порядка n*n*n (однократное
применение  рекуррентной  формулы  требует выбора минимума из не
более чем n чисел).

     8.1.4. Матрицей размера m*n называется прямоугольная табли-
ца из m строк и n столбцов, заполненная числами. Матрицу размера
m*n  можно умножить на матрицу размера n*k (ширина левого сомно-
жителя  должна  равняться  высоте правого), и получается матрица
размером m*k. Ценой такого умножения будем считать  произведение
m*n*k (таково число умножений, которые нужно выполнить при стан-
дартном способе умножения - но сейчас это нам не важно). Умноже-
ние матриц ассоциативно, поэтому произведение n матриц можно вы-
числять в разном порядке. Для каждого порядка подсчитаем суммар-
ную цену всех матричных умножений. Найти минимальную цену вычис-
ления произведения, если известны  размеры  всех  матриц.  Число
действий должно быть ограничено многочленом от числа матриц.

     Пример.  Матрицы  размером  2*3, 3*4, 4*5 можно перемножать
двумя способами. В первом цена равна 2*3*4 + 2*4*5 = 24 +  40  =
64, во втором цена равна 3*4*5 + 2*3*5 = 90.

     Решение.  Представим  себе,  что первая матрица написана на
отрезке [0,1], вторая - на отрезке [1,2],..., s-ая - на  отрезке
[s-1,s]. Матрицы на отрезках [i-1,i] и [i,i+1] имеют общий  раз-
мер, позволяющих их перемножить. Обозначим его через d[i]. Таким
образом, исходным данным в задаче является массив d[0]..d[s].
     Через a(i,j) обозначим минимальную цену вычисления произве-
дения  матриц на участке [i,j] (при 0<=i<j<=s). Искомая величина
равна a(0,s). Величины a(i,i+1) равны нулю (матрица одна  и  пе-
ремножать ничего не надо). Рекуррентная формула будет такой:

    a(i,j) = min {a(i,k)+ a(k,j) + d[i]*d[k]*d[j]}

где  минимум берется по всем возможных местам последнего умноже-
ния, то есть по всем k=i+1..j-1. В самом деле, произведение мат-
риц на отрезке [i,k] есть матрица размера d[i]*d[k],  произведе-
ние  матриц  на отрезке [k,j] имеет размер d[k]*d[j], и цена вы-
числения их произведения равна d[i]*d[k]*d[j].

     Замечание. Две последние задачи похожи. Это сходство станет
яснее, если написать  матрицы  -  множители  на  сторонах  1--2,
2--3,..., s-1--s многоугольника, а на каждой хорде i--j написать
произведение всех матриц, стягиваемых этой хордой.

     8.1.5. Железная дорога с односторонним  движением  имеет  n
станций.  Известны цены белетов от i-ой станции до j-ой (при i <
j - в обратную сторонону проезда нет).  Найти  минимальную  сто-
имость  проезда  от начала до конца (с учетом возможной экономии
за счет пересадок).

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

     8.1.6.  Задано конечное множество с бинарной операцией (во-
обще говоря, не коммутативной и даже не ассоциативной).  Имеется
n  элементов  a[1]..a[n]  этого  множества и еще один элемент x.
Проверить,  можно  ли  так  расставить  скобки  в   произведении
a[1]..a[n],  чтобы  в  результате  получился  x.  Число операций
должно не превосходить C*n*n*n для некоторой константы C  (зави-
сищей от числа элементов в выбранном конечном множестве).

     Решение. Заполняем таблицу, в которой для  каждого  участка
a[i]..a[j]  нашего  произведения  хранится список всех возможных
его значений (при разной расстановке скобок).

     По существу этот же прием применяется в полиномиальном  ал-
горитме   проверки   принадлежности   слова  произвольному  кон-
текстно-свободному языку (см. главу 13).

     Следующая задача (задача о рюкзаке) уже упоминалась в главе
3 (Обход дерева).

     8.1.7.  Имеется  n  положительных  целых чисел x[1]..x[n] и
число N. Выяснить, можно ли получить N, складывая  некоторые  из
чисел x[1]..x[n]. Число действий должно быть порядка N*n.
     Указание. После i шагов хранится множество тех чисел на от-
реке   0..N,  которые  предствимы  в  виде  суммы  некоторых  из
x[1]..x[i].

     8.2. Стек отложенных заданий.

     Другой прием устранения рекурсии продемонстрируем на приме-
ре задачи о ханойских башнях.

     8.2.1. Написать нерекурсивную программу для нахождения пос-
ледовательности перемещений дисков в задаче о ханойских башнях.

     Решение. Вспомним рекурсивную программу:

    procedure move(i,m,n: integer);
    | var s: integer;
    begin
    | if i = 1 then begin
    | | writeln ('сделать ход', m, '->', n);
    | end else begin
    | | s:=6-m-n; {s - третий стержень: сумма номеров равна 6}
    | | move (i-1, m, s);
    | | writeln ('сделать ход', m, '->', n);
    | | move (i-1, s, n);
    | end;
    end;

Видно, что задача "переложить i верхних дисков с m-го стержня на
n-ый"  сводится  к трем задачам того же типа: двум задачам с i-1
дисками и к одной задаче с единственным диском. Выполняя эти за-
дачи, важно не позабыть, что еще осталось сделать.

     Для этой цели заведем стек отложенных  заданий,  элементами
которого будут тройки <i,m,n>. Каждая такая тройка интерпретиру-
ется  как  заказ  "переложить i верхних дисков с m-го стержня на
n-ый". Заказы упорядочены в соответствии с требуемым порядком их
выполнения: самый срочный - вершина стека. Получам  такую  прог-
рамму:

    procedure move(i,m,n: integer);
    begin
    | сделать стек заказов пустым
    | положить в стек тройку <i,m,n>
    | {инвариант: осталось выполнить заказы в стеке}
    | while стек непуст do begin
    | | удалить верхний элемент, переложив его в <j,p,q>
    | | if j = 1 then begin
    | | | writeln ('сделать ход', p, '->', q);
    | | end else begin
    | | | s:=6-p-q;
    | | |      {s - третий стержень: сумма номеров равна 6}
    | | | положить в стек тройки <j-1,s,q>, <1,p,q>, <j-1,p,s>
    | | end;
    | end;
    end;

(Заметим,  что  сначала в стек кладется тройка, которую надо вы-
полнять последней.) Стек троек может быть  реализован  как  стри
отдельных  стека.  (Кроме  того, в паскале есть специальный тип,
называемый "запись", который может быть применен.)

     8.2.2. (Сообщил А.К.Звонкин со ссылкой на Анджея  Лисовско-
го.)  Для  задачи  о ханойских башнях есть и другие нерекусивные
алгоритмы. Вот один из них: простаивающим стержнем  (не  тем,  с
которого  переносят, и не тем, на который переносят) должны быть
все стержни по очереди. Другое  правило:  поочередно  перемещать
наименьшее кольцо и не наименьшее кольцо, причем наименьшее - по
кругу.

     8.2.3. Использовать замену рекурсии стеком отложенных зада-
ний в рекурсивной программе печати десятичной записи целого чис-
ла.

     Решение. Цифры добываются с конца и закладываются в стек, а
затем печатаются в обратном порядке.

     8.2.4. Написать  нерекурсивную  программу,  печатающую  все
вершины двоичного дерева.

     Решение. В этом случае стек отложенных заданий будет содер-
жать  заказы двух сортов: заказ напечатать (в свое время) данную
вершину и заказ напечатать все вершины поддерева с данным корнем
(при этом nil считается корнем пустого дерева).  Таким  образом,
элемент стека есть пара: <тип заказа, номер вершины>.
     Вынимая элемент из стека, мы либо сразу исполняем его (если
это  заказ первого типа) либо помещаем в стек три порожденных им
заказа - в одном из шести возможных порядков.

     8.2.5. Что изменится, если требуется  не  печатать  вершины
двоичного дерева, а подсчитать их количество?

     Решение.  Печатание  вершины  следует заменить прибавлением
единицы к счетчику. Другими  словами,  инвариант  таков:  (общее
число  вершин)  = (счетчик) + (сумма чисел вершин в поддеревьях,
корни которых лежат в стеке).

     8.2.6. Для некоторых из шести возможных  порядков  возможны
упрощения, делающие ненужным хранение в стеке элементов двух ви-
дов. Указать некоторые из них.

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

     Замечание. Другую программу печати всех вершин дерева можно
построить на основе программы обхода дерева, разобранной в соот-
ветствующей  главе.  Там  используется команда "вниз". Поскольку
теперешнее представление дерева с помощью массивов l и r не поз-
воляет  найти  предка  заданной вершины, придется хранить список
всех вершин на пути от корня к  текущей  вершине.  Cмотри  также
главу об алгоритмах на графах.

     8.2.7.  Написать  нерекурсивный  вариант  программы быстрой
сортировки. Как обойтись  стеком,  глубина  которого  ограничена
C*log n, где n - число сортируемых элементов?

     Решение.  В  стек кладутся пары <i,j>, интерпретируемые как
отложенные задания на сортировку соответствующих участков масси-
ва. Все эти заказы не пересекаются, поэтому размер стека не  мо-
жет  превысить n. Чтобы ограничиться стеком логарифмической глу-
бины, будем придерживаться такого правила: глубже в  стек  поме-
щать больший из возникающих двух заказов. Пусть  f(n)  -  макси-
мальная  глубина стека, которая может встретиться при сортировке
массива из не более чем n элементов таким способом. Оценим  f(n)
сверху таким способом: после разбиения массива на два участка мы
сначала сортируем более короткий (храня в стеке про запас) более
длинный, при этом глубина стека не больше f(n/2)+1, затем сорти-
руем более длинный, так что

      f(n) <= max (f(n/2)+1, f(n-1)),

откуда очевидной индукцией получаем f(n) = O(log n).

     8.3. Более сложные случаи рекурсии.

     Пусть функция f с натуральными аргументами и значениями оп-
ределена рекурсивно условиями
        f(0) = a,
        f(x) = h(x, f(l(x))),
где a - некоторое число, а h и l -  известные  функции.  Другими
словами,  значение функции f в точке x выражается через значение
f в точке l(x). При этом предполагается, что для любого x в пос-
ледовательности
        x, l(x), l(l(x)),...
рано или поздно встретится 0.
     Если  дополнительно  известно,  что l(x) < x для всех x, то
вычисление f не представляет  труда:  вычисляем  последовательно
f(0), f(1), f(2),...

     8.3.1.  Написать  нерекурсивную  программу вычисления f для
общего случая.

     Решение. Для вычисления f(x) вычисляем последовательность
        l(x), l(l(x)), l(l(l(x))),...
до появления нуля и запоминаем ее, а затем вычисляем значения  f
в точках этой последовательности, идя справа налево.

     Еще более сложный случай из следующей задачи вряд ли встре-
тится  на  практике  (а  если  и встретися, то проще рекурсию не
устранять, а оставить). Но тем не менее: пусть функция f с нату-
ральными аргументами и значениями определяется соотношениями
        f(0) = a,
        f(x) = h(x, f(l(x)), f(r(x))),
где a - некоторое число, а l, r и h - известные функции. Предпо-
лагается, что если взять произвольное число и начать применять к
нему функции l и r в произвольном порядке, то  рано  или  поздно
получится 0.

     8.3.2. Написать нерекурсивную программу вычисления f.

     Решение. Можно было бы сначала построить дерево, у которого
в корне находится x, а в сыновьях вершины i стоят l(i) и r(i)  -
если только i не равно нулю, а затем вычислять значения функции,
идя от листьев к корню. Однако есть и другой способ.

     "Обратной польской записью" (или "постфиксной записью") вы-
ражения  называют  запись,  где знак функции стоит после всех ее
аргументов, а скобки не используются. Вот несколько примеров:

          f(2)                  2 f
          f(g(2))               2 g f
          s(2,t(7))             2 7 t s
          s(2, u(2, s(5,3))     2 2 5 3 s u s

Постфиксная  запись  выражения  позволяет удобно вычислять его с
помощью "стекового калькулятора". Этот калькулятор  имеет  стек,
который  мы  будем представлять себе расположенным горизонтально
(числа вынимаются и кладутся справа). При нажатии на  клавишу  с
числом  это число кладется в стек. При нажатии на функциональную
клавишу соответствующая функция применяется к  нескольким  аргу-
ментам у вершины стека. Например, если в стеке были числа
        2 3 4 5 6
и  нажата  функциональная клавиша s, соотвтетствующая функции от
двух аргументов, то в стеке окажутся числа
        2 3 4 s(5,6)

Перейдем теперь к нашей задаче. В процессе  вычисления  значения
функции  f мы будем работать со стеком чисел, а также с последо-
вательностью чисел и символов "f", "l", "r", "h", которую мы бу-
дем интерпретировать как последовательность  нажатий  кнопок  на
стековом калькуляторе.  Инвариант такой:

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

Пусть нам требуется вычислить значение, к примеру, f(100). Тогда
вначале мы помещаем в стек число 100, а  последовательность  со-
держит  единственный  символ "f". (При этом инвариант соблюдает-
ся.) Далее с последовательностью и стеком выполняются такие пре-
образования:

 старый       старая           новый       новая
 стек      последовательность  стек    последовательность

  X          x P               X x           P
  X x        l P               X l(x)        P
  X x        r P               X r(x)        P
  X x y z    h P               X h(x,y,z)    P
  X 0        f P               X a           P
  X x        f P               X             x x l f x r f h P

Обозначения: x, y, z,.. - числа, X - последовательность чисел, P
- последовательность чисел и символов "f", "l", "r", "h". В пос-
ледней строке предполагается, что m не равно 0. Эта строка соот-
ветствует равенству

        f(x) = h(x, f(l(x)), f(r(x))),

Эти  преобразования выполняются, пока последовательность не ста-
нет пуста. В этот момент в стеке  окажется  единственное  число,
которое и будет ответом.

     Замечание.  Последовательность по существу представляет со-
бой стек отложенных заданий (вершина которого находится слева).
     Глава 9. Разные алгоритмы на графах

     9.1. Кратчайшие пути

     В этом разделе рассматриваются различные варианты одной за-
дач. Пусть имеется n городов, пронумерованных числами от 1 до n.
Для каждой пары городов с номерами i, j в таблице  a[i][j]  хра-
нится  целое число - цена прямого авиабилета из города i в город
j. Считается, что рейсы существуют между любыми городами, a[i,i]
= 0 при всех i, a[i][j] может отличаться от  a[j,i].  Наименьшей
стоимостью проезда из i в j считается минимально возможная сумма
цен  билетов  для маршрутов (в том числе с пересадками), ведущих
из i в j. (Она не превосходит a[i][j], но может быть меньше.)

     В предлагаемых ниже задачах требуется найти наименьшую сто-
имость проезда для некоторых пар городов при тех или иных  огра-
ничениях на массив a и на время работы алгоритма.

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

     Решение. Маршрут длиной больше n всегда содержит цикл,  по-
этому минимум можно искать среди маршрутов длиной не более n,  а
их конечное число.

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

     9.1.2. Найти наименьшую стоимость проезда из 1-го города во
все остальные за время O(n в степени 3).

     Решение. Обозначим через МинСт(1,s,к) наименьшую  стоимость
проезда из 1 в s менее чем с k  пересадками.  Тогда  выполняется
такое соотношение:

   МинСт (1,s,k+1) = наименьшему из чисел МинСт(1,s,k) и
                     МинСт(1,i,k) + a[i][s] (i=1..n)

Как отмечалось выше, искомым ответом является  МинСт(1,i,n)  для
всех i=1..n.

     k:= 1;
     for i := 1 to n do begin x[i] := a[1][i]; end;
     {инвариант: x[i] := МинСт(1,i,k)}
     while k <> n do begin
     | for s := 1 to n do begin
     | | y[s] := x[s];
     | | for i := 1 to n do begin
     | | | if y[s] > x[i]+a[i][s] then begin
     | | | | y[s] := x[i]+a[i][s];
     | | | end;
     | | end
     | | {y[s] = МинСт(1,s,k+1)}
     | | for i := 1 to n do begin x[s] := y[s]; end;
     | end;
     | k := k + 1;
     end;

Приведенный  алгоритм называют алгоритмом динамического програм-
мирования, или алгоритмом Форда - Беллмана.

     9.1.3. Доказать, что программа останется  правильной,  если
не заводить массива y, а производить изменения в самом массиве x
(заменив в программе все вхождения буквы y на x и затем  удалить
ставшие лишними строки).

     Решение. Инвариант будет таков:
     МинСт(1,i,n) <= x[i] <= MинСт(1,i,k)

     Этот алгоритм может быть улучшен в двух  отношениях:  можно
за то же время O(n в степени 3) найти наименьшую стоимость  про-
езда i->j для ВСЕХ пар i,j (а не только с i=1), а  можно  сокра-
тить время работы до O(n в степени 2). Правда, в последнем  слу-
чае нам потребуется, чтобы все цены a[i][j] были неотрицательны.

     9.1.4. Найти наименьшую стоимость проезда i->j для всех i,j
за время O(n в степени 3).

     Решение. Для k = 0..n через А(i,j,k)  обозначим  наименьшую
стоимость маршрута из i в j, если в качестве пересадочных разре-
шено использовать только пункты с номерами не больше k. Тогда

     A(i,j,0) = a[i][j],
а
     A(i,j,k+1) = min (A(i,j,k), A(i,k+1,k)+A(k+1,j,k))

(два  варианта  соответствуют  неиспользованию  и  использованию
пункта k+1 в качестве пересадочного; отметим, что в нем  незачем
бывать более одного раза).
     Этот алгоритм называют алгоритмом Флойда.

     9.1.5.  Известны,  что  все  цены неотрицательны. Найти на-
именьшую стоимость проезда 1->i для всех i=1..n за время  O(n  в
степени 2).

     Решение. В процессе работы алгоритма некоторые города будут
выделенными (в начале - только город 1,  в  конце  -  все).  При
этом:

     для каждого выделенного города i хранится  наименьшая  сто-
имость пути 1->i; при этом известно, что минимум достигается  на
пути, проходящем только через выделенные города;
     для каждого невыделенного города i хранится наименьшая сто-
имость пути 1->i, в котором в качестве промежуточных используют-
ся только выделенные города.

     Множество  выделенных городов расширяется на основании сле-
дующего замечания: если среди всех  невыделенных  городов  взять
тот,  для которого хранимое число минимально, то это число явля-
ется истинной наименьшей стоимостью. В самом  деле,  пусть  есть
более  короткий  путь.  Рассмотрим  первый невыделенный город на
этом пути - уже до него путь длиннее! (Здесь существенна неотри-
цательность цен.)
     Добавив выбранный город к выделенным, мы должны  скорректи-
ровать информацию, хранимую для невыделенных городов.  При  этом
достаточно учесть лишь пути, в которых новый город является пос-
ледним пунктом пересадки, а это легко сделать, так как минималь-
ную стоимость проезда в новый город мы уже знаем.
     При самом бесхитростном способе хранения множества выделен-
ных городов (в булевском векторе)  добавление  одного  города  к
числу выделенных требует времени O(n).
     Этот алгоритм называют алгоритмом Дейкстры.

     Отыскании кратчайшего пути имеет естественную интерпретацию
в терминах матриц. Пусть A - матрица цен одной аваиакомпании,  а
B  -  матрица цен другой. (Мы считаем, что диагональные элементы
матриц равны 0.) Пусть мы хотим лететь с одной пересадкой,  при-
чем  сначала самолетом компании A, а затем - компании B. Сколько
нам придется заплатить, чтобы попасть из города i в город j?

     9.1.6. Доказать, что эта  матрица  вычисляется  по  обычной
формуле  для произведения матриц, только вместо суммы надо брать
минимум, а вместо умножения - сумму.

     9.1.7. Доказать, что таким образом определенное  произведе-
ние матриц ассоциативно.

     9.1.8. Доказать, что задача о кратчайших путях эквивалентна
вычислению "бесконечной степени" матрицы  цен  A:  в  последова-
тельности  A, A*A, A*A*A,... все элементы, начиная с некоторого,
равны искомой матрице стоимостей кратчайших путей. (Если нет от-
рицательных циклов!)

     9.1.9.  Начиная  с  какого элемента можно гарантировать ра-
венство в предыдущей задаче?

     Обычное  (не  модифицированное) умножение матриц тоже может
оказаться полезным, только матрицы  должны  быть  другие.  Пусть
есть не все рейсы (как в следующем разделе), а только некоторые,
a[i,j]  равно  1,  если рейс есть, и 0, если рейса нет. Возведем
матрицу a (обычным образом) в степень k и посмотрим на ее i-j-ый
элемент.

     9.1.10. Чему он равен?

     Ответ. Числу различных способов попасть  из  i  в  j  за  k
рейсов.

     Случай,  когда есть не все рейсы, можно свести к исходному,
введя фиктивные  рейсы  с  бесконечно  большой  (или  достаточно
большой)  стоимостью. Тем не менее возникает такой вопрос. Число
реальных рейсов может быть существенно меньше n*n, поэтому инте-
ресны алгоритмы, которые работают эффективно в  такой  ситуации.
Исходные  данные  естественно  представлять тогда в такой форме:
для каждого города известно число выходящих из него  рейсов,  их
пункты назначения и цены.

     9.1.11.  Доказать,  что алгоритм Дейкстры можно модифициро-
вать так, чтобы для n городов и k маршрутов он требовал не более
C*(n+k log n) операций.
     Указание. Что надо сделать на каждом шаге? Выбрать  невыде-
ленный город с минимальной стоимостью и скорректировать цены для
всех  городов,  в  которые из него есть маршруты. Если бы кто-то
сообщал нам, для какого города стоимость минимальна, то  хватило
бы C*(n+k) действий. А поддержание сведений о том, какой элемент
в  массиве  минимален  (см. задачу 6.4.1 в главе о типах данных)
обходится еще в множитель log n.

     9.2. Связные компоненты, поиск в глубину и ширину

     Наиболее простой случай задачи о кратчайших  путях  -  если
все цены равны 0 или бесконечны. Другими словами, мы интересуем-
ся  возможностью попасть из i в j, но за ценой не постоим (и она
нас не интересует). В других терминах: мы имеем  ориентированный
граф (картинку из точек, некоторые из которых соединены стрелка-
ми) и нас интересуют вершины, доступные из данной.

     Для  этого  случая  задачи о кратчайших путях приведенные в
предыдущем разделе алгоритмы - не наилучшие. В самом деле, более
быстрая  рекурсивная  программа  решения этой задачи приведена в
главе 7 (Рекурсия), а нерекурсивная - в главе 6  (Типы  данных).
Сейчас  нас  интересует  такая задача: не просто перечислить все
вершины, доступные из данной, но перечислить их  в  определенном
порядке. Два популярных случая - поиск в ширину и в глубину.

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

     9.2.1.  Придумать  алгоритм  решения  этой  задачи с числом
действий не более C*(число ребер, выходящих из интересующих  нас
вершин).

     Решение.  Эта  задача  рассматривалась в главе 6 (Типы дан-
ных), 6.3.7 - 6.3.8. Здесь мы приведём подробное решение.  Пусть
num[i]  -  количество  ребер,  выходящих  из  i,  out[i][1],...,
out[i][num[i]] - вершины, куда ведут ребра. Вот программа,  при-
ведённая ранее:

  procedure Доступные (i: integer);
  |   {напечатать все вершины, доступные из i, включая i}
  | var  X: подмножество 1..n;
  |      P: подмножество 1..n;
  |      q, v, w: 1..n;
  |      k: integer;
  begin
  | ...сделать X, P пустыми;
  | writeln (i);
  | ...добавить i к X, P;
  | {(1) P = множество напечатанных вершин; P содержит i;
  |  (2) напечатаны только доступные из i вершины;
  |  (3) X - подмножество P;
  |  (4) все напечатанные вершины, из которых выходит
  |      ребро в ненапечатанную вершину, принадлежат X}
  | while X непусто do begin
  | | ...взять какой-нибудь элемент X в v;
  | | for k := 1 to num [v] do begin
  | | | w := out [v][k];
  | | | if w не принадлежит P then begin
  | | | | writeln (w);
  | | | | добавить w в P;
  | | | | добавить w в X
  | | | end;
  | | end;
  | end;
  end;

Тогда нам было безразлично, какой именно элемент множества X вы-
бирается. Если мы будем считать X очередью (первым пришел - пер-
мым ушел), то эта программа напечатает все вершины, доступные из
i, в порядке возрастания их расстояния  от  i  (числа  ребер  на
кратчайшем пути из i). Докажем это.

     Обозначим  через V(k) множество всех вершин, расстояние ко-
торых от i (в описанном смысле) равно k. Имеет место такое соот-
ношение:

 V(k+1) = (концы ребер с началами в V(k))-V(0)-V(1)-...-V(k)

(знак "-" обозначает вычитание множеств). Докажем, что для любо-
го k=0,1,2... в ходе работы программы будет такой момент  (после
очередной итерации цикла while), когда

     в очереди стоят все элементы V(k) и только они
     напечатаны все элементы V(1),...,V(k)

(Для  k=0  - это состояние перед циклом.) Рассуждая по индукции,
предположим, что в очереди скопились все элементы V(k). Они  бу-
дут  просматривать  в  цикле,  пока не кончатся (поскольку новые
элементы добавляются в конец, они не перемешаются  со  старыми).
Концы  ведущих из них ребер, если они уже не напечатаны, печата-
ются и ставятся в очередь - то есть всё как  в  записанном  выше
соотношении для V(k+1). Так что когда все старые  элементы  кон-
чатся, в очереди будут стоять все элементы V(k+1).

     Поиск в глубину.

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

     Будем предполагать, что для каждой вершины графа  выходящие
из  нее  ребра  упорядочены (напрмиер, пронумерованы). Тем самым
для каждой вершины дерева выходящие из нее ребра также  упорядо-
чены.  Будем обходить дерево так: сначала корень, а потом подде-
ревья (в порядке  ведущих  в  них  ребер).  Такой  обход  дерева
рассматривался  нами в главе о рекурсии. Ему соответствует обход
графа. Если выкинуть из этого обхода повторные посещения уже по-
сещенных вершин, то получится то, что называется "поиск в глуби-
ну".

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

     9.2.2. Написать программу поиска в глубину.
     Указание.  Возьмем  программу обхода дерева (корень - левое
поддерево - правое поддерево) из главы о рекурсии или  из  гравы
об  устранении  рекурсии  и используем ее применительно к обсто-
ятельствам. Главное изменение: не надо посещать вершины  повтор-
но.  Так что если мы попали в уже посещенную вершину, то можно с
ней ничего не делать. (Если путь не минимален  среди  ведущих  в
данную  вершину,  то  и  все  его продолжения не минимальны - их
просматривать не надо).

     Замечание. Существуют две возможности устранения рекурсии в
программе обхода дерева. Можно хранить в стеке корни  подлежащих
посещению  поддеревьев  (как  это делалось в главе об устранении
рекурсии). А можно применять метод из главы об обходе дерева, то
есть реализовать операции  "вверх_налево",  "вправо"  и  "вниз".
Чтобы их реализовать, необходимо хранить в стеке путь из корня к
текущей  вершине. Оба способа - примерно одинаковой сложности, и
в конкретной ситуации любой из них может оказаться  более  удоб-
ным.

     Поиск в глубину лежит в основе многих алгоритмов на графах,
порой в несколько модифицированном виде.

      9.2.3. Неориентированный граф называется двудольным,  если
его  можно  раскрасить в два цвета так, что концы любого ребра -
разного цвета. Составить алгоритм проверки, является ли заданный
граф двудольным (число действий не провосходит C*(число ребер  +
число вершин).

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

     Замечание.  В  этой задаче безразлично, производить поиск в
ширину или в глубину.

     9.2.4. Составить нерекурсивный алгоритм топологической сор-
тировки  ориентированного  графа без циклов. (См. задачу 7.4.2 в
главе о рекурсии.)

     Решение.  Предположим,  что  граф  имеет вершины с номерами
1..n, для каждой вершины i известно число  num[i]  выходящих  из
нее ребер и номера вершин dest[i][1],..., dest[i][num[i]], в ко-
торые эти ребра ведут. Будем условно считать, что ребра перечис-
лены "слева направо": левее то ребро, у которого  номер  меньше.
Нам надо напечатать все вершины в таком порядке, чтобы конец лю-
бого ребра был напечатан перед его началом. Мы предполагаем, что
в графе нет ориентированных циклов - иначе такое невозможно.
      Для начала добавим к графу вершину 0, из которой ребра ве-
дут в вершины 1,...,n. Если ее удастся напечатать с  соблюдением
правил, то тем самым все вершины будут напечатаны.

      Алгоритм  хранит путь, выходящий из нулевой вершины и иду-
щий по ребрам графа. Переменная l отводится для длины этого  пу-
ти.  Путь  образован  вершинами  vert[1],..., vert[l] и ребрами,
имеющими номера edge[1]...edge[l]. Номер edge[s] относится к ну-
мерации ребер, выходящих из вершины vert[s]. Тем самым для  всех
s должны выполняться неравенство
        edge[s] <= num[vert[s]]
и равенство
        vert[s+1] = dest [vert[s]] [edge[s]]
Впрочем,  для  последнего  ребра мы сделаем исключение, разрешив
ему указывать "в пустоту",  т.е.  разрешим
edge[l] равняться num[vert[l]]+1.

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

(И)     вершины  пути, кроме последней (т.е. vert[1]..vert[l])
        не напечатаны, но свернув с пути налево, мы немедленно
        упираемся в напечатанную вершину

Вот что получается:

        l:=1; vert[1]:=0; edge[1]:=1;
        while not( (l=1) and (edge[1]=n+1)) do begin
        | if edge[l]=num[vert[l]]+1 then begin
        | | {путь кончается в пустоте, поэтому все вершины,
        | |     следующие за vert[l], напечатаны - можно
        | |     печатать vert[l]}
        | | writeln (vert[l]);
        | | l:=l-1; edge[l]:=egde[l]+1;
        | end else begin
        | |  {edge[l] <= num[vert[l]], путь кончается в
        | |     вершине}
        | |  lastvert:= dest[vert[l]][edge[l]]; {последняя}
        | |  if lastvert напечатана then begin
        | |  | edge[l]:=edge[l]+1;
        | |  end else begin
        | |  | l:=l+1; vert[l]:=lastvert; edge[l]:=1;
        | |  end;
        | end;
        end;
        {путь сразу же ведет в пустоту, поэтому все вершины
         левее, то есть 1..n, напечатаны}

     9.2.4. Доказать, что если в графе нет циклов, то этот алго-
ритм заканчивает работу.

     Решение. Пусть это не так. Каждая вершина может  печататься
только  один раз, тако что с некоторого момента вершины не печа-
таются. В графе без циклов длина пути ограничена (вершина не мо-
жет входить дважды), поэтому подождав еще,  мы  можем  дождаться
момента,  после  которого  путь не удлиняется. После этого может
разве что увеличиваться edge[l] - но и это не беспредельно.
     Глава 10. Сопоставление с образцом.

     10.1. Простейший пример.

     10.1.1. Имеется последовательность символов x[1]..x[n]. Оп-
ределить, имеются ли в ней идущие друг за другом символы "abcd".
(Другими словами, требуется выяснить, есть ли в слове x[1]..x[n]
подслово "abcd".)

    Решение. Имеется примерно n (если быть точным, n-3) позиций,
на  которых  может находиться искомое подслово в исходном слове.
Для каждой из позиций можно проверить, действительно ли там  оно
находится, сравнив четыре символа. Однако есть более эффективный
способ. Читая слово x[1]..x[n] слева направо, мы ожидаем появле-
ния  буквы  'a'.  Как только она появилась, мы ждем за ней букву
'b', затем 'c', и, наконец, 'd'. Если наши ожидания оправдывают-
ся, то слово "abcd" обнаружено. Если же какая-то из нужных  букв
не  появляется, мы оказываемся у разбитого корыта и начинаем все
сначала.

     Этот простой алгоритм можно описать в разных терминах.  Ис-
пользуя  терминологию  так  называемых конечных автоматов, можно
сказать, что при чтении слова x слева направо мы в каждый момент
находимся в  одном  из  следующих  состояний:  "начальное"  (0),
"сразу после a" (1), "сразу после ab" (2), "сразу после abc" (3)
и  "сразу после abcd" (4). Читая очередную букву, мы переходим в
следующее состояние по правилу

         Текущее         Очередная      Новое
         состояние       буква          состояние
          0                a             1
          0              кроме a         0
          1                b             2
          1                a             1
          1              кроме a,b       0
          2                c             3
          2                a             1
          2              кроме a,c       0
          3                d             4
          3                a             1
          3              кроме a,d       0

Как только мы попадем в состояние 4,  работа заканчивается.

     Соответствующая программа очевидна:
        i:=1; state:=0;
        {i - первая непрочитанная буква, state - состояние}
        while (i<> n+1) and (state <> 4) do begin
          if state = 0 then begin
            if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 1 then begin
            if x[i] = b then begin
              state:= 2;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 2 then begin
            if x[i] = c then begin
              state:= 3;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 3 then begin
            if x[i] = d then begin
              state:= 4;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end;
        end;
        answer := (state = 4);

     Иными  словами, мы в каждый момент храним информацию о том,
какое максимальное начало нашего образца "abcd" является  концом
прочитанной  части.  (Его длина и есть то "состояние", о котором
шла речь.)

     Терминология,  нами используемая, такова. Слово - это любая
последовательность символов из некоторого фиксированного  конеч-
ного множества. Это множество называется алфавитом, его элементы
- буквами. Если отбросить несколько букв с конца слова, останет-
ся  другое  слово, называемое началом первого. Любое слово также
считается своим началом. Конец слова - то, что  останется,  если
отбросить  несколько  первых  букв.  Любое слово считается своим
концом. Подслово - то, что останется, если отбросить буквы  и  с
начала, и с конца. (Другими словами, подслова - это концы начал,
или, что то же, начала концов.)

     В  терминах  индуктивных  функций (см. раздел 1.3) ситуацию
можно описать так: рассмотрим функцию на словах, которая  прини-
мает два значения "истина" и "ложь" и истинна на словах, имеющих
"abcd"  своим подсловом. Эта функция не является индуктивной, но
имеет индуктивное расширение

 x ->длина максимального начала слова abcd, являющегося концом x

     10.2. Повторения в образце - источник проблем.

     10.2.1. Можно ли в предыдущих рассуждениях  заменить  слово
"abcd" на произвольное слово?

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

Вот картинка, поясняющая сказанное:

 x   y   z   a   b   a   b   a   b   c   ....  <- входное слово

             a   b   a   b   c       <-  мы ждали образца здесь

                     a   b   a   b   c  <-  а он оказался здесь

Таким образом, к моменту
                           |
 x   y   z   a   b   a   b |             <- входное слово
                           |
             a   b   a   b | c       <-  мы ждали образца здесь
                           |
                     a   b | a   b   c  <-  а он оказался здесь
                           |
есть два возможных положения образца, каждое из которых подлежит
проверке. Тем не менее по-прежнему  возможен  конечный  автомат,
читающий  входное  слово буква за буквой и переходящий из состо-
яния в состояние в зависимости от прочитанных букв.

     10.2.2. Указать состояния соответствующего автомата и  таб-
лицу  перехода (новое состояние в зависимости от старого и чита-
емой буквы).

     Решение. По-прежнему состояния  будут  соответствовать  на-
ибольшему  началу  образца, являющемуся концом прочитанной части
слова. Их будет шесть: 0,  1  ("a"),  2  ("ab"),  3  ("aba"),  4
("abab"), 5 ("ababc"). Таблица перехода:

         Текущее         Очередная      Новое
         состояние       буква          состояние
          0                a             1 (a)
          0              кроме a         0
          1 (a)            b             2 (ab)
          1 (a)            a             1 (a)
          1 (a)          кроме a,b       0
          2 (ab)           a             3 (aba)
          2 (ab)         кроме a         0
          3 (aba)          b             4 (abab)
          3 (aba)          a             1 (a)
          3 (aba)        кроме a,b       0
          4 (abab)         c             5 (ababc)
          4 (abab)         a             3 (aba)
          4 (abab)       кроме a,c       0

Для проверки посмотрим, к примеру, на вторую снизу строку.  Если
прочитанная  часть  кончалась на "abab", а затем появилась буква
"a", то теперь  прочитанная  часть  кончается  на  "ababa".  На-
ибольшее  начало  образца ("ababc"), которое есть ее конец - это
"aba".

     Философский вопрос: мы говорили, что  трудность  состоит  в
том,  что  есть несколько возможных положений образца, каждое из
которых может оказаться истинным. Им соответствуют несколько на-
чал образца, являющихся концами входного слова. Но конечный  ав-
томат помнит лишь самое длинное из них. Как же остальные?

     Философский ответ. Дело в том, что самое длинное из них оп-
ределяет  все остальные - это его концы, одновременно являющиеся
его началами.

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

     10.3. Вспомогательные утверждения

     Для произвольного слова X рассмотрим все его начала, однов-
ременно  являющиеся его концами, и выберем из них самое длинное.
(Не считая, конечно, самого слова X.) Будем обозначать его n(X).

     Примеры: n(aba)=a, n(abab)=ab, n(ababa)=aba, n(abc) =  пус-
тое слово.

     10.3.1. Доказать, что все слова n(X), n(n(X)), n(n(n(X)))
и т.д. являются началами слова X.

     Решение.  Каждое из них (согласно определению) является на-
чалом предыдущего.

     По той же причине все они являются концами слова X.

     10.3.2. Доказать, что последовательность предыдущей  задачи
обрывается (на пустом слове).

     Решение. Каждое слово короче предыдущего.

     Задача.  Доказать, что любое слово, одновременно являющееся
началом и концом слова X (кроме самого X)  входит  в  последова-
тельность n(X), n(n(X)),...

     Решение. Пусть слово Y есть одновременно начало и конец  X.
Слово  n(X)  - самое длинное из таких слов, так что Y не длиннее
n(X). Оба эти слова являются началами X, поэтому более  короткое
из них является началом более длинного: Y есть начало n(X). Ана-
логично, Y есть конец n(X). Рассуждая по индукции, можно предпо-
лагать, что утверждение задачи верно для всех слов короче  X,  в
частности,  для слова n(X). Так что слово Y, являющееся концом и
началом  n(X), либо равно n(X), либо входит в последовательность
n(n(X)), n(n(n(X))), ..., что и требовалось доказать.

     10.4. Алгоритм Кнута - Морриса - Пратта

     Алгоритм Кнута - Морриса - Пратта (КМП)  получает  на  вход
слово

        X = x[1]x[2]...x[n]

и просматривает его слева направо буква за буквой, заполняя  при
этом массив натуральных чисел l[1]..l[n], так что

      l[i] = длина слова n(x[1]...x[i])

(функция  n  определена в предыдущем пункте). Словами: l[i] есть
длина наибольшего начала слова x[1]..x[i], одновременно являюще-
гося его концом.

     10.4.1.  Какое  отношение  все это имеет к поиску подслова?
Другими словами, как использовать алгоритм КМП  для  определения
того, является ли слово A подсловом слова B?

     Решение.  Применим алгоритм КМП к слову A#B, где # - специ-
альная буква, не встречающаяся ни в A, ни в B. Слово A  является
подсловом слова B тогда и только тогда, когда среди чисел в мас-
сиве l будет число, равное длине слова A.

     10.4.2. Описать алгоритм заполнения таблицы l[1]..l[n].

     Решение.  Предположим, что первые i значений l[1]..l[i] уже
найдены. Мы читаем очередную букву слова (т.е. x[i+1]) и  должны
вычислить l[i+1].

     1                                              i   i+1
    --------------------------------------------------------
    |           уже прочитанная часть X                |   |
    --------------------------------------------------------
    \-----------Z-----------/    \------------Z------------/

Другими словами, нас интересуют начала Z слова x[1]..x[i+1], од-
новременно являющиеся его концами - из них нам надо выбрать  са-
мое длинное. Откуда берутся эти начала? Каждое из них получается
из  некоторого слова Z' приписыванием буквы x[i+1]. Слово Z' яв-
ляется началом и концом слова x[1]..x[i]. Однако не любое слово,
являющееся началом и концом слова x[1]..x[i],  годится  -  надо,
чтобы за ним следовала буква x[i+1].

     Получаем такой рецепт отыскания слова Z. Рассмотрим все на-
чала слова x[1]..x[i], являющиеся одновременно его  концами.  Из
них  выберем  подходящие - те, за которыми идет буква x[i+1]. Из
подходящих выберем самое длинное. Приписав в его  конец  x[i+1],
получим искомое слово Z.

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

    i:=1; l[1]:= 0;
    {таблица l[1]..l[i] заполнена правильно}
    while i <> n do begin
    | len := l[i]
    | {len - длина начала слова x[1]..x[i], которое  является
    |    его концом; все более длинные начала оказались
    |    неподходящими}
    | while (x[len+1] <> x[i+1]) and (len > 0) do begin
    | | {начало оказалось неподходящим, применяем к нему n}
    | | len := l[len];
    | end;
    | {нашли подходящее или убедились в отсутствии}
    | if x[len+1] = x[i+1] do begin
    | | {x[1]..x[len] - самое длинное подходящее начало}
    | | l[i+1] := len+1;
    | end else begin
    | | {подходящих нет}
    | | l[i+1] := 0;
    | end;
    | i := i+1;
    end;

     10.4.3. Доказать, что число действий в  приведенном  только
что алгоритме не превосходит Cn для некоторой константы C.

     Решение. Это не вполне очевидно: обработка каждой очередной
буквы может потребовать многих итераций во внутреннем цикле. Од-
нако каждая такая итерация уменьшает len по крайней мере на 1, и
в этом случае l[i+1] окажется заметно меньше l[i]. С другой сто-
роны, при увеличении i на единицу величина l[i]  может  возрасти
не более чем на 1, так что часто и сильно убывать она не может -
иначе убывание не будет скомпенсировано возрастанием.
     Более точно, можно записать неравенство
    l[i+1] <= l[i] - (число итераций на i-м шаге) + 1
или
    (число итераций на i-м шаге) <= l[i] - l[i+1] + 1
и остается сложить эти неравества по всем i  и  получить  оценку
сверху для общего числа итераций.

     10.4.4.  Будем  использовать этот алгоритм, чтобы выяснить,
является ли слово X длины n подсловом слова Y длины m. (Как  это
делать  с помощью специального разделителя #, описано выше.) При
этом число действий будет не более C*(n+m), и  используемая  па-
мять  тоже. Придумать, как обойтись памятью не более Cn (что мо-
жет быть существенно меньше, если искомый  образец  короткий,  а
слово, в котором его ищут - длинное).

     Решение.  Применяем  алгоритм КМП к слову A#B. При этом вы-
числение значений l[1],...,l[n] проводим для слова X длины  m  и
запоминаем  эти  значения. Дальше мы помним только значение l[i]
для текущего i - кроме него и кроме таблицы l[1]..l[n], нам  для
вычислений ничего не нужно.

     На практике слова X и Y могут не находиться подряд, поэтому
просмотр  слова  X и затем слова Y удобно оформить в виде разных
циклов. Это избавляет также от хлопот с разделителем.

     10.4.5. Написать соответствующий алгоритм (проверяющий, яв-
ляется ли слово X=x[1]..x[n] подсловом слова Y=y[1]..y[m]).

     Решение. Сначала вычисляем таблицу l[1]..l[n]  как  раньше.
Затем пишем такую программу:
     j:=0; len:=0
     {len - длина максимального начала слова X, одновременно
            являющегося концом слова y[1]..j[j]}
     while (len <> n) and (j <> m) do begin
     | while (x[len+1] <> y[j+1]) and (len > 0) do begin
     | | {начало оказалось неподходящим, применяем к нему n}
     | | len := l[len];
     | end;
     | {нашли подходящее или убедились в отсутствии}
     | if x[len+1] = y[j+1] do begin
     | | {x[1]..x[len] - самое длинное подходящее начало}
     | | len := len+1;
     | end else begin
     | | {подходящих нет}
     | | len := 0;
     | end;
     | i := i+1;
     end;
     {если len=n, слово X встретилось; иначе мы дошли до конца
        слова Y, так и не встретив X}

     10.5. Алгоритм Бойера - Мура

     Этот алгоритм делает то, что на первый взгляд  кажется  не-
возможным:  в  типичной  ситуации он читает лишь небольшую часть
всех букв слова, в котором ищется заданный образец. Как так  мо-
жет  быть? Идея проста. Пусть, например, мы ищем образец "abcd".
Посмотрим на четвертую букву слова: если, к примеру,  это  буква
"e",  то  нет  никакой необходимости читать первые три буквы. (В
самом деле, в образце буквы "e" нет, поэтому он  может  начаться
не раньше пятой буквы.)

     Мы приведем самую простой вариант этого алгоритма,  который
не  гарантирует быстрой работы во всех случаях. Пусть x[1]..x[n]
- образец, который надо искать. Для каждого символа s найдем са-
мое правое его вхождение в слово X, то есть  наибольшее  k,  при
котором x[k]=s. Эти сведения будем хранить в массиве pos[s]; ес-
ли  символ  s вовсе не встречается, то нам будет удобно положить
pos[s] = 0 (мы увидим дальше, почему).

     10.5.1. Как заполнить массив pos?

     Решение.
        положить все pos[s] равными 0
        for i:=1 to n do begin
          pos[x[i]]:=i;
        end;

В  процессе поиска мы будем хранить в переменной last номер буквы
в слове, против которой последняя буква образца. Вначале last = m
(длине образца), затем постепенно увеличивается.

     last:=m;
     {все предыдущие положения образца уже проверены}
     while last <= m do begin {слово не кончилось}
     | if x[m] <> y[last] then begin {последние буквы разные}
     | | last := last + (m - pos[y[last]]);
     | | {m - pos[y[last]]  - это минимальный сдвиг образца,
     | |    при котором напротив y[last] встанет такая же
     | |    буква в образце. Если такой буквы нет вообще,
     | |    то сдвигаем на всю длину образца}
     | end else begin
     | | если нынешнее положение подходит, т.е. если
     | | x[1]..x[m] = y[last-m+1]..y[last],
     | | то сообщить о совпадении;
     | | last := last+1;
     | end;
     end;

Знатоки рекомендуют проверку совпадения проводить справа налево,
т.е. начиная с последней буквы образца (в которой совпадение за-
ведомо есть). Можно также немного сэкономить, произведы  вычита-
ние заранее и храня не pos[s], а m-pos[s], т.е. число букв в об-
разце справа от последнего вхождения буквы s.

     Возможны разные модификации этого алгоритма. Например, мож-
но строку last:=last+1 заменить на last:=last+(m-u), где u - ко-
ордината второго справа вхождения буквы x[m]  в образец.

     10.5.2. Как проще всего учесть это в программе?

     Решение. При построении таблицы pos написать
        for i:=1 to n-1 do...
в основной программе вместо last:=last+1 написать
        last:= last+m-pos[y[last]];

     Приведенная нами упрощенный вариант алгоритма Бойера - Мура
в некоторых случаях требует существенно больше n действий (число
действий  порядка  mn),  проигрывая  алгоритму Кнута - Морриса -
Пратта.

     10.5.3. Привести пример ситуации, в которой образец не вхо-
дит в слово, но авлгоритму требуется порядка mn действий,  чтобы
это установить.

     Решение. Пусть образец имеет вид  baaa..aa,  а  само  слово
состоит  только  из  букв a. Тогда на каждом шаге несоответствие
выясняется лишь в последний момент.

     Настоящий (не упрощенный) алгоритм Бойера - Мура гарантиру-
ет, что число действий не првосходит C*(m+n) в худшем случае. Он
использует  идеи,  близкие  к  идеям алгоритма Кнута - Морриса -
Пратта. Представим себе, что мы сравнивали  образец  со  входным
словом, идя справа налево. При этом некоторый кусок Z (являющий-
ся  концом образца) совпал, а затем обнаружилось различие: перед
Z в образце стоит не то, что во входном слове. Что можно сказать
в этот момент о входном слове? В нем обнаружен фрагмент,  равный
Z,  а перед ним стоит не та буква, что в образце. Эта информация
может позволить сдвинуть образец на несколько позиций вправо без
риска пропустить его вхождение. Эти сдаиги следует вычислить за-
ранее для каждого конца Z нашего образца. Как  говорят  знатоки,
все  это  (вычисление  таблицы  сдвигов и использовани ее) можно
уложэить в C*(m+n) действий.

     10.6. Алгоритм Рабина

     Этот алгоритм основан на простой идее. Представим себе, что
в слове длины m мы ищем образем длины n. Вырежем окошечко разме-
ра  n  и будем двигать его по входному слову. Нас интересует, не
совпадает ли слово в окошечке с заданным образцом. Сравнивать по
буквам долго. Вместо этого фиксируем некоторую функцию на словах
длины n. Если значения этой функции на слове в окошечке и на об-
разце различны, то совпадения нет. Только если значения одинако-
вы, нужно проверять совпадение по буквам.

     Что мы выигрываем при таком подходе? Казалось бы, ничего  -
ведь  чтобы  вычислить значение функции на слове в окошечке, все
равно нужно прочесть все буквы этого слова. Так уж лучше их сра-
зу сравнить с образцом. Тем не менее выигрыш возможен, и вот  за
счет  чего.  При  сдвиге окошечка слово не меняется полностью, а
лишь добавляется буква в конце и убирается в начале. Хорошо  бы,
чтобы по этим данным можно было бы легко рассчитать, как меняет-
ся функция.

     10.6.1. Привести пример удобной для вычисления функции.

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

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

     10.6.2. Привести пример семейства удобных функций.

     Решение.  Выберем  некоторое  число  p (желательно простое,
смотри далее) и некоторый вычет x по модулю p. Каждое слово дли-
ны n будем рассматривать как последовательность целых чисел (за-
менив буквы кодами). Эти числа будем рассматривать как коэффици-
енты многочлена степени n-1 и вычислим значение этого многочлена
по модулю p в точке x. Это и будет  одна  из  функций  семейства
(для каждой пары p и x получается, таким образом, своя функция).
Сдвиг  окошка на 1 соответствует вычитанию старшего члена, умно-
жению на x и добавлению свободного члена.
     Следующее соображение говорит в пользу того, что совпадения
не слишком вероятны. Пусть число p фиксировано и к тому же прос-
тое,  а  X  и  Y  -  два различных слова длины n. Тогда им соот-
ветствуют различные многочлены (мы предполагаем, что  коды  всех
букв  различны  - это возможно при p, большем числа букв алфави-
та). Совпадение значений функции означает, что в точке x эти два
различных многочлена совпадают, то есть их разность обращается в
0. Разность есть многочлен степени n-1 и имеет не более n-1 кор-
ней. Таким образом, если n много меньше p, то случайному x  мало
шансов попасть в неудачную точку.

     10.7. Более сложные образцы и автоматы

     Мы можем искать не конкретно слово,  а  подслова  заданного
вида.  Например, можно искать слова вида a?b, где вместо ? может
стоять любая буква (иными словами, нас  интересует  буква  b  на
расстоянии 2 после буквы a).

     10.7.1  Указать  конечный  автомат, проверяющий, есть ли во
входном слове фрагмент вида a?b.

     Решение.  Читая  слово, следует помнить, есть ли буква a на
последнем месте и на предпоследнем - пока  не  встретим  искомый
фрагмент. Получаем такой автомат:

    Старое состояние    Очередная буква   Новое состояние

       00                     a                 01
       00                  не a                 01
       01                     a                 11
       01                  не a                 10
       10                     a                 01
       10                     b                 найдено
       10                не a и не b            00
       11                     a                 11
       11                     b                 найдено
       11                не a и не b            10

     Другой стандартный знак в образце - это звездочка  (*),  на
место  которой может быть подставлено любое слово. Например, об-
разец ab*cd означает, что мы ищем подслово ab, за которым следу-
ет что угодно, а затем (на любом расстоянии) следует cd.

     10.7.2. Указать конечный автомат, проверяющий, есть  ли  во
входном слове образец ab*cd (в описанном только что смысле).

     Решение.

    Старое состояние    Очередная буква   Новое состояние

       нач                    a                 a
       нач                 не a                 нач
        a                     b                 ab
        a                     a                 a
        a                  не a и не b          нач
        ab                    c                 abc
        ab                 не c                 ab
        abc                   d                 найдено
        abc                   c                 abc
        abc                не с и не d          ab

     Еще один вид поиска - это поиск любого из слово  некоторого
списка.

     10.7.3.  Дан  список  слов X[1],...,X[k] и слово Y. Опреде-
лить, входит ли хотя бы одно из слов X[i] в слово Y (как подсло-
во). Количество действий не должно превосходить константы, умно-
женной на суммарную длину всех слов (из списка и того, в котором
происходит поиск).

     Решение. Очевидный способ состоит в том, чтобы каждое слово
из  списка проверять отдельно (с помощью одного из рассмотренных
алгоритмов). Однако при этом мы не укладываемся в заданное число
действий (из-за умножения k на длину слова Y).

     Посмотрим на дело с  другой  стороны.  Каждому  образцу  из
списка соответствует конечный автомат с некоторым множество сос-
тояний. Их можно объединить в один автомат, множеством состояний
которого будет произведение множеств состояний всех тех  автома-
тов.  Это  -  очень  большое  множество.  Однако  на  самом деле
большинство его элементов недоступны  (не  могут  появиться  при
чтении входного слова) и за счет этого получается экономия. При-
мерно эту идею (но в измененном виде) мы и будем использовать.

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

     Склеим  все  образцы в дерево, объединив их совпадающие на-
чальные участки. Например, набору образцов

      {aaa, aab, abab}

соответствует дерево

                       a/ *
           a     a    / b
        * --- * --- * --- *
                \b     a     b
                  \ * --- * --- *

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

Читая  входное слово, мы двигаемся по этому дереву: текущая вер-
шина - это наибольшая (самая правая) из вершин, являющихся  кон-
цом  прочитанной части (=наибольший конец прочитанной части, яв-
ляющийся началом одного из образцов).

Определим функцию n, аргументами и значениями  которой  являются
вершины  дерева. Именно, n(P) = наибольшая вершина дерева, явля-
ющаяся концом P. (Напомним, вершины дерева - это слова.) Нам по-
надобится такое утверждение:

     10.7.4. Пусть P - вершина дерева. Докажите,  что  множество
всех вершин, являющихся концами P, равно {n(P), n(n(P)),...}

     Решение.  См.  доказательство  аналогичного утверждения для
алгоритма Кнута - Морриса - Пратта.

     Теперь ясно, что нужно делать, находясь в вершине P и читая
букву y входного слова. Надо просматривать последовательно  вер-
шины  P, n(P), n(n(P)) и т.д., пока не обнаружится такая, из ко-
торой выходит стрелка с буквой y.  Та  вершина,  в  которую  эта
стрелка ведет, и будет нашим следующим положением.

     Остается  понять,  как  для каждой вершины дерева вычислить
указатель на значение функции n в этой вершине. Это делается как
раньше, при этом значения n для более коротких слов используются
при вычислении очередного значения функции n. Это означает,  что
вершины  дерева  следует  просматривать в порядке возрастания их
длины. Нетрудно понять, что все это можно  уложить  в  требуемое
число действий (хотя константа зависит от числа букв  в  алфави-
те).  Относящиеся  к этому подробности см. в главе об алгоритмах
на графах.

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

     Определение.  Пусть  фикисирован конечный алфавит Г, не со-
держащий  символов  'l', 'e', '(', ')', '*' и '|' (они будут ис-
пользоваться для построения регулярных выражений и не должны пе-
ремешиваться с буквами). Регулярные выражения строятся по  таким
правилам:

     (а) буква алфавита Г - регулярное выражение;
     (б) символы 'l', 'e' - регулярные выражения;
     (в)  если A,B,C,..,E - регулярные выражения, то (ABC...E) -
          регулярное выражение.
     (г)   если   A,B,C,..,E   -   регулярные   выражения,    то
          (A|B|C|...|E) - регулярное выражение.
     (д) если A - регулярное выражение, то A* - регулярное выра-
          жение.

Каждое  регулярное  выражение задает множество слов в алфавите Г
по таким правилам:

     (а) букве соответствует одноэлементное множество, состоящее
         из однобуквенного слова, состоящего из этой буквы;
     (б)  символу  'e' соответствует пустое множество, а символу
         'l' - одноэлементное множество, единственным  элементом
         которого является пустое слово;
     (в) регулярному выражению (ABC...E) соответствует множество
         всех слов, которые можно получить, если к  слову  из  A
         приписать слово из B, затем из C,..., затем из E ("кон-
         катенация" множеств);
     (г)   регулярному   выражению  (A|B|C|...|E)  соответствует
         объединение   множеств,   соответствующих    выражениям
         A,B,C,..,E;
     (д) регулярному выражению A* соответствует "итерация"  мно-
         жества, соответствующего выражению A, то есть множество
         всех  слов,  которые  можно так разрезать на куски, что
         каждый кусок  принадлежит  множеству,  соответствующему
         выражению  A.  (В частности, пустое слово всегда содер-
         жится в A*.)

     Примеры

Выражение               Множество

(a|b)*                  все слова из букв a и b
(aa)*                   все слова из четного числа букв a
(l|a|b|aa|ab|ba|bb)     любое слово из не более чем 2 букв a,b

     10.7.5.   Написать  регулярное  выражение,  которому  соот-
ветствует множество всех слов из букв a и  b,  в  которых  число
букв a четно.

     Решение. Выражение b* задает все слова без a, а выражение
               (b* a b* a b*)
- все слова ровно с двумя буквами  a.  Остается  объединить  эти
множества, а потом применить итерацию:
              ((b* a b* a b*) | b*)*

     10.7.6.  Написать регулярное выражение, которое задает мно-
жество всех слов из букв a,b,c, в  которых  слово  bac  является
подсловом.

     Решение. ((a|b|c)* bac (a|b|c)*)

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

     10.7.7. Какие выражения соответствуют образцам a?b и ab*cd,
рассмотренным  ранее? (В образце '*' используется не в том смыс-
ле, что в регулярных выражениях!) Предполается, что алфавит  со-
держит буквы a,b,c,d,e.

     Решение. ((a|b|c|d|e)* a (a|b|c|d|e) b (a|b|c|d|e)*)  и
              ((a|b|c|d|e)* ab (a|b|c|d|e)* cd (a|b|c|d|e)*).

     10.7.8.  Доказать,  что  для  всякого регулярного выражения
можно  построить  конечный  автомат,  который  распознает  соот-
ветствующее этому выражению множество слов.

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

     Будем двигаться различными способами из Н в К, читая  буквы
по  дороге  (на тех стрелках, где они есть). Каждому пути из Н в
К, таким образом, соответствует некоторое слово. А  источнику  в
целом  соответствует  множество  слов  - тех слов, которые можно
прочесть на путях из Н в К.

     Замечание. Если нарисовать состояния конечного  автомата  в
виде  точек,  а переходы при чтении букв изобразить в виде стре-
лок, то станет ясно, что конечный автомат - это  частный  случай
источника.

     Мы  будем строить конечный автомат по регулярному выражению
в два приема.  Сначала  мы  построим  источник,  которому  соот-
ветствует  то  же  самое множество слов. Затем для произвольного
источника построим автомат, который  проверяет,  принадлежит  ли
слово соответствующему множеству.

     10.7.9.  По регулярному выражению построить источник, зада-
ющий то же множество.

     Решение. Индукция по построению регулярного выражения. Бук-
вам соответствуют графы из одной стрелки. Объединение реализует-
ся так:

               |---------|
          ---->|*Н1   К1*|->---
        /      |---------|      \
      /         |---------|       \
    * --------->|*Н2   К2*|--->-----* К
    Н  \        |---------|        /
         \     |---------|       /
           --->|*Н3   К3*|--->--
               |---------|

Нарисована  картинка  для  объединения  трех  множеств,  прямо-
угольники - это источники, соответствующие им; указаны начальные
и конечные вершины. На новых стрелках (их 6) букв не написано.

     Конкатенации соответствует картинка

       |--------|         |--------|          |--------|
 Н*--->|*Н1  К1*|---->----|*Н2  К2*| ---->----|*Н3  К3*|-->--*К
       |--------|         |--------|          |--------|

     Наконец, итерации соответствует картинка

    Н*--------->----------*----------->----------*К
                        /   \
                      /       \
                      |       |
                      V       ^
                      |       |
                    -------------
                    | *Н1   К1* |
                    -------------

     10.7.10. Дан источник. Построить конечный автомат, проверя-
ющий, принадлежит ли входное слово  множеству,  соответствующему
источнику (т.е. можно ли прочесть это слово, идя из Н в К).

     Решение. Состояниями автомата будут множества вершин источ-
ника. Именно, прочтя некоторое начало X входного слова, мы будем
помнить  множество всех вершин источника, в которые можно пройти
из начальной, прочитав на пути слово X.

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

     10.7.11.  Дан источник. Построить регулярное выражение, за-
дающее то же множество, что и этот источник.

     Решение.  (Сообщено  участниками  просеминара  по  логике.)
Пусть источник имеет вершины 1..k. Будем считать, что  1  -  это
начало,  а  k  - конец. Через D[i,j, s] обозначим множество всех
слов, которые можно прочесть на пути из i в j, если  в  качестве
промежуточных  пунктов  разрешается  использовать только вершины
1,...,s. Согласно определению, источнику соответствует множество
D[1,k,k].
     Индукцией  по s будем доказывать регулярность всех множеств
D[i,j,s] при всех i и j. При  s=0  это  очевидно  (промежуточные
вершины  запрещены, поэтому каждое из множеств состоит только из
букв).
     Из чего состоит множество D[i,j,s+1]? Отметим на  пути  мо-
менты, в которых он заходит в s+1-ую вершину. При этом путь раз-
бивается  на  части, каждая из которых уже не заходит в нее. По-
этому легко сообразить, что

 D[i,j,s+1] = (D[i,j,s]| (D[i,s+1,s] D[s+1,s+1,s]* D[s+1,j,s]))

(вольность записи: мы используем для  операций  над  множествами
обозначения  как  для регулярных выражений). Остается воспользо-
ваться предположением индукции.

     10.7.12. Где еще используется то же самое рассуждение?

     Ответ. В алгоритме Флойда вычисления цены кратчайшего пути,
см. главу 9 (Некоторые алгоритмы на графах).

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

     Решение. Для автоматов переход к отрицанию очевиден.

     Замечание.  На  практике важную роль играет число состояний
автомата. Оказывается, что тут все не так просто,  и  переход  о
источника  к автомату требует экспоненциального роста числа сос-
тояний.  Подробное рассмотрение связанных с этим теоретических и
практических вопросов - дело особое.
     Глава 11. Представление множеств. Хеширование.

     11.1. Хеширование с открытой адресацией

     В предыдущей главе было несколько  представлений  для  мно-
жеств,  элементами которых являются целые числа произвольной ве-
личины. Однако в любом из них хотя бы одна из операций  проверки
принадлежности,  добавления  и удаления элемента требовала коли-
чества действий, пропорционального числу элементов множества. На
практике это бывает слишком много. Существуют способы,  позволя-
ющие  получить для всех трех упомянутых операций оценку C*log n.
Один из таких способов мы рассмотрим в следующей главе.  В  этой
главе мы разберем способ, которые хотя и приводит к C*n действи-
ям  в  худшем  случае,  но  зато "в среднем" требует значительно
меньшего их числа. (Мы не будем уточнять слов "в среднем",  хотя
это и можно сделать.) Этот способ называется хешированием.
     Пусть  нам необходимо представлять множества элементов типа
T, причем число элементов заведомо меньше n.  Выберем  некоторую
функцию h, определенную на значениях типа T и принимающую значе-
ния  0..(n-1).  Было  бы  хорошо, чтобы эта функция принимала на
элементах будущего множества по возможности более  разнообразные
значения.  Худший случай - это когда ее значения на всех элемен-
тах хранимого множества одинаковы. Эту  функцию  будем  называть
хеш-функцией.

     Введем два массива

         val:  array [0..n-1] of T;
         used: array [0..n-1] of boolean;

(мы  позволяем  себе писать n-1 в качестве границы в определении
типа, хотя в паскале это не разрешается). В этих массивах  будут
храниться  элементы  множества: оно равно множеству всех val [i]
для тех i, для которых used [i], причем все эти val [i]  различ-
ны.  По  возможности  мы  будем хранить элемент t на месте h(t),
считая это место "исконным" для элемента t.  Однако  может  слу-
читься  так,  что новый элемент, который мы хотим добавить, пре-
тендует на уже занятое место (для которого used истинно). В этом
случае мы отыщем ближайшее справа свободное место и запишем эле-
мент туда. ("Справа" значит  "в  сторону  увеличения  индексов";
дойдя  до  края,  мы  перескакиваем в начало.) По предположению,
число элементов всегда меньше n, так что пустые места гарантиро-
ванно будут.
     Формально говоря, в любой момент должно  соблюдаться  такое
требование:  для любого элемента множества участок справа от его
исконного места до его фактического места полностью заполнен.
     Благодаря этому проверка принадлежности заданного  элемента
t  осуществляется  легко: встав на h(t), двигаемся направо, пока
не дойдем до пустого места или до элемента t.  В  первом  случае
элемент  t отсутствует в множестве, во втором присутствует. Если
элемент отсутствует, то его можно добавить на  найденное  пустое
место.  Если  присутствует, то можно его удалить (положив used =
false).

     11.1.1. В предыдущем  абзаце  есть  ошибка.  Найдите  ее  и
исправьте.

     Решение.  Дело  в  том, что при удалении требуемое свойство
"отсутствия пустот" может нарушиться. Поэтому будем делать  так.
Создав дыру, будем двигаться направо, пока не натолкнемся на еще
одно  пустое место (тогда на этом можно успокоиться) или на эле-
мент, стоящий не на исконном месте. Во втором случае  посмотрим,
не  нужно  ли этот элемент поставить на место дыры. Если нет, то
продолжаем поиск, если да, то затыкаем им старую дыру. При  этом
образуется новая дыра, с которой делаем все то же самое.

     11.1.2.  Написать программы проверки принадлежности, добав-
ления и удаления.

     Решение.
  function принадлежит (t: T): boolean;
  | var i: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | belong := used [i] and (val [i] = t);
  end;

  procedure добавить (t: T);
  | var i: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | if not used [i] then begin
  | | used [i] := true;
  | | val [i] := t;
  | end;
  end;

  procedure исключить (t: T);
  | var i, gap: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | if used [i] and (val [i] = t) then begin
  | | used [i] := false;
  | | gap := i;
  | | i := (i + 1) mod n;
  | | while used [i] do begin
  | | | if i = h (val[i]) then begin
  | | | | i := (i + 1) mod n;
  | | | end else if dist(h(val[i]),i) < dist(gap,i) then begin
  | | | | i := (i + 1) mod n;
  | | | end else begin
  | | | | used [gap] := true;
  | | | | val [gap] := val [i];
  | | | | used [i] := false;
  | | | | gap := i;
  | | | | i := i + 1;
  | | | end;
  | | end;
  | end;
  end;

     Здесь  dist  (a, b) - измеренное по часовой стрелке (слева
направо) расстояние от a до b, т.е.

     dist (a,b) = (b - a + n) mod n.

(Мы прибавили n, так как функция mod правильно работает  только
при положительном делимом.)

     11.1.3. Существует много вариантов хеширования. Один из них
таков: обнаружив, что исконное место (обозначим его  i)  занято,
будем  искать  свободное  не  среди  i+1, i+2,..., а среди r(i),
r(r(i)), r(r(r(i))),..., где r - некоторое отображение 0..n-1  в
себя. Какие при этом будут трудности?

     Ответ. (1) Не гарантируется, что если пустые места есть, то
мы их найдем. (2) При удалении неясно, как заполнять  дыры.  (На
практике во многих случаях удаление не нужно, так что такой спо-
соб  также  применяется.  Считается,  что удачный подбор r может
предотвратить образование "скоплений" занятых ячеек.)

     11.1.4.  Пусть  для  хранения  множества  всех   правильных
русских  слов  в  программе орфографии используется хеширование.
Что нужно добавить, чтобы к тому же  уметь  находить  английский
перевод любого правильного слова?

     Решение.  Помимо  массива  val,  элементы которого являются
русскими словами, нужен параллельный массив их английских  пере-
водов.

     11.2. Хеширование со списками

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

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

     11.2.1. Пусть хеш-функция принимает значения 1..k. Для каж-
дого  значения хеш-функции рассмотрим список всех элементов мно-
жества с данным значением хеш-функции. Будем хранить эти k спис-
ков с помощью переменных

     Содержание: array [1..n] of T;
     Следующий: array [1..n] of 1..n;
     ПервСвоб: 1..n;
     Вершина: array [1..k] of 1..n;

так же, как мы это делали для k  стеков  ограниченной  суммарной
длины.  Напишите  соответствующие программы. (Теперь с удалением
будет меньше проблем.)

     Решение. Перед началом работы  надо  положить  Вершина[i]=0
для  всех  i=1..k,  и  связать  все  места  в  список свободного
пространства,  положив   ПервСвоб=1   и   Следующий[i]=i+1   для
i=1..n-1, а также Следующий[n]=0.

  function принадлежит (t: T): boolean;
  | var i: integer;
  begin
  | | i := Вершина[h(t)];
  | i := Вершина[h(t)];
  | {осталось искать в списке, начиная с i}
  | while (i <> 0) and (Содержание[i] <> t) do begin
  | | i := Следующий[i];
  | end; {(i=0) or (Содержание [i] = t)}
  | belong := Содержание[i]=t;
  end;

  procedure добавить (t: T);
  | var i: integer;
  begin
  | if not принадлежит(t) then begin
  | | i := ПервСвоб;
  | | {ПервСвоб <> 0 - считаем, что не переполняется}
  | | ПервСвоб := Следующий[ПервСвоб]
  | | Содержание[i]:=t;
  | | Следующий[i]:=Вершина[h(t)];
  | | Вершина[h(t)]:=i;
  | end;
  end;

  procedure исключить (t: T);
  | var i, pred: integer;
  begin
  | i := Вершина[h(t)]; pred := 0;
  | {осталось искать в списке, начиная с i;  pred -
  |    предыдущий. если он есть, и 0, если нет}
  | while (i <> 0) and (Содержание[i] <> t) do begin
  | | pred := i; i := Следующий[i];
  | end; {(i=0) or (Содержание [i] = t)}
  | if Содержание[i]=t then begin
  | | {элемент есть, надо удалить}
  | | if pred = 0 then begin
  | | | {элемент оказался первым в списке}
  | | | Вершина[h(t)] := Следующий[i];
  | | end else begin
  | | | Следующий[pred] := Следующий[i]
  | | end;
  | | {осталось вернуть i  в список свободных}
  | | Следующий[i] :=  ПервСвоб;
  | | ПервСвоб:=i;
  | end;
  end;

     11.2.2.   (Для  знакомых  с  теорией  вероятностей.)  Пусть
хеш-функция с m значениями используется для хранения  множества,
в  котором  в данный момент n элементов. Доказать, что математи-
ческое ожидание числа действий в предыдущей задаче не  превосхо-
дит  С*(1+n/m),  если добавляемый (удаляемый, искомый) элемент t
выбран случайно, причем все значения h(t) имеют  равные  вероят-
ности (равные 1/m).

     Решение.   Если   l(i)  -  длина  списка,  соответствующего
хеш-значению i, то число операцией не превосходит C*(1+l(h(i)));
усредняя, получаем искомый ответ, так как сумма всех l(i)  равна
n.

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

     Пусть H - семейство функций, каждая из  которых  отображает
множество T в множество из n элементов (например, 0..n-1). Гово-
рят, что H - универсальное семейство хеш-функций, если для любых
двух различных значений s и t из множества T вероятность события
"h(s)=h(t)"  для  случайной  функции h из семейства H равна 1/n.
(Другими словами, те функции из H, для которых  h(s)=h(t),  сос-
тавляют 1/n-ую часть всех функций в H.)

     Замечание.  Более сильное требование к семейству H могло бы
состоять в том, чтобы для любых двух различных элементов s  и  t
множества  T  значения h(s) и h(t) случайной функции h являются
независимыми случайными величинами,  равномерно  распределенными
на 0..n-1.

     11.2.3. Пусть t[1]..t[u] - произвольная  последовательность
различных элементов множества T. Рассмотрим количество действий,
происходящих при помещении элементов t[1]..t[u] в множество, хе-
шируемое  с помощью функции h из универсального семейства H. До-
казать, что среднее количество действий (усреднение - по всем  h
из H) не превосходит C*u*(1+u/n).

     Решение. Обозначим через m[i] количество элементов последо-
вательности,   для   которых   хеш-функция   равна   i.   (Числа
m[0]..m[n-1] зависят, конечно,  от  выбора  хеш-функции.)  Коли-
чество действий, которое мы хотим оценить, с точностью до посто-
янного множителя равно сумме квадратов чисел m[0]..m[n-1]. (Если
k  чисел попадают в одну хеш-ячейку, то для этого требуется при-
мерно 1+2+...+k действий.) Эту же сумму квадратов можно записать
как число пар <p,q>, для которых h[t[p]]=h[t[q]]. Последнее  ра-
венство,  если его рассматривать как событие при фиксированных p
и q, имеет вероятность 1/n при p<>q,  поэтому  среднее  значение
соответствующего члена суммы равно 1/n, а для всей суммы получа-
ем оценку порядка u*u/n, а точнее u*u/n + u, если учесть члены с
p=q.

   Оценка  этой  задачи  показывает, что в на каждый добавляемый
элемент  приходится  в среднем C*(1+u/n) операций. В этой оценке
дробь u/n имеет смысл "коэффициента заполнения" хеш-таблицы.

     11.2.4. Доказать аналогичное утверждение  для  произвольной
последовательности  операций добавления, поиска и удаления (а не
только для добавления, как в предыдущей задаче).

     Указание. Будем представлять себе, что в ходе  поиска,  до-
бавления  и удаления элемент проталкивается по списку своих кол-
лег с тем же хеш-значением, пока не найдет своего  двойника  или
не  дойдет  до  конца  списка.  Будем называть i-j-столкновением
столкновение t[i] с t[j]. Общее число  действий  примерно  равно
числу всех столкновений плюс число элементов. При t[i]<>t[j] ве-
роятность i-j-столкновения равна  1/n.  Осталось  проследить  за
столкновениями  между  равными  элементами.  Фиксируем некоторое
значение x из множества T и посмотрим на связанные с ним  опера-
ции.  Они  идут по циклу: добавление - проверки - удаление - до-
бавление - проверки - удаление -  ...  Столкновения  между  ними
происходят  между добавляемым элементом и следующими за ним про-
верками (до удаления включительно), поэтому общее  их  число  не
превосходит числа элементов, равных x.

     Теперь приведем примеры универсальных  семейств.  Очевидно,
для  любых конечных множеств A и B семейство всех функций, отоб-
ражающих A в B, является универсальным.  Однако  этот  пример  с
практической  точки зрения бесполезен: для запоминания случайной
функции из этого семейства нужен массив, число элементов в кото-
ром равно числу элементов в множестве A. (А если мы  можем  себе
позволить  такой массив, то никакого хеширования нам не требует-
ся!)

     Более практичные примеры универсальных семейств могут  быть
построены  с помощью несложных алгебраических конструкций. Через
Z[p] мы обозначаем множество вычетов по простому модулю p,  т.е.
{0,1,...,p-1}; арифметические операции в этом множестве выполня-
ются  по модулю p. Универсальное семейство образуют все линейные
функционалы на Z[p] в степени n со значениями в Z[p]. Более под-
робно,  пусть  a[1],...,a[n]  -  произвольные   элементы   Z[p];
рассмотрим отображение

   h: <x[1]...x[n]> |-> a[1]x{1]+...+a{n]z[n]

Мы получаем семейство из (p в степени n) отображений, параметри-
зованное наборами a[1]...a[n].

     11.2.5. Доказать, что это семейство является универсальным.

     Указание. Пусть x и y - различные точки пространства Z[p] в
степени  n.  Какова  вероятность  того, что случайный функционал
принимает на них одинаковые значения?  Другими  словами,  какова
вероятность  того,  что  он равен нулю на их разности x-y? Ответ
дается таким утверждением: пусть u - ненулевой вектор; тогда все
значения случайного функционала на нем равновероятны.

     В  следующей  задаче  множество B={0,1} рассматривается как
множество вычетов по модулю 2.

     11.2.6. Семейство всех линейных отображений из (B в степени
m) в (B в степени n) является универсальным.

     Родственные идеи неожиданно оказываются полезными в  следу-
ющей ситуации (рассказал Д.Варсонофьев). Пусть мы хотим написать
программу, которая обнаруживала (большинство) опечаток в тексте,
но не хотим хранить список всех правильных словоформ.  Предлага-
ется   поступить  так:  выбрать  некоторое  N  и  набор  функций
f[1],...,f[k], отображающих русские слова в 1..N. В массиве из N
битов положим все биты равными нулю, кроме тех, которые являются
значением какой-то функции набора на какой-то правильной  слово-
форме.  Теперь  приближённый тест на правильность словоформы та-
ков: проверить, что значения всех функций набора на этой  слово-
форме попадают на места, занятые единицами.
     Глава 12. Множества и деревья.

     12.1. Представление множеств с помощью деревьев.

     Полное двоичное дерево. T-деревья.

     Нарисуем точку. Из нее проведем две стрелки (влево вверх  и
вправо вверх) в две другие точки. Из каждой из этих точек прове-
дем по две стрелки и так далее. Полученную картинку (в n-ом слое
будет  (2 в степени (n - 1)) точек) называют полным двоичным де-
ревом. Нижнюю точку называют корнем. У каждой вершины  есть  два
сына  (две  вершины, в которые идут стрелки) - левый и правый. У
всякой вершины, кроме корня, есть единственный отец.
     Пусть выбрано некоторое конечное множество  вершин  полного
двоичного  дерева, содержащее вместе с каждой вершиной и всех ее
предков. Пусть на каждой вершине этого множества написано значе-
ние фиксированного типа T (то есть задано отображение  множества
вершин  в  множество  значений типа T). То, что получится, будем
называть T-деревом. Множество всех T-деревьев обозначим Tree(T).
     Рекурсивное определение. Всякое непустое T-дерево  разбива-
ется на три части: корень (несущий пометку из T), левое и правое
поддеревья  (которые  могут быть и пустыми). Это разбиение уста-
навливает взаимно однозначное соответствие между множеством  не-
пустых T-деревьев и произведением T * Tree (T) * Tree (T). Обоз-
начив через empty пустое дерево, можно написать

     Tree (T) = {empty} + T * Tree (T) * Tree (T).

     Поддеревья. Высота.

     Фиксируем  некоторое T-дерево. Для каждой его вершины x оп-
ределено ее левое поддерево (левый сын вершины x и все  его  по-
томки),  правое поддерево (правый сын вершины x и все его потом-
ки) и поддерево с корнем в x (вершина x и все ее потомки). Левое
и правое поддеревья вершины x могут быть пустыми, а поддерево  с
корнем  в x всегда непусто (содержит по крайней мере x). Высотой
поддерева будем считать максимальную длину цепи  y[1]..y[n]  его
вершин, в которой y [i+1] - сын y [i] для всех i. (Высота пусто-
го дерева равна нулю, высота дерева из одного корня - единице.)

     Упорядоченные T-деревья.

     Пусть  на множестве значений типа T фиксирован порядок. На-
зовем T-дерево упорядоченным, если выполнено такое свойство: для
любой вершины x все пометки в ее левом поддереве меньше  пометки
в x, а все пометки в ее правом поддереве больше пометки в x.

     12.1.1.  Доказать,  что  в упорядоченном дереве все пометки
различны.
     Указание. Индукция по высоте дерева.

     Представление множеств с помощью деревьев.

     Каждое дерево будем считать представлением  множества  всех
пометок  на  его вершинах. При этом одно и то же множество может
иметь различные представления.
     Благодаря упорядоченности каждый элемент легко может "найти
свое место" в дереве: придя в какую-то вершину и сравнив себя  с
тем, кто там находится, элемент решает, идти ему налево или нап-
раво. Начав с корня и двигаясь по этому правилу, он либо обнару-
жит, что такой элемент уже есть, либо найдет место, в котором он
должен  быть. Всюду далее мы предполагаем, что на значениях типа
T задан порядок, и рассматриваем только упорядоченные деревья.

     Хранение деревьев в программе.

     Можно было бы сопоставить вершины полного двоичного  дерева
с  числами  1,  2, 3,... (считая, что левый сын (n) = 2n, правый
сын (n) = 2n + 1) и хранить пометки в массиве val [1...]. Однако
этот способ неэкономен, поскольку  тратится  место  на  хранение
пустых вакансий в полном двоичном дереве.

     Более экономен такой способ. Введем три массива

       val: array [1..n] of T;
       left, right: array [1..n] of 0..n;

(n  -  максимальное  возможное число вершин дерева) и переменную
root: 0..n. Каждая вершина хранимого T-дерева будет иметь  номер
- число от 1 до n. Разные вершины будут иметь разные номера. По-
метка  в  вершине  с номером x равна val [x]. Корень имеет номер
root. Если вершина с номером i имеет сыновей, то их номера равны
left [i] и right [i]. Отсутствующим сыновьям соответствует число
0. Аналогичным образом значение root = 0  соответствует  пустому
дереву.
     Для  хранения  дерева  используется лишь часть массива; для
тех i, которые свободны - т.е. не  являются  номерами  вершин  -
значения  val  [i] безразличны. Нам будет удобно, чтобы все сво-
бодные числа были "связаны в список": первое хранится  в  специ-
альное  переменной  free: 0..n, а следующее за i свободное число
хранится в left [i], так что свободны числа

     free, left [free], left [left[free]],...

Для последнего свободного числа i значение left  [i]  =  0.  Ра-
венство  free = 0 означает, что свободных чисел больше нет. (За-
мечание. Мы использовали для связывания свободных вершин  массив
left, но, конечно, с тем же успехом можно было использовать мас-
сив right.)
     Вместо  значения 0 (обозначающего отсутствие вершины) можно
было бы воспользоваться любым другим числом вне 1..n. Чтобы под-
черкнуть это, будем вместо 0 использовать константу null = 0.

     12.1.2. Составить программу,  определяющую,  содержится  ли
элемент  t:  T  в упорядоченном дереве (хранимом так, как только
что описано).

     Решение.

  if root = null then begin
  | ..не принадлежит
  end else begin
  | x := root;
  | {инвариант: остается проверить наличие t в непустом подде-
  |  реве с корнем x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin {left [x] <> null}
  | | | x := left [x];
  | | end else begin {t > val [x], right [x] <> null}
  | | | x := right [x];
  | | end;
  | end;
  | {либо t = val [x], либо t отсутствует в дереве}
  | ..ответ = (t = val [x])
  end;

     12.1.3. Упростить решение, используя следующий трюк. Расши-
рим область определения массива val, добавив  ячейку  с  номером
null и положим val [null] = t.

     Решение.

  val [null] := t;
  x := root;
  while t <> val [x] do begin
  | if t < val [x] then begin
  | | x := left [x];
  | end else begin
  | | x := right [x];
  | end;
  end;
  ..ответ: (x <> null).

     12.1.4.  Составить  программу  добавления элемента t в мно-
жество, представленное упорядоченным деревом (если элемент t уже
есть, ничего делать не надо).

     Решение. Определим процедуру get_free (var i: integer), да-
ющую свободное (не являющееся номером) число i и соответствующим
образом корректирующую список свободных чисел.

  procedure get_free (var i: integer);
  begin
  | {free <> null}
  | i := free;
  | free := left [free];
  end;

С ее использованием программа приобретает вид:

  if root = null then begin
  | get_free (root);
  | left [root] := null; right [root] := null;
  | val [root] := t;
  end else begin
  | x := root;
  | {инвариант: осталось добавить t к непустому поддереву с
  |  корнем в x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | x := right [x];
  | | end;
  | end;
  | if t <> val [x] then begin {t нет в дереве}
  | | get_free (i);
  | | left [i] := null; right [i] := null;
  | | val [i] := t;
  | | if t < val [x] then begin
  | | | left [x] := i;
  | | end else begin {t > val [x]}
  | | | right [x] := i;
  | | end;
  | end;
  end;

     12.1.5. Составить программу удаления  элемента  t  из  мно-
жества, представленного упорядоченным деревом (если его там нет,
ничего делать не надо).

     Решение.

  if root = null then begin
  | {дерево пусто, ничего делать не надо}
  end else begin
  | x := root;
  | {осталось удалить t из поддерева с корнем в x; поскольку
  |  это может потребовать изменений в отце x, введем
  |  переменные  father: 1..n и direction: (l, r);
  |  поддерживаем такой инвариант: если x не корень, то father
  |  - его отец, а direction равно l или r в зависимости от
  |  того, левым или правым сыном является x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | father := x; direction := l;
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | father := x; direction := r;
  | | | x := right [x];
  | | end;
  | end;
  | {t = val [x] или t нет в дереве}
  | if t = val [x] then begin
  | | ..удаление вершины x  с отцом father и направлением
  | |   direction
  | end;
  end;

Удаление  вершины  x происходит по-разному в разных случаях. При
этом используется процедура

  procedure make_free (i: integer);
  begin
  | left [i] := free;
  | free := i;
  end;

она включает число i в список свободных. Различаются 4 случая  в
зависимости от наличия или отсутствия сыновей у удаляемой верши-
ны.

  if (left [x] = null) and (right [x] = null) then begin
  | {x - лист, т.е. не имеет сыновей}
  | make_free (x);
  | if x = root then begin
  | | root := null;
  | end else if direction = l then begin
  | | left [father] := null;
  | end else begin {direction = r}
  | | right [father] := null;
  | end;
  end else if (left[x]=null) and (right[x] <> null) then begin
  | {x удаляется, а right [x] занимает место x}
  | make_free (x);
  | if x = root then begin
  | | root := right [x];
  | end else if direction = l then begin
  | | left [father] := right [x];
  | end else begin {direction = r}
  | | right [father] := right [x];
  | end;
  end else if (left[x] <> null) and (right[x]=null) then begin
  | ..симметрично
  end else begin {left [x] <> null, right [x] <> null}
  | ..удалить вершину с двумя сыновьями
  end;

Удаление вершины с двумя сыновьями нельзя сделать просто так, но
ее  можно предварительно поменять с вершиной, пометка на которой
является непосредственно следующим (в порядке возрастания)  эле-
ментом за пометкой на x.

    y := right [x];
    father := x; direction := r;
    {теперь father и direction относятся к вершине y}
    while left [y] <> null do begin
    | father := y; direction := r;
    | y := left [y];
    end;
    {val [y] - минимальная из пометок, больших val [x],
     y не имеет левого сына}
    val [x] := val [y];
    ..удалить вершину y (как удалять вершину, у которой нет ле-
      вого сына, мы уже знаем)

     12.1.6. Упростить программу удаления, заметив, что  некото-
рые случаи (например, первые два из четырех) можно объединить.

     12.1.7.  Использовать упорядоченные деревья для представле-
ния функций, область определения которых  -  конечные  множества
значений типа T, а значения имеют некоторый тип U. Операции: вы-
числение  значения  на  данном  аргументе, изменение значения на
данном аргументе, доопределение  функции  на  данном  аргументе,
исключение элемента из области определения функции.

     Решение. Делаем как раньше, добавив еще один массив

         func_val: array [1..n] of U;

если val [x] = t, func_val [x] = u, то значение хранимой функции
на t равно u.

     Оценка количества действий.

     Для  каждой из операций (проверки, добавления и исключения)
количество действий не превосходит  C  *  (высота  дерева).  Для
"ровно подстриженного" дерева (когда все листья на одной высоте)
высота  по порядку величины равна логарифму числа вершин. Однако
для кривобокого дерева все может быть гораздо хуже: в  наихудшем
случае  все  вершины  образуют цепь и высота равна числу вершин.
Так случится, если элементы множества добавляются в возрастающем
или убывающем порядке. Можно доказать, однако, что при  добавле-
нии  элементов "в случайном порядке" средняя высота дерева будет
не больше C * (логарифм числа вершин). Если этой оценки "в сред-
нем" мало, необходимы  дополнительные  действия  по  поддержанию
"сбалансированности" дерева. Об этом см. в следующем пункте.

     12.1.8.  Предположим, что необходимо уметь также отыскивать
k-ый элемент множества (в  порядке  возрастания),  причем  коли-
чество  действий  должно  быть не более C*(высота дерева). Какую
дополнительную информацию надо хранить в вершинах дерева?

     Решение. В каждой вершине будем хранить число всех  ее  по-
томков.  Добавление  и исключение вершины требует коррекции лишь
на пути от корня к этой вершине. В процессе поиска k-ой  вершины
поддерживается  такой  инвариант:  искомая вершина является s-ой
вершиной поддерева с корнем в x (здесь s и x - переменные).)

     12.2. Сбалансированные деревья.

     Дерево называется сбалансированным (или АВЛ-деревом в честь
изобретателей этого метода Г.М.Адельсона-Вельского и  Е.М.Ланди-
са),  если  для любой его вершины высоты левого и правого подде-
ревьев этой вершины отличаются не более чем на 1. (В  частности,
когда одного из сыновей нет, другой - если он есть - обязан быть
листом.)

     12.2.1.  Найти  минимальное  и максимальное возможное коли-
чество вершин в сбалансированном дереве высоты n.

     Решение. Максимальное число вершин равно (2 в степени n)  -
1. Если m (n) - минимальное число вершин, то, как легко видеть,
     m (n + 2) = 1 + m (n) + m (n+1),
откуда
     m (n) = fib (n+1) - 1
(fib(n)  -  n-ое число Фибоначчи, fib(0)=1, fib(1)=1, fib(n+2) =
fib(n) + fib(n+1)).

     12.2.2. Доказать, что сбалансированное дерево с n вершинами
имеет высоту не больше C * (log n) для некоторой константы C, не
зависящей от n.

     Решение. Индукцией по n легко доказать, что fib [n+1] >= (a
в степени n), где a - больший корень квадратного уравнения a*a =
1 + a, то есть a = (sqrt(5)  +  1)/2.  Остается  воспользоваться
предыдущей задачей.

     Вращения.

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

     Пусть вершина a имеет правого сына b. Обозначим через P ле-
вое поддерево вершины a, через Q и R - левое и правое поддеревья
вершины b.

     Упорядоченность дерева требует, чтобы P < a < Q  <  b  <  R
(точнее  следовало бы сказать "любая пометка на P меньше пометки
на a", "пометка на a меньше любой пометки на Q" и  т.д.,  но  мы
позволим  себе  этого не делать). Точно того же требует упорядо-
ченность дерева с корнем b, его левым сыном a, в котором P и Q -
левое и правое поддеревья a, R -  правое  поддерево  b.  Поэтому
первое дерево можно преобразовать во второе, не нарушая упорядо-
ченности.  Такое  преобразование  назовем малым правым вращением
(правым - поскольку существует симметричное, левое, малым - пос-
кольку есть и большое, которое мы сейчас опишем).

     Пусть b - правый сын a, c - левый сын b, P -левое поддерево
a, Q и R -левое и правое поддеревья c, S - правое  поддерево  b.
Тогда P < a < Q < c < R < b < S.

Такой же порядок соответствует дереву с корнем c, имеющим левого
сына a и правого сына b, для которого P и Q - поддеревья вершины
a,  а R и S - поддеревья вершины b. Соответствующее преобразова-
ние будем называть большим правым вращением. (Аналогично опреде-
ляется симметричное ему большое левое вращение.)

     12.2.3. Дано дерево, сбалансированное всюду, кроме корня, в
котором разница высот равна 2 (т.е. левое  и  правое  поддеревья
корня сбалансированы и их высоты отличаются на 2). Доказать, что
оно  может  быть  превращено в сбалансированное одним из четырех
описанных преобразований, причем высота  его  останется  прежней
или уменьшится на 1.

     Решение.  Пусть более низким является, например, левое под-
дерево, и его высота равна k.  Тогда  высота  правого  поддерева
равна k+2. Обозначим корень через a, а его правого сына (он обя-
зательно  есть)  через  b.  Рассмотрим левое и правое поддеревья
вершины b. Одно из них обязательно имеет высоту  k+1,  а  другое
может  иметь  высоту  k или k+1 (меньше k быть не может, так как
поддеревья сбалансированы). Если высота левого  поддерева  равна
k+1,  а  правого  - k, до потребуется большое правое вращение; в
остальных случаях помогает малое.

------------------------------------
------------------------------------
------------------------------------

                                        высота уменьшилась на 1

------------------------------------
------------------------------------
------------------------------------

                                         высота не изменилась

   k-1 или k (в одном из случаев k)

------------------------------------
------------------------------------
------------------------------------
                                        высота уменьшилась на 1

        Три случая балансировки дерева.

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

     Решение. Будем доказывать более общий факт:

     Лемма.  Если в сбалансированном дереве X одно из его подде-
ревьев Y заменили на сбалансированное дерево Z, причем высота  Z
отличается  от  высоты  Y не более чем на 1, то полученное такой
"прививкой" дерево можно превратить в сбалансированное  вращени-
ями  (причем количество вращений не превосходит высоты, на кото-
рой делается прививка).
     Частным случаем прививки является замена пустого  поддерева
на лист или наоборот, так что достаточно доказать эту лемму.
     Доказательство  леммы. Индукция по высоте, на которой дела-
ется прививка. Если она происходит в корне (заменяется все дере-
во целиком), то все очевидно ("привой"  сбалансирован  по  усло-
вию). Пусть заменяется некоторое поддерево, например, левое под-
дерево некоторой вершины x. Возможны два случая.
     (1)  После прививки сбалансированность в вершине x не нару-
шилась (хотя, возможно, нарушилась сбалансированность в  предках
x:  высота поддерева с корнем в x могла измениться). Тогда можно
сослаться на предположение индукции, считая,  что  мы  прививали
целиком поддерево с корнем в x.
     (2) Сбалансированность в x нарушилась. При этом разница вы-
сот  равна 2 (больше она быть не может, так как высота Z отлича-
ется от высоты Y не более чем на 1). Разберем два варианта.
    (2а) Выше правое  (не  заменявшееся)  поддерево  вершины  x.
Пусть высота левого (т.е. Z) равна k, правого - k+2. Высота ста-
рого  левого поддерева вершины x (т.е. Y) была равна k+1. Подде-
рево с корнем x имело в исходном дереве высоту k+3, и эта высота
не изменилась после прививки.
     По предыдущей задаче вращение преобразует поддерево с  кор-
нем в x в сбалансированное поддерево высоты k+2 или k+3. То есть
высота  поддерева с корнем x - в сравнении с его прежней высотой
- не изменилась или уменьшилась на 1, и мы можем воспользоваться
предположением индукции.

      -------------                     ----------------
      -------------                     ----------------
      -------------k                    ----------------k
 2а                                 2б

     (2б) Выше левое поддерево вершины x.  Пусть  высота  левого
(т.е. Z) равна k+2, правого - k. Высота старого левого поддерева
(т.е.  Y) была равна k+1. Поддерево с корнем x в исходном дереве
X имело высоту k+2, после прививки она стала  равна  k+3.  После
подходящего  вращения (см. предыдущую задачу) поддерево с корнем
в x станет сбалансированным, его высота будет равна k+2 или k+3,
так что изменение высоты по сравнению с высотой поддерева с кор-
нем x в дереве X не превосходит 1 и можно сослаться на предполо-
жение индукции.

     12.2.5. Составить программы добавления и  удаления  элемен-
тов,  сохраняющие  сбалансированность.  Число действий не должно
превосходить C*(высота дерева). Разрешается хранить  в  вершинах
дерева дополнительную информацию, необходимую при балансировке.

     Решение. Будем хранить для каждой  вершины  разницу  между
высотой ее правого и левого поддеревьев:

  diff [i] = (высота правого поддерева вершины с номером i) -
             (высота левого поддерева вершины с номером i).

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

          Малое правое вращение

          Большое правое вращение

     (2)  После  преобразований  мы  должны также изменить соот-
ветственно значения в массиве diff. Для этого  достаточно  знать
высоты деревьев P, Q, ... с точностью до константы, поэтому мож-
но предполагать, что одна из высот равна нулю.

     Вот процедуры вращений:

  procedure SR (a:integer); {малое правое вращение с корнем a}
  | var b: 1..n; val_a,val_b: T; h_P,h_Q,h_R: integer;
  begin
  | b := right [a]; {b <> null}
  | val_a := val [a]; val_b := val [b];
  | h_Q := 0; h_R := diff[b]; h_P := (max(h_Q,h_R)+1)-diff[a];
  | val [a] := val_b; val [b] := val_a;
  | right [a] := right [b] {поддерево R}
  | right [b] := left [b] {поддерево Q}
  | left [b] := left [a] {поддерево P}
  | left [a] := b;
  | diff [b] := h_Q - h_P;
  | diff [a] := h_R - (max (h_P, h_Q) + 1);
  end;

  procedure BR (a:integer);{большое правое вращение с корнем a}
  | var b,c: 1..n; val_a,val_b,val_c: T;
  |     h_P,h_Q,h_R,h_S: integer;
  begin
  | b := right [a]; c := left [b]; {b,c <> null}
  | val_a := val [a]; val_b := val [b]; val_c := val [c];
  | h_Q := 0; h_R := diff[c]; h_S := (max(h_Q,h_R)+1)+diff[b];
  | h_P := 1 + max (h_S, h_S-diff[b]) - diff [a];
  | val [a] := val_c; val [c] := val_a;
  | left [b] := right [c] {поддерево R}
  | right [c] := left [c] {поддерево Q}
  | left [c] := left [a] {поддерево P}
  | left [a] := c;
  | diff [b] := h_S - h_R;
  | diff [c] := h_Q - h_P;
  | diff [a] := max (h_S, h_R) - max (h_P, h_Q);
  end;

Левые вращения (большое и малое) записываются симметрично.

     Процедуры  добавления  и  удаления  элементов  пишутся  как
раньше, но только добавление и  удаление  должно  сопровождаться
коррекцией  массива  diff  и восстановлением сбалансированности.
При этом используется процедура с такими свойствами:

   дано:  левое и правое поддеревья вершины с номером a сбалан-
       сированы, в самой вершине разница высот не больше  2,  в
       поддереве с корнем a массив diff заполнен правильно;
   надо:  поддерево с корнем a сбалансировано и массив diff со-
       ответственно изменен, d - изменение его высоты (равно  0
       или -1); в остальной части все осталось как было}

  procedure balance (a: integer; var d: integer);
  begin {-2 <= diff[a] <= 2}
  | if diff [a] = 2 then begin
  | | b := right [a];
  | | if diff [b] = -1 then begin
  | | | BR (a); d := -1;
  | | end else if diff [b] = 0 then begin
  | | | SR (a); d := 0;
  | | end else begin {diff [b] = 1}
  | | | SR (a); d := - 1;
  | | end;
  | end else if diff [a] = -2 then begin
  | | b := left [a];
  | | if diff [b] = 1 then begin
  | | | BL (a); d := -1;
  | | end else if diff [b] = 0 then begin
  | | | SL (a); d := 0;
  | | end else begin {diff [b] = -1}
  | | | SL (a); d := - 1;
  | | end;
  | end else begin {-2 < diff [a] < 2, ничего делать не надо}
  | | d := 0;
  | end;
  end;

     Восстановление  сбалансированности  требует   движения   от
листьев  к  корню, поэтому будем хранить в стеке путь от корня к
рассматриваемой в данный момент вершине. Элементами стека  будут
пары (вершина, направление движения из нее), т.е. значения типа

        record
        | vert: 1..n; {вершина}
        | direction : (l, r); {l - левое, r- правое}
        end;

Программа добавления элемента t теперь выглядит так:

  if root = null then begin
  | get_free (root);
  | left [root] := null; right [root] := null; diff[root] := 0;
  | val [root] := t;
  end else begin
  | x := root; ..сделать стек пустым
  | {инвариант: осталось добавить t к непустому поддереву с
  |  корнем в x; стек содержит путь к x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | ..добавить в стек пару <x, l>
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | ..добавить в стек пару <x, r>
  | | | x := right [x];
  | | end;
  | end;
  | if t <> val [x] then begin {t нет в дереве}
  | | get_free (i); val [i] := t;
  | | left [i] := null; right [i] := null; diff [i] := 0;
  | | if t < val [x] then begin
  | | | ..добавить в стек пару <x, l>
  | | | left [x] := i;
  | | end else begin {t > val [x]}
  | | | ..добавить в стек пару <x, r>
  | | | right [x] := i;
  | | end;
  | | d := 1;
  | | {инвариант: стек содержит путь к изменившемуся поддереву,
  | |  высота  которого увеличилась по сравнению с высотой в
  | |  исходном дереве на d (=0 или 1); это поддерево  сбалан-
  | |  сировано; значения diff для его вершин правильны; в ос-
  | |  тальном дереве  все  осталось  как  было  - в частности,
  | |  значения diff}
  | | while (d <> 0) and ..стек непуст do begin {d = 1}
  | | | ..взять из стека пару в <v, direct>
  | | | if direct = l then begin
  | | | | if diff [v] = 1 then begin
  | | | | | c := 0;
  | | | | end else begin
  | | | | | c := 1;
  | | | | end;
  | | | | diff [v] := diff [v] - 1;
  | | | end else begin {direct = r}
  | | | | if diff [v] = -1 then begin
  | | | | | c := 0;
  | | | | end else begin
  | | | | | c := 1;
  | | | | end;
  | | | | diff [v] := diff [v] + 1;
  | | | end;
  | | | {c = изменение высоты поддерева с корнем в v по сравне-
  | | |  нию с исходным деревом; массив diff содержит правиль-
  | | |  ные значения для этого поддерева; возможно нарушение
  | | |  сбалансированности в v}
  | | | balance (v, d1); d := c + d1;
  | | end;
  | end;
  end;

Легко  проверить, что значение d может быть равно только 0 или 1
(но не -1): если c = 0, то diff [v] = 0 и балансировка не произ-
водится.

     Программа удаления строится аналогично. Ее  основной  фраг-
мент таков:

  {инвариант: стек содержит путь к изменившемуся поддереву,
   высота которого изменилась по сравнению с высотой в
   исходном дереве на d (=0 или -1); это поддерево
   сбалансировано; значения diff для его вершин правильны;
   в остальном дереве все осталось как было -
   в частности, значения diff}
  while (d <> 0) and ..стек непуст do begin
  | {d = -1}
  | ..взять из стека пару в <v, direct>
  | if direct = l then begin
  | | if diff [v] = -1 then begin
  | | | c := -1;
  | | end else begin
  | | | c := 0;
  | | end;
  | | diff [v] := diff [v] + 1;
  | end else begin {direct = r}
  | | if diff [v] = 1 then begin
  | | | c := -1;
  | | end else begin
  | | | c := 0;
  | | end;
  | | diff [v] := diff [v] - 1;
  | end;
  | {c = изменение высоты поддерева с корнем в v по срав-
  |  нению с исходным деревом; массив diff содержит
  |  правильные значения для этого поддерева;
  |  возможно нарушение сбалансированности в v}
  | balance (v, d1);
  | d := c + d1;
  end;

Легко проверить, что значение d может быть равно только 0 или -1
(но  не -2): если c = -1, то diff [v] = 0 и балансировка не про-
изводится.
     Отметим также, что наличие стека делает излишними  перемен-
ные father и direction (их роль теперь играет вершина стека).

     12.2.6. Доказать, что при добавлении элемента
     (а)  второй из трех случаев балансировки (см. рисунок выше)
невозможен;
     (б) полная балансировка требует не  более  одного  вращения
(после чего все дерево становится сбалансированным),
     в  то  время  как  при удалении элемента может понадобиться
много вращений.

     Замечание. Мы старались  записать  программы  добавления  и
удаления  так,  чтобы  они были как можно более похожими друг на
друга. Используя специфику каждой из них,  можно  многое  упрос-
тить.

     Существуют  и другие способы представления множеств, гаран-
тирующие число действий порядка log n на каждую операцию. Опишем
один из них (называемый Б-деревьями).
     До сих пор каждая вершина содержала один элемент  хранимого
множества.  Этот  элемент  служил  границей между левым и правым
поддеревом. Будем теперь хранить в вершине k >= 1 элементов мно-
жества (число k может меняться от вершины к вершине, а также при
добавлении и удалении новых элементов, см. далее). Эти k элемен-
тов служат разделителями для k+1  поддерева.  Пусть  фиксировано
некоторое  число n >= 1. Будем рассматривать деревья, обладающие
такими свойствами:
     (1) Каждая вершина содержит от n до 2n элементов (за исклю-
чением корня, который может содержать любое число элементов от 0
до 2n).
     (2) Вершина с k элементами либо имеет  k+1  сына,  либо  не
имеет сыновей вообще (такие вершины называются листьями).
     (3) Все листья находятся на одной и той же высоте.
     Добавление элемента происходит так. Если лист, в который он
попадает,  неполон  (т.е.  содержит  менее 2n элементов), то нет
проблем. Если он полон, то 2n+1 элемент (все  элементы  листа  и
новый  элемент) разбиваем на два листа по n элементов и разделя-
ющий их серединный элемент. Этот серединный элемент  надо  доба-
вить  в вершину предыдущего уровня. Это возможно, если в ней ме-
нее 2n элементов. Если и она полна, то ее разбивают на две,  вы-
деляют  серединный элемент и т.д. Если в конце концов мы захотим
добавить элемент в корень, а он окажется полным, то корень  рас-
щепляется на две вершины, а высота дерева увеличивается на 1.
     Удаление элемента. Удаление элемента, находящемся не в лис-
те, сводится к удалению непосредственно следующего за ним, кото-
рый находится в листе. Поэтому достаточно научиться удалять эле-
мент  из  листа.  Если лист при этом становится неполным, то его
можно пополнить за счет соседнего листа - если только  и  он  не
имеет  минимально  возможный  размер  n. Если же оба листа имеют
размер n, то на них вместе 2n элементов, вместе с разделителем -
2n+1. После удаления одного элемента остается 2n элементов - как
раз на один лист. Если при этом вершина предыдущего уровня  ста-
новится меньше нормы, процесс повторяется и т.д.

     12.2.7. Реализовать описанную схему хранения множеств, убе-
дившись,  что она также позволяет обойтись C*log(n) действий для
операций включения, исключения и проверки принадлежности.

     12.2.8. Можно определять сбалансированность  дерева  иначе:
требовать, чтобы для каждой вершины ее левое и правое поддеревья
имели не слишком сильно отличающиеся количества вершин. (Преиму-
щество такого определения состоит в том, что при вращениях изме-
няется  сбалансированность  только в одной вершине.) Реализовать
на основе этой  идеи  способ  хранения  множеств,  гарантирующий
оценку  в  C*log(n)  действий для включения, удаления и проверки
принадлежности. (Указание. Он также использует большие  и  малые
вращения.  Подробности см. в книге Рейнгольда, Нивергельта и Део
"Комбинаторные алгоритмы".)
        Н Е   П О К У П А Й Т Е   Э Т У   К Н И Г У !

                (Предупреждение автора)

     В этой книге ничего не  говорится  об  особенностях  BIOSа,
DOSа, OSа, GEMа и Windows, представляющих основную сложность при
настоящем программировании.

     В ней нет ни слова об объектно-ориентированном программиро-
вании, открывшем новую эпоху в построении дружественных и эффек-
тивных программных систем.

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

     Не рассказано в ней и  о  написании  резидентных  программ,
тонкости взаимодействия которых должен знать каждый.

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

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

     Логическое  программирование,  постепенно вытесняющее уста-
ревший операторный стиль программирования, не затронуто.

     Драматический поворот от баз данных к базам знаний, вызвав-
ший в жизни новую профессию -- инженер знаний -- остался незаме-
ченным автором.

     Проблемы отладки и сопровождения программ,  занимающие,  по
общему  мнению профессионалов, 90% в программировании, игнориру-
ются.

     В  книге  используются  лишь самые элементарные возможности
паскаля. Обширные возможности, предоставляемые современными  ин-
тегрированными программными средами, остаются невостребованными.
(Не  говоря уже о том, что паскаль уже вообще устарел, вытеснен-
ный языком Си.)

     Игрушечные головоломки, которым посвящена книга, никому  не
нужны.  Если  же перед Вами встанет действительно важная задача,
неужели Вы не справитесь с ней сами, без непрошеных  учителей  и
советчиков?

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

     1.1. Задачи без массивов

     1.1.1. Даны две целые переменные a, b.  Составить  фрагмент
программы, после исполнения которого значения переменных поменя-
лись бы местами (новое значение a равно старому значению b и на-
оборот).

     Решение. Введем дополнительную целую переменную t.
        t := a;
        a := b;
        b := t;
Попытка обойтись без дополнительной переменной, написав
        a := b;
        b := a;
не приводит к цели (безвозвратно утрачивается начальное значение
переменной a).

     1.1.2.  Решить  предыдущую  задачу,  не  используя дополни-
тельных переменных (и предполагая, что значениями целых перемен-
ных могут быть произвольные целые числа).

     Решение. (Начальные значения a и b обозначим a0, b0.)
        a := a + b; {a = a0 + b0, b = b0}
        b := a - b; {a = a0 + b0, b = a0}
        a := a - b; {a = b0, b = a0}

     1.1.3.  Дано  целое  число а и натуральное (целое неотрица-
тельное) число n. Вычислить а в степени n. Другими словами,  не-
обходимо  составить  программу,  при исполнении которой значения
переменных а и n не меняются, а значение некоторой другой  пере-
менной  (например, b) становится равным а в степени n. (При этом
разрешается использовать и другие переменные.)

     Решение. Введем целую переменную k, которая меняется от  0
до  n,  причем  поддерживается такое свойство: b = (a в степени
k).

        k := 0; b := 1;
        {b = a в степени k}
        while k <> n do begin
        | k := k + 1;
        | b := b * a;
        end;

Другое решение той же задачи:

        k := n; b := 1;
        {a в степени n = b * (a в степени k)}
        while k <> 0 do begin
        | k := k - 1;
        | b := b * a;
        end;

     1.1.4. Решить предыдущую задачу, если требуется, чтобы чис-
ло действий (выполняемых операторов присваивания)  было  порядка
log n (то есть не превосходило бы C*log n для некоторой констан-
ты C; log n - это степень, в которую нужно возвести 2, чтобы по-
лучить n).

     Решение. Внесем некоторые изменения во второе из предложен-
ных решений предыдущей задачи:

        k := n; b := 1; c:=a;
        {a в степени n = b * (c в степени k)}
        while k <> 0 do begin
        | if k mod 2 = 0 then begin
        | | k:= k div 2;
        | | c:= c*c;
        | end else begin
        | | k := k - 1;
        | | b := b * c;
        | end;
        end;

Каждый второй раз (не реже)  будет  выполняться  первый  вариант
оператора  выбора  (если  k  нечетно, то после вычитания единицы
становится четным), так что за два цикла величина k  уменьшается
по крайней мере вдвое.

     1.1.5.  Даны натуральные числа а, b. Вычислить произведение
а*b, используя в программе лишь операции +, -, =, <>.

     Решение.
        var a, b, c, k : integer;
        k := 0; c := 0;
        {инвариант: c = a * k}
        while k <> b do begin
        | k := k + 1;
        | c := c + a;
        end;
        {c = a * k и k = b, следовательно, c = a * b}

     1.1.6.  Даны  натуральные  числа  а и b. Вычислить их сумму
а+b. Использовать операторы присваивания лишь вида

        <переменная1> := <переменная2>,
        <переменная> := <число>,
        <переменная1> := <переменная2> + 1.

     Решение.
          ...
         {инвариант: c = a + k}
          ...

     1.1.7. Дано натуральное (целое неотрицательное) число  а  и
целое положительное число d. Вычислить частное q и остаток r при
делении а на d, не используя операций div и mod.

     Решение. Согласно определению, a = q * d + r, 0 <= r < d.

        {a >= 0; d > 0}
        r := a; q := 0;
        {инвариант: a = q * d + r, 0 <= r}
        while not (r < d) do begin
        | {r >= d}
        | r := r - d; {r >= 0}
        | q := q + 1;
        end;

     1.1.8.  Дано  натуральное  n,  вычислить n!
        (0!=1, n! = n * (n-1)!).

     1.1.9.   Последовательность  Фибоначчи  определяется  так:
a(0)= 1, a(1) = 1, a(k) = a(k-1) + a(k-2) при k >= 2.  Дано  n,
вычислить a(n).

     1.1.10.  Та же задача, если требуется, чтобы число операций
было пропорционально log n. (Переменные должны быть  целочислен-
ными.)

     Указание.  Пара соседних чисел Фибоначчи получается из пре-
дыдущей умножением на матрицу
            |1 1|
            |1 0|
так что задача сводится к возведению матрицы в  степень  n.  Это
можно сделать за C*log n действий тем же способом, что и для чи-
сел.

     1.1.11. Дано натуральное n, вычислить 1/0!+1/1!+...+1/n!.

     1.1.12.  То  же, если требуется, чтобы количество операций
(выполненных команд присваивания) было бы не более C*n для  не-
которой константы С.
     Решение.  Инвариант:  sum  =  1/1! +...+ 1/k!, last = 1/k!
(важно не вычислять заново каждый раз k!).

     1.1.13.  Даны  два  натуральных числа a и b, не равные нулю
одновременно. Вычислить НОД (a,b) - наибольший общий делитель  а
и b.

     Решение (1 вариант).

        if a > b then begin
        | k := a;
        end else begin
        | k := b;
        end;
        {k = max (a,b)}
        {инвариант: никакое  число, большее k, не является об-
          щим делителем}
        while not (((a mod k)=0) and ((b mod k)=0)) do begin
        | k := k - 1;
        end;
        {k - общий делитель, большие - нет}

       (2  вариант - алгоритм Евклида). Будем считать , что НОД
(0,0) = 0. Тогда НОД (a,b) = НОД (a-b,b)  =  НОД  (a,b-a);  НОД
(a,0) = НОД (0,a) = a для всех a,b>=0.

         m := a; n := b;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0 }
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n;
        | end else begin
        | | n := n - m;
        | end;
        end;
        if m = 0 then begin
        | k := n;
        end else begin
        | k := m;
        end;

     1.1.14. Написать модифицированный вариант алгоритма Евкли-
да,  использующий соотношения НОД (a, b) = НОД (a mod b, b) при
a >= b, НОД (a, b) = НОД (a, b mod a) при b >= a.

     1.1.15. Даны натуральные а и b, не равные 0  одновременно.
Найти d = НОД (a,b) и такие целые x и y, что d = a*x + b*y.

     Решение.  Добавим в алгоритм Евклида переменные p, q, r, s
и впишем в инвариант условия m = p*a + q*b; n = r*a + s*b.

        m:=a; n:=b; p := 1; q := 0; r := 0; s := 1;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0
                    m = p*a + q*b; n = r*a + s*b.}
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n; p := p - r; q := q - s;
        | end else begin
        | | n := n - m; r := r - p; s := s - q;
        | end;
        end;
        if m = 0 then begin
        | k :=n; x := r; y := s;
        end else begin
        | k := m; x := p; y := q;
        end;

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

     1.1.17. (Э.Дейкстра).  Добавим  в алгоритм Евклида дополни-
тельные переменные u, v, z:

         m := a; n := b; u := b; v := a;
        {инвариант: НОД (a,b) = НОД (m,n); m,n >= 0 }
        while not ((m=0) or (n=0)) do begin
        | if m >= n then begin
        | | m := m - n; v := v + u;
        | end else begin
        | | n := n - m; u := u + v;
        | end;
        end;
        if m = 0 then begin
        | z:= v;
        end else begin {n=0}
        | z:= u;
        end;

Доказать, что после исполнения алгоритма z равно удвоенному  на-
именьшему общему кратному чисел a, b: z = 2 * НОК (a,b).

     Решение. Заметим, что величина m*u + n*v не меняется в ходе
выполнения  алгоритма. Остается воспользоваться тем, что вначале
она равна 2*a*b и что НОД (a, b) * НОК (a, b) = a*b.

     1.1.18.  Написать  вариант  алгоритма Евклида, использующий
соотношения
        НОД(2*a, 2*b) = 2*НОД(a,b)
        НОД(2*a, b)   =   НОД(a,b) при нечетном b,
не включающий деления с остатком, а использующий лишь деление на
2 и проверку четности. (Число действий должно быть порядка log k
для исходных данных, не превосходящих k.)

     Решение.

  m:= a; n:=b; d:=1;
  {НОД(a,b) = d * НОД(m,n)}
  while not ((m=0) or (n=0)) do begin
  | if (m mod 2 = 0) and (n mod 2 = 0) then begin
  | | d:= d*2; m:= m div 2; n:= n div 2;
  | end else if (m mod 2 = 0) and (n mod 2 = 1) then begin
  | | m:= m div 2;
  | end else if (m mod 2 = 1) and (n mod 2 = 0) then begin
  | | n:= n div 2;
  | end else if (m mod 2=1) and (n mod 2=1) and (m>=n)then begin
  | | m:= m-n;
  | end else if (m mod 2=1) and (n mod 2=1) and (m<=n)then begin
  | | n:= n-m;
  | end;
  end;
  {m=0 => ответ=d*n; n=0 => ответ=d*m}

Оценка числа действий: каждое второе действие делит хотя бы одно
из чисел m и n пополам.

     1.1.19. Дополнить алгоритм предыдущей задачи поиском x и y,
для которых ax+by=НОД(a,b).

     Решение. (Идея сообщена Д.Звонкиным) Прежде всего  заметим,
что  одновременое деление a и b пополам не меняет искомых x и y.
Поэтому можно считать, что с самого начала одно из чисел a  и  b
нечетно. (Это свойство будет сохраняться и далее.)
     Теперь  попытаемся,  как  и  раньше,  хранить  такие  числа
p,q,r,s, что
     m = ap + bq
     n = ar + bs
Проблема в том, что при делении, скажем, m на 2 надо разделить p
и  q  на 2, и они перестанут быть целыми (а станут двоично-раци-
ональными). Двоично-рациональное число естественно хранить в ви-
де пары (числитель, показатель степени двойки в знаменателе).  В
итоге  мы  получаем  d  в  виде комбинации a и b с двоично-раци-
ональными коэффициентами. Иными словами, мы имеем
        (2 в степени i)* d = ax + by
для  некоторых  целых x,y и натурального i. Что делать, если i >
1? Если x и y чётны, то на 2 можно сократить. Если это  не  так,
положение можно исправить преобразованием
        x := x + b
        y := y - a
(оно  не меняет ax+by). Убедимся в этом. Напомним, что мы счита-
ем, что одно из чисел a и b нечётно. Пусть это будет a. Если при
этом y чётно, то и x должно быть чётным (иначе ax+by  будет  не-
чётным). А при нечётном y вычитание из него нёчетного a делает y
чётным.

     1.1.20. Составить программу, печатающую квадраты всех нату-
ральных чисел от 0 до заданного натурального n.

     Решение.

        k:=0;
        writeln (k*k);
        {инвариант: k<=n, напечатаны все
          квадраты до k включительно}
        while not (k=n) do begin
        | k:=k+1;
        | writeln (k*k);
        end;

     1.1.21.  Та же задача, но разрешается использовать из ариф-
метических операций лишь сложение и вычитание, причем общее чис-
ло действий должно быть порядка n.

     Решение.  Введем  переменную k_square (square - квадрат),
связанную с k соотношением k_square = k*k:

        k := 0; k_square := 0;
        writeln (k_square);
        while not (k = n) do begin
        | k := k + 1;
        | {k_square = (k-1) * (k-1) = k*k - 2*k + 1}
        | k_square := k_square + k + k - 1;
        | writeln (k_square);
        end;

     1.1.22. Составить программу, печатающую разложение на прос-
тые множители заданного натурального числа n > 0 (другими слова-
ми, требуется печатать только простые числа и произведение напе-
чатанных  чисел должно быть равно n; если n = 1, печатать ничего
не надо).

     Решение (1 вариант).

        k := n;
        {инвариант:  произведение напечатанных чисел и k равно
         n, напечатаны только простые числа}
        while not (k = 1) do begin
        | l := 2;
        | {инвариант: k не имеет делителей в интервале (1,l)}
        | while k mod l <> 0 do begin
        | | l := l + 1;
        | end;
        | {l - наименьший делитель k, больший 1, следовательно,
        |  простой}
        | writeln (l);
        | k:=k div l;
        end;

     (2 вариант).

         k := n; l := 2;
         {произведение  k и напечатанных чисел равно n; напеча-
          танные числа просты; k не имеет делителей, меньших l}
         while not (k = 1) do begin
         | if k mod l = 0  then begin
         | | {k делится на l и не имеет делителей,
         | |   меньших l, значит, l просто}
         | | k := k div l;
         | | writeln (l);
         | end else begin
         | | { k не делится на l }
         | | l := l + 1;
         | end;
         end;

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

     Решение. Во втором варианте решения вместо l:=l+1 можно на-
писать

                if l*l > k then begin
                | l:=k;
                end else begin
                | l:=l+1;
                end;

     1.1.24. Проверить, является ли заданное натуральное  число
n > 1 простым.

     1.1.25. (Для знакомых с основами алгебры). Дано целое  га-
уссово  число n + mi (принадлежащее Z[i]). (a) Проверить, явля-
ется ли оно простым (в Z[i]); (б) напечатать его разложение  на
простые (в Z[i]) множители.

     1.1.26. Разрешим использовать команды write (i) лишь при i
=  0,1,2,...,9.  Составить программу, печатающую десятичную за-
пись заданного натурального числа n > 0. (Случай n =  0  явился
бы некоторым исключением, так как обычно нули в начале числа не
печатаются, а для n = 0 - печатаются.)

     Решение.

        base:=1;
        {base - степень 10, не превосходящая n}
        while 10 * base <= n do begin
        | base:= base * 10;
        end;
        {base - максимальная степень 10, не превосходящая n}
        k:=n;
        {инвариант: осталось напечатать k с тем же числом
         знаков, что в base; base = 100..00}
        while base <> 1 do begin
        | write(k div base);
        | k:= k mod base;
        | base:= base div 10;
        end;
        {base=1; осталось напечатать однозначное число k}
        write(k);

(Типичная ошибка при решении этой задачи: неправильно  обрабаты-
ваются числа с нулями посередине. Приведенный инвариант допуска-
ет  случай, когда k < base; в этом случае печатание k начинается
со старших нулей.)

     1.1.27. То же самое, но надо напечатать десятичную запись в
обратном порядке. (Для n = 173 надо напечатать 371.)

     Решение.

        k:= n;
        {инвариант: осталось напечатать k в обратном порядке}
        while k <> 0 do begin
        | write (k mod 10);
        | k:= k div 10;
        end;

     1.1.28. Дано натуральное n. Подсчитать  количество  решений
неравенства  x*x + y*y < n в натуральных (неотрицательных целых)
числах, не используя действий с вещественными числами.

     Решение.

        k := 0; s := 0;
        {инвариант: s = количество решений неравенства
          x*x + y*y < n c x < k}
        while k*k < n do begin
        | ...
        | {t = число решений неравенства k*k + y*y < n
        |  (при данном k) }
        | k := k + 1;
        | s := s + t;
        end;
        {k*k >= n, поэтому s = количество всех решений
          неравенства}

     Здесь ... - пока еще не написанный кусок программы, который
будет таким:

        l := 0; t := 0;
        {инвариант: t = число решений
          неравенства k*k + y*y < n c y < l }
        while k*k + l*l < n do begin
        | l := l + 1;
        | t := t + 1;
        end;
        {k*k + l*l >= n,  поэтому  t = число
          всех решений неравенства k*k + y*y < n}

     1.1.29. Та же задача, но количество  операций  должно  быть
порядка (n в степени 1/2). (В предыдущем решении, как можно
подсчитать, порядка n операций.)

     Решение. Нас интересуют точки решетки (с целыми координата-
  *              ми) в первом квадранте, попадающие внутрь круга
  * * *          радиуса  (n  в  степени  1/2). Интересующее нас
  * * * *        множество (назовем его X) состоит из  объедине-
  * * * *        ния  вертикальных  столбцов  убывающей  высоты.
  * * * * *      Идея решения состоит в  том,  чтобы  "двигаться
вдоль  его  границы",  спускаясь  по  верхнему  его краю, как по
лестнице. Координаты движущейся точки  обозначим  <k,l>.  Введем
еще одну переменную s и будем поддерживать истинность такого ус-
ловия:
     <k,l> находится сразу над k-ым столбцом;
     s - число точек в предыдущих столбцах.

     Формально:
l  - минимальное среди тех l >= 0, для которых <k,l> не принад-
    лежит X;
s - число пар натуральных x, y, для которых x < k и <x,y>  при-
    надлежит X.
Обозначим эти условия через (И).

  k := 0; l := 0;
  while "<0,l> принадлежит X" do begin
  | l := l + 1;
  end;
  {k = 0, l - минимальное среди тех l >= 0,
   для которых <k,l> не принадлежит X }
  s := 0;
  {инвариант: И}
  while not (l = 0) do begin
  | s := s + l;
  | {s - число точек в столбцах до k-го включительно}
  | k := k + 1;
  | {точка <k,l> лежит вне X, но,  возможно,  ее  надо сдвинуть
  |    вниз, чтобы восстановить И }
  | while (l <> 0) and ("<k, l-1> не принадлежит X") do begin
  | | l := l - 1;
  | end;
  end;
  {И, l = 0, поэтому k-ый столбец и все следующие пусты, а
    s равно искомому числу}

Оценка числа действий очевидна: сначала мы движемся вверх не бо-
лее  чем  на  (n в степени 1/2) шагов, а затем вниз и вправо - в
каждую сторону не более чем на (n в степени 1/2) шагов.

     1.1.30. Даны натуральные числа n и k, n > 1.  Напечатать  k
десятичных знаков числа 1/n. (При наличии двух десятичных разло-
жений  выбирается то из них, которое не содержит девятки в пери-
оде.) Программа должна использовать только целые переменные.

     Решение. Сдвинув в десятичной записи числа 1/n запятую на k
мест вправо, получим число (10 в степени k)/n. Нам надо  напеча-
тать  его целую часть, т. е. разделить (10 в степени k) на n на-
цело. Стандартный способ требует использования больших по  вели-
чине  чисел, которые могут выйти за границы диапазона представи-
мых чисел. Поэтому мы сделаем иначе (следуя обычному методу "де-
ления уголком") и будем хранить "остаток" r:

  l := 0; r := 1;
  {инв.: напечатано l разрядов 1/n, осталось напечатать
    k - l разрядов дроби r/n}
   while l <> k do begin
   | write ( (10 * r) div n);
   |   r := (10 * r) mod n;
   |   l := l + 1;
   end;

     1.1.31. Дано натуральное число n > 1. Определить длину  пе-
риода десятичной записи дроби 1/n.

     Решение.  Период  дроби  равен периоду в последовательности
остатков (докажите это; в частности, надо доказать,  что  он  не
может  быть  меньше).  Кроме того, в этой последовательности все
периодически повторяющиеся все члены различны, а предпериод име-
ет длину не более n. Поэтому достаточно найти (n+1)-ый член пос-
ледовательности остатков и  затем  минимальное  k,  при  котором
(n+1+k)-ый член совпадает с (n+1)-ым.

  l := 0; r := 1;
  {инвариант: r/n = результат отбрасывания l знаков в 1/n}
  while l <> n+1 do begin
  | r := (10 * r) mod n;
  | l := l + 1;
  end;
  c := r;
  {c = (n+1)-ый член последовательности остатков}
  r := (10 * r) mod n;
  k := 0;
  {r = (n+k+1)-ый член последовательности остатков}
  while r <> c do begin
  | r := (10 * r) mod n;
  | k := k + 1;
  end;

     1.1.32 (Э. Дейкстра). Функция f с натуральными  аргументами
и  значениями определена так: f(0) = 0, f(1) = 1, f (2n) = f(n),
f (2n+1) = f (n) + f (n+1). Составить программу вычисления f (n)
по заданному n, требующую порядка log  n  операций.

     Решение.
  k := n; a := 1; b := 0;
  {инвариант: 0 <= k, f (n) = a * f(k) + b * f (k+1)}
  while k <> 0 do begin
  | if k mod 2 = 0  then begin
  | | l := k div 2;
  | | {k = 2l, f(k) = f(l), f (k+1) = f (2l+1) = f(l) + f(l+1),
  | |  f (n) = a*f(k) + b*f(k+1) = (a+b)*f(l) + b*f(l+1)}
  | | a := a + b; k := l;
  | end else begin
  | | l := k div 2;
  | | {k = 2l + 1, f(k) = f(l) + f(l+1),
  | |  f(k+1) = f(2l+2) = f(l+1),
  | |  f(n) = a*f(k) + b*f(k+1) = a*f(l) + (a+b)*f(l+1)}
  | | b := a + b; k := l;
  | end;
  end;
  {k = 0, f(n) = a * f(0) + b * f(1) = b, что и требовалось}

     1.1.33.  То  же,  если  f(0) = 13, f(1) = 17, а f(2n) =
43 f(n) + 57 f(n+1), f(2n+1) = 91 f(n) + 179 f(n+1) при n>=1.
     Указание.  Хранить  коэффициенты в выражении f(n) через три
соседних числа.

     1.1.34. Даны натуральные числа а и b, причем b >  0.  Найти
частное  и  остаток  при  делении а на b, оперируя лишь с целыми
числами и не используя операции div и mod, за исключением  деле-
ния  на  2  четных  чисел;  число  шагов  не должно превосходить
C1*log(a/b) + C2 для некоторых констант C1, C2.

     Решение.

  b1 := b;
  while b1 <= a do begin
  | b1 := b1 * 2;
  end;
  {b1 > a, b1 = b * (некоторая степень 2)}
  q:=0; r:=a;
  {инвариант: q, r - частное и остаток при делении a на b1,
   b1 = b * (некоторая степень 2)}
  while b1 <> b do begin
  | b1 := b1 div 2 ; q := q * 2;
  | { a = b1 * q + r, 0 <= r, r < 2 * b1}
  | if r >= b1 then begin
  | | r := r - b1;
  | | q := q + 1;
  | end;
  end;
  {q, r - частное и остаток при делении a на b}

     1.2. Массивы.

     В следующих задачах переменные x, y, z предполагаются  опи-
санными  как  array [1..n] of integer (n - некоторое натуральное
число, большее 0), если иное не оговорено явно.

     1.2.1. Заполнить массив x нулями. (Это означает, что  нужно
составить фрагмент программы, после выполнения которого все зна-
чения  x[1]..x[n]  равнялись  бы  нулю, независимо от начального
значения переменной x.)

     Решение.

          i := 0;
          {инвариант: первые i значений x[1]..x[i] равны 0}
          while i <> n do begin
          | i := i + 1;
          | {x[1]..x[i-1] = 0}
          | x[i] := 0;
          end;

     1.2.2. Подсчитать количество нулей в массиве x.  (Составить
фрагмент программы, не меняющий значения x, после исполнения ко-
торого  значение некоторой целой переменной k равнялось бы числу
нулей среди компонент массива x.)

     Решение.
          ...
          {инвариант: k= число нулей среди x[1]...x[i] }
          ...

     1.2.3. Не используя оператора  присваивания  для  массивов,
составить фрагмент программы, эквивалентный оператору x:=y.

     Решение.

  i := 0;
  {инвариант: значение y не изменилось, x[l] = y[l] при l <= i}
  while i <> n do begin
  | i := i + 1;
  | x[i] := y[i];
  end;

     1.2.4. Найти максимум из x[1]..x[n].

     Решение.
          i := 1; max := x[1];
          {инвариант: max = максимум из x[1]..x[i]}
          while i <> n do begin
          | i := i + 1;
          | {max = максимум из x[1]..x[i-1]}
          | if x[i] > max then begin
          | | max := x[i];
          | end;
          end;

     1.2.5.  Дан  массив x: array [1..n] of integer, причём x[1]
<= x[2] <= ... <= x[n]. Найти количество различных  чисел  среди
элементов этого массива.

     Решение. (1 вариант)

  i := 1; k := 1;
  {инвариант: k - количество различных чисел среди x[1]..x[i]}
  while i <> n do begin
  | i := i + 1;
  | if x[i] <> x[i-1] then begin
  | | k := k + 1;
  | end;
  end;

     (2 вариант) Искомое число на 1 больше количества тех  чисел
i из 1..n-1, для которых x[i] <> x[i+1].

  k := 1;
  for i := 1 to n-1 do begin
  | if x[i]<> x[i+1] then begin
  | | k := k + 1;
  | end;
  end;

     1.2.6. (Сообщил А.Л.Брудно.) Прямоугольное поле m на n раз-
бито  на mn квадратных клеток. Некоторые клетки покрашены в чер-
ный цвет. Известно, что все черные клетки могут быть разбиты  на
несколько непересекающихся и не имеющих общих вершин черных пря-
моугольников. Считая, что цвета клеток даны в виде массива типа
        array [1..m] of array [1..n] of boolean;
подсчитать  число  черных  прямоугольников,  о которых шла речь.
Число действий должно быть порядка m*n.

     Решение. Число прямоугольников равно числу их левых верхних
углов. Является ли клетка верхним углом, можно узнать, посмотрев
на ее цвет, а также цвет верхнего  и  левого  соседей.  (Не  за-
будьте, что их может не быть, если клетка с краю.)

     1.2.7. Дан массив x: array [1..n] of integer.  Найти  коли-
чество  различных  чисел  среди  элементов этого массива. (Число
действий должно быть порядка n*n.)

     1.2.8.  Та  же  задача,  если  требуется,  чтобы количество
действий было порядка n* log n. (Указание. Смотри главу о сорти-
ровке.)

     1.2.9. Та же задача, если известно, что все элементы масси-
ва - числа от 1 до k и число действий должно быть порядка n+k.

     1.2.10. Дан массив x [1]..x[n] целых  чисел.  Не  используя
других  массивов, переставить элементы массива в обратном поряд-
ке.

     Решение. Числа x [i] и x [n+1-i] нужно поменять местами для
всех i, для которых i < n + 1 - i, т.е. 2*i < n + 1 <=> 2*i <= n
<=> i <= n div 2:
  for i := 1 to n div 2 do begin
  | ...обменять x [i] и x [n+1-i];
  end;

     1.2.11.  (из  книги  Д.Гриса)  Дан   массив   целых   чисел
x[1]..x[m+n],  рассматриваемый как соединение двух его отрезков:
начала x[1]..x[m] длины m и конца x[m+1]..x[m+n] длины n. Не ис-
пользуя дополнительных массивов,  переставить  начало  и  конец.
(Число действий порядка m+n.)

     Решение. (1 вариант). Перевернем (расположим в обратном по-
рядке) отдельно начало и конец массива, а затем перевернем  весь
массив как единое целое.

     (2 вариант, А.Г.Кушниренко). Рассматривая массив записанным
по кругу, видим, что требуемое действие - поворот круга. Как из-
вестно, поворот есть композиция двух осевых симметрий.

     (3  вариант).  Рассмотрим  более  общую задачу - обмен двух
участков массива x[p+1]..x[q] и x[q+1]..x[s].  Предположим,  что
длина  левого  участка  (назовем  его A) не больше длины правого
(назовем его B). Выделим в B начало той же длины, что и A, назо-
вем его B1, а остаток B2. (Так что B = B1 + B2, если  обозначать
плюсом приписывание массивов друг к другу.) Нам надо из A + B1 +
B2 получить B1 + B2 + A. Меняя местами участки A и B1 - они име-
ют одинаковую длину, и сделать это легко,- получаем B1 + A + B2,
и  осталось  поменять  местами A и B2. Тем самым мы свели дело к
перестановке двух отрзков меньшей длины. Итак,  получаем  такую
схему программы:

  p := 0; q := m; r := m + n;
  {инвариант: осталось переставить x[p+1]..x[q], x[q+1]..x[s]}
  while (p <> q) and (q <> s) do begin
  | {оба участка непусты}
  | if (q - p) <= (s - q) then begin
  | | ..переставить x[p+1]..x[q] и x[q+1]..x[q+(q-p)]
  | | pnew := q; qnew := q + (q - p);
  | | p := pnew; q := qnew;
  | end else begin
  | | ..переставить x[q-(r-q)+1]..x[q] и x[q+1]..x[r]
  | | qnew := q - (r - q); rnew := q;
  | | q := qnew; r := rnew;
  | end;
  end;

Оценка времени работы: на очередном шаге оставшийся для обработ-
ки участок становится короче на длину A; число действий при этом
также пропорционально длине A.

     1.2.12. Коэффициенты многочлена хранятся в массиве a: array
[0..n]  of  integer (n - натуральное число, степень многочлена).
Вычислить значение этого многочлена в точке x (т. е.  a[n]*(x  в
степени n)+...+a[1]*x+a[0]).

     Решение. (Описываемый алгоритм называется схемой Горнера.)

  k := 0; y := a[n];
  {инвариант: 0 <= k <= n,
   y= a[n]*(x в степени k)+...+a[n-1]*(x в степени k-1)+...+
                     + a[n-k]*(x в степени 0)}
  while k<>n do begin
  | k := k + 1;
  | y := y * x + a [n - k];
  end;

     1.2.13. (Для знакомых с основами анализа. Сообщил  А.Г.Куш-
ниренко.)  Дополнить  алгоритм  вычисления значения многочлена в
заданной точке по схеме Горнера вычислением значения его  произ-
водной в той же точке.

     Решение. Добавление нового коэффициента соответствует пере-
ходу от многочлена P(x) к многочлену P(x)*x + c. Его производная
в  точке  x равна P'(x)*x + P(x). (Это решение обладает забавным
свойством: не надо знать заранее степень многочлена. Если требо-
вать выполнения этого условия, да еще просить  вычислять  только
значение производной, не упоминая о самом многочлене, получается
не такая уж простая задача.)

     1.2.14.  В  массивах
  a:array  [0..k] of integer и b: array [0..l] of integer
хранятся коэффициенты двух многочленов степеней k и  l.  Помес-
тить в массив c: array [0..m] of integer коэффициенты их произ-
ведения.  (Числа k, l, m - натуральные, m = k + l; элемент мас-
сива с индексом i содержит коэффициент при x в степени i.)

     Решение.

          for i:=0 to m do begin
          | c[i]:=0;
          end;
          for i:=0 to k do begin
          | for j:=0 to l do begin
          | | c[i+j] := c[i+j] + a[i]*b[j];
          | end;
          end;

     1.2.15. Предложенный выше алгоритм перемножения многочленов
требует порядка n*n действий для перемножения  двух  многочленов
степени n. Придумать более эффективный (для больших n) алгоритм,
которому  достаточно  порядка  (n  в  степени  (log  4)/(log 3))
действий.
     Указание. Представим себе, что надо перемножить два многоч-
лена степени 2k. Их можно представить в виде
        A(x)*x^k + B(x)    и    C(x)*x^k + D(x)
(здесь x^k обозначает x  в степени k). Произведение их равно
       A(x)C(x)*x^{2k}  +  (A(x)D(x)+B(x)C(x))*x^k  + B(x)D(x)
Естественный способ вычисления AC, AD+BC, BD требует четырех ум-
ножений многочленов степени k, однако их количество можно сокра-
тить  до  трех  с  помощью  такой  хитрости:  вычислить AC, BD и
(A+B)(C+D), а затем заметить, что AD+BC=(A+B)(C+D)-AC-BD.

     1.2.16.  Даны  два  возрастающих массива x: array [1..k] of
integer и y: array [1..l] of  integer.  Найти  количество  общих
элементов в этих массивах (т. е. количество тех целых t, для ко-
торых  t = x[i] = y[j] для некоторых i и j). (Число действий по-
рядка k+l.)

     Решение.

  k1:=0; l1:=0; n:=0;
  {инвариант: 0<=k1<=k; 0<=l1<=l; искомый ответ = n + количество
   общих элементов в x[k1+1]...x[k] и y[l1+1]..y[l]}
  while (k1 <> k) and (l1 <> l) do begin
  | if x[k1+1] < y[l1+1] then begin
  | | k1 := k1 + 1;
  | end else if x[k1+1] > y[l1+1] then begin
  | | l1 := l1 + 1;
  | end else begin {x[k1+1] = y[l1+1]}
  | | k1 := k1 + 1;
  | | l1 := l1 + 1;
  | | n := n + 1;
  | end;
  end;
  {k1 = k или l1 = l, поэтому одно из множеств, упомянутых в
   инварианте, пусто, а n равно искомому ответу}

Замечание. В третьей альтернативе достаточно было бы увеличивать
одну из переменных k1, l1; вторая добавлена для симметрии.

     1.2.17.  Решить  предыдущую задачу, если известно лишь, что
x[1] <= ... <= x[k] и y[1] <= ... <= y[l] (возрастание  заменено
неубыванием).

     Решение.  Условие  возрастания  было использовано в третьей
альтернативе выбора: сдвинув k1 и l1 на 1, мы тем самым уменьша-
ли  на  1  количество  общих  элементов   в   x[k1+1]...x[k]   и
x[l1+1]...x[l]. Теперь это придется делать сложнее.

          ...
          end else begin {x[k1+1] = y[l1+1]}
          | t := x [k1+1];
          | while (k1<k) and (x[k1+1]=t) do begin
          | | k1 := k1 + 1;
          | end;
          | while (l1<l) and (x[l1+1]=t) do begin
          | | l1 := l1 + 1;
          | end;
          end;

     Замечание. Эта программа имеет дефект: при проверке условия
                  (l1<l) and (x[l1+1]=t)
(или второго, аналогичного) при ложной первой скобке вторая ока-
жется бессмысленной (индекс выйдет за границы массива) и возник-
нет ошибка. Некоторые версии паскаля, вычисляя (A and B), снача-
ла  вычисляют  A и при ложном A не вычисляют B. (Так ведет себя,
например, система Turbo Pascal, 5.0 - но не 3.0.) Тогда  описан-
ная ошибка не возникнет.
     Но если мы не хотим полагаться на такое свойство  использу-
емой  нами  реализации  паскаля  (не предусмотренное его автором
Н.Виртом), то можно поступить так. Введем  дополнительную  пере-
менную b: boolean и напишем:

  if k1 < k  then b := (x[k1+1]=t)  else  b:=false;
  {b = (k1<k) and (x[k1+1] = t}
  while  b  do  begin
  | k1:=k1+1;
  | if k1 < k then b := (x[k1+1]=t) else b:=false;
  end;

Можно также сделать иначе:

          end else begin {x[k1+1] = y[l1+1]}
          | if k1 + 1 = k then begin
          | | k1 := k1 + 1;
          | | n := n + 1;
          | end else if x[k1+1] = x [k1+2] then begin
          | | k1 := k1 + 1;
          | end else begin
          | | k1 := k1 + 1;
          | | n := n + 1;
          | end;
          end;

Так будет короче, хотя менее симметрично.

     Наконец, можно увеличить размер  массива  в  его  описании,
включив  в  него  фиктивные элементы.

     1.2.18. Даны два неубывающих массива  x:  array  [1..k]  of
integer и y: array [1..l] of integer. Найти число различных эле-
ментов  среди  x[1],...,x[k], y[1],...,y[l]. (Число действий по-
рядка k+l.)

     1.2.19.  Даны два массива x[1] <= ... <= x[k] и y[1] <= ...
<= y[l]. "Соединить" их в массив z[1] <= ... <= z[m] (m  =  k+l;
каждый  элемент  должен  входить в массив z столько раз, сколько
раз он входит в общей сложности в массивы x и y). Число действий
порядка m.

     Решение.

  k1 := 0; l1 := 0;
  {инвариант: ответ получится, если к  z[1]..z[k1+l1]  приписать
   справа соединение массивов x[k1+1]..x[k] и y[l1+1]..y[l]}
  while (k1 <> k) or (l1 <> l) do begin
  | if k1 = k then begin
  | | {l1 < l}
  | | l1 := l1 + 1;
  | | z[k1+l1] := y[l1];
  | end else if l1 = l then begin
  | | {k1 < k}
  | | k1 := k1 + 1;
  | | z[k1+l1] := x[k1];
  | end else if x[k1+1] <= y[l1+1] then begin
  | | k1 := k1 + 1;
  | | z[k1+l1] := x[k1];
  | end else if x[k1+1] >= y[l1+1] then begin
  | | l1 := l1 + 1;
  | | z[k1+l1] := y[l1];
  | end else begin
  | | { такого не бывает }
  | end;
  end;
  {k1 = k, l1 = l, массивы соединены}

Этот  процесс  можно  пояснить  так. Пусть у нас есть две стопки
карточек, отсортированных по алфавиту. Мы соединяем  их  в  одну
стопку,  выбирая каждый раз ту из верхних карточек обеих стопок,
которая идет раньше в алфавитном порядке.

     1.2.20. Даны два массива x[1] <= ... <= x[k] и y[1] <=  ...
<=  y[l].  Найти  их  "пересечение",  т.е. массив z[1] <= ... <=
z[m], содержащий их общие  элементы,  причем  кратность  каждого
элемента в массиве z равняется минимуму из его кратностей в мас-
сивах x и y. Число действий порядка k+l.

     1.2.21. Даны два массива x[1]<=...<=x[k] и  y[1]<=...<=y[l]
и  число q. Найти сумму вида x[i]+y[j], наиболее близкую к числу
q. (Число действий порядка k+l, дополнительная память - фиксиро-
ванное число целых переменных, сами массивы менять не разрешает-
ся.)
     Указание. Надо найти минимальное расстояние между элемента-
ми x[1]<=...<=x[k] и q-y[l]<=..<=q-y[1], что нетрудно сделать  в
ходе их слияния в один (воображаемый) массив.

     1.2.22. (из книги Д.Гриса) Некоторое  число  содержится  в
каждом из трех целочисленных неубывающих массивов x[1] <= ... <=
x[p],  y[1]  <=  ... <= y[q], z[1] <= ... <= z[r]. Найти одно из
таких чисел. Число действий должно быть порядка p + q + r.

     Решение.

  p1:=1; q1=1; r1:=1;
  {инвариант: x[p1]..x[p], y[q1]..y[q], z[r1]..z[r]
   содержат общий элемент }
  while not ((x[p1]=y[q1]) and (y[q1]=z[r1])) do begin
  | if x[p1]<y[q1] then begin
  | | p1:=p1+1;
  | end else if y[q1]<z[r1] then begin
  | | q1:=q1+1;
  | end else if z[r1]<x[p1] then begin
  | | r1:=r1+1;
  | end else begin
  | | { так не бывает }
  | end;
  end;
  {x[p1] = y[q1] = z[r1]}
  writeln (x[p1]);

     1.2.23. Та же задача, только заранее не известно, существу-
ет  ли общий элемент в трех неубывающих массивах и требуется это
выяснить (и найти один из общих элементов, если они есть).

     1.2.24. Элементами  массива  a[1..n]  являются  неубывающие
массивы  [1..m]  целых чисел (a: array [1..n] of array [1..m] of
integer; a[1][1] <= ... <=  a[1][m],  ...,  a[n][1]  <=  ...  <=
a[n][m]). Известно, что существует число, входящее во все масси-
вы  a[i]  (существует  такое  х,  что  для  всякого  i из [1..n]
найдётся j из [1..m], для которого a[i][j]=x). Найти одно из та-
ких чисел х.

     Решение. Введем массив b[1]..b[n], отмечающий начало "оста-
ющейся части" массивов a[1]..a[n].

  for k:=1 to n do begin
  |  b[k]:=1;
  end;
  eq := true;
  for k := 2 to n do begin
  | eq := eq and (a[1][b[1]] = a[k][b[k]]);
  end;
  {инвариант: оставшиеся части  пересекаются,  т.е.  существует
   такое  х,  что для всякого i из [1..n] найдётся j из [1..m],
   не меньшее b[i], для которого a[i][j] =  х;  eq  <=>  первые
   элементы оставшихся частей равны}
  while not eq do begin
  | s := 1; k := 1;
  | {a[s][b[s]] - минимальное среди a[1][b[1]]..a[k][b[k]]}
  | while k <> n do begin
  | | k := k + 1;
  | | if a[k][b[k]] < a[s][b[s]] then begin
  | | | s := k;
  | | end;
  | end;
  | {a[s][b[s]] - минимальное среди a[1][b[1]]..a[n][b[n]]}
  | b [s] := b [s] + 1;
  | for k := 2 to n do begin
  | | eq := eq and (a[1][b[1]] = a[k][b[k]]);
  | end;
  end;
  writeln (a[1][b[1]]);

     1.2.25.  Приведенное  решение предыдущей задачи требует по-
рядка m*n*n действий. Придумать способ с числом действий порядка
m*n.
     Указание.  Придется  пожертвовать симметрией и выбрать одну
из строк за основную. Двигаясь по основной строке,  поддерживаем
такое  соотношение:  во  всех  остальных  строках отмечен макси-
мальный элемент, не  превосходящий  текущего  элемента  основной
строки.

     1.2.26. (Двоичный поиск) Дана  последовательность  x[1]  <=
...  <=  x[n] целых чисел и число a. Выяснить, содержится ли a в
этой последовательности, т. е. существует ли i из 1..n, для  ко-
торого x[i]=a. (Количество действий порядка log n.)

     Решение. (Предполагаем, что n > 0.)

  l := 1; r := n+1;
  {если a есть вообще, то есть и среди x[l]..x[r-1], r > l}
  while r - l <> 1 do begin
  | m := l + (r-l) div 2 ;
  | {l < m < r }
  | if x[m] <= a then begin
  | | l := m;
  | end else begin {x[m] > a}
  | | r := m;
  | end;
  end;
(Обратите внимание, что и в случае x[m] = a инвариант не наруша-
ется.)
     Каждый раз r-l уменьшается примерно вдвое, откуда и вытека-
ет требуемая оценка числа действий.
     Замечание.
l + (r-l) div 2 = (2l + (r-l)) div 2 = (r+l) div 2.

     1.2.27. (Из книги Д.Гриса) Дан массив x:  array  [1..n]  of
array  [1..m]  of  integer,  упорядоченный  по  "строкам"  и  по
"столбцам":
         x[i][j] <= x[i+1][j],
         x[i][j] <= x[i][j+1]
и число a. Требуется выяснить, встречается ли a среди x[i][j].

     Решение. Представляя себе  массив  a  как  матрицу  (прямо-
угольник,  заполненный числами), мы выберем прямоугольник, в ко-
тором только и может содержаться a, и будем его  сужать.  Прямо-
угольник этот будет содержать x[i][j] при 1<=i<=l и k<=j<=m.
                1                     k         m
               -----------------------------------
              1|                     |***********|
               |                     |***********|
               |                     |***********|
              l|                     |***********|
               |---------------------------------|
               |                                 |
              n|                                 |
               -----------------------------------
(допускаются пустые прямоугольники при l = 0 и k = m+1).

  l:=n; k:=1;
  {l>=0, k<=m+1, если a есть, то в описанном прямоугольнике}
  while (l > 0) and (k < m+1) and (x[l][k] <> a) do begin
  | if x[l][k] < a then begin
  | | k := k + 1; {левый столбец не содержит a, удаляем его}
  | end else begin {x[l][k] > a}
  | | l := l - 1; {нижняя строка не содержит a, удаляем ее}
  | end;
  end;
  {x[l][k] = a или прямоугольник пуст }
  answer:= (l > 0) and (k < m+1) ;

     Замечание.  Здесь та же ошибка: x[l][k] может оказаться не-
определенным. (Её исправление предоставляется читателю.)

     1.2.28. (Московская олимпиада по программированию) Дан не-
убывающий массив положительных целых чисел a[1] <= a[2]  <=...<=
a[n].  Найти наименьшее целое положительное число, не представи-
мое в виде суммы нескольких элементов этого массива (каждый эле-
мент массива может быть использован не более одного раза). Число
действий порядка n.

     Решение. Пусть известно, что  числа,  представимые  в  виде
суммы элементов a[1],...,a[k], заполняют отрезок от 1 до некото-
рого N. Если a[k+1] > N+1, то N+1 и будет минимальным числом, не
представимым  в виде суммы элементов массива a[1]..a[n]. Если же
a[k+1] <= N+1, то числа, представимые  в  виде  суммы  элементов
a[1]..a[k+1], заполняют отрезок от 1 до N+a[k+1].

  k := 0; N := 0;
  {инвариант: числа, представимые в виде суммы элементов массива
   a[1]..a[k], заполняют отрезок 1..N}
  while (k <> n) and (a[k+1] <= N+1) do begin
  | N := N + a[k+1];
  | k := k + 1;
  end;
  {(k = n) или (a[k+1] > N+1); в обоих случаях ответ N+1}
  writeln (N+1);

(Снова тот же дефект: в условии цикла при ложном первом  условии
второе не определено.)

     1.2.29.  (Для  знакомых с основами алгебры) В целочисленном
массиве a[1]..a[n] хранится перестановка чисел 1..n  (каждое  из
чисел встречается по одному разу).
     (а) Определить четность перестановки. (И в (а), и в (б) ко-
личество действий порядка n.)
     (б)  Не используя других массивов, заменить перестановку на
обратную (если до работы программы a[i]=j, то после должно  быть
a[j]=i).

     Указание.  (а)  Четность  перестановки  определяется  коли-
чеством циклов. Чтобы отличать уже пройденные циклы, у  их  эле-
ментов можно, например, менять знак. (б) Обращение производим по
циклам.

     1.2.30. Дан массив a[1..n] и число b. Переставить  числа  в
массиве  таким  образом, чтобы слева от некоторой границы стояли
числа, меньшие или равные b, а справа от границы -  большие  или
равные b.

     Решение.

        l:=0; r:=n;
        {инвариант: a[1]..a[l]<=b; a[r+1]..a[n]>=b}
        while l <> r do begin
        | if a[l+1] <= b then begin
        | | l:=l+1;
        | end else if a[r] >=b then begin
        | | r:=r-1;
        | end else begin {a[l+1]>b; a[r]<b}
        | | поменять a[l+1] и  a[r]
        | | l:=l+1; r:+r-1;
        | end;
        end;

     1.2.31. Та же задача, но требуется, чтобы сначала шли  эле-
менты,  меньшие  b, затем равные b, а лишь затем большие b.

     Решение.  Теперь  потребуются  три границы: до первой будут
идти элементы, меньшие b, от первой до второй - равные b,  затем
неизвестно какие до третьей, а после третьей - большие b. (Более
симметричное  решение использовало бы четыре границы, но вряд ли
игра стоит свеч.) В качестве очередного рассматриваемого элемен-
та берем элемент справа от средней границы.

     l:=0; m:=0; r:=n;
     {инвариант: a[1..l]<b; a[l+1..m]=b; a[r+1]..a[n]>b}
     while m <> r do begin
     | if a[m+1]=b then begin
     | | m:=m+1;
     | end else if a[m+1]>b then begin
     | | обменять a[m+1] и a[r]
     | | r:=r-1;
     | end else begin {a[m+1]<b}
     | | обменять a[m+1] и a[l+1]
     | | l:=l+1; m:=m+1;
     end;

     1.2.32.  (вариант  предыдущей  задачи,  названный  в  книге
Дейкстры задачей о голландском флаге) В массиве стоят числа 0, 1
и  2.  Переставить  их  в порядке возрастания, если единственной
разрешенной операцией (помимо чтения) над массивом является  пе-
рестановка двух элементов.

     1.2.33. Дан массив a[1]..a[n]  и  число  m<=n.  Для  каждой
группы  из m стоящих рядом членов (таких групп, очевидно, n-m+1)
вычислить ее сумму. Общее число действий должно быть порядка n.

     Решение.  Переходя  от группы к соседней, мы добавляем один
член, а другой вычитаем.

     1.2.34. Дана квадратная таблица a[1..n][1..n] и число m<=n.
Для каждого квадрата размера m на m  в  этой  таблице  вычислить
сумму  стоящих в нем чисел. Общее число действий должно быть по-
рядка n*n.

     Решение. Сначала для каждого горизонтального прямоугольника
размером n на 1 вычисляем сумму стоящих в нем чисел. (При сдвиге
такого  прямоугольника  по  горизонтали на 1 нужно добавить одно
число и одно вычесть.) Затем,  используя  эти  суммы,  вычисляем
суммы в квадратах. (При сдвиге квадрата по вертикали добавляется
полоска, а другая полоска убавляется.)

     1.3. Индуктивные функции (по А.Г.Кушниренко).

     Пусть M - некоторое множество. Функция f, аргументами кото-
рой являются последовательности элементов множества M, а  значе-
ниями - элементы некоторого множества N, называется индуктивной,
если  ее значение на последовательности x[1]..x[n] можно восста-
новить по ее значению на последовательности  x[1]..x[n-1]  и  по
x[n],  т.  е.  если  существует  функция F из N*M (множество пар
<n,m>, где n - элемент множества N, а m - элемент множества M) в
N, для которой

      f(<x[1],...,x[n]>) = F (f (<x[1],...,x[n-1]>), x[n]).

     Схема алгоритма вычисления индуктивной функции:

  k := 0; f := f0;
  {инвариант: f - значение функции на <x[1],...,x[k]>}
  while  k<> n do begin
  | k := k + 1;
  | f := F (f, x[k]);
  end;

     Здесь f0 - значение функции  на  пустой  последовательности
(последовательности  длины  0). Если функция f определена только
на непустых последовательностях, то первая строка заменяется  на
"k := 1; f := f (<x[1]>);".

     Индуктивные расширения.

     Если функция f не является индуктивной, полезно  искать  ее
индуктивное  расширение  - такую индуктивную функцию g, значения
которой определяют значения f (это значит, что существует  такая
функция  t,  что  f  (<x[1]...x[n]>) = t (g (<x[1]...x[n]>)) при
всех <x[1]...x[n]>). Можно доказать, что среди всех  индуктивных
расширений  существует  минимальное  расширение F (минимальность
означает, что для любого индуктивного расширения  g  значения  F
определяются значениями g).

     1.3.1.  Указать  индуктивные  расширения   для   следующих
функций:
   а)  среднее  арифметическое  последовательности вещественных
чисел;
   б) число элементов последовательности целых чисел, равных ее
максимальному элементу;
   в)  второй по величине элемент последовательности целых чисел
(тот, который будет вторым, если переставить члены в неубывающем
порядке);
   г) максимальное число идущих подряд одинаковых элементов;
   д) максимальная длина монотонного (неубывающего  или  невоз-
растающего)  участка  из  идущих  подряд элементов в последова-
тельности целых чисел;
   е) число групп из единиц, разделенных нулями  (в  последова-
тельности нулей и единиц).

     Решение.

а) <сумма всех членов последовательности; длина>;

б)  <число  элементов,  равных  максимальному;  значение макси-
     мального>;

в) <наибольший элемент последовательности; второй  по  величине
     элемент>;

г) <максимальное число идущих подряд одинаковых элементов; чис-
     ло  идущих  подряд одинаковых элементов в конце последова-
     тельности; последний элемент последовательности>;

д) <максимальная длина монотонного участка; максимальная  длина
      неубывающего  участка  в конце последовательности; макси-
      мальная длина невозрастающего участка в конце  последова-
      тельности; последний член последовательности>;

е) <число групп из единиц, последний член>.

     1.3.2. (Сообщил Д.Варсонофьев.) Даны две последовательности
x[1]..x[n] и y[1]..y[k] целых чисел. Выяснить, является ли  вто-
рая последовательность подпоследовательностью первой, т. е. мож-
но  ли  из первой вычеркнуть некоторые члены так, чтобы осталась
вторая. Число действий порядка n+k.

       Решение.  (1  вариант)  Будем  сводить  задачу  к  задаче
меньшего размера.

  n1:=n;
  k1:=k;
  {инвариант:  искомый ответ <=> возможность из x[1]..x[n1] по-
   лучить y[1]..y[k1] }
  while (n1 > 0) and (k1 > 0) do begin
  | if x[n1] = y[k1] then begin
  | | n1 := n1 - 1;
  | | k1 := k1 - 1;
  | end else begin
  | | n1 := n1 - 1;
  | end;
  end;
  {n1 = 0 или k1 = 0; если k1 = 0, то ответ - да, если k1 <>  0
   (и n1 = 0), то ответ - нет}
  answer := (k1 = 0);

     Мы использовали то, что если x[n1] = y[k1] и y[1]..y[k1] -
подпоследовательность x[1]..x[n1], то y[1]..y[k1-1] - подпосле-
довательность x[1]..x[n1-1].

     (2  вариант)  Функция x[1]..x[n1] |-> (максимальное k1, для
которого y[1]..y[k1] есть подпоследовательность x[1]..x[n1]) ин-
дуктивна.

     1.3.3. Даны две последовательности x[1]..x[n] и  y[1]..y[k]
целых  чисел. Найти максимальную длину последовательности, явля-
ющейся подпоследовательностью обеих  последовательностей.  Коли-
чество операций порядка n*k.

     Решение  (сообщено М.Н.Вайнцвайгом, А.М.Диментманом). Обоз-
начим через  f(n1,k1)  максимальную  длину  общей  подпоследова-
тельности последовательностей x[1]..x[n1] и y[1]..y[k1]. Тогда

   x[n1] <> y[k1] => f(n1,k1) = max (f(n1,k1-1), f(n1-1,k1));
   x[n1] = y[k1]  => f(n1,k1) = max (f(n1,k1-1), f(n1-1,k1),
                              f(n1-1,k1-1)+1 );

(Поскольку  f(n1-1,k1-1)+1  >= f(n1,k1-1), f(n1-1,k1), во втором
случае максимум трех чисел можно заменить на третье из них.)
     Поэтому можно заполнять таблицу значений функции f, имеющую
размер n*k. Можно обойтись и памятью порядка k (или n), если ин-
дуктивно  (по  n1) выписать <f(n1,0), ..., f(n1,k)> (как функция
от n1 этот набор индуктивен).

     1.3.4 (из книги Д.Гриса) Дана последовательность целых  чи-
сел  x[1],...,  x[n].  Найти  максимальную длину ее возрастающей
подпоследовательности (число действий порядка n*log(n)).

     Решение. Искомая функция не индуктивна, но имеет  следующее
индуктивное  расширение: в него входит помимо максимальной длины
возрастающей подпоследовательности (обозначим ее k) также и чис-
ла u[1],...,u[k], где u[i] = (минимальный  из  последних  членов
возрастающих  подпоследовательностей длины i). Очевидно, u[1] <=
... <= u[k]. При добавлении нового члена x значения u и  k  кор-
ректируются.

  n1 := 1; k := 1; u[1] := x[1];
  {инвариант: k и u соответствуют данному выше описанию}
  while n1 <> n do begin
  | n1 := n1 + 1;
  | ...
  | {i - наибольшее из тех чисел отрезка 1..k, для кото-
  |   рых u[i] < x[n1]; если таких нет, то i=0 }
  | if i = k then begin
  | | k := k + 1;
  | | u[k+1] := x[n1];
  | end else begin {i < k, u[i] < x[n1] <= u[i+1] }
  | | u[i+1] := x[n1];
  | end;
  end;

     Фрагмент ... использует идею двоичного поиска; в инвариан-
те условно полагаем u[0] равным минус бесконечности, а  u[k+1]
- плюс бесконечности; наша цель: u[i] < x[n1] <= u[i+1].

  i:=0; j:=k+1;
  {u[i] < x[n1] <= u[j], j > i}
  while (j - i) <> 1 do begin
  | s := i + (j-i) div 2;    {i < s < j}
  | if u[s] >= x[n1] then begin
  | | j := s;
  | end else begin {u[s] < x[n1]}
  | | i := s;
  | end;
  end;
  {u[i] < x[n1] <= u[j], j-i = 1}

     Замечание.  Более  простое  (но не минимальное) индуктивное
расширение получится, если для каждого  i  хранить  максимальную
длину   возрастающей  подпоследовательности,  оканчивающейся  на
x[i]. Это расширение приводит к алгоритму с числом действий  по-
рядка n*n.

     1.3.5.  Какие  изменения  нужно внести в решение предыдущей
задачи, если надо  искать  максимальную  неубывающую  последова-
тельность?
     Глава 2. Порождение комбинаторных объектов.

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

     2.1. Размещения с повторениями.

     2.1.1. Напечатать все последовательности длины k  из  чисел
1..n.

     Решение.  Будем  печатать  их  в лексикографическом порядке
(последовательность a предшествует  последовательности  b,  если
для  некоторого s их начальные отрезки длины s равны, а (s+1)-ый
член  последовательности  a  меньше).  Первой  будет  последова-
тельность  <1, 1, ..., 1>, последней - последовательность <n, n,
..., n>. Будем хранить последнюю напечатанную последовательность
в массиве x[1]...x[k].

        ...x[1]...x[k] положить равным 1
        ...напечатать x
        ...last[1]...last[k] положить равным n
        while x <> last do begin
        | ...x := следующая за x последовательность
        | ...напечатать x
        end;

     Опишем, как можно  перейти  от  x  к  следующей  последова-
тельности.  Согласно определению, у следующей последовательности
первые s членов должны быть такими же, а (s+1)-ый - больше.  Это
возможно, если x[s+1] было меньше n. Среди таких s нужно выбрать
наибольшее  (иначе полученная последовательность не будет непос-
редственно следующей). Соответствующее x[s+1] нужно увеличить на
1. Итак, надо, двигаясь с конца последовательности, найти  самый
правый  член,  меньший  n (он найдется, так как по предположению
x<>last), увеличить его на 1, а идущие  за  ним  члены  положить
равными 1.

        p:=k;
        while not (x[p] < n) do begin
        | p := p-1;
        end;
        {x[p] < n, x[p+1] =...= x[k] = n}
        x[p] := x[p] + 1;
        for i := p+1 to k do begin
        | x[i]:=1;
        end;

     Замечание. Если членами последовательности считать числа не
от  1 до n, а от 0 до n-1, то переход к следующему соответствует
прибавлению 1 в n-ичной системе счисления.

     2.1.2. В предложенном алгоритме используется сравнение двух
массивов x <> last. Устранить его, добавив булевскую  переменную
l и включив в инвариант соотношение l <=> последовательность x -
последняя.

     2.1.3. Напечатать все подмножества множества {1...k}.

     Решение.  Подмножества находятся во взаимно однозначном со-
ответствии с последовательностями нулей и единиц длины k.

     2.1.4. Напечатать все последовательности из k положительных
целых чисел, у которых i-ый член не превосходит i.

     2.2. Перестановки.

     2.2.1. Напечатать все перестановки чисел 1..n (то есть пос-
ледовательности  длины  n, в которые каждое из чисел 1..n входит
по одному разу).

     Решение. Перестановки будем  хранить  в  массиве  x[1],...,
x[n]  и  печатать в лексикографическом порядке. (Первой при этом
будет перестановка <1 2...n>, последней - <n...2 1>.)  Для  сос-
тавления  алгоритма  перехода к следующей перестановке зададимся
вопросом: в каком случае k-ый член перестановки можно увеличить,
не меняя предыдущих? Ответ: если он меньше какого-либо из следу-
ющих членов (членов с номерами больше k). Мы  должны  найти  на-
ибольшее  k,  при  котором  это  так,  т. е. такое k, что x[k] <
x[k+1] > ... > x[n]. После  этого  x[k]  нужно  увеличить  мини-
мальным  возможным способом, т. е. найти среди x[k+1], ..., x[n]
наименьшее число, большее его. Поменяв x[k] с ним, остается рас-
положить числа с номерами k+1, ..., n  так,  чтобы  перестановка
была наименьшей, то есть в возрастающем порядке. Это облегчается
тем, что они уже расположены в убывающем порядке.

     Алгоритм перехода к следующей перестановке.

  {<x[1],...,x[n-1], x[n]> <> <n,...,2, 1>.}
  k:=n-1;
  {последовательность справа от k убывающая: x[k+1] >...> x[n]}
  while x[k] > x[k+1] do begin
  | k:=k-1;
  end;
  {x[k] < x[k+1] > ... > x[n]}
  t:=k+1;
  {t <=n, x[k+1] > ... > x[t] > x[k]}
   while (t < n) and (x[t+1] > x[k]) do begin
   | t:=t+1;
   end;
   {x[k+1] > ... > x[t] > x[k] > x[t+1] > ... > x[n]}
   ... обменять x[k] и x[t]
   {x[k+1] > ... > x[n]}
   ... переставить участок x[k+1] ... x[n] в обратном порядке

Замечание. Программа имеет знакомый  дефект:  если  t  =  n,  то
x[t+1] не определено.

     2.2.2. Модифицировать алгоритм перехода к следующей  перес-
тановке так, чтобы он сам проверял, не является ли данная перес-
тановка последней.

     2.3. Подмножества.

     2.3.1. Перечислить все k-элементные подмножества  множества
{1..n}.

     Решение.  Будем представлять каждое подмножество последова-
тельностью x[1]..x[n] нулей и единиц длины n, в которой ровно  k
единиц. (Другой способ представления разберем позже.) Такие пос-
ледовательности упорядочим лексикографически (см. выше). Очевид-
ный  способ  решения  задачи - перебирать все последовательности
как раньше, а затем отбирать среди них те, у которых k единиц  -
мы отбросим, считая его неэкономичным (число последовательностей
с  k  единицами  может  быть  много меньше числа всех последова-
тельностей). Будем искать такой алгоритм, чтобы  получение  оче-
редной последовательности требовало порядка n действий.
     В каком случае s-ый член  последовательности  можно  увели-
чить,  не  меняя предыдущие? Если x[s] меняется с 0 на 1, то для
сохранения общего числа единиц нужно справа от х[s]  заменить  1
на 0. Таким образом, х[s] - первый справа нуль, за которым стоят
единицы.  Легко  видеть,  что х[s+1] = 1 (иначе х[s] не первый).
Таким образом надо искать наибольшее  s,  для  которого  х[s]=0,
x[s+1]=1;

                  ______________________
               x |________|0|1...1|0...0|
                           s

За х[s+1] могут идти еще несколько единиц, а после них несколько
нулей. Заменив х[s] на 1, надо выбрать идущие за ним члены  так,
чтобы последовательность была бы минимальна с точки зрения наше-
го  порядка,  т. е. чтобы сначала шли нули, а потом единицы. Вот
что получается:

  первая последовательность    0...01...1 (n-k нулей, k единиц)
  последняя последовательность 1...10...0 (k единиц, n-k нулей)

  алгоритм перехода к следующей за х[1]...x[n] последовательнос-
  ти (предполагаем, что она есть):

        s := n - 1;
        while not ((x[s]=0) and (x[s+1]=1)) do begin
        | s := s - 1;
        end;
        {s - член, подлежащий изменению с 0 на 1}
        num:=0;
        for k := s to n do begin
        | num := num + x[k];
        end;
        {num - число единиц на участке x[s]...x[n], число нулей
         равно (длина - число единиц), т. е. (n-s+1) - num}
        x[s]:=1;
        for k := s+1 to n-num+1 do begin
        | x[k] := 0;
        end;
        for k := n-num+2 to n do begin
        | x[k]:=1;
        end;

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

     2.3.2. Перечислить все возрастающие последовательности дли-
ны  k  из  чисел 1..n в лексикографическом порядке. (Пример: при
n=5, k=2 получаем 12 13 14 15 23 24 25 34 35 45.)

     Решение. Минимальной будет последовательность 1, 2, ..., k;
максимальной - (n-k+1),..., (n-1), n. В каком случае  s-ый  член
последовательности можно увеличить? Ответ: если он меньше n-k+s.
После увеличения s-го элемента все следующие должны возрастать с
шагом 1. Получаем такой алгоритм перехода к следующему:

        s:=n;
        while not (x[s] < n-k+s) do begin
        | s:=s-1;
        end;
        {s - элемент, подлежащий увеличению};
        x[s] := x[s]+1;
        for i := s+1 to n do begin
        | x[i] := x[i-1]+1;
        end;

     2.3.3.  Пусть  мы  решили представлять k-элементные подмно-
жества множества {1..n} убывающими последовательностями длины k,
упорядоченными по-прежнему лексикографически. (Пример : 21 31 32
41 42 43 51 52 53 54.) Как выглядит тогда  алгоритм  перехода  к
следующей?

     Ответ. Ищем наибольшее s, для которого х[s]-x[s+1]>1. (Если
такого s нет, полагаем s = 0.) Увеличив x [s+1] на 1, кладем ос-
тальные минимально возможными (x[t] = k+1-t для t>s).

     2.3.4. Решить две предыдущие задачи, заменив  лексикографи-
ческий  порядок  на  обратный  (раньше идут те, которые больше в
лексикографическом порядке).

     2.3.5. Перечислить все вложения (функции, переводящие  раз-
ные  элементы в разные) множества {1..k} в {1..n} (предполагает-
ся, что k <= n). Порождение очередного элемента должно требовать
порядка k действий.

     Указание.  Эта  задача  может  быть  сведена к перечислению
подмножеств и перестановок элементов каждого подмножества.

     2.4. Разбиения.

     2.4.1. Перечислить все разбиения целого положительного чис-
ла  n  на целые положительные слагаемые (разбиения, отличающиеся
лишь порядком слагаемых, считаются за одно). (Пример: n=4,  раз-
биения 1+1+1+1, 2+1+1, 2+2, 3+1, 4.)

     Решение. Договоримся, что (1) в разбиениях слагаемые идут в
невозрастающем порядке, (2) сами разбиения мы перечисляем в лек-
сикографическом  порядке.  Разбиение  храним  в  начале  массива
x[1]...x[n], при этом количество входящих в него чисел обозначим
k. В начале x[1]=...=x[n]=1, k=n, в конце x[1]=n, k=1.
     В  каком  случае  x[s] можно увеличить не меняя предыдущих?
Во-первых, должно быть x[s-1] > x[s] или s  =  1.  Во-вторых,  s
должно  быть не последним элементом (увеличение s надо компенси-
ровать уменьшением следующих). Увеличив s, все следующие элемен-
ты надо взять минимально возможными.

        s := k - 1;
        while not ((s=1) or (x[s-1] > x[s])) do begin
        | s := s-1;
        end;
        {s - подлежащее увеличению слагаемое}
        x [s] := x[s] + 1;
        sum := 0;
        for i := s+1 to k do begin
        | sum := sum + x[i];
        end;
        {sum - сумма членов, стоявших после x[s]}
        for i := 1 to sum-1 do begin
        | x [s+i] := 1;
        end;
        k := s+sum-1;

     2.4.2. Представляя по-прежнему разбиения как невозрастающие
последовательности, перечислить их в порядке, обратном лексиког-
рафическому (для n=4, например, должно получиться 4,  3+1,  2+2,
2+1+1, 1+1+1+1).
     Указание. Уменьшать можно первый справа член, не равный  1;
найдя  его,  уменьшим на 1, а следующие возьмем максимально воз-
можными  (равными ему, пока хватает суммы, а последний - сколько
останется).

     2.4.3. Представляя  разбиения  как  неубывающие  последова-
тельности,  перечислить  их в лексикографическом порядке. Пример
для n=4: 1+1+1+1, 1+1+2, 1+3, 2+2, 4;
     Указание. Последний член увеличить нельзя, а  предпоследний
- можно; если после увеличения на 1 предпоследнего члена за счет
последнего нарушится возрастание, то из двух членов надо сделать
один,  если  нет,  то  последний член надо разбить на слагаемые,
равные предыдущему, и остаток, не меньший его.

     2.4.4.  Представляя  разбиения  как  неубывающие последова-
тельности, перечислить их в порядке, обратном лексикографическо-
му. Пример для n=4: 4, 2+2, 1+3, 1+1+2, 1+1+1+1.
     Указание.  Чтобы элемент x[s] можно было уменьшить, необхо-
димо, чтобы s = 1 или x[s-1] < x[s]. Если x[s] не последний,  то
этого и достаточно. Если он последний, то нужно, чтобы x[s-1] <=
(целая часть (x[s]/2)) или s=1.

     2.5. Коды Грея и аналогичные задачи.

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

     2.5.1.  Перечислить все последовательности длины n из чисел
1..k в таком порядке, чтобы каждая следующая отличалась от  пре-
дыдущей в единственной цифре, причем не более, чем на 1.

     Решение. Рассмотрим прямоугольную доску ширины n  и  высоты
k.  На каждой вертикали будет стоять шашка. Таким образом, поло-
жения шашек соответствуют последовательностям из чисел 1..k дли-
ны n (s-ый член последовательности соответствует высоте шашки на
s-ой горизонтали). На каждой шашке нарисуем  стрелочку,  которая
может быть направлена вверх или вниз. Вначале все шашки поставим
на  нижнюю  горизонталь стрелочкой вверх. Далее двигаем шашки по
такому правилу: найдя самую правую шашку, которую  можно  подви-
нуть  в направлении (нарисованной на ней) стрелки, двигаем ее на
одну клетку в этом направлении, а все стоящие  правее  ее  шашки
(они уперлись в край) разворачиваем кругом.
     Ясно, что на каждом шаге только одна шашка сдвигается, т.е.
один член последовательности меняется на 1. Докажем индукцией по
n,  что проходятся все последовательности из чисел 1...k. Случай
n = 1 очевиден. Пусть n > 1. Все ходы поделим на те, где  двига-
ется  последняя шашка, и те, где двигается не последняя. Во вто-
ром случае последняя шашка стоит у стены, и мы ее  поворачиваем,
так  что  за каждым ходом второго типа следует k-1 ходов первого
типа, за время которых последняя шашка побывает во всех клетках.
Если мы теперь забудем о последней шашке, то движения первых n-1
по предположению индукции пробегают все последовательности длины
n-1 по одному разу; движения же последней шашки из каждой после-
довательности длины n-1 делают k последовательностей длины n.
     В  программе,  помимо последовательности x[1]...x[n], будем
хранить массив d[1]...d[n] из чисел +1 и  -1  (+1  соответствует
стрелке вверх, -1 -стрелке вниз).

Начальное состояние: x[1] =...= x[n] = 1; d[1] =...= d[n] = 1.

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

  {если можно, сделать шаг и положить p := true, если нет,
   положить p := false }
  i := n;
  while (i > 1) and
  | (((d[i]=1) and (x[i]=n)) or ((d[i]=-1) and (x[i]=1)))
  |   do begin
  | i:=i-1;
  end;
  if (d[i]=1 and x[i]=n) or (d[i]=-1 and x[i]=1)
  |    then begin {i=1}
  | p:=false;
  end else begin
  | p:=true;
  | x[i] := x[i] + d[i];
  | for j := i+1 to n do begin
  | | d[j] := - d[j];
  | end;
  end;

     Замечание.  Для последовательностей нулей и единиц возможно
другое решение, использующее двоичную систему. (Именно оно  свя-
зывается обычно с названием "коды Грея".)
     Запишем подряд все числа от 0 до (2 в степени n) - 1 в дво-
ичной системе. Например, для n = 3 напишем:

            000 001 010 011 100 101 110 111

Затем  каждое из чисел подвергнем преобразованию, заменив каждую
цифру, кроме первой, на ее сумму с предыдущей цифрой (по  модулю
2). Иными словами, число

     a[1], a[2],...,a[n]  преобразуем в
     a[1], a[1] + a[2], a[2] + a[3],...,a[n-1] + a[n]

(сумма по модулю 2). Для n=3 получим:

            000 001 011 010  110  111 101 100.

     Легко проверить, что описанное преобразование чисел обрати-
мо (и тем самым дает все  последовательности  по  одному  разу).
Кроме  того,  двоичные  записи соседних чисел отличаются заменой
конца 011...1 на конец 100...0, что  -  после  преобразования  -
приводит к изменению единственной цифры.

     Применение кода Грея. Пусть есть вращающаяся ось, и мы  хо-
тим  поставить датчик угла поворота этой оси. Насадим на ось ба-
рабан, выкрасим половину барабана в черный цвет, половину в  бе-
лый и установим фотоэлемент. На его выходе будет в половине слу-
чаев  0,  а в половине 1 (т. е. мы измеряем угол "с точностью до
180").

     Развертка барабана:
                     0       1
             -> |_|_|_|_|*|*|*|*| <- (склеить бока).

     Сделав рядом другую дорожку из двух черных и белых частей и
поставив  второй фотоэлемент, получаем возможность измерить угол
с точностью до 90 градусов:

                   0   0   1   1
                   0   1   0   1
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|_|_|*|*|

Сделав третью,

                 0 0 0 0 1 1 1 1
                 0 0 1 1 0 0 1 1
                 0 1 0 1 0 1 0 1
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|_|_|*|*|
                |_|*|_|*|_|*|_|*|

мы  измерим угол с точностью до 45 градусов и т.д. Эта идея име-
ет, однако, недостаток: в момент пересечения границ  сразу  нес-
колько  фотоэлементов  меняют  сигнал, и если эти изменения про-
изойдут не одновременно, на какое-то время показания фотоэлемен-
тов будут бессмысленными.  Коды  Грея  позволяют  избежать  этой
опасности.  Сделаем так, чтобы на каждом шаге менялось показание
лишь одного фотоэлемента (в том числе и на последнем, после  це-
лого оборота).

                 0 0 0 0 1 1 1 1
                 0 0 1 1 1 1 0 0
                 0 1 1 0 0 1 1 0
                 _ _ _ _
                |_|_|_|_|*|*|*|*|
                |_|_|*|*|*|*|_|_|
                |_|*|*|_|_|*|*|_|

     Написанная нами формула позволяет легко преобразовать  дан-
ные от фотоэлементов в двоичный код угла поворота.

     2.5.2. Напечатать все перестановки чисел  1..n  так,  чтобы
каждая   следующая   получалась   из   предыдущей  перестановкой
(транспозицией) двух соседних чисел. Например, при n = 3  допус-
тим такой порядок: 3.2 1 -> 2 3.1 -> 2.1 3 -> 1 2.3 -> 1.3 2  ->
3 1 2 (между переставляемыми числами вставлены точки).

     Решение. Наряду с множеством перестановок  рассмотрим  мно-
жество  последовательностей y[1]..y[n] целых неотрицательных чи-
сел, у которых y[1] <= 0,..., y[n] <= n-1. В нем столько же эле-
ментов, сколько в множестве всех перестановок, и мы сейчас уста-
новим между ними взаимно однозначное соответствие. Именно,  каж-
дой  перестановке  поставим  в  соответствие  последовательность
y[1]..y[n], где y[i] - количество чисел, меньших i и стоящих ле-
вее i в этой перестановке. Взаимная  однозначность  вытекает  из
такого  замечания. Перестановка чисел 1...n получается из перес-
тановки чисел 1..n-1 добавлением числа n, которое можно вставить
на любое из n мест. При этом к сопоставляемой с  ней  последова-
тельности  добавляется  еще один член, принимающий значения от 0
до n-1, а предыдущие члены не меняются.  При  этом  оказывается,
что  изменение  на единицу одного из членов последовательности y
соответствует перестановке двух соседних чисел, если все  следу-
ющие  числа последовательности y принимают максимально или мини-
мально возможные для них значения. Именно, увеличение y[i] на  1
соответствует  перестановке  числа  i  с  его  правым соседом, а
уменьшение - с левым.
     Теперь вспомним решение задачи о перечислении всех последо-
вательностей, на каждом шаге которого один член меняется на еди-
ницу. Заменив прямоугольную доску доской в форме лестницы (высо-
та i-ой вертикали равна i) и двигая шашки по тем же правилам, мы
перечислим все последовательности y, причем i-ый член будет  ме-
няться,  лишь  если  все  следующие шашки стоят у края. Надо еще
уметь параллельно с изменением  y  корректировать  перестановку.
Очевидный  способ требует отыскания в ней числа i; это можно об-
легчить, если помимо самой перестановки хранить функцию i  |--->
позиция  числа i в перестановке (обратное к перестановке отобра-
жение), и соответствующим образом ее корректировать.  Вот  какая
получается программа:

 program test;
 | const n=...;
 | var
 |   x: array [1..n] of 1..n; {перестановка}
 |   inv_x: array [1..n] of 1..n; {обратная перестановка}
 |   y: array [1..n] of integer; {Y[i] < i}
 |   d: array [1..n] of -1..1; {направления}
 |   b: boolean;
 |
 | procedure print_x;
 | | var i: integer;
 | begin
 | | for i:=1 to n do begin
 | | | write (x[i], ' ');
 | | end;
 | | writeln;
 | end;
 |
 | procedure set_first;{первая перестановка: y[i]=0 при всех i}
 | | var i : integer;
 | begin
 | | for i := 1 to n do begin
 | | | x[i] := n + 1 - i;
 | | | inv_x[i] := n + 1 - i;
 | | | y[i]:=0;
 | | | d[i]:=1;
 | | end;
 | end;
 |
 | procedure move (var done : boolean);
 | | var i, j, pos1, pos2, val1, val2, tmp : integer;
 | begin
 | | i := n;
 | | while (i > 1) and (((d[i]=1) and (y[i]=i-1)) or
 | | |          ((y[i]=-1) and (y[i]=0))) do begin
 | | | i := i-1;
 | | end;
 | | done := (i>1);
 | | {упрощение связано с тем, что первый член нельзя менять}
 | | if done then begin
 | | | y[i] := y[i]+d[i];
 | | | for j := i+1 to n do begin
 | | | | d[j] := -d[j];
 | | | end;
 | | | pos1 := inv_x[i];
 | | | val1 := i;
 | | | pos2 := pos1 + d[i];
 | | | val2 := x[pos2];
 | | | {pos1, pos2 - номера переставляемых элементов;
 | | |   val1, val2 - их значения}
 | | | tmp := x[pos1];
 | | | x[pos1] := x[pos2];
 | | | x[pos2] := tmp;
 | | | tmp := inv_x[val1];
 | | | inv_x[val1] := inv_x[val2];
 | | | inv_x[val2] := tmp;
 | | end;
 | end;
 |
 begin
 | set_first;
 | print_x;
 | b := true;
 | {напечатаны все перестановки до текущей включительно;
 |   если b ложно, то текущая - последняя}
 | while b do begin
 | | move (b);
 | | if b then print_x;
 | end;
 end.

     2.6. Несколько замечаний.

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

     2.6.1. Перечислить все последовательности длины 2n, состав-
ленные из n единиц и n минус единиц, у которых сумма любого  на-
чального  отрезка положительна (т.е. число минус единиц в нем не
превосходит числа единиц).

     Решение. Изображая единицу вектором (1,1), а минус  единицу
вектором  (1,-1), можно сказать, что мы ищем пути из точки (0,0)
в точку (n,0), не опускающиеся ниже оси абсцисс.
     Будем перечислять последовательности  в  лексикографическом
порядке,  считая,  что  -1  предшествует  1.  Первой  последова-
тельностью будет "пила"
        1, -1, 1, -1, ...
а последней - "горка"
        1, 1, 1, ..., 1, -1, -1, ..., -1.
     Как перейти от последовательности к следующей? До некоторо-
го места они должны совпадать, а затем надо заменить  -1  на  1.
Место  замены должно быть расположено как можно правее. Но заме-
нять -1 на 1 можно только в том случае, если справа от нее  есть
единица (которую можно заменить на -1). Заменив -1 на 1, мы при-
ходим  к  такой  задаче:  фиксирован  начальный кусок последова-
тельности, надо найти минимальное продолжение. Ее решение:  надо
приписывать -1, если это не нарушит условия неотрицательности, а
иначе приписывать 1. Получаем такую программу:

    ...
    type array2n = array [1..2n] of integer;
    ...
    procedure get_next (var a: array2n; var last: Boolean);
    | {в a помещается следующая последовательность, если}
    | {она есть (при этом last=false), иначе last:=true}
    | var k, i, sum: integer;
    begin
    | k:=2*n;
    | {инвариант: в a[k+1..2n] только минус единицы}
    | while a[k] = -1 do begin k:=k-1; end;
    | {k - максимальное среди тех, для которых a[k]=1}
    | while (k>0) and (a[k] = 1) do begin k:=k-1; end;
    | {a[k] - самая правая -1, за которой есть 1;
    |  если таких нет, то k=0}
    | if k = 0 then begin
    | | last := true;
    | end else begin
    | | last := false;
    | | i:=0; sum:=0;
    | | {sum = a[1]+...+a[i]}
    | | while i<> k do begin
    | | | i:=i+1; sum:= sum+a[i];
    | | end;
    | | {sum = a[1]+...+a[k]}
    | |  a[k]:= 1; sum:= sum+2;
    | | {вплоть до a[k] все изменено, sum=a[1]+...+a[k]}
    | | while k <> 2*n do begin
    | | | k:=k+1;
    | | | if sum > 0 then begin
    | | | | a[k]:=-1
    | | | end else begin
    | | | | a[k]:=1;
    | | | end;
    | | | sum:= sum+a[k];
    | | end;
    | | {k=n, sum=a[1]+...a[2n]=0}
    | end;
    end;

     2.6.2.  Перечислить все расстановки скобок в произведении n
сомножителей. Порядок сомножителей не меняется, скобки полностью
определяют порядок действий. (Например, для n = 4 есть 5 расста-
новок ((ab)c)d, (a(bc))d, (ab)(cd), a((bc)d), a(b(cd)).)

     Указание. Каждому порядку действий соответствует последова-
тельность команд стекового калькулятора.

     2.6.3.  На окружности задано 2n точек, пронумерованных от 1
до 2n. Перечислить все способы провести n непересекающихся  хорд
с вершинами в этих точках.

     2.6.4. Перечислить все способы разрезать n-угольник на тре-
угольники, проведя n - 2 его диагонали.

     Еще  один класс задач на перечисление всех элементов задан-
ного множества мы  рассмотрим  ниже,  обсуждая  метод  поиска  с
возвратами (backtracking).

     2.7. Подсчет количеств.

     Иногда  можно  найти  количество  объектов  с  тем или иным
свойством, не перечисляя их. Классический пример: C(n,k) - число
всех k-элементных подмножеств n-элементного  множества  -  можно
найти, заполняя таблицу значений функции С по формулам:

    C (n,0) = C (n,n) = 1            (n >= 1)
    C (n,k) = C (n-1,k-1) + C (n-1,k) (n > 1, 0 < k < n);

или по формуле n!/((k!)*(n-k)!). (Первый способ эффективнее, ес-
ли надо вычислить много значений С(n,k).)

    Приведем другие примеры.

     2.7.1 (Число разбиений). (Предлагалась на всесоюзной  олим-
пиаде  по программированию 1988 года.) Пусть P(n) - число разби-
ений целого положительного n на  целые  положительные  слагаемые
(без учета порядка, 1+2 и 2+1 - одно и то же разбиение). При n=0
положим P(n) = 1 (единственное разбиение не содержит слагаемых).
Построить алгоритм вычисления P(n) для заданного n.
     Решение.  Можно  доказать  (это нетривиально) такую формулу
для P(n):

 P(n) = P(n-1)+P(n-2)-P(n-5)-P(n-7)+P(n-12)+P(n-15) +...

(знаки у пар членов чередуются, вычитаемые в  одной  паре  равны
(3*q*q-q)/2 и (3*q*q+q)/2).
     Однако и без ее использования можно придумать способ вычис-
ления  P(n), который существенно эффективнее перебора и подсчета
всех разбиений.
     Обозначим через R(n,k) (при n >= 0, k >= 0) число разбиений
n  на  целые  положительные  слагаемые, не превосходящие k. (При
этом  R(0,k) считаем равным 1 для всех k >= 0.) Очевидно, P(n) =
R(n,n). Все разбиения n на слагаемые, не  превосходящие  k,  ра-
зобьем  на  группы  в  зависимости  от  максимального слагаемого
(обозначим его i). Число R(n,k) равно сумме (по всем i от  1  до
k)  количеств разбиений со слагаемыми не больше k и максимальным
слагаемым, равным i. А разбиения n на слагаемые  не  более  k  с
первым  слагаемым, равным i, по существу представляют собой раз-
биения n - i на слагаемые, не превосходящие i (при i <= k).  Так
что

    R(n,k) = сумма по i от 1 до k чисел R(n-i,i) при k <= n;
    R(n,k) = R(n,n) при k >= n,

что позволяет заполнять таблицу значений функции R.

     2.7.2 (Счастливые билеты). (Задача предлагалась на Всесоюз-
ной олимпиаде по программированию 1989 года). Последовательность
из 2n цифр (каждая цифра от 0 до 9) называется счастливым  биле-
том, если сумма первых n цифр равна сумме последних n цифр. Най-
ти число счастливых последовательностей данной длины.

     Решение. (Сообщено одним из участников олимпиады; к сожале-
нию,  не могу указать фамилию, так как работы проверялись зашиф-
рованными.) Рассмотрим более общую задачу: найти число  последо-
вательностей,  где  разница  между суммой первых n цифр и суммой
последних n цифр равна k (k = -9n,..., 9n). Пусть T(n, k) - чис-
ло таких последовательностей.
     Разобьем  множество  таких  последовательностей на классы в
зависимости от разницы между первой и  последней  цифрами.  Если
эта разница равна t, то разница между суммами групп из оставших-
ся  n-1 цифр равна k-t. Учитывая, что пар цифр с разностью t бы-
вает 10 - (модуль t), получаем формулу
   T(n,k) = сумма по t от -9 до 9 чисел (10-|t|) * T(n-1,  k-t).
(Некоторые слагаемые могут отсутствовать, так как k-t может быть
слишком велико.)
      Глава 3. Обход дерева. Перебор с возвратами.

     3.1. Ферзи, не бьющие друг друга: обход дерева позиций

     В  предыдущей главе мы рассматривали несколько задач одного
и того же типа: "перечислить все элементы  некоторого  множества
A". Схема решения была такова: на множестве A вводился порядок и
описывалась  процедура  перехода  от произвольного элемента мно-
жества A к следующему за ним (в этом порядке).  Такую  схему  не
всегда  удается  реализовать  непосредственно, и в этой главе мы
рассмотрим другой полезный прием перечисления всех элементов не-
которого множества. Его называют "поиск  с  возвратами",  "метод
ветвей  и границ", "backtracking". На наш взгляд наиболее точное
название этого метода - обход дерева.

     3.1.1. Перечислить все способы расстановки n ферзей на шах-
матной доске n на n, при которых они не бьют друг друга.

     Решение. Очевидно, на каждой из n горизонталей должно  сто-
ять  по  ферзю.  Будем  называть k-позицией (для k = 0, 1,...,n)
произвольную расстановку k ферзей на k нижних горизонталях (фер-
зи могут бить друг друга). Нарисуем "дерево позиций": его корнем
будет единственная 0-позиция, а из каждой  k-позиции  выходит  n
стрелок  вверх в (k+1)-позиции. Эти n позиций отличаются положе-
нием ферзя на (k+1)-ой горизонтали. Будем считать, что  располо-
жение  их  на рисунке соответствует положению этого ферзя: левее
та позиция, в которой ферзь расположен левее.

                                        Дерево позиций для
                                           n = 2

Среди позиций этого дерева нам надо отобрать те n-позиции, в ко-
торых ферзи не бьют друг друга. Программа будет "обходить  дере-
во" и искать их. Чтобы не делать лишней работы, заметим вот что:
если  в  какой-то  k-позиции  ферзи  бьют друг друга, то ставить
дальнейших ферзей смысла нет. Поэтому, обнаружив это,  мы  будем
прекращать построение дерева в этом направлении.

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

                                         Дерево допустимых
                                         позиций для n = 3

     Разобьем задачу на две части: (1) обход произвольного дере-
ва и (2) реализацию дерева допустимых позиций.
     Сформулируем задачу обхода произвольного дерева. Будем счи-
тать, что у нас имеется Робот, который в каждый момент находится
в одной из вершин дерева (вершины изображены на рисунке  кружоч-
ками). Он умеет выполнять команды:

                              вверх_налево  (идти по самой левой
                                 из выходящих вверх стрелок)

                              вправо (перейти в соседнюю  справа
                                 вершину)

                              вниз (спуститься вниз на один уро-
                                 вень)

            вверх_налево
            вправо
            вниз

и проверки, соответствующие возможности выполнить каждую из  ко-
манд,   называемые  "есть_сверху",  "есть_справа",  "есть_снизу"
(последняя истинна всюду, кроме корня). Обратите  внимание,  что
команда "вправо" позволяет перейти лишь к "родному брату", но не
к "двоюродному".

                                    Так команда "вправо"
                                    НЕ действует!

     Будем считать, что у Робота есть команда "обработать" и что
его задача - обработать все  листья  (вершины,  из  которых  нет
стрелок вверх, то есть где условие "есть_сверху" ложно). Для на-
шей  шахматной  задачи  команде обработать будет соответствовать
проверка и печать позиции ферзей.

     Доказательство  правильности приводимой далее программы ис-
пользует такие определения. Пусть фиксировано положение Робота в
одной из вершин дерева. Тогда все листья дерева  разбиваются  на
три  категории: над Роботом, левее Робота и правее Робота. (Путь
из корня в лист может проходить через вершину с Роботом,  свора-
чивать  влево,  не доходя до нее и сворачивать вправо, не доходя
до нее.) Через (ОЛ) обозначим условие "обработаны все листья ле-
вее Робота", а через (ОЛН) - условие "обработаны все листья  ле-
вее и над Роботом".

Нам понадобится такая процедура:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОЛ), надо: (ОЛН)}
  begin
  | {инвариант: ОЛ}
  | while есть_сверху do begin
  | | вверх_налево
  | end
  | {ОЛ, Робот в листе}
  | обработать;
  | {ОЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, листья не обработаны
  надо: Робот в корне, листья обработаны

  {ОЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОЛН, есть справа}
  | | вправо;
  | | {ОЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | end;
  end;
  {ОЛН, Робот в корне => все листья обработаны}

Осталось  воспользоваться  следующими  свойствами  команд Робота
(сверху записаны условия, в которых выполняется команда, снизу -
утверждения о результате ее выполнения):

   (1) {ОЛ, не есть_сверху}  (2) {ОЛ}
       обработать                вверх_налево
       {ОЛН}                     {ОЛ}

   (3) {есть_справа, ОЛН}    (4) {не есть_справа, ОЛН}
       вправо                    вниз
       {ОЛ}                      {ОЛН}

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

     3.1.3. Доказать правильность следующей программы обхода де-
рева:

  var state: (WL, WLU);
  state := WL;
  while есть_снизу or (state <> WLU) do begin
  | if (state = WL) and есть_сверху then begin
  | | вверх;
  | end else if (state = WL) and not есть_сверху then begin
  | | обработать; state := WLU;
  | end else if (state = WLU) and есть_справа then begin
  | |  вправо; state := WL;
  | end else begin {state = WLU, not есть_справа, есть_снизу}
  | |  вниз;
  | end;
  end;

     Решение. Инвариант цикла:
        state = WL  => ОЛ
        state = WLU => ОЛН
Доказательство завершения работы: переход из состояния ОЛ в  ОЛН
возможен  только  при  обработке вершины, поэтому если программа
работает бесконечно, то с некоторого момента значение  state  не
меняется, что невозможно.

    3.1.4.  Решить задачу об обходе дерева, если мы хотим, чтобы
обрабатывались все вершины (не только листья).

    Решение. Пусть x - некоторая вершина. Тогда любая вершина  y
относится к одной из четырех категорий. Рассмотрим путь из корня
в y. Он может:
    (а) быть частью пути из корня в x (y ниже x);
    (б) свернуть налево с пути в x (y левее x);
    (в) пройти через x (y над x);
    (г) свернуть направо с пути в x (y правее x);
В  частности,  сама вершина x относится к категории (в). Условия
теперь будут такими:
    (ОНЛ) обработаны все вершины ниже и левее;
    (ОНЛН) обработаны все вершины ниже, левее и над.
Вот как будет выглядеть программа:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОНЛ), надо: (ОНЛН)}
  begin
  | {инвариант: ОНЛ}
  | while есть_сверху do begin
  | | обработать
  | | вверх_налево
  | end
  | {ОНЛ, Робот в листе}
  | обработать;
  | {ОНЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, ничего не обработано
  надо: Робот в корне, все вершины обработаны

  {ОНЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОНЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОНЛН, есть справа}
  | | вправо;
  | | {ОНЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | end;
  end;
  {ОНЛН, Робот в корне => все вершины обработаны}

     3.1.5. Приведенная только что программа обрабатывает верши-
ну до того, как обработан любой из ее потомков. Как изменить ее,
чтобы каждая вершина, не являющаяся листом, обрабатывалась дваж-
ды: один раз до, а другой раз после всех своих потомков? (Листья
по-прежнему обрабатываются по разу.)

    Решение.  Под "обработано ниже и левее" будем понимать "ниже
обработано по разу, слева обработано полностью (листья по  разу,
останые по два)". Под "обработано ниже, левее и над" будем пони-
мать "ниже обработано по разу, левее и над - полностью".

Программа будет такой:

  procedure вверх_до_упора_и_обработать
  | {дано: (ОНЛ), надо: (ОНЛН)}
  begin
  | {инвариант: ОНЛ}
  | while есть_сверху do begin
  | | обработать
  | | вверх_налево
  | end
  | {ОНЛ, Робот в листе}
  | обработать;
  | {ОНЛН}
  end;

Основной алгоритм:

  дано: Робот в корне, ничего не обработано
  надо: Робот в корне, все вершины обработаны

  {ОНЛ}
  вверх_до_упора_и_обработать
  {инвариант: ОНЛН}
  while есть_снизу do begin
  | if есть_справа then begin {ОНЛН, есть справа}
  | | вправо;
  | | {ОНЛ}
  | | вверх_до_упора_и_обработать;
  | end else begin
  | | {ОЛН, не есть_справа, есть_снизу}
  | | вниз;
  | | обработать;
  | end;
  end;
  {ОНЛН, Робот в корне => все вершины обработаны полностью}

     3.1.6. Доказать, что число операций в этой программе по по-
рядку равно числу вершин дерева. (Как и в других программах, ко-
торые  отличаются от этой лишь пропуском некоторых команд "обра-
ботать".)
     Указание. Примерно каждое второе  действие  при  исполнении
этой программы - обработка вершины, а каждая вершина обрабатыва-
ется максимум дважды.

     Теперь реализуем операции с деревом позиций. Позицию  будем
представлять  с помощью переменной k: 0..n (число ферзей) и мас-
сива c: array [1..n] of 1..n (c [i] - координаты ферзя  на  i-ой
горизонтали; при i > k значение c [i] роли не играет). Предпола-
гается,  что  все позиции допустимы (если убрать верхнего ферзя,
остальные не бьют друг друга).

  program queens;
  | const n = ...;
  | var
  |   k: 0..n;
  |   c: array [1..n] of 1..n;
  |
  | procedure begin_work; {начать работу}
  | begin
  | | k := 0;
  | end;
  |
  | function danger: boolean; {верхний ферзь под боем}
  | | var b: boolean; i: integer;
  | begin
  | | if k <= 1 then begin
  | | | danger := false;
  | | end else begin
  | | | b := false; i := 1;
  | | | {b <=> верхний ферзь под боем ферзей с номерами < i}
  | | | while i <> k do begin
  | | | | b := b or (c[i]=c[k]) {вертикаль}
  | | | |     or (abs(c[[i]-c[k]))=abs(i-k)); {диагональ}
  | | | | i := i+ 1;
  | | | end;
  | | | danger := b;
  | | end;
  | end;
  |
  | function is_up: boolean {есть_сверху}
  | begin
  | | is_up := (k < n) and not danger;
  | end;
  |
  | function is_right: boolean {есть_справа}
  | begin
  | | is_right := (k > 0) and (c[k] < n);
  | end;
  | {возможна ошибка: при k=0 не определено c[k]}
  |
  | function is_down: boolean {есть_снизу}
  | begin
  | | is_up := (k > 0);
  | end;
  |
  | procedure up; {вверх_налево}
  | begin {k < n}
  | | k := k + 1;
  | | c [k] := 1;
  | end;
  |
  | procedure right; {вправо}
  | begin {k > 0,  c[k] < n}
  | | c [k] := c [k] + 1;
  | end;
  |
  | procedure down; {вниз}
  | begin {k > 0}
  | | k := k - 1;
  | end;
  |
  | procedure work; {обработать}
  | | var i: integer;
  | begin
  | | if (k = n) and not danger then begin
  | | | for i := 1 to n do begin
  | | | | write ('<', i, ',' , c[i], '> ');
  | | | end;
  | | | writeln;
  | | end;
  | end;
  |
  | procedure UW; {вверх_до_упора_и_обработать}
  | begin
  | | while is_up do begin
  | | | up;
  | | end
  | | work;
  | end;
  |
  begin
  | begin_work;
  | UW;
  | while is_down do begin
  | | if is_right then begin
  | | | right;
  | | | UW;
  | | end else begin
  | | | down;
  | | end;
  | end;
  end.

     3.1.7. Приведенная программа тратит довольно много  времени
на  выполнение  проверки  есть_сверху  (проверка,  находится  ли
верхний ферзь под боем, требует числа действий порядка n). Изме-
нить реализацию операций с деревом позиций так,  чтобы  все  три
проверки есть_сверху/справа/снизу и соответствующие команды тре-
бовали  бы  количества действий, ограниченного не зависящей от n
константой.

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

     3.2.  Обход дерева в других задачах.

     3.2.1. Использовать метод обхода дерева для решения  следу-
ющей   задачи:   дан  массив  из  n  целых  положительных  чисел
a[1]..a[n] и число s; требуется узнать, может ли  число  s  быть
представлено  как  сумма  некоторых  из чисел массива a. (Каждое
число можно использовать не более чем по одному разу.)

     Решение. Будем задавать k-позицию последовательностью из  k
булевских  значений,  определяющих,  входят  ли  в  сумму  числа
a[1]..a[k] или не входят. Позиция допустима, если  ее  сумма  не
превосходит s.

     Замечание. По сравнению с полным перебором всех (2 в степе-
ни  n) подмножеств тут есть некоторый выигрыш. Можно также пред-
варительно отсортировать массив a в убывающем порядке,  а  также
считать  недопустимыми  те  позиции, в которых сумма отброшенных
членов больше, чем разность суммы всех  членов  и  s.  Последний
приём  называют  "методом  ветвей  и границ". Но принципиального
улучшения по сравнению с полным перебором тут не получается (эта
задача, как говорят, NP-полна,  см.  подробности  в  книге  Ахо,
Хопкрофта и Ульмана "Построение и анализ вычислительных алгорит-
мов").  Традиционное  название  этой задачи - "задача о рюкзаке"
(рюкзак общей грузоподъемностью s нужно упаковать  под  завязку,
располагая  предметами  веса  a[1]..a[n]).  См.  также в главе 7
(раздел о динамическом программировании)  алгоритм  её  решения,
полиномиальный по n+s.

     3.2.2.  Перечислить все последовательности из n нулей, еди-
ниц и двоек, в которых никакая группа цифр  не  повторяется  два
раза подряд (нет куска вида XX).

     3.2.3.  Аналогичная  задача для последовательностей нулей и
единиц, в которых никакая группа цифр не  повторяется  три  раза
подряд (нет куска вида XXX).

     К этой же категории относятся задачи типа "можно ли сложить
данную фигуру из пентамино" и им подобные. В  них  важно  умелое
сокращение  перебора (вовремя распознать, что имеющееся располо-
жение фигурок уже противоречит требованиям, и по этой ветви  по-
иск не продолжать).
        Глава 4. Сортировка.

     4.1. Квадратичные алгоритмы.

     4.1.1. Пусть a[1],  ...,  a[n]  -  целые  числа.  Требуется
построить  массив  b[1],  ..., b[n], содержащий те же числа, для
которых b[1] <= ... <= b[n].
     Замечание. Среди чисел a[1]...a[n] могут быть равные.  Тре-
буется,  чтобы  каждое целое число входило в b[1]...b[n] столько
же раз, сколько и в a[1]...a[n].

     Решение. Удобно считать, что числа a[1]..a[n] и  b[1]..b[n]
представляют собой начальное и конечное значения массива x. Тре-
бование  "a  и b содержат одни и те же числа" будет заведомо вы-
полнено, если в процессе работы  мы  ограничимся  перестановками
элементов x.
  ...
  k := 0;
  {k наименьших элементов массива x установлены на свои места}
  while k <> n do begin
  | s := k + 1; t := k + 1;
  | {x[s] - наименьший среди x[k+1]...x[t] }
  | while t<>n do begin
  | | t := t + 1;
  | | if x[t] < x[s] then begin
  | | | s := t;
  | | end;
  | end;
  | {x[s] - наименьший среди x[k+1]..x[n] }
  | ... переставить x[s] и x[k+1];
  | k := k + 1;
  end;

     4.1.2.  Дать другое решение задачи сортировки, использующее
инвариант {первые k элементов упорядочены: x[1] <= ... <= x[k]}

     Решение.

  k:=1
  {первые k элементов упорядочены}
  while k <> n do begin
  | {k+1-ый элемент продвигается к началу, пока не займет
  |   надлежащего места }
  | t := k+1;
  | {x[1] <= ... <= x[t-1] и x[t-1], x[t] <= ... <= x[k+1] }
  | while (t > 1) and (x[t] < x[t-1]) do begin
  | | ... поменять x[t-1] и x[t];
  | | t := t - 1;
  | end;
  end;

     Замечание. Дефект программы: при ложном выражении (t  >  1)
проверка x[t] < x[t-1] требует несуществующего значения x[0].
     Оба  предложенных решения требуют числа действий, пропорци-
онального n*n. Существуют более эффективные алгоритмы.

     4.2. Алгоритмы порядка n log n.

     4.2.1. Предложить алгоритм сортировки, число действий кото-
рого  было  бы  порядка  n  log  n,  то  есть не превосходило бы
C*n*log(n) для некоторого C и для всех n.

     Мы предложим два решения.

     Решение 1. (сортировка слиянием).
     Пусть  k  -  положительное  целое  число.  Разобьем  массив
x[1]..x[n]  на  отрезки  длины  k.  (Первый  - x[1]..x[k], затем
x[k+1]..x[2k] и т.д.) Последний отрезок будет неполным,  если  n
не  делится на k. Назовем массив k-упорядоченным, если каждый из
этих отрезков упорядочен. Любой массив 1-упорядочен. Если массив
k-упорядочен и n<=k, то он упорядочен.
     Мы  опишем,  как  преобразовать  k-упорядоченный  массив  в
2k-упорядоченный (из тех же элементов). С помощью этого преобра-
зования алгоритм записывается так:

  k:=1;
  {массив x является k-упорядоченным}
  while k < n do begin
  | .. преобразовать k-упорядоченный массив в 2k-упорядоченный;
  | k := 2 * k;
  end;

     Требуемое  преобразование  состоит в том,что мы многократно
"сливаем" два упорядоченных отрезка длины не  больше  k  в  один
упорядоченный  отрезок. Пусть процедура слияние (p,q,r: integer)
при p <=q <= r сливает отрезки  x[p+1]..x[q]  и  x[q+1]..x[r]  в
упорядоченный  отрезок x[p+1]..x[r] (не затрагивая других частей
массива x).
                  p               q               r
            -------|---------------|---------------|-------
                   | упорядоченный | упорядоченный |
            -------|---------------|---------------|-------
                                  |
                                  |
                                  V
            -------|-------------------------------|-------
                   |     упорядоченный             |
            -------|-------------------------------|-------

Тогда преобразование k-упорядоченного массива в 2k-упорядоченный
осуществляется так:

  t:=0;
  {t кратно 2k или t = n, x[1]..x[t] является
   2k-упорядоченным; остаток массива x не изменился}
  while t + k < n do begin
  | p := t;
  | q := t+k;
  | ...r := min (t+2*k, n); {в паскале нет функции min }
  | слияние (p,q,r);
  | t := r;
  end;

Слияние требует вспомогательного массива для записи  результатов
слияния  -  обозначим его b. Через p0 и q0 обозначим номера пос-
ледних элементов участков, подвергшихся слиянию, s0 -  последний
записанный  в  массив b элемент. На каждом шаге слияния произво-
дится одно из двух действий:

        b[s0+1]:=x[p0+1];
        p0:=p0+1;
        s0:=s0+1;
или
        b[s0+1]:=x[q0+1];
        q0:=q0+1;
        s0:=s0+1;

Первое действие (взятие элемента из первого отрезка) может  про-
изводиться при двух условиях:
    (1) первый отрезок не кончился (p0 < q);
    (2) второй отрезок кончился (q0 = r)  или  не  кончился,  но
элемент в нем не меньше [(q0 < r) и (x[p0+1] <= x[q0+1])].
     Аналогично для второго действия. Итак, получаем

  p0 := p; q0 := q; s0 := p;
  while (p0 <> q) or (q0 <> r) do begin
  | if (p0 < q) and ((q0 = r) or ((q0 < r) and
  | |                (x[p0+1] <= x[q0+1]))) then begin
  | | b [s0+1] := x [p0+1];
  | | p0 := p0+1;
  | | s0 := s0+1;
  | end else begin
  | | {(q0 < r) and ((p0 = q) or ((p0<q) and
  | |   (x[p0+1] >= x[q0+1])))}
  | | b [s0+1] := x [q0+1];
  | | q0 := q0 + 1;
  | | s0 := s0 + 1;
  | end;
  end;

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

     Решение 2 (сортировка деревом).
     Нарисуем "полное двоичное дерево"  -  картинку,  в  которой
снизу один кружок, из него выходят стрелки в два других, из каж-
дого - в два других и так далее:

               .............
                 o  o o  o
                  \/   \/
                   o   o
                    \ /
                     o

     Будем  говорить, что стрелки ведут "от отцов к сыновьям": у
каждого кружка два сына и один отец (если  кружок  не  верхний).
Предположим  для  простоты, что количество подлежащих сортировке
чисел есть степень двойки, и они могут заполнить один  из  рядов
целиком. Запишем их туда. Затем заполним часть дерева под ним по
правилу:
   число в кружке = минимум из чисел в кружках-сыновьях
Тем  самым  в  корне дерева (нижнем кружке) будет записано мини-
мальное число во всем массиве.
     Изымем из сортируемого  массива  минимальный  элемент.  Для
этого  его  надо вначале найти. Это можно сделать, идя от корня:
от отца переходим к тому сыну, где записано то же  число.  Изъяв
минимальный  элемент,  заменим  его  символом  "бесконечность" и
скорректируем более низкие ярусы (для этого  надо  снова  пройти
путь к корню). При этом считаем, что минимум из n и бесконечнос-
ти  равен  n. Тогда в корне появится второй по величине элемент,
мы изымаем его, заменяя бесконечностью и корректируя дерево. Так
постепенно мы изымем все элементы в порядке возрастания, пока  в
корне не останется бесконечность.
     При записи этого алгоритма полезно нумеровать кружочки чис-
лами 1, 2, ...: сыновьями кружка номер n являются кружки  2*n  и
2*n+1. Подробное изложение этого алгоритма мы опустим, поскольку
мы  изложим  более  эффективный  вариант,  не требующий дополни-
тельной памяти, кроме конечного числа переменных (в дополнении к
сортируемому массиву).
     Мы будем записывать сортируемые числа во всех вершинах  де-
рева,  а не только на верхнем уровне. Пусть x[1]..x[n] - массив,
подлежащий сортировке. Вершинами дерева будут числа от 1 до n; о
числе x[i] мы будем говорить как о числе, стоящем в вершине i. В
процессе сортировки количество вершин дерева будет  сокращаться.
Число вершин текущего дерева будем хранить в переменной k. Таким
образом,  в  процессе работы алгоритма массив x[1]..x[n] делится
на две части: в x[1]..x[k] хранятся числа на дереве, а в  x[k+1]
.. x[n] хранится уже отсортированная в порядке возрастания часть
массива - элементы, уже занявшие свое законное место.
     На каждом шаге алгоритм будет изымать максимальный  элемент
дерева и помещать его в отсортированную часть, на освободившееся
в результате сокращения дерева место.
     Договоримся о терминологии. Вершинами дерева считаются чис-
ла от 1 до текущего значения переменной k. У  каждой  вершины  s
могут  быть  сыновья 2s и 2s+1. Если оба этих числа больше k, то
сыновей нет; такая вершина называется листом. Если 2s=k, то вер-
шина s имеет ровно одного сына (2s).
     Для каждого s из 1..k рассмотрим "поддерево" с корнем в  s:
оно  содержит вершину s и всех ее потомков (сыновей, сыновей сы-
новей и т.д. - до тех пор, пока мы не выйдем из  отрезка  1..k).
Вершину  s будем называть регулярной, если стоящее в ней число -
максимальный элемент s-поддерева; s-поддерево  назовем  регуляр-
ным,  если  все  его вершины регулярны. (В частности, любой лист
образует регулярное одноэлементное поддерево.)

     Схема алгоритма такова:

  k:= n
  ... Сделать 1-поддерево регулярным;
  {x[1],..,x[k] <= x[k+1] <= ... <= x[n]; 1-поддерево регулярно,
   в частности, x[1] - максимальный элемент среди x[1]..x[k]}
  while k <> 1 do begin
  | ... обменять местами x[1] и x[k];
  | k := k - 1;
  | {x[1]..x[k-1] <= x[k] <=...<= x[n]; 1-поддерево регу-
  |   лярно везде, кроме, возможно, самого корня }
  | ... восстановить регулярность 1-поддерева всюду
  end;

В качестве вспомогательной процедуры нам  понадобится  процедура
восстановления регулярности s-поддерева в корне. Вот она:

  {s-поддерево регулярно везде, кроме, возможно, корня}
  t := s;
  {s-поддерево регулярно везде, кроме, возможно, вершины t}
  while ((2*t+1 <= k) and (x[2*t+1] > x[t])) or
  |     ((2*t <= k) and (x[2*t] > x[t])) do begin
  | if (2*t+1 <= k) and (x[2*t+1] >= x[2*t]) then begin
  | | ... обменять x[t] и x[2*t+1];
  | | t := 2*t + 1;
  | end else begin
  | | ... обменять x[t] и x[2*t];
  | | t := 2*t;
  | end;
  end;

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

  while наибольшее число не в t, а в одном из сыновей do begin
  | if оно в правом сыне then begin
  | | поменять t с ее правым сыном; t:= правый сын
  | end else begin {наибольшее число - в левом сыне}
  | | поменять t с ее левым сыном; t:= левый сын
  | end
  end

После  обмена  вершина  t  становится регулярной (в нее попадает
максимальное число t-поддерева). Не принявший участия  в  обмене
сын остается регулярным, а принявший участие может и не быть ре-
гулярным. В остальных вершинах s-поддерева не изменились ни чис-
ла,  ни поддеревья их потомков (разве что два элемента поддерева
переставились), так что регуярность не нарушилась.

   Эта же процедура может использоваться для того, чтобы сделать
1-поддерево регулярным на начальной стадии сортировки:

  k := n;
  u := n;
  {все s-поддеревья с s>u регулярны }
  while u<>0 do begin
  | {u-поддерево регулярно везде, кроме разве что корня}
  | ... восстановить регулярность u-поддерева в корне;
  | u:=u-1;
  end;

     Теперь запишем процедуру сортировки на паскале  (предпола-
гая,  что  n  -  константа,  x  имеет тип arr = array [1..n] of
integer).

  procedure sort (var x: arr);
  | var u, k: integer;
  | procedure exchange(i, j: integer);
  | | var tmp: integer;
  | | begin
  | | tmp  := x[i];
  | | x[i] := x[j];
  | | x[j] := tmp;
  | end;
  | procedure restore (s: integer);
  | | var t: integer;
  | | begin
  | | t:=s;
  | | while ((2*t+1 <= k) and (x[2*t+1] > x[t]) ) or
  | | |     ((2*t <= k) and (x[2*t] > x[t])) do begin
  | | | if (2*t+1 <= k) and (x[2*t+1] >= x[2*t]) then begin
  | | | | exchange (t, 2*t+1);
  | | | | t := 2*t+1;
  | | | end else begin
  | | | | exchange (t, 2*t);
  | | | | t := 2*t;
  | | | end;
  | | end;
  | end;
  begin
  | k:=n;
  | u:=n;
  | while u <> 0 do begin
  | | restore (u);
  | | u := u - 1;
  | end;
  | while k <> 1 do begin
  | | exchange (1, k);
  | | k := k - 1;
  | | restore (1);
  | end;
  end;

     Несколько замечаний.

     Метод, использованный при сортировке деревом, бывает полез-
ным в других случах. (См. в главе 6 (о типах данных)  раздел  об
очереди с приоритетами.)

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

     Еще один практически важный алгоритм сортировки таков: что-
бы  отсортировать массив, выберем случайный его элемент b, и ра-
зобъем массив на три части: меньшие b, равные  b  и  большие  b.
(Эта  задача  приведена в главе о массивах.) Теперь осталось от-
сортировать первую и третью части: это делается тем же способом.
Время работы этого алгоритма - случайная величина;  можно  дока-
зать, что в среднем он работает не больше C*n*log n. На практике
- он один из самых быстрых. (Мы еще вернемся к нему, приведя его
рекурсивную и нерекурсивную реализации.)

     Наконец, отметим, что сортировка за время порядка C*n*log n
может быть выполнена с помощью техники сбалансированных деревьев
(см.  главу  12), однако программы тут сложнее и константа C до-
вольно велика.

     4.3. Применения сортировки.

     4.3.1. Найти количество  различных  чисел  среди  элементов
данного массива. Число действий порядка n*log n. (Эта задача уже
была в главе о массивах.)

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

     4.3.2. Дано n отрезков [a[i],  b[i]]  на  прямой  (i=1..n).
Найти максимальное k, для которого существует точка прямой, пок-
рытая k отрезками ("максимальное число слоев"). Число действий -
порядка n*log n.

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

     4.3.3. Дано n точек на плоскости. Указать (n-1)-звенную не-
самопересекающуюся незамкнутую ломаную, проходящую через все эти
точки.  (Соседним  отрезкам  ломаной разрешается лежать на одной
прямой.) Число действий порядка n*log n.

     Решение. Упорядочим точки по  x-координате,  а  при  равных
x-координатах  - по y-координате. В таком порядке и можно прово-
дить ломаную.

     4.3.4. Та же задача, если ломаная должна быть замкнутой.

     Решение. Возьмем самую левую точку (т.е. точку с наименьшей
x-координатой) и проведем из нее лучи во  все  остальные  точки.
Теперь упорядочим эти лучи, а точки на одном луче поместим в по-
рядке увеличения расстояния от начала луча.

     4.3.5. Дано n точек на  плоскости.  Построить  их  выпуклую
оболочку  -  минимальную  выпуклую фигуру, их содержащую. (Форму
выпуклой оболочки примет резиновое колечко, если его натянуть на
гвозди, вбитые в точках.)  Число операций не более n*log n.

    Указание. Упорядочим точки - годится любой из порядков,  ис-
пользованных в двух предыдущих задачах. Затем, рассматривая точ-
ки по очереди, будем строить выпуклую оболочку уже рассмотренных
точек. (Для хранения выпуклой оболочки полезно использовать дек,
см. главу 6 о типах данных.)

     4.4. Нижние оценки для числа сравнений при сортировке.

     Пусть  имеется  n  различных по весу камней и весы, которые
позволяют за одно взвешивание определить, какой из двух  выбран-
ных  нами  камней тяжелее. (В программистских терминах: мы имеем
доступ к функции  тяжелее(i,j:1..n):boolean.)  Надо  упорядочить
камни  по  весу,  сделав  как  можно меньше взвешиваний (вызовов
функции "тяжелее").

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

    4.4.1. Доказать, что сложность любого алгоритма сортировки n
камней не меньше log (n!). (Логарифм берется по основанию 2,  n!
- произведение чисел 1..n.)

     Решение. Пусть имеется алгоритм сложности не более  d.  Для
каждого  из n! возможных расположений камней запротоколируем ре-
зультаты взвешиваний (обращений к функции "тяжелее");  их  можно
записать  в  виде  последовательности  из не более чем d нулей и
единиц. Для  единообразия  дополним  последовательность  нулями,
чтобы ее длина стала равной d. Тем самым у нас имеется n! после-
довательностей  из  d нулей и единиц. Все эти последовательности
разные - иначе наш алгоритм дал бы одинаковые ответы для  разных
порядков  (и один из ответов был бы неправильным). Получаем, что
2 в степени d не меньше n! - что и требовалось доказать.

     Другой способ объяснить то же самое  -  рассмотреть  дерево
вариантов,  возникающее в ходе выполнения алгоритма, и сослаться
на то, что дерево высоты d не может иметь более (2 в степени  d)
листьев.

     Это  рассуждение показывает, что любой алгоритм сортировки,
использующий только сравнения элементов массива и их перестанов-
ки, требует не менее C*n*log n действий, так что наши  алгоритмы
близки  к  оптимальным. Однако алгоритм сортировки, использующий
другие операции, может действовать быстрее. Вот один  из  приме-
ров.

     4.4.2. Имеется массив целых чисел  a[1]..a[n],  причем  все
числа неотрицательны и не превосходят m. Отсортировать этот мас-
сив; число действий порядка m+n.

     Решение.  Для каждого числа от 0 до m подсчитываем, сколько
раз оно встречается в массиве. После этого исходный массив можно
стереть и заполнить заново в порядке возрастания, используя све-
дения о кратности каждого числа.

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

Есть также метод сортировки, в котором последовательно проводится 
ряд  "частичных  сортировок"  по отдельным битам. Начнём с такой
задачи:

     4.4.3. В массиве a[1]..a[n] целых чисел переставить элемен-
ты так, чтобы чётные шли перед нечётными (не меняя взаимный  по-
рядок в каждой из групп).

     Решение.  Сначала  спишем  (во  вспомогательный массив) все
чётные, а потом - все нечётные.

     4.4.4. Имеется массив из n чисел от 0 до (2 в степени k)  -
1, каждое из которых мы будем рассматривать как k-битовое слово.
Используя проверки "i-ый бит равен 0" и "i-ый бит равен 1" вмес-
то сравнений, отсортировать все числа за время порядка n*k.

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

     Аналогичный алгоритм может быть применен для m-ичной систе-
мы  счисления  вместо двоичной. При этом полезна такая вспомога-
тельная задача:

     4.4.5. Даны n чисел и функция f, принимающая (на них)  зна-
чения  1..m.  Требуется переставить числа в таком порядке, чтобы
значения функции f не убывали (сохраняя  притом  порядок  внутри
каждой из групп). Число действий порядка m+n.
     Указание. Завести m списков суммарной длины n (как это сде-
лать,  смотри в главе 6 о типах данных) и помещать в i-ый список
числа, для которых значение функции f равно i.  Вариант:  посчи-
тать  для  всех  i, сколько имеется чисел x c f(x)=i, после чего
легко определить, с какого места нужно начинать размещать  числа
с f(x)=i.

     4.5. Родственные сортировке задачи.

     4.5.1. Какова минимально возможная сложность (число сравне-
ний  в наихудшем случае) алгоритма отыскания самого легкого из n
камней?

     Решение. Очевидный алгоритм  с  инвариантом  "найден  самый
легкий  камень  среди первых i" требует n-1 сравнений. Алгоритма
меньшей сложности нет. Это вытекает из следующего более сильного
утверждения.

     4.5.2. Эксперт хочет докать суду, что данный камень - самый
легкий среди n камней, сделав менее n-1  взвешиваний.  Доказать,
что  это  невозможно.  (Веса камней неизвестны суду, но известны
эксперту.)

     Решение. Изобразим камни точками, а взвешивания  -  линиями
между  ними. Получим граф с n вершинами и менее чем n-1 ребрами.
Такой  граф  несвязен  (добавление  каждого   следующего   ребра
уменьшает число компонент не более чем на 1). Поэтому суд ничего
не  знает  относительно  соотношения весов камней в двух связных
компонентах и может допустить, что самый легкий камень - в любой
из них.

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

     4.5.3. Дано n различных по весу камней и число k (от  1  до
n). Требуется найти k-ый по весу камень,  сделав  не  более  C*n
взвешиваний, где C - некоторая константа, не зависящая от k.

     Замечание.  Сортировка  позволяет  сделать это за C*n*log n
взвешиваний. Указание к этой (трудной) задаче приведено в  главе
про рекурсию.

     Следующая задача имеет неожиданно простое решение.

     4.5.4. Имеется n одинаковых на вид камней, некоторые из ко-
торых на самом деле различны по весу. Имеется  прибор,  позволя-
ющий  по  двум камням определить, одинаковы они или различны (но
не говорящий, какой тяжелее). Известно, что  среди  этих  камней
большинство  (более n/2) одинаковых. Сделав не более n взвешива-
ний, найти хотя бы один камень из этого большинства.

     Предостережение. Если два камня одинаковые, это не гаранти-
рует их принадлежности к большинству.

     Указание. Если найдены два различных камня, то их оба можно
выбросить - хотя бы один из них плохой и  большинство  останется
большинством.

     Решение. Программа просматривает камни по очереди, храня  в
переменной i число просмотренных камней. (Считаем камни пронуме-
рованными от 1 до n.) Помимо этого программа хранит номер "теку-
щего  кандидата"  c  и  его  "кратность"  k. Смысл этих названий
объясняется инвариантом:

   если к непросмотренным камням (с номерами i+1..n)  до-
   бавили бы k копий c-го камня, то наиболее частым среди  (И)
   них был бы такой же камень, что и для исходного массива

Получаем такую программу:

   k:=0; i:=0
   {(И)}
   while i<>n do begin
   | if k=0 then begin
   | | k:=1; c:=i+1; i:=i+1;
   | end else if i+1-ый камень одинаков с c-ым then begin
   | | i:=i+1; k:=k+1;
   | |  {заменяем материальный камень идеальным}
   | end else begin
   | | i:=i+1; k:=k-1;
   | |  {выкидываем один материальный и один идеальный камень}
   | end;
   end;
   искомым является c-ый камень

Замечание.  Поскольку во всех трех вариантах выбора стоит
команда i:=i+1, ее можно вынести наружу.

     Следующая задача не имеет на первый взгляд никакого отноше-
ния к сортировке.

     4.5.5.  Имеется квадратная таблица a[1..n, 1..n]. Известно,
что для некоторого i строка с номером i заполнена одними нулями,
а столбец с номером i - одними единицами (за исключением их  пе-
ресечения на диагонали, где стоит неизвестно что). Найти такое i
(оно, очевидно, единственно). Число действий не превосходит C*n.
(Заметим, что это существенно меньше числа элементов в таблице).

     Указание. Рассмотрите a[i][j] как результат "сравнения" i с
j  и  вспомните, что самый тяжелый из n камней может быть найден
за n сравнений. (Не забудьте, впрочем, что таблица может не быть
"транзитивной".)
     Глава 5. Конечные автоматы в задачах обработки текстов

     5.1. Составные символы, комментарии и т.п.

     5.1.1.  В  тексте  возведение  в степень обозначалось двумя
идущими подряд звездочками. Решено заменить это  обозначение  на
'^'  (так  что,  к  примеру, 'x**y' заменится на 'x^y'). Как это
проще всего сделать? Исходный текст разрешается читать символ за
символом, получающийся текст требуется печатать символ за симво-
лом.

     Решение. В каждый момент программа  находится  в  одном  из
двух состояний: "основное" и "после звездочки"

Состояние    Очередной        Новое       Действие
           входной символ   состояние

основное        *             после          нет
основное     x <> '*'        основное     печатать x
после           *            основное     печатать '^'
после        x <> '*'        основное     печатать *, x

Замечание.  При  этом '***' заменится на '^*' (но не на '*^'). В
условии задачи мы не оговаривали деталей, как это часто делается
- предполагается, что программа "должна действовать разумно".  В
данном  случае,  пожалуй,  самый  простой  способ объяснить, как
программа действует - это описать ее состояния и действия в них.

     5.1.2. Написать программу, удалающую из текста подслова ви-
да 'abc'.

     5.1.3. В паскале комментарии заключаются в фигурные скобки:

                begin {начало цикла}
                i:=i+1; {увеличиваем i на 1}

Написать программу, которая удаляла бы комментарии  и  вставляла
бы  вместо  исключенного  комментария  пробел  (чтобы '1{один}2'
превратилось бы не в '12', а в '1 2').

     Решение. Программа имеет два состояния: "основное" и "внут-
ри комментария".

Состояние    Очередной        Новое       Действие
           входной символ   состояние

основное        {             внутри         нет
основное     x <> '{'        основное     печатать x
внутри          }            основное     печатать пробел
внутри       x <> '}'         внутри         нет

     Замечание. Эта программа не воспринимает вложенные  коммен-
тарии: строка вроде
       '{{комментарий внутри} комментария}'
превратится в
        '  комментария}'
(в  начале  стоят два пробела). Обработка вложенных комментариев
конечным автоматом невозможна (нужно "помнить число скобок" -  а
произвольное натуральное число не помещается в конечную память).

     5.1.4. В паскалевских программах бывают также строки,  зак-
люченные в кавычки. Если фигурная скобка стречается внутри стро-
ки, то она не означает начала или конца комментария. В свою оче-
редь, кавычка в комментарии не означает начала или конца строки.
Как изменить программу, чтобы это учесть?

     Указание. Состояний будет три: основное,  внутри  коммента-
рия, внутри строки.

     5.1.5. Еще одна возможность многих реализаций паскаля - это
комментарии вида

      i:=i+1;     (*   here i is increased by 1  *)

при этом закрывающая скобка должна  соответствовать  открываюшей
(то  есть  { ... *) не разрешается). Как удалять такие коммента-
рии?

     5.2. Ввод чисел

     Пусть  десятичная  запись  числа подается на вход программы
символ за символом. Мы хотим "прочесть" это число  (поместить  в
переменную типа real его значение). Кроме того, надо сообщить об
ошибке, если число записано неверно.

     Более конкретно, представим себе такую ситуацию. Последова-
тельность  символов на входе делится на прочитанную и оставшуюся
части. Мы можем пользоваться функцией Next:char, которая возвра-
щает первый символ оставшей части, а также функцией Move,  кото-
рая перводит забирает первый символ из оставшейся части, перево-
дя его в категорию прочитанных.

        ---------------------|--------------------------
          прочитанная часть  | Next |  ?  |  ?  |  ?  |
        ---------------------|--------------------------

Будем  называть десятичной записью такую последовательность сим-
волов:

  <0 или более пробелов> <1 или более цифр>

а также такую:

  <0 или более пробелов> <1 или более цифр>.<1 или более цифр>

Заметим, что согласно этому  определению  '1.',  '.1',  '1.  1',
'-1.1' не являются десятичными записями. Сформулируем теперь за-
дачу точно:

     5.2.1. Прочесть из входной строки максимальную часть, кото-
рая  может  быть началом десятичной записи. Определить, является
ли эта часть десятичной записью или нет.

     Решение. Запишем программу на паскале (используя  "перечис-
лимый тип" для наглядности записи: переменная state может прини-
мать одно из значений, указанных в скобках).

    var state:
     (Accept, Error, Initial, IntPart, DecPoint, FracPart);

    state := Initial;
    while (state <> Accept) or (state <> Error) do begin
    | if state = Initial then begin
    | | if Next = ' ' then begin
    | | | state := Initial; Move;
    | | end else if Digit(Next) then begin
    | | | state := IntPart; {после начала целой части}
    | | | Move;
    | | end else begin
    | | | state := Error;
    | | end;
    | end else if state = IntPart then begin
    | | if Digit (Next) then begin
    | | | state := IntPart; Move;
    | | end else if Next = '.' then begin
    | | | state := DecPoint; {после десятичной точки}
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if state = DecPoint then begin
    | | if Digit (Next) then begin
    | | | state := FracPart; Move;
    | | end else begin
    | | | state := Error; {должна быть хоть одна цифра}
    | | end;
    | end else if state = FracPart then begin
    | | if Digit (Next) then begin
    | | | state := FracPart; Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if
    | | {такого  быть не может}
    | end;
    end;

Заметьте,  что присваивания state:=Accept и state:=Error не соп-
ровождаются сдвигом (символ, который не может быть частью числа,
не забирается).

     Приведенная программа не запоминает  значение  прочитанного
числа.

     5.2.2. Решить предыдущую задачу с дополнительным требовани-
ем: если прочитанный кусок является десятичной записью, то в пе-
ременную val:real следует поместить ее значение.

     Решение.  При  чтении дробной части используется переменная
step - множитель при следующей десятичной цифре.

    state := Initial; val:= 0;
    while (state <> Accept) or (state <> Error) do begin
    | if state = Initial then begin
    | | if Next = ' ' then begin
    | | | state := Initial; Move;
    | | end else if Digit(Next) then begin
    | | | state := IntPart; {после начала целой части}
    | | | val := DigitValue (Next);
    | | | Move;
    | | end else begin
    | | | state := Error;
    | | end;
    | end else if state = IntPart then begin
    | | if Digit (Next) then begin
    | | | state := IntPart; val := 10*val + DigitVal(Next);
    | | | Move;
    | | end else if Next = '.' then begin
    | | | state := DecPoint; {после десятичной точки}
    | | | step := 0.1;
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if state = DecPoint then begin
    | | if Digit (Next) then begin
    | | | state := FracPart;
    | | | val := val + DigitVal(Next)*step; step := step/10;
    | | | Move;
    | | end else begin
    | | | state := Error; {должна быть хоть одна цифра}
    | | end;
    | end else if state = FracPart then begin
    | | if Digit (Next) then begin
    | | | state := FracPart;
    | | | val := val + DigitVal(Next)*step; step := step/10;
    | | | Move;
    | | end else begin
    | | | state := Accept;
    | | end;
    | end else if
    | | {такого  быть не может}
    | end;
    end;

     5.2.3. Та же задача, если перед  число  может  стоять  знак
"минус" или знак "плюс" (а может ничего не стоять).

     Формат  чисел  в этой задаче обычно иллюстрируют такой кар-
тинкой:

   -----      ---------
---| + |---->-| цифра |-------->--------------------->
 | -----  | | --------- | |                      |
 | -----  | |           | | -----     ---------  |
 |-| - |--| |----<------| |-| . |->---| цифра |--|
 | -----  |                 -----   | --------- |
 |        |                         |-----<-----|
 |--->----|

     5.2.4.  Та же задача, если к тому же после числа может сто-
ять показатель степени десяти, как  в  254E-4  (=0.0254)  или  в
0.123E+9 (=123000000). Нарисуйте соответствующую картинку.

     5.2.5. Что надо изменить в программе  задачи  5.2.2,  чтобы
разрешить пустые целую и дробную части (как в '1.', '.1' или да-
же '.' - последнее число считаем равным нулю)?

     Мы  вернемся  к  конечным автоматам в главе 10 (Сравнение с
образцом).
     Глава 6. Типы данных.

     6.1. Стеки.

     Пусть Т - некоторый тип. Рассмотрим (отсутствующий в паска-
ле)  тип "стек элементов типа Т". Его значениями являются после-
довательности значений типа Т.

     Операции:

Сделать_пустым (var s: стек элементов типа Т).
Добавить (t: T; var s: стек элементов типа Т).
Взять (var t: T; var s: стек элементов типа Т).
Пуст (s: стек элементов типа Т): boolean
Вершина (s: стек элементов типа Т): T

     (Мы пользуемся обозначениями, наполняющими паскаль, хотя  в
паскале типа "стек" нет.) Процедура "Сделать_пустым" делает стек
s  пустым.  Процедура  "Добавить" добавляет t в конец последова-
тельности  s.  Процедура  "Взять"  определена,  если  последова-
тельность  s непуста; она забирает из неё последний элемент, ко-
торый становится значением переменной t. Выражение "Пуст(s)" ис-
тинно, если последовательность s пуста.  Выражение  "Вершина(s)"
определено, если последовательность s непуста, и равно последне-
му элементу последовательности s.
     Мы  покажем,  как моделировать стек в паскале и для чего он
может быть нужен.

     Моделирование ограниченного стека в массиве.

     Будем считать, что количество элементов в стеке не  превос-
ходит  некоторого  числа  n. Тогда стек можно моделировать с по-
мощью двух переменных:
        Содержание: array [1..n] of T;
        Длина: integer;
считая, что в стеке находятся элементы Содержание [1],...,Содер-
жание [длина].

     Чтобы сделать стек пустым, достаточно положить
        Длина := 0

     Добавить элемент t:
         {Длина < n}
         Длина := Длина+1;
         Содержание [Длина] :=t;

     Взять элемент в переменную t:
         t := Содержание [Длина];
         Длина := Длина - 1;

     Стек пуст, если Длина = 0.

     Вершина стека равна Содержание [Длина].

Таким образом, вместо переменной типа стек в программе на паска-
ле можно использовать две переменные Содержание и  Длина.  Можно
также определить тип стек, записав

    const N = ...
    type  stack = record
                    Содержание: array [1..N] of T;
                    Длина: integer;
                  end;

(Мы позволяем себе  использовать  имена  переменных  из  русских
букв,  хотя  обычно  паскаль  этого не любит.) После этого могут
быть - в соответствии с правилами паскаля  -  описаны  процедуры
работы со стеком. Например, можно написать

        procedure Добавить (t: T; var s: stack);
        begin
        | {s.Длина , N}
        | s.Длина := s.Длина + 1;
        | s.Содержание [s.Длина] := t;
        end;

     Использование стека.

     Будем рассматривать последовательности открывающихся и зак-
рывающихся круглых и квадратных скобок ( ) [ ]. Среди всех таких
последовательностей  выделим правильные - те, которые могут быть
получены по таким правилам:

        1) пустая последовательность правильна.
        2) если А и В правильны, то и АВ правильна.
        3) если А правильна, то [A] и (A) правильны.

     Пример. Последовательности (), [[]], [()[]()][]  правильны,
а последовательности ], )(, (], ([)] - нет.

     6.1.1.  Проверить правильность последовательности за время,
не превосходящее константы, умноженной на её длину.  Предполага-
ется, что члены последовательности закодированы числами:
         (   1
         [   2
         )  -1
         ]  -2

     Решение. Пусть a[1]..a[n] - проверяемая последовательность.
Рассмотрим  стек,  элементами  которого  являются  открывающиеся
круглые и квадратные скобки (т. е. 1 и 2).
     Вначале стек делаем пустым. Далее просматриваем члены  пос-
ледовательности  слева  направо.  Встретив  открывающуюся скобку
(круглую или квадратную), помещаем её в стек. Встретив  закрыва-
ющуюся,  проверяем, что вершина в стеке - парная ей скобка; если
это не так, то можно утверждать, что  последовательность  непра-
вильна,  если  скобка  парная, то заберем её (вершину) из стека.
Последовательность правильна,  если  в  конце  стек  оказывается
пуст.
        Сделать_Пустым (s);
        i := 0; Обнаружена_Ошибка := false;
        {прочитано i символов последовательности}
        while (i < n) and not Обнаружена_Ошибка do begin
        | i := i + 1;
        | if (a[i] = 1) or (a[i] = 2) then begin
        | | Добавить (a[i], s);
        | end else begin  {a[i] равно -1 или -2}
        | | if Пуст (s) then begin
        | | | Обнаружена_Ошибка := true;
        | | end else begin
        | | | Взять (t, s);
        | | | Обнаружена ошибка := (t <> - a[i]);
        | | end;
        | end;
        end;
        Правильно := (not Обнаружена_Ошибка) and Пуст (s);

       Убедимся  в  правильности  программы. (1) Если последова-
тельность построена по правилам, то программа даст  ответ  "да".
Это легко доказать индукцией по построению правильной последова-
тельности.  Надо проверить для пустой, для последовательности AB
в предположении, что для A и B уже проверено - и для  последова-
тельностей [A] и (A) - в предположении, что для A уже проверено.
Для  пустой  очевидно.  Для AB действия программы происходят как
для A и кончаются с пустым стеком; затем все происходит как  для
B.  Для  [A]  сначала  помещается  в стек открывающая квадратная
скобка и затем все идет как для A - с той разницей, что в глуби-
не стека лежит лишняя скобка. По  окончании  A  стек  становится
пустым  - если не считать этой скобки - а затем и совсем пустым.
Аналогично для (A).
     (2) Покажем, что если программа завершает работу с  ответом
"да",  то последовательность правильная. Рассуждаем индукцией по
длине последовательности. Проследим за состоянием стека  в  про-
цессе работы программы. Если он в некоторый промежуточный момент
пуст, то последовательность разбивается на две части, для каждой
из  которых  программа дает ответ "да"; остается воспользоваться
предположением индукции и определением правильности. Пусть  стек
все  время  непуст.  Это значит, что положенная в него на первом
шаге скобка будет вынута на последнем шаге. Тем самым, первый  и
последний символы последовательности - это парные скобки, и пос-
ледовательность имеет вид (A) или [A], а работа программы (кроме
первого  и  последнего  шагов) отличается от ее работы на A лишь
наличием лишней скобки на дне стека (раз ее не вынимают, она ни-
как не влияет на работу программы). Снова ссылаемся на предполо-
жение индукции и определение правильности.

     6.1.2. Как упростится программа, если известно, что в  пос-
ледовательности могут быть только круглые скобки?

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

     6.1.3. Реализовать с помощью одного массива два стека, сум-
марное количество элементов в которых ограничено длиной массива;
все  действия со стеками должны выполняться за время, ограничен-
ное константой, не зависящей от длины стеков.

     Решение. Стеки должны расти с концов массива навстречу друг
другу: первый должен занимать места
        Содержание[1] ... Содержание[Длина1],
а второй  -
        Содержание[n] ... Содержание[n - Длина2 + 1]
(вершины обоих стеков записаны последними).

     6.1.4. Реализовать k стеков с элементами типа T, общее  ко-
личество  элементов в которых не превосходит n, с использованием
массивов суммарной длины C*(n+k), затрачивая на каждое  действие
со  стеками (кроме начальных действий, делающих все стеки пусты-
ми) время, ограниченное некоторой константой.

     Решение. Применяемый метод называется "ссылочной реализаци-
ей". Он использует три массива:
        Содержание: array [1..n] of T;
        Следующий: array [1..n] of 0..n;
        Вершина: array [1..k] of 0..n.
     Массив Содержание будем изображать как n ячеек  с  номерами
1..n,  каждая  из которых содержит элемент типа T. Массив Следу-
ющий изобразим в виде стрелок, проведя стрелку из i  в  j,  если
Следующий[i] = j. (Если Следующий[i] = 0, стрелок из i не прово-
дим.) Содержимое s-го стека (s из 1..k)  хранится  так:  вершина
равна Содержание[Вершина[s]], остальные элементы s-го стека мож-
но  найти,  идя  по стрелкам - до тех пор, пока они не кончатся.
При этом (s-ый стек пуст) <=> Вершина[s] = 0.
     Стрелочные траектории, выходящие из Вершина[1], ..., Верши-
на[k] (из тех, которые не равны 0) не должны пересекаться. Поми-
мо них, нам понадобится еще одна стрелочная траектория, содержа-
щая все неиспользуемые в данный момент ячейки. Ее начало мы  бу-
дем  хранить в переменной Свободная (равенство Свободная = 0 оз-
начает, что пустого места не осталось). Вот что получается:

 n=8 | a | p | q | d | s | t | v | w |

 k=2  |  |  |            Свободная

Содержание = <a,p,q,d,s,t,v,w>, Следующий  =  <3,0,6,0,0,2,5,4>
Вершина = <1, 7>, Свободная = 8
Стеки: 1-ый: p t q a (a-вершина); 2-ой: s v (v-вершина).

  procedure Начать_работу; {Делает все стеки пустыми}
  | var i: integer;
  begin
  | for i := 1 to k do begin
  | | Вершина [i]:=0;
  | end;
  | for i := 1 to n-1 do begin
  | | Следующий [i] := i+1;
  | end;
  | Свободная:=1;
  end;

  function  Есть_место: boolean;
  begin
  | Есть Место := (Свободная <> 0);
  end;

  procedure Добавить (t: T; s: integer);
  | {Добавить t к s-му стеку}
  | var i: 1..n;
  begin
  | {Есть_место}
  | i := Свободная;
  | Свободная := Следующий [i];
  | Вершина [s] :=i;
  | Содержание [i] := t;
  | Следующий [i] := Вершина [s];
  end;

  function Пуст (s: integer): boolean; {s-ый стек пуст}
  begin
  | Пуст := (Вершина [s] = 0);
  end;

  procedure Взять (var t: T; s: integer);
  | {взять из s-го стека в t}
  | var i: 1..n;
  | begin
  | {not Пуст (s)}
  | i := Вершина [s];
  | t := Содержание [i];
  | Вершина [s] := Следующий [i];
  | Следующий [i] := Свободная;
  | Свободная := i;
  end;

     6.2. Очереди.

     Значениями типа "очередь элементов типа T", как и для  сте-
ков, являются последовательности значений типа T. Разница состо-
ит  в том, что берутся элементы не с конца, а с начала (а добав-
ляются по-прежнему в конец).

     Операции с очередями.

        Сделать_пустой (var x: очередь элементов типа T);
        Добавить (t: T, var x: очередь элементов типа T);
        Взять (var t: T, var x: очередь элементов типа T);
        Пуста (x: очередь элементов типа T): boolean;
        Очередной (x: очередь элементов типа T): T.

     При выполнении команды "Добавить" указанный элемент  добав-
ляется  в  конец  очереди.  Команда "Взять" выполнима, лишь если
очередь непуста, и  забирает  из  нее  первый  (положенный  туда
раньше  всех)  элемент, помещая его в t. Значением функции "Оче-
редной" (определенной для непустой очереди) является первый эле-
мент очереди.
     Английские названия стеков - Last In First  Out  (последним
вошел  -  первым вышел), а очередей - First In First Out (первым
вошел - первым вышел).

     Реализация очередей в массиве.

     6.2.1. Реализовать операции с очередью  ограниченной  длины
так,  чтобы количество действий для каждой операции было ограни-
чено константой, не зависящей от длины очереди.

     Решение. Будем хранить элементы очереди в соседних  элемен-
тах  массива.  Тогда  очередь  будет прирастать справа и убывать
слева. Поскольку при этом она может дойти до края, свернем  мас-
сив в окружность.
     Введем массив Содержание: array [0..n-1] of T и переменные
         Первый: 0..n-1,
         Длина : 0..n.
При этом элементами очереди будут
         Содержание [Первый], Содержание [Первый + 1],...,
                   Содержание [Первый + Длина - 1],
где  сложение рассматривается по модулю n. (Предупреждение. Если
вместо этого ввести переменные Первый и  Последний,  принимающие
значения  в  вычетах  по  модулю n, то пустая очередь может быть
спутана с очередью из n элементов.)

     Моделирование операций:

     Сделать Пустой:
        Длина := 0;
        Первый := 0;

     Добавить элемент:
        {Длина < n}
        Содержание [(Первый + Длина) mod n] := элемент;
        Длина := Длина + 1;

     Взять элемент;
        {Длина > 0}
        элемент := Содержание [Первый];
        Первый := (Первый + 1) mod n;
        Длина := Длина - 1;

     Пуста = (Длина = 0);

     Очередной = Содержание [Первый];

     6.2.2.  (Сообщил А.Г.Кушниренко) Придумать способ моделиро-
вания очереди с помощью двух стеков (и фиксированного числа  пе-
ременных  типа T). При этом отработка n операций с очередью (на-
чатых, когда очередь была  пуста)  должна  требовать  порядка  n
действий.

     Решение.  Инвариант:  стеки, составленные концами, образуют
очередь. (Перечисляя элементы одного стека вглубь и  затем  эле-
менты  второго  наружу,  мы  перечисляем все элементы очереди от
первого до последнего.) Ясно, что добавление сводится к добавле-
нию к одному из стеков, а проверка пустоты - к проверке  пустоты
обоих стеков. Если мы хотим взять элемент, есть два случая. Если
стек,  где  находится  начало очереди, не пуст, то берем из него
элемент. Если он пуст, то предварительно переписываем в него все
элементы второго стека, меняя порядок (это происходит  само  при
перекладывании  из стека в стек) и сводим дело к первому случаю.
Хотя число действий при этом и не ограничено константой, но тре-
бование задачи выполнено, так как каждый элемент  очереди  может
участвовать в этом процессе не более одного раза.

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

     6.2.4. (Сообщил А.Г.Кушниренко.) Имеется дек элементов типа
T  и конечное число переменных типа T и целого типа. В начальном
состоянии в деке некоторое число элементов. Составить программу,
после исполнения которой в деке остались бы те же самые  элемен-
ты, а их число было бы в одной из целых переменных.

     Указание.  (1) Элементы дека можно циклически переставлять,
забирая с одного конца и помещая в другой. После  этого,  сделав
столько  же  шагов  в обратном направлении, можно вернуть все на
место. (2) Как понять, прошли мы полный круг или не прошли? Если
бы был какой-то элемент, заведомо отсутствующий в деке, то можно
было бы его подсунуть и ждать  вторичного  появления.  Но  таких
элементов нет. Вместо этого можно для данного n выполнить цикли-
ческий  сдвиг  на  n дважды, подсунув разные элементы, и посмот-
реть, появятся ли разные элементы через n шагов.

     Применение очередей.

     6.2.5. Напечатать в  порядке  возрастания  первые  n  нату-
ральных  чисел, в разложение которых на простые множители входят
только числа 2, 3, 5.

       Решение. Введем три очереди x2, x3, x5, в  которых  будем
хранить элементы, которые в 2 (3, 5) раз больше напечатанных, но
еще не напечатанные. Определим процедуру

        procedure напечатать_и_добавить (t: integer);
        begin
        | writeln (t);
        | добавить (2*t, x2);
        | добавить (3*t, x3);
        | добавить (5*t, x5);
        end;

Вот схема программы:

  напечатать_и_добавить (1);
  k := 1; { k - число напечатанных }
  {инвариант:  напечатано  в  порядке  возрастания k минимальных
  членов нужного множества; в очередях элементы, вдвое, втрое  и
  впятеро  большие напечатанных, но не напечатанные, расположен-
  ные в возрастающем порядке}
  while k <> n do begin
  | x := min (очередной (x2), очередной (x3), очередной (x5));
  | напечатать_и_добавить (x);
  | k := k+1;
  | ...взять x из тех очередей, где он был очередным;
  end;

     Пусть инвариант выполняется. Рассмотрим наименьший из нена-
печатанных элементов множества. Тогда он делится нацело на  одно
из чисел 2, 3, 5, и частное также принадлежит множеству. Значит,
оно  напечатано. Значит, x находится в одной из очередей и, сле-
довательно, является в ней первым (меньшие напечатаны, а элемен-
ты очередей не напечатаны). Напечатав x, мы должны его изъять  и
добавить его кратные.
     Длины очередей не превосходят числа напечатанных элементов.

     Следующая задача связана с графами (к которым мы вернёмся в
главе 9).

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

     6.2.6.  Известно, что ориентированный граф связен, т. е. из
любой вершины можно пройти в любую по  ребрам.  Кроме  того,  из
каждой  вершины  выходит столько же ребер, сколько входит. Дока-
зать, что существует замкнутый цикл, проходящий по каждому ребру
ровно один раз. Составить алгоритм отыскания такого цикла.

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

while змея включает не все ребра do begin
| if из головы выходит неиспользованное в змее ребро then begin
| | удлинить змею этим ребром
| end else begin
| | {хвост змеи в той же вершине, что и голова}
| | отрезать конец хвоста и добавить его к голове
| | {"змея откусывает конец хвоста"}
| end;
end;

     Докажем, что мы достигнем цели.

     1)  Идя по змее от хвоста к голове, мы входим в каждую вер-
шину столько же раз, сколько выходим. Так как  в  любую  вершину
входит столько же ребер, сколько выходит, то невозможность выйти
означает, что мы начали движение в этой же точке.
     2) Змея не укорачивается, поэтому либо она  охватывает  все
ребра,  либо, начиная с некоторого момента, будет иметь постоян-
ную длину. Во втором случае змея будет бесконечно "скользить  по
себе".  Это возможно, только если из всех вершин змеи не выходит
неиспользованных ребер. Из связности следует,  что  использованы
все ребра.

     Замечание  по  реализации на паскале. Вершинами графа будем
считать числа 1..n. Для каждой вершины  i  будем  хранить  число
Out[i]  выходящих  из  нее  ребер, а также номера Num[i][1],...,
Num[i][Out[i]] тех вершин, куда  эти  ребра  ведут.  В  процессе
построения  змеи  будет  выбирать  первое свободное ребро. Тогда
достаточно будет хранить для каждой вершины число  выходящих  из
нее  использованных  ребер  -  это  будут ребра, идущие в начале
списка.

     6.2.7. Доказать, что для всякого  n  существует  последова-
тельность  нулей  и  единиц  длины  (2 в степени n) со следующим
свойством: если "свернуть ее в кольцо" и рассмотреть  все  фраг-
менты  длины  n  (их число равно (2 в степени n)), то мы получим
все возможные последовательности нулей и единиц длины n. Постро-
ить алгоритм отыскания такой  последовательности,  требующий  не
более (C в степени n) действий для некоторой константы C.

     Указание. Рассмотрим граф, вершинами которого являются пос-
ледовательности  нулей  и единиц длины (n-1). Будем считать, что
из вершины x ведет ребро в вершину y, если x может быть началом,
а y - концом некоторой последовательности длины n. Тогда из каж-
дой вершины входит и выходит два ребра. Цикл, проходящий по всем
ребрам, и даст требуемую последовательность.

     6.2.8. Реализовать k очередей с ограниченной суммарной дли-
ной  n,  используя  память  порядка  n+k, причем каждая операция
(кроме начальной, делающей все очереди пустыми) должна требовать
ограниченного константой числа действий.

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

        Содержание: array [1..n] of T;
        Следующий: array [1..n] of 0..n;
        Первый: array [1..n] of 0..n;
        Последний: array [1..k] of 0..n;
        Свободная : 0..n;

  procedure Сделать_пустым;
  | var i: integer;
  begin
  | for i := 1 to n-1 do begin
  | | Следующий [i] := i + 1;
  | end;
  | Свободная := 1;
  | for i := 1 to k do begin
  | | Первый [i]:=0;
  | end;
  end;

  function Есть_место : boolean;
  begin
  | Есть_место := Свободная <> 0;
  end;

  function Пуста (номер_очереди: integer): boolean;
  begin
  | Пуста := Первый [номер_очереди] = 0;
  end;

  procedure Взять (var t: T; номер_очереди: integer);
  | var перв: integer;
  begin
  | {not Пуста (номер_очереди)}
  | перв := Первый [номер_очереди];
  | t := Содержание [перв]
  | Первый [номер_очереди] := Следующий [перв];
  | Следующий [перв] := Свободная;
  | Свободная := Перв;
  end;

  procedure Добавить (t: T; номер_очереди: integer);
  | var нов, посл: 1..n;
  begin
  | {Есть_свободное_место }
  | нов := Свободная; Свободная := Следующий [Свободная];
  | {из списка свободного места изъят номер нов}
  | if Пуста (номер_очереди) then begin
  | | Первый [номер_очереди] := нов;
  | | Последний [номер_очереди] := нов;
  | | Следующий [нов] := 0;
  | | Содержание [нов] := t;
  | end else begin
  | | посл := Последний [номер_очереди];
  | | {Следующий [посл] = 0 }
  | | Следующий [посл] := нов;
  | | Следующий [нов] := 0;
  | | Содержание [нов] := t
  | | Последний [номер_очереди] := нов;
  | end;
  end;

  function Очередной (номер_очереди: integer): T;
  begin
  | Очередной := Содержание [Первый [номер_очереди]];
  end;

     6.2.9. Та же задача для деков вместо очередей.

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

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

     6.2.10.  На плоскости задано n точек, пронумерованных слева
направо (а при равных абсциссах - снизу вверх). Составить  прог-
рамму, которая строит многоугольник, являющийся их выпуклой обо-
лочкой, за не более чем C*n действий.

     Решение. Будем присоединять точки к выпуклой оболочке  одна
за  другой.  Легко  показать, что последняя присоединенная точка
будет одной из вершин выпуклой оболочки. Эту  вершину  мы  будем
называть выделенной. Очередная присоединяемая точка видна из вы-
деленной  (почему?). Дополним наш многоугольник, выпустив из вы-
деленной вершины "иглу", ведущую в присоединяемую  точку.  Полу-
чится  вырожденный многоугольник, и остается ликвидировать в нем
"впуклости".

                                               [Рисунок]

     Будем хранить вершины многоугольника в деке в порядке обхо-
да его периметра по часовой стрелке. При этом выделенная вершина
является началом и концом (головой и хвостом) дека.  Присоедине-
ние  "иглы" теперь состоит в добавлении присоединяемой вершины в
голову и в хвост дека.  Устранение  впуклостей  несколько  более
сложно.  Назовем  подхвостом и подподхвостом элементы дека, сто-
ящие за его хвостом. Устранение впуклости у хвоста делается так:

    while по дороге из хвоста в подподхвост  мы поворачиваем
    |                  у подхвоста влево ("впуклость") do begin
    | выкинуть подхвост из дека
    end

Таким же способом устраняется впуклость у головы дека.

    Замечание. Действия с подхвостом и подподхвостом не входят в
определение дека, однако сводятся к небольшому числу манипуляций
с деком (надо забрать три элемента с хвоста, сделать что надо  и
вернуть).

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

     6.3. Множества.

     Пусть  Т - некоторый тип. Существует много способов хранить
(конечные) множества элементов типа Т; выбор между ними  опреде-
ляется типом T и набором требуемых операций.

     Подмножества множества {1..n}.

     6.3.1.  Используя  память,  пропорциональную   n,   хранить
подмножества множества {1..n}.

          Операции              Число действий

        Сделать пустым                C*n
        Проверить принадлежность      C
        Добавить                      C
        Удалить                       С
        Минимальный элемент           C*n
        Проверка пустоты              C*n

     Решение. Храним множество как array [1..n] of boolean.

     6.3.2.  То  же,  но  проверка пустоты должна выполняться за
время C.

       Решение. Храним дополнительно количество элементов.

     6.3.3. То же при следующих ограничениях на число действий:

          Операции             Число действий

        Сделать пустым                C*n
        Проверить принадлежность      C
        Добавить                      C
        Удалить                       C*n
        Минимальный элемент           C
        Проверка пустоты              C

     Решение.  Дополнительно  храним  минимальный  элемент  мно-
жества.

     6.3.4 То же при следующих ограничениях на число действий:

          Операции             Число действий

        Сделать пустым                С*n
        Проверить принадлежность      С
        Добавить                      С*n
        Удалить                       С
        Минимальный элемент           С
        Проверка пустоты              C

       Решение.  Храним минимальный, а для каждого - следующий и
предыдущий по величине.

     Множества целых чисел.

     В следующих задачах величина элементов множества не ограни-
чена, но их количество не превосходит n.

     6.3.5. Память C*n.

          Операции             Число действий

        Сделать пустым                C
        Число элементов               C
        Проверить принадлежность      C*n
        Добавить новый
         (заведомо отсутствующий)     C
        Удалить                       C*n
        Минимальный элемент           C*n
        Взять какой-то элемент        C

     Решение.   Множество   представляем  с  помощью  переменных
a:array [1..n] of integer, k: 0..n; множество содержит k элемен-
тов a[1],...,a[k]; все они различны. По существу мы храним  эле-
менты множества в стеке (без повторений).

     6.3.6. Память C*n.

          Операции             Число действий

        Сделать пустым                C
        Проверить пустоту             C
        Проверить принадлежность      C*(log n)
        Добавить                      С*n
        Удалить                       C*n
        Минимальный элемент           С

     Решение. См. решение предыдущей задачи с дополнительным ус-
ловием a[1] < ... < a[k]. При проверке принадлежности используем
двоичный поиск.

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

     6.3.7.  Используя описанное в предыдущей задаче представле-
ние множеств, найти все вершины ориентированного графа,  доступ-
ные  из  данной по ребрам. (Вершины считаем числами 1..n.) Время
не больше C * (общее число ребер, выходящих  из  доступных  вер-
шин).

     Решение.  (Другое решение смотри в главе о рекурсии.) Пусть
num[i]  -  число  ребер,  выходящих  из   i,   out[i][1],   ...,
out[i][num[i]] - вершины, куда ведут ребра.

  procedure Доступные (i: integer);
  |   {напечатать все вершины, доступные из i, включая i}
  | var  X: подмножество 1..n;
  |      P: подмножество 1..n;
  |      q, v, w: 1..n;
  |      k: integer;
  begin
  | ...сделать X, P пустыми;
  | writeln (i);
  | ...добавить i к X, P;
  | {(1) P = множество напечатанных вершин; P содержит i;
  |  (2) напечатаны только доступные из i вершины;
  |  (3) X - подмножество P;
  |  (4) все напечатанные вершины, из которых выходит
  |      ребро в ненапечатанную вершину, принадлежат X}
  | while X непусто do begin
  | | ...взять какой-нибудь элемент X в v;
  | | for k := 1 to num [v] do begin
  | | | w := out [v][k];
  | | | if w не принадлежит P then begin
  | | | | writeln (w);
  | | | | добавить w в P;
  | | | | добавить w в X
  | | | end;
  | | end;
  | end;
  end;

     Свойство (1) не нарушается, так как печать  происходит  од-
новременно с добавлением в P. Свойства (2): раз v было в X, то v
доступно,  поэтому  w  доступно. Свойство (3) очевидно. Свойство
(4): мы удалили из X элемент v, но все вершины, куда из  v  идут
ребра, перед этим напечатаны.

     Оценка  времени  работы. Заметим, что изъятые из X элементы
больше туда не добавляются, так как они  в  момент  изъятия  (и,
следовательно, всегда позже) принадлежат P, а добавляются только
элементы  не  из P. Поэтому цикл while выполняется не более, чем
по разу, для всех  доступных  вершин,  а  цикл  for  выполняется
столько раз, сколько из вершины выходит ребер.
     Для  X  надо  использовать представление со стеком или оче-
редью (см. выше), для P - булевский массив.

     6.3.8. Решить предыдущую задачу, если требуется, чтобы дос-
тупные вершины печатались в таком порядке: сначала заданная вер-
шина, потом ее соседи, потом соседи соседей (еще  не  напечатан-
ные) и т.д.

     Указание. Так получится, если использовать очередь в приве-
денном выше решении: докажите индукцией по k, что существует мо-
мент, в который напечатаны все вершины на расстоянии  не  больше
k, а в очереди находятся все вершины, удаленные ровно на k.

Более  сложные  способы представления множеств будут разобраны в
главах 11 (Хеширование) и 12 (Деревья).

     6.4. Разные задачи.

     6.4.1. Реализовать структуру данных, которая имеет  все  те
же операции, что массив длины n, а именно

        начать работу
        положить в i-ю ячейку число n
        узнать, что лежит в i-ой ячейке

а также операцию "указать номер минимального элемента" (или  од-
ного  из  минимальных  элементов).  Количество действий для всех
операций  должно  быть не более C*log n, не считая операции "на-
чать работу" (которая требует не более C*n действий).

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

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

     Решение. Следуя алгоритму сортировки деревом (в его оконча-
тельном  варианте),  будем  размещать элементы очереди в массиве
x[1]..x[k],  поддерживая  такое  свойство:  x[i]  старше  (имеет
больший  приоритет)  своих сыновей x[2i] и x[2i+1], если таковые
существуют - и, следовательно, всякий элемент старше  своих  по-
томков. (Сведения о приоритета также хранятся в массиве, так что
мы  имеем  дело  с  массивом пар (элемент, приоритет).) Удаление
элемента с сохранением этого свойства описано в алгоритме сорти-
ровки. Надо еще уметь восстанавливать свойство после  добавления
элемента в конец. Это делается так:

    t:= номер добавленного элемента
    {инвариант: в дереве любой предок приоритетнее потомка,
        если этот потомок - не t}
    while t - не корень и t старше своего отца do begin
    | поменять t с его отцом
    end;

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

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

     7.1. Примеры рекурсивных программ.

При  анализе  рекурсивной  программы  возникает, как обычно, два
вопроса:

        (а) почему программа заканчивает работу?
        (б) почему она работает правильно, если заканчивает
            работу?

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

     7.1.1. Написать рекурсивную процедуру вычисления факториала
целого  положительного  числа  n  (т.е. произведения чисел 1..n,
обозначаемого n!).

     Решение. Используем равенства 1!=1, n!= (n-1)!*n.

        procedure factorial (n: integer; var fact: integer);
        | {положить fact равным факториалу числа y}
        begin
        | if n=1 then begin
        | | fact:=1;
        | end else begin {n>1}
        | | factorial (n-1, fact);
        | | fact:= fact*n;
        | end;
        end;

С использованием процедур-функций можно написать так:

        function factorial (n: integer): integer;
        begin
        | if n=1 then begin
        | | factorial:=1;
        | end else begin {n>1}
        | | factorial:=  factorial (n-1)*n;
        | end;
        end;

Обратите внимание на некоторую двойственность использования име-
ни factorial внутри описания функции: оно обозначает  как  пере-
менную,  так и вызываемую рекурсивно функцию. К счастью, в нашем
случае они различаются  по  скобкам  после  имени,  но  если  бы
функция  была  без  параметров,  то  дело  было бы плохо. (Стан-
дартная, но трудно находимая ошибка возникает, если автор  пола-
гает, что он использует значение переменной, а компилятор в этом
месте видит рекурсивный вызов.)

    7.1.2.  Обычно  факториал определяют и для нуля, считая, что
0!=1. Измените программы соответственно.

    7.1.3. Напишите рекурсивную программу возведения в целую не-
отрицательную степень.

    7.1.4. То же, если требуется, чтобы глубина рекурсии не пре-
восходила C*log n, где n - степень.

    Решение.

        function power (a,n: integer): integer;
        begin
        | if n = 0 then begin
        | | power:= 1;
        | end else if n mod 2 = 0 then begin
        | | power:= power(a*2, n div 2);
        | end else begin
        | | power:= power(a, n-1)*a;
        | end;
        end;

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

        power:= power(a*2, n div 2)
на
        power:= power(a, n div 2)* power(a, n div 2)?

     Решение. Программа останется правильной. Однако она  станет
работать  медленнее. Дело в том, что теперь вызов может породить
два вызова (хотя и одинаковых) вместо одного - и  число  вызовов
быстро  растет  с глубиной рекурсии. Программа по-прежнему имеет
логарифмическую глубину рекурсии, но число шагов  работы  стано-
вится линейным вместо логарифмического.
     Этот недостаток можно устранить, написав
        t:= power(a, n div 2);
        power:= t*t;
или воспользовавшись функцией возведения в квадрат (sqr).

     7.1.6. Используя лишь команды write(x) при x=0..9, написать
рекурсивную программу печати десятичной  записи  целого  положи-
тельного числа n.

     Решение.  Здесь  использование  рекурсии  облегчает   жизнь
(проблема  была в том, что цифры легче получать с конца, а печа-
тать надо с начала).

     procedure print (n:integer); {n>0}
     begin
     | if n<10 then begin
     | | write (n);
     | end else begin
     | | print (n div 10);
     | | write (n mod 10);
     | end;
     end;

     7.1.7. Игра "Ханойские башни" состоит в следующем. Есть три
стержня.  На  первый из них надета пирамидка из n колец (большие
кольца снизу, меньшие сверху). Требуется переместить  кольца  на
другой  стержень. Разрешается перекладывать кольца со стержня на
стержень,  но класть большее кольцо поверх меньшего нельзя. Сос-
тавить программу, указывающую требуемые действия.

     Решение.  Напишем  рекурсивную  процедуру   перемещения   i
верхних  колец  с  m-го стержня на n-ый (предполагается, что ос-
тальные кольца больше по размеру и лежат на стержнях без  движе-
ния).

    procedure move(i,m,n: integer);
    | var s: integer;
    begin
    | if i = 1 then begin
    | | writeln ('сделать ход', m, '->', n);
    | end else begin
    | | s:=6-m-n; {s - третий стержень: сумма номеров равна 6}
    | | move (i-1, m, s);
    | | writeln ('сделать ход', m, '->', n);
    | | move (i-1, s, n);
    | end;
    end;

(Сначала  переносится  пирамидка из i-1 колец на третью палочку.
После этого i-ое кольцо освобождается, и его можно перенести ку-
да следует. Остается положить на него пирамидку.)

     7.2. Рекурсивная обработка деревьев

     Двоичным деревом называется картинка вроде

                   o
                    \
                     o   o
                      \ /
                   o   o
                    \ /
                     o

Нижняя вершина называется корнем. Из каждой вершины  могут  идти
две  линии: влево вверх и вправо вверх. Вершины, куда они ведут,
называются левым и правым сыновьями  исходной  вершины.  Вершина
может  иметь двух сыновей, а может иметь только одного сына (ле-
вого или правого). Она может и вовсе не иметь сыновей, и в  этом
случае называется листом.
     Пусть x - какая-то вершина двоичного дерева. Она сама вмес-
те  с сыновьями, внуками, правнуками и т.д. образует поддерево с
корнем в x - "поддерево потомков x".

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

        l,r: array [1..N] of integer

и левый и правый сын вершины с номером  i  имеют  соответственно
номера  l[i]  и  r[i].  Если вершина с номером i не имеет левого
(или правого) сына, то l[i] (соответственно r[i]) равно  0.  (По
традиции при записи программ мы используем вместо нуля константу
nil, равную нулю.)

     Здесь N - достаточно большое натуральное число (номера всех
вершин  не  превосходят  N). Отметим, что номер вершины никак не
связан с ее положением в дереве и что не все числа  от  1  до  N
обязаны  быть  номерами вершин (и, следовательно, часть данных в
массивах l и r - это мусор).

    7.2.1. Пусть N=7, root=3, массивы l и r таковы:

         i  |   1  2  3  4  5  6  7
       l[i] |   0  0  1  0  6  0  7
       r[i] |   0  0  5  3  2  0  7

Нарисовать соответствующее дерево.

     Ответ:          6   2
                      \ /
                   1   5
                    \ /
                     3

     7.2.2. Написать программу подсчета числа вершин в дереве.

     Решение. Рассмотрим функцию n(x),  равную  числу  вершин  в
поддереве с корнем в вершине номер x. Считаем, что n(nil)=0 (по-
лагая соответствующее поддерево пустым), и не заботимся о значе-
ниях  nil(s)  для чисел s, не являющихся номерами вершин. Рекур-
сивная программа для s такова:

     function n (x:integer):integer;
     begin
     | if x = nil then begin
     | | n:= 0;
     | end else begin
     | | n:= n(l[x]) + n(r[x]) + 1;
     | end;
     end;

(Число вершин в поддереве над вершиной x равно сумме чисел  вер-
шин  над  ее сыновьями плюс она сама.) Глубина рекурсии конечна,
так  как  с  каждым  шагом  высота  соответствующего   поддерева
уменьшается.

     7.2.3. Написать программу подсчета числа листьев в дереве.

     Ответ.

     function n (x:integer):integer;
     begin
     | if x = nil then begin
     | | n:= 0;
     | end else if (l[x]=nil) and (r[x]=nil) then begin {лист}
     | | n:= 1;
     | end;
     | end else begin
     | | n:= n(l[x]) + n(r[x]);
     | end;
     end;

     7.2.4. Написать программу подсчета  высоты  дерева  (корень
имеет высоту 0, его сыновья - высоту 1, внуки - 2 и т.п.; высота
дерева - это максимум высот его вершин).

     Указание.  Рекурсивно  определяется  функция  f(x) = высота
поддерева с корнем в x.

     7.2.5.  Написать  программу, которая по заданному n считает
число всех вершин высоты n (в заданном дереве).

     Вместо подсчета количества вершин того или иного рода можно
просить напечатать список этих вершин (в том или ином порядке).

     7.2.6. Написать программу, которая печатает (по одному  ра-
зу) все вершины дерева.

     Решение.  Процедура  print_subtree(x)  печатает все вершины
поддерева с корнем в x по одному разу; главная программа  содер-
жит вызов print_subtree(root).

     procedure print_subtree (x:integer);
     begin
     | if x = nil then begin
     | | {ничего не делать}
     | end else begin
     | | writeln (x);
     | | print_subtree (l[x]);
     | | print_subtree (r[x]);
     | end;
     end;

Данная программа печатает сначала корень поддерева, затем подде-
рево над левым сыном, а затем над правым. Три строки в else-час-
ти  могут быть переставлены 6 способами, и каждый из этих спосо-
бов дает свой порядок печати вершин.

     7.3. Порождение комбинаторных объектов, перебор

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

     7.3.1. Написать программу, которая печатает по одному  разу
все  последовательности  длины n, составленные из чисел 1..k (их
количество равно k в степени n).

     Решение. Программа будет оперировать с массивом  a[1]..a[n]
и числом t. Рекурсивная процедура generate печатает все последо-
вательности, начинающиеся на a[1]..a[t]; после  ее  окончания  t
имеет то же значение, что и в начале:

     procedure generate;
     | var i,j : integer;
     begin
     | if t = n then begin
     | | for i:=1 to n do begin
     | | | write(a[i]);
     | | end;
     | | writeln;
     | end else begin {t < n}
     | | for j:=1 to k do begin
     | | | t:=t+1;
     | | | a[t]:=j;
     | | | generate;
     | | | t:=t-1;
     | | end;
     | end;
     end;

Основная программа теперь состоит из двух операторов:
     t:=0; generate;

     7.3.2. Написать программу, которая печатала бы все переста-
новки чисел 1..n по одному разу.

     Решение. Программа оперирует с массивом a[1]..a[n], в кото-
ром  хранится  перестановка  чисел  1..n.  Рекурсивная процедура
generate в такой ситуации печатает все перестановки, которые  на
первых  t позициях совпадают с перестановкой a; по выходе из нее
переменные t и a имеют те же значения, что и до входа.  Основная
программа такова:

    for i:=1 to n do begin a[i]:=i; end;
    t:=0;
    generate;

вот описание процедуры:

     procedure generate;
     | var i,j : integer;
     begin
     | if t = n then begin
     | | for i:=1 to n do begin
     | | | write(a[i]);
     | | end;
     | | writeln;
     | end else begin {t < n}
     | | for j:=t+1 to n do begin
     | | | поменять местами a[t+1] и a[j]
     | | | t:=t+1;
     | | | generate;
     | | | t:=t-1;
     | | | поменять местами a[t+1] и a[j]
     | | end;
     | end;
     end;

     7.3.3. Напечатать все возрастающие последовательности длины
k, элементами которых являются натуральные  числа  от  1  до  n.
(Предполагается, что k не превосходит n - иначе таких последова-
тельностей не существует.)

     Решение. Программа оперирует с массивом a[1]..a[k] и  целой
переменной  t. Предполагая, что a[1]..a[t] - возрастающая после-
довательность чисел натуральных чисел из отрезка 1..n, рекурсив-
но определенная процедура generate печатает все ее  возрастающие
продолжения длины k.

     procedure generate;
     | var i: integer;
     begin
     | if t = k then begin
     | | печатать a[1]..a[k]
     | end else begin
     | | t:=t+1;
     | | for i:=a[t-1]+1 to t-k+n do begin
     | | | a[t]:=i;
     | | | generate;
     | | end;
     | | t:=t-1;
     | end;
     end;

     Замечание. Цикл for мог бы иметь верхней границей n (вместо
t-k+n). Наш вариант экономит часть работы,  учитывая  тот  факт,
что  предпоследний  (k-1-ый)  член  не  может  превосходить n-1,
k-2-ой член не может превосходить n-2 и т.п.
     Основная программа теперь выглядит так:

        t:=1;
        for j:=1 to 1-k+n do begin
        | a[1]:=j;
        | generate;
        end;

Можно было бы добавить к массиву a слева еще и a[0]=0,  положить
t=0 и ограничиться единственным вызовом процедуры generate.

     7.3.4.  Перечислить все представления положительного целого
числа n в виде суммы последовательности невозрастающих целых по-
ложительных слагаемых.

     Решение.  Программа  оперирует  с  массивом a[1..n] (макси-
мальное число слагаемых равно n) и с целой переменной t. Предпо-
лагая, что a[1],...,a[t] - невозрастающая последовательность це-
лых чисел, сумма которых не превосходит  n,  процедура  generate
печатает  все  представления  требуемого  вида, продолжающие эту
последовательность. Для экономии вычислений сумма  a[1]+...+a[t]
хранится в специальной переменной s.

     procedure generate;
     | var i: integer;
     begin
     | if s = n then begin
     | | печатать последовательность a[1]..a[t]
     | end else begin
     | | for i:=1 to min(a[t], n-s) do begin
     | | | t:=t+1;
     | | | a[t]:=i;
     | | | s:=s+i;
     | | | generate;
     | | | s:=s-i;
     | | | t:=t-1;
     | | end;
     | end;
     end;

Основная программа при этом может быть такой:

     t:=1;
     for j:=1 to n do begin
     | a[1]:=j
     | s:=j;
     | generate;
     end;

     Замечание.  Можно немного сэконмить, вынеся операции увели-
чения и уменьшения t из цикла, а также не возвращая s каждый раз
к исходному значению (а увеличивая его на 1 и возвращая к исход-
ному значению в конце). Кроме того,  добавив  фиктивный  элемент
a[0]=n, можно упростить основную программу:

     t:=0; s:=0; a[0]:=n; generate;

     7.3.5.  Написать  рекурсивную  программу обхода дерева (ис-
пользуя те же команды и проверки, что и в главе про обход  дере-
ва).

     Решение.  Процедура  обработать_над обрабатывает все листья
над текущей вершиной и заканчивает работу в той же вершине,  что
и начала. Вот ее рекурсивное описание:

     procedure обработать_над;
     begin
     | if есть_сверху then begin
     | | вверх_налево;
     | | обработать_над;
     | | while есть_справа do begin
     | | | вправо;
     | | | обработать_над;
     | | end;
     | | вниз;
     | end else begin
     | | обработать;
     | end;
     end;

     7.4. Другие применения рекурсии

     Топологическая сортировка. Представим  себе  n  чиновников,
каждый  из  которых  выдает справки определенного вида. Мы хотим
получить все эти справки,  соблюдая  ограничения,  установленные
чиновниками.  Ограничения состоят в том, что у каждого чиновника
есть список справок, которые нужно собрать  перед  обращением  к
нему.  Дело  безнадежно,  если  схема  зависимостей  имеет  цикл
(справку  A  нельзя получить без B, B без C,..., Y без Z и Z без
A). Предполагая, что такого цикла нет, требуется составить план,
указывающий один из возможных порядков получения справок.

     Изображая чиновников точками, а  зависимости  -  стрелками,
приходим  к такой формулировке. Имеется n точек, пронумерованных
от 1 до n. Из каждой точки ведет несколько (возможно, 0) стрелок
в другие точки. (Такая картинка называется ориентированным  гра-
фом.)  Циклов нет. Требуется расположить вершины графа (точки) в
таком порядке, чтобы конец любой стрелки предшествовал ее  нача-
лу. Эта задача называется топологической сортировкой.

     7.4.1. Доказать, что это всегда возможно.

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

     7.4.2.  Предположим,  что  ориентированный  граф без циклов
хранится в такой форме: для каждого i от 1 до n в num[i] хранит-
ся число выходящих из i стрелок, в adr[i][1],..., adr[i][num[i]]
- номера вершин, куда эти стрелки ведут. Составить (рекурсивный)
алгоритм, который производит топологическую сортировку не  более
чем за C*(n+m) действий, где m - число ребер графа (стрелок).

     Замечание.  Непосредственная  реализация  приведенного выше
доказательства существования не дает требуемой оценки; ее прихо-
дится немного подправить.

     Решение. Наша программа будет  печатать  номера  вершин.  В
массиве  printed: array[1..n] of boolean мы будем хранить сведе-
ния о том, какие вершины напечатаны (и корректировать их  однов-
ременно  с  печатью  вершины).  Будем говорить, что напечатанная
последовательность вершин корректна, если никакая вершина не на-
печатана дважды и для любого номера i, входящего в эту последос-
тельность,  все вершины, в которые ведут стрелки из i, напечата-
ны, и притом до i.

     procedure add (i: 1..n);
     | {дано: напечатанное корректно;}
     | {надо: напечатанное корректно и включает вершину i}
     begin
     | if printed [i] then begin {вершина i уже напечатана}
     | | {ничего делать не надо}
     | end else begin
     | | {напечатанное корректно}
     | | for j:=1 to num[i] do begin
     | | | add(adr[i][j]);
     | | end;
     | | {напечатанное корректно, все вершины, в которые из
     | |  i ведут стрелки, уже напечатаны - так что можно
     | |  печатать i, не нарушая корректности}
     | |  if not printed[i] then begin
     | |  | writeln(i); printed [i]:= TRUE;
     | |  end;
     | end;
     end;

Основная программа:

     for i:=1 to n do begin
     | printed[i]:= FALSE;
     end;
     for i:=1 to n do begin
     | add(i)
     end;

     7.4.3.  В  приведенной  программе можно выбросить проверку,
заменив
          if not printed[i] then begin
          | writeln(i); printed [i]:= TRUE;
          end;
на
          writeln(i); printed [i]:= TRUE;
Почему? Как изменится спецификация процедуры?

     Решение.  Спецификацию можно выбрать такой:
       дано: напеватанное корректно
       надо: напечатанное корректно и включает вершину i;
             все вновь напечатанные вершины доступны из i.

     7.4.4. Где использован тот факт, что граф не имеет циклов?

     Решение.  Мы опустили доказательство конечности глубины ре-
курсии. Для каждой вершины  рассмотрим  ее  "глубину"  -  макси-
мальную длину пути по стрелкам, из нее выходящего.  Условие  от-
сутствия циклов гарантирует, что эта величина конечна. Из верши-
ны  нулевой глубины стрелок не выходит. Глубина конца стрелки по
крайней мере на 1 меньше, чем глубина начала. При работе  проце-
дуры  add(i)  все рекурсивные вызовы add(j) относятся к вершинам
меньшей глубины.

     Связная  компонента  графа.  Неориентированный граф - набор
точек (вершин), некоторые из которых соединены  линиями  (ребра-
ми). Неориентированный граф можно считать частным случаем ориен-
тированного графа, в котором для каждой стрелки есть обратная.
     Связной компонентой вершины i называется множество всех тех
вершин, в которые можно попасть из i, идя по ребрам графа. (Пос-
кольку  граф неориентированный, отношение "j принадлежит связной
компоненте i" является отношением эквивалентности.)

     7.4.5. Дан неориентированный граф (для каждой вершины  ука-
зано  число  соседей  и массив номеров соседей, как в предыдущей
задаче). Составить алгоритм, который по заданному i печатает все
вершины связной компоненты i по одному разу (и только их). Число
действий не должно превосходить C*(общее число вершин и ребер  в
связной компоненте).

     Решение.  Программа  в  процессе работы будет "закрашивать"
некоторые вершины графа. Незакрашенной частью графа будем  назы-
вать то, что останется, если выбросить все закрашенные вершины и
ведущие в них ребра. Процедура add(i) закрашивает связную компо-
ненту  i в незакрашенном графе (и не делает ничего, если вершина
i уже закрашена).

     procedure  add (i:1..n);
     begin
     | if вершина i закрашена then begin
     | | ничего делать не надо
     | end else begin
     | | закрасить i (напечатать и пометить как закрашенную)
     | | для всех j, соседних с i
     | | | add(j);
     | | end;
     | end;
     end;

Докажем, что эта процедура действует правильно (в предположении,
что рекурсивные вызовы работают правильно). В самом деле,  ниче-
го, кроме связной компоненты незакрашенного графа, она закрасить
не  может. Проверим, что вся она будет закрашена. Пусть k - вер-
шина, доступная из вершины  i  по  пути  i-j-...-k,  проходящему
только по незакрашенным вершинам. Будем рассматривать только пу-
ти,  не  возвращающиеся  снова  в i. Из всех таких путей выберем
путь с наименьшим j (в порядке просмотра соседей в цикле в  про-
цедуре).  Тогда  при  рассмотрении предыдущих соседей ни одна из
вершин j-...-k не будет закрашена (иначе  j  не  было  бы  мини-
мальным) и потому k окажется в связной компоненте незакрашенного
графа к моменту вызова add(j). Что и требовалось.
     Чтобы установить конечность глубины рекурсии, заметим,  что
на каждом уровне рекурсии число незакрашенных вершин уменьшается
хотя бы на 1.
     Оценим число действий. Каждая вершина закрашивается не  бо-
лее  одного раза - при первым вызове add(i) с данным i. Все пос-
ледующие вызовы происходят при закрашивании соседей - количество
таких  вызовов  не  больше числа соседей - и сводятся к проверке
того, что вершина i уже закрашена. Первый  же  вызов  состоит  в
просмотре  всех  соседей  и  рекурсивных вызовах add(j) для всех
них. Таким образом, общее число действий, связанных  с  вершиной
i, не превосходит константы, умноженной на число ее соседей. От-
сюда и вытекает требуемая оценка.

     7.4.6.  Решить ту же задачу для ориентированного графа (на-
печатать все вершины, доступные из данной по стрелкам; граф  мо-
жет содержать циклы).

     Ответ.  Годится  по  существу  та же программа (строку "для
всех соседей" надо заменить на  "для  всех  вершин,  куда  ведут
стрелки").

     Быстрая сортировка Хоара. В заключение приведем рекурсивный
алгоритм сортировки массива, который на практике является  одним
из  самых быстрых. Пусть дан массив a[1]..a[n]. Рекурсивная про-
цедура  sort (l,r:integer) сортирует участок массива с индексами
из полуинтервала (l,r] (т.е. a[l+1]..a[r]),  не  затрагивая  ос-
тального массива.

     procedure sort (l,r: integer);
     begin
     | if (l = r) then begin
     | | ничего делать не надо - участок пуст
     | end else begin
     | | выбрать случайное число s в полуинтервале (l,r]
     | | b := a[s]
     | | переставить элементы сортируемого участка так, чтобы
     | |   сначала шли элементы, меньшие b - участок (l,ll]
     | |   затем элементы, равные b        - участок (ll,rr]
     | |   затем элементы, большие b       - участок (rr,r]
     | | sort (l,ll);
     | | sort (rr,r);
     | end;
     end;

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

     7.4.7. (Для знакомых с основами теории вероятностей). Дока-
зать, что математическое ожидание числа операций при работе это-
го алгоритма не превосходит C*n*log n, причем константа C не за-
висит от сортируемого массива.

     Указание. Пусть T(n) -  максимум  математического  ожидания
числа  операций для всех входов длины n. Из текста процедуры вы-
текает такое неравенство:

     T(n) <= Cn + 1/n [сумма по всем  k+l=(n-1) чисел T(k)+T(l)]

Первый член соответствует распределению  элементов  на  меньшие,
равные  и большие. Второй член - это среднее математическое ожи-
дание для всех вариантов случайного выбора. (Строго говоря, пос-
кольку среди элементов могут быть равные, в правой части  вместо
T(k) и T(l) должны стоять максимумы T(x) по всем x, не превосхо-
дящим  k или l, но это не мешает дальнейшим рассуждениям.) Далее
индукцией по n нужно доказывать оценку T(n)  <=  C'nlog  n.  При
этом   для   вычисления  среднего  значения  x  log  x  по  всем
x=1,..,n-1 нужно интегрировать x lnx по частям как lnx * d(x*x).
При достаточно большом C' член Cn в правой части  перевешивается
за счет интеграла x*x*d(ln x), и индуктивный шаг проходит.

     7.4.8. Имеется массив из n различных целых чисел a[1]..a[n]
и число k. Требуется найти k-ое по величине число в этом  масси-
ве,  сделав  не более C*n действий, где C - некоторая константа,
не зависящая от k.

     Замечание. Сортировка позволяет очевидным  образом  сделать
это  за  C*n*log(n) действий. Очевидный способ: найти наименьший
элемент, затем найти второй, затем третий,..., k-ый требует  по-
рядка  k*n действий, то есть не годится (константа при n зависит
от k).

      Указание.  Изящный  (хотя  практически  и  бесполезный   -
константы слишком велики) способ сделать это таков:
     А. Разобьем наш массив на n/5 групп, в каждой из которых по
5 элементов. Каждую группу упорядочим.
     Б.  Рассмотрим средние элементы всех групп и перепишем их в
массив из n/5 элементов. С помощью  рекурсивного  вызова  найдем
средний по величине элемент этого массива.
     В.  Сравним этот элемент со всеми элементами исходного мас-
сива: они разделятся на большие его и меньшие его (и один равный
ему). Подсчитав количество тех и других, мы узнаем, в  какой  из
этих  частей  должен находится искомый (k-ый) элемент и каков он
там по порядку.
     Г. Применим рекурсивно наш алгоритм к выбранной части.

     Пусть  T(n)  -  максимально  возможное число действий, если
этот способ применять к массивам из не более чем n элементов  (k
может быть каким угодно). Имеем оценку:
     T(n) <= Cn + T(n/5) + T(примерно 0.7n)
Последнее слагаемое объясняется так: при разбиении на части каж-
дая часть содержит не менее 0.3n элементов. В самом деле, если x
-  средний  из средних, то примерно половина всех средних меньше
x. А если в пятерке средний элемент меньше x, то еще два заведо-
мо меньше x. Тем самым по крайней мере 3/5 от половины элементов
меньше x.
    Теперь  по  индукции можно доказать оценку T(n) <= Cn (реша-
ющую роль при этом играет то обстоятельство, что 1/5 + 0.7 < 1).
        Глава 8. Как обойтись без рекурсии.

     Для универсальных языков программирования (каковым является
паскаль)  рекурсия не дает ничего нового: для всякой рекурсивной
программы можно написать эквивалентную программу  без  рекурсии.
Мы  не будем доказывать этого, а продемонстрируем некоторые при-
емы, позволяющие избавиться от рекурсии в конкретных ситуациях.
     Зачем  это  нужно?  Ответ  прагматика мог бы быть таким: во
многих компьютерах (в том числе, к сожалению, и  в  современных,
использующих  так называемые RISC-процессоры), рекурсивные прог-
раммы в несколько раз  медленнее  соответствующих  нерекурсивных
программ.  Еще один возможный ответ: в некоторых языках програм-
мирования рекурсивные программы запрещены. А главное, при удале-
нии рекурсии возникают изящные и поучительные конструкции.

     8.1. Таблица значений (динамическое программирование)

     8.1.1. Следующая рекурсивная процедура вычисляет числа  со-
четаний  (биномиальные коэффициенты). Написать эквивалентную не-
рекурсивную программу.

        function C(n,k: integer):integer;
        | {n,k >=0; k <=n}
        begin
        | if (k = 0) or (k = n) then begin
        | | C:=1;
        | end else begin {0<k<n}
        | | C:= C(n-1,k-1)+C(n-1,k)
        | end;
        end;

Замечание. C(n,k) - число k-элементных подмножеств n-элементного
множества. Соотношение C(n,k) =  C(n-1,k-1)+C(n-1,k)  получится,
если  мы  фиксируем  некоторый элемент n-элементного множества и
отдельно подсчитаем  k-элементные  множества,  включающие  и  не
включающие этот элемент. Таблица значений C(n,k)

                        1
                      1   1
                    1   2   1
                  1   3   3   1
                .................

называется  треугольником  Паскаля  (того  самого). В нем каждый
элемент, кроме крайних единиц, равен сумме двух стоящих над ним.

     Решение. Можно воспользоваться формулой
        C(n,k) = n! / (k! * (n-k)!)
Мы, однако, не будем этого делать, так как хотим продемонстриро-
вать более общие приемы устранения  рекурсии.  Составим  таблицу
значений  функции  C(n,k), заполняя ее для n = 0, 1, 2,..., пока
не дойдем до интересующего нас элемента.

     8.1.2. Что можно сказать о времени работы рекурсивной и не-
рекурсивной версий в предыдущей задаче? Тот же вопрос о памяти.

     Решение. Таблица занимает место порядка n*n, его можно сок-
ратить до n, если заметить, что для вычисления следующей  строки
треугольника  Паскаля  нужна  только  предыдущая. Время работы в
обоих случаях порядка n*n.  Рекурсивная  программа  требует  су-
щественно большего времени: вызов C(n,k) сводится к двум вызовам
для C(n-1,..), те - к четырем вызовам для C(n-2,..) и т.д. Таким
образом, время оказывается экспоненциальным (порядка 2 в степени
n). Используемая рекурсивной версией память пропорциональна n  -
умножаем глубину рекурсии (n) на количество памяти, используемое
одним экземпляром процедуры (константа).

Кардинальный выигрыш во времени при переходе от рекурсивной вер-
сии к нерекурсивной связан с тем, что в рекурсивном варианте од-
ни  и  те  же  вычисления  происходят много раз. Например, вызов
C(5,3) в конечном счете порождает два вызова C(3,2):

                        C(5,3)
                       /     \
                     C(4,2)  C(4,3)
                    /  \     /   \
                 C(3,1) C(3,2)   C(3,3)
                ......................

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

     8.1.2. Порассуждать на ту же тему на примере рекурсивной  и
(простейшей)  нерекурсивной  программ для вычисления чисел Фибо-
наччи, заданных соотношением
        f(1) = f (2) = 1;  f(n) = f(n-1) + f(n-2) для n > 2.

     8.1.3. Дан выпуклый n-угольник (заданный координатами своих
вершин в порядке обхода). Его разрезают на треугольники диагона-
лями, для чего необходимо n-2 диагонали (докажите  индукцией  по
n). Стоимостью разрезания назовем сумму длин всех использованных
диагоналей.   Найти   минимальную  стоимость  разрезания.  Число
действий должно быть ограничено некоторым многочленом от n. (Пе-
ребор не подходит, так как число вариантов не ограничено многоч-
леном.)

     Решение. Будем считать, что вершины пронумерованы от 1 до n
и  идут  по  часовой стрелке. Пусть k, l - номера вершин, причем
l>k. Через A(k,l) обозначим многоугольник, отрезаемый от  нашего
хордой  k--l.  (Эта  хорда разрезает многоугольник на 2, один из
которых включает сторону 1--n; через A(k,l) мы  обозначаем  дру-
гой.)  Исходный многоугольник естественно обозначить A(1,n). При
l=k+1 получается "двуугольник" с совпадающими сторонами.

Через  a(k,l)  обозначим  стоимость  разрезания   многоугольника
A(k,l) диагоналями на треугольники. Напишем рекуррентную формулу
для  a(k,l).  При  l=k+1  получается  двуугольник, и мы полагаем
a(k,l)=0. При l=k+2 получается треугольник, и в этом случае так-
же a(k,l)=0. Пусть l > k+2. Хорда k--l является стороной  много-
угольника  A(k,l)  и,  следовательно,  стороной  одного  из тре-
угольников,  на  которые он разрезан. Противоположной вершиной i
этого треугольника может быть любая из вершин k+1,...,l-1, и ми-
нимальная стоимость разрезания может быть вычислена как

    min {(длина хорды k--i)+(длина хорды i--l)+a(k,i)+a(i,l)}

по всем i=k+1,..., i=l-1. При этом надо учесть,  что  при  i=k+1
хорда k--i - не хорда, а сторона, и ее длину надо считать равной
0 (по стороне разрез не проводится).

     Составив таблицу для a(k,l) и заполняя ее в порядке возрас-
тания числа вершин (равного l-k+2), мы получаем  программу,  ис-
пользующую память порядка n*n и время порядка n*n*n (однократное
применение  рекуррентной  формулы  требует выбора минимума из не
более чем n чисел).

     8.1.4. Матрицей размера m*n называется прямоугольная табли-
ца из m строк и n столбцов, заполненная числами. Матрицу размера
m*n  можно умножить на матрицу размера n*k (ширина левого сомно-
жителя  должна  равняться  высоте правого), и получается матрица
размером m*k. Ценой такого умножения будем считать  произведение
m*n*k (таково число умножений, которые нужно выполнить при стан-
дартном способе умножения - но сейчас это нам не важно). Умноже-
ние матриц ассоциативно, поэтому произведение n матриц можно вы-
числять в разном порядке. Для каждого порядка подсчитаем суммар-
ную цену всех матричных умножений. Найти минимальную цену вычис-
ления произведения, если известны  размеры  всех  матриц.  Число
действий должно быть ограничено многочленом от числа матриц.

     Пример.  Матрицы  размером  2*3, 3*4, 4*5 можно перемножать
двумя способами. В первом цена равна 2*3*4 + 2*4*5 = 24 +  40  =
64, во втором цена равна 3*4*5 + 2*3*5 = 90.

     Решение.  Представим  себе,  что первая матрица написана на
отрезке [0,1], вторая - на отрезке [1,2],..., s-ая - на  отрезке
[s-1,s]. Матрицы на отрезках [i-1,i] и [i,i+1] имеют общий  раз-
мер, позволяющих их перемножить. Обозначим его через d[i]. Таким
образом, исходным данным в задаче является массив d[0]..d[s].
     Через a(i,j) обозначим минимальную цену вычисления произве-
дения  матриц на участке [i,j] (при 0<=i<j<=s). Искомая величина
равна a(0,s). Величины a(i,i+1) равны нулю (матрица одна  и  пе-
ремножать ничего не надо). Рекуррентная формула будет такой:

    a(i,j) = min {a(i,k)+ a(k,j) + d[i]*d[k]*d[j]}

где  минимум берется по всем возможных местам последнего умноже-
ния, то есть по всем k=i+1..j-1. В самом деле, произведение мат-
риц на отрезке [i,k] есть матрица размера d[i]*d[k],  произведе-
ние  матриц  на отрезке [k,j] имеет размер d[k]*d[j], и цена вы-
числения их произведения равна d[i]*d[k]*d[j].

     Замечание. Две последние задачи похожи. Это сходство станет
яснее, если написать  матрицы  -  множители  на  сторонах  1--2,
2--3,..., s-1--s многоугольника, а на каждой хорде i--j написать
произведение всех матриц, стягиваемых этой хордой.

     8.1.5. Железная дорога с односторонним  движением  имеет  n
станций.  Известны цены белетов от i-ой станции до j-ой (при i <
j - в обратную сторонону проезда нет).  Найти  минимальную  сто-
имость  проезда  от начала до конца (с учетом возможной экономии
за счет пересадок).

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

     8.1.6.  Задано конечное множество с бинарной операцией (во-
обще говоря, не коммутативной и даже не ассоциативной).  Имеется
n  элементов  a[1]..a[n]  этого  множества и еще один элемент x.
Проверить,  можно  ли  так  расставить  скобки  в   произведении
a[1]..a[n],  чтобы  в  результате  получился  x.  Число операций
должно не превосходить C*n*n*n для некоторой константы C  (зави-
сищей от числа элементов в выбранном конечном множестве).

     Решение. Заполняем таблицу, в которой для  каждого  участка
a[i]..a[j]  нашего  произведения  хранится список всех возможных
его значений (при разной расстановке скобок).

     По существу этот же прием применяется в полиномиальном  ал-
горитме   проверки   принадлежности   слова  произвольному  кон-
текстно-свободному языку (см. главу 13).

     Следующая задача (задача о рюкзаке) уже упоминалась в главе
3 (Обход дерева).

     8.1.7.  Имеется  n  положительных  целых чисел x[1]..x[n] и
число N. Выяснить, можно ли получить N, складывая  некоторые  из
чисел x[1]..x[n]. Число действий должно быть порядка N*n.
     Указание. После i шагов хранится множество тех чисел на от-
реке   0..N,  которые  предствимы  в  виде  суммы  некоторых  из
x[1]..x[i].

     8.2. Стек отложенных заданий.

     Другой прием устранения рекурсии продемонстрируем на приме-
ре задачи о ханойских башнях.

     8.2.1. Написать нерекурсивную программу для нахождения пос-
ледовательности перемещений дисков в задаче о ханойских башнях.

     Решение. Вспомним рекурсивную программу:

    procedure move(i,m,n: integer);
    | var s: integer;
    begin
    | if i = 1 then begin
    | | writeln ('сделать ход', m, '->', n);
    | end else begin
    | | s:=6-m-n; {s - третий стержень: сумма номеров равна 6}
    | | move (i-1, m, s);
    | | writeln ('сделать ход', m, '->', n);
    | | move (i-1, s, n);
    | end;
    end;

Видно, что задача "переложить i верхних дисков с m-го стержня на
n-ый"  сводится  к трем задачам того же типа: двум задачам с i-1
дисками и к одной задаче с единственным диском. Выполняя эти за-
дачи, важно не позабыть, что еще осталось сделать.

     Для этой цели заведем стек отложенных  заданий,  элементами
которого будут тройки <i,m,n>. Каждая такая тройка интерпретиру-
ется  как  заказ  "переложить i верхних дисков с m-го стержня на
n-ый". Заказы упорядочены в соответствии с требуемым порядком их
выполнения: самый срочный - вершина стека. Получам  такую  прог-
рамму:

    procedure move(i,m,n: integer);
    begin
    | сделать стек заказов пустым
    | положить в стек тройку <i,m,n>
    | {инвариант: осталось выполнить заказы в стеке}
    | while стек непуст do begin
    | | удалить верхний элемент, переложив его в <j,p,q>
    | | if j = 1 then begin
    | | | writeln ('сделать ход', p, '->', q);
    | | end else begin
    | | | s:=6-p-q;
    | | |      {s - третий стержень: сумма номеров равна 6}
    | | | положить в стек тройки <j-1,s,q>, <1,p,q>, <j-1,p,s>
    | | end;
    | end;
    end;

(Заметим,  что  сначала в стек кладется тройка, которую надо вы-
полнять последней.) Стек троек может быть  реализован  как  стри
отдельных  стека.  (Кроме  того, в паскале есть специальный тип,
называемый "запись", который может быть применен.)

     8.2.2. (Сообщил А.К.Звонкин со ссылкой на Анджея  Лисовско-
го.)  Для  задачи  о ханойских башнях есть и другие нерекусивные
алгоритмы. Вот один из них: простаивающим стержнем  (не  тем,  с
которого  переносят, и не тем, на который переносят) должны быть
все стержни по очереди. Другое  правило:  поочередно  перемещать
наименьшее кольцо и не наименьшее кольцо, причем наименьшее - по
кругу.

     8.2.3. Использовать замену рекурсии стеком отложенных зада-
ний в рекурсивной программе печати десятичной записи целого чис-
ла.

     Решение. Цифры добываются с конца и закладываются в стек, а
затем печатаются в обратном порядке.

     8.2.4. Написать  нерекурсивную  программу,  печатающую  все
вершины двоичного дерева.

     Решение. В этом случае стек отложенных заданий будет содер-
жать  заказы двух сортов: заказ напечатать (в свое время) данную
вершину и заказ напечатать все вершины поддерева с данным корнем
(при этом nil считается корнем пустого дерева).  Таким  образом,
элемент стека есть пара: <тип заказа, номер вершины>.
     Вынимая элемент из стека, мы либо сразу исполняем его (если
это  заказ первого типа) либо помещаем в стек три порожденных им
заказа - в одном из шести возможных порядков.

     8.2.5. Что изменится, если требуется  не  печатать  вершины
двоичного дерева, а подсчитать их количество?

     Решение.  Печатание  вершины  следует заменить прибавлением
единицы к счетчику. Другими  словами,  инвариант  таков:  (общее
число  вершин)  = (счетчик) + (сумма чисел вершин в поддеревьях,
корни которых лежат в стеке).

     8.2.6. Для некоторых из шести возможных  порядков  возможны
упрощения, делающие ненужным хранение в стеке элементов двух ви-
дов. Указать некоторые из них.

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

     Замечание. Другую программу печати всех вершин дерева можно
построить на основе программы обхода дерева, разобранной в соот-
ветствующей  главе.  Там  используется команда "вниз". Поскольку
теперешнее представление дерева с помощью массивов l и r не поз-
воляет  найти  предка  заданной вершины, придется хранить список
всех вершин на пути от корня к  текущей  вершине.  Cмотри  также
главу об алгоритмах на графах.

     8.2.7.  Написать  нерекурсивный  вариант  программы быстрой
сортировки. Как обойтись  стеком,  глубина  которого  ограничена
C*log n, где n - число сортируемых элементов?

     Решение.  В  стек кладутся пары <i,j>, интерпретируемые как
отложенные задания на сортировку соответствующих участков масси-
ва. Все эти заказы не пересекаются, поэтому размер стека не  мо-
жет  превысить n. Чтобы ограничиться стеком логарифмической глу-
бины, будем придерживаться такого правила: глубже в  стек  поме-
щать больший из возникающих двух заказов. Пусть  f(n)  -  макси-
мальная  глубина стека, которая может встретиться при сортировке
массива из не более чем n элементов таким способом. Оценим  f(n)
сверху таким способом: после разбиения массива на два участка мы
сначала сортируем более короткий (храня в стеке про запас) более
длинный, при этом глубина стека не больше f(n/2)+1, затем сорти-
руем более длинный, так что

      f(n) <= max (f(n/2)+1, f(n-1)),

откуда очевидной индукцией получаем f(n) = O(log n).

     8.3. Более сложные случаи рекурсии.

     Пусть функция f с натуральными аргументами и значениями оп-
ределена рекурсивно условиями
        f(0) = a,
        f(x) = h(x, f(l(x))),
где a - некоторое число, а h и l -  известные  функции.  Другими
словами,  значение функции f в точке x выражается через значение
f в точке l(x). При этом предполагается, что для любого x в пос-
ледовательности
        x, l(x), l(l(x)),...
рано или поздно встретится 0.
     Если  дополнительно  известно,  что l(x) < x для всех x, то
вычисление f не представляет  труда:  вычисляем  последовательно
f(0), f(1), f(2),...

     8.3.1.  Написать  нерекурсивную  программу вычисления f для
общего случая.

     Решение. Для вычисления f(x) вычисляем последовательность
        l(x), l(l(x)), l(l(l(x))),...
до появления нуля и запоминаем ее, а затем вычисляем значения  f
в точках этой последовательности, идя справа налево.

     Еще более сложный случай из следующей задачи вряд ли встре-
тится  на  практике  (а  если  и встретися, то проще рекурсию не
устранять, а оставить). Но тем не менее: пусть функция f с нату-
ральными аргументами и значениями определяется соотношениями
        f(0) = a,
        f(x) = h(x, f(l(x)), f(r(x))),
где a - некоторое число, а l, r и h - известные функции. Предпо-
лагается, что если взять произвольное число и начать применять к
нему функции l и r в произвольном порядке, то  рано  или  поздно
получится 0.

     8.3.2. Написать нерекурсивную программу вычисления f.

     Решение. Можно было бы сначала построить дерево, у которого
в корне находится x, а в сыновьях вершины i стоят l(i) и r(i)  -
если только i не равно нулю, а затем вычислять значения функции,
идя от листьев к корню. Однако есть и другой способ.

     "Обратной польской записью" (или "постфиксной записью") вы-
ражения  называют  запись,  где знак функции стоит после всех ее
аргументов, а скобки не используются. Вот несколько примеров:

          f(2)                  2 f
          f(g(2))               2 g f
          s(2,t(7))             2 7 t s
          s(2, u(2, s(5,3))     2 2 5 3 s u s

Постфиксная  запись  выражения  позволяет удобно вычислять его с
помощью "стекового калькулятора". Этот калькулятор  имеет  стек,
который  мы  будем представлять себе расположенным горизонтально
(числа вынимаются и кладутся справа). При нажатии на  клавишу  с
числом  это число кладется в стек. При нажатии на функциональную
клавишу соответствующая функция применяется к  нескольким  аргу-
ментам у вершины стека. Например, если в стеке были числа
        2 3 4 5 6
и  нажата  функциональная клавиша s, соотвтетствующая функции от
двух аргументов, то в стеке окажутся числа
        2 3 4 s(5,6)

Перейдем теперь к нашей задаче. В процессе  вычисления  значения
функции  f мы будем работать со стеком чисел, а также с последо-
вательностью чисел и символов "f", "l", "r", "h", которую мы бу-
дем интерпретировать как последовательность  нажатий  кнопок  на
стековом калькуляторе.  Инвариант такой:

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

Пусть нам требуется вычислить значение, к примеру, f(100). Тогда
вначале мы помещаем в стек число 100, а  последовательность  со-
держит  единственный  символ "f". (При этом инвариант соблюдает-
ся.) Далее с последовательностью и стеком выполняются такие пре-
образования:

 старый       старая           новый       новая
 стек      последовательность  стек    последовательность

  X          x P               X x           P
  X x        l P               X l(x)        P
  X x        r P               X r(x)        P
  X x y z    h P               X h(x,y,z)    P
  X 0        f P               X a           P
  X x        f P               X             x x l f x r f h P

Обозначения: x, y, z,.. - числа, X - последовательность чисел, P
- последовательность чисел и символов "f", "l", "r", "h". В пос-
ледней строке предполагается, что m не равно 0. Эта строка соот-
ветствует равенству

        f(x) = h(x, f(l(x)), f(r(x))),

Эти  преобразования выполняются, пока последовательность не ста-
нет пуста. В этот момент в стеке  окажется  единственное  число,
которое и будет ответом.

     Замечание.  Последовательность по существу представляет со-
бой стек отложенных заданий (вершина которого находится слева).
     Глава 9. Разные алгоритмы на графах

     9.1. Кратчайшие пути

     В этом разделе рассматриваются различные варианты одной за-
дач. Пусть имеется n городов, пронумерованных числами от 1 до n.
Для каждой пары городов с номерами i, j в таблице  a[i][j]  хра-
нится  целое число - цена прямого авиабилета из города i в город
j. Считается, что рейсы существуют между любыми городами, a[i,i]
= 0 при всех i, a[i][j] может отличаться от  a[j,i].  Наименьшей
стоимостью проезда из i в j считается минимально возможная сумма
цен  билетов  для маршрутов (в том числе с пересадками), ведущих
из i в j. (Она не превосходит a[i][j], но может быть меньше.)

     В предлагаемых ниже задачах требуется найти наименьшую сто-
имость проезда для некоторых пар городов при тех или иных  огра-
ничениях на массив a и на время работы алгоритма.

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

     Решение. Маршрут длиной больше n всегда содержит цикл,  по-
этому минимум можно искать среди маршрутов длиной не более n,  а
их конечное число.

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

     9.1.2. Найти наименьшую стоимость проезда из 1-го города во
все остальные за время O(n в степени 3).

     Решение. Обозначим через МинСт(1,s,к) наименьшую  стоимость
проезда из 1 в s менее чем с k  пересадками.  Тогда  выполняется
такое соотношение:

   МинСт (1,s,k+1) = наименьшему из чисел МинСт(1,s,k) и
                     МинСт(1,i,k) + a[i][s] (i=1..n)

Как отмечалось выше, искомым ответом является  МинСт(1,i,n)  для
всех i=1..n.

     k:= 1;
     for i := 1 to n do begin x[i] := a[1][i]; end;
     {инвариант: x[i] := МинСт(1,i,k)}
     while k <> n do begin
     | for s := 1 to n do begin
     | | y[s] := x[s];
     | | for i := 1 to n do begin
     | | | if y[s] > x[i]+a[i][s] then begin
     | | | | y[s] := x[i]+a[i][s];
     | | | end;
     | | end
     | | {y[s] = МинСт(1,s,k+1)}
     | | for i := 1 to n do begin x[s] := y[s]; end;
     | end;
     | k := k + 1;
     end;

Приведенный  алгоритм называют алгоритмом динамического програм-
мирования, или алгоритмом Форда - Беллмана.

     9.1.3. Доказать, что программа останется  правильной,  если
не заводить массива y, а производить изменения в самом массиве x
(заменив в программе все вхождения буквы y на x и затем  удалить
ставшие лишними строки).

     Решение. Инвариант будет таков:
     МинСт(1,i,n) <= x[i] <= MинСт(1,i,k)

     Этот алгоритм может быть улучшен в двух  отношениях:  можно
за то же время O(n в степени 3) найти наименьшую стоимость  про-
езда i->j для ВСЕХ пар i,j (а не только с i=1), а  можно  сокра-
тить время работы до O(n в степени 2). Правда, в последнем  слу-
чае нам потребуется, чтобы все цены a[i][j] были неотрицательны.

     9.1.4. Найти наименьшую стоимость проезда i->j для всех i,j
за время O(n в степени 3).

     Решение. Для k = 0..n через А(i,j,k)  обозначим  наименьшую
стоимость маршрута из i в j, если в качестве пересадочных разре-
шено использовать только пункты с номерами не больше k. Тогда

     A(i,j,0) = a[i][j],
а
     A(i,j,k+1) = min (A(i,j,k), A(i,k+1,k)+A(k+1,j,k))

(два  варианта  соответствуют  неиспользованию  и  использованию
пункта k+1 в качестве пересадочного; отметим, что в нем  незачем
бывать более одного раза).
     Этот алгоритм называют алгоритмом Флойда.

     9.1.5.  Известны,  что  все  цены неотрицательны. Найти на-
именьшую стоимость проезда 1->i для всех i=1..n за время  O(n  в
степени 2).

     Решение. В процессе работы алгоритма некоторые города будут
выделенными (в начале - только город 1,  в  конце  -  все).  При
этом:

     для каждого выделенного города i хранится  наименьшая  сто-
имость пути 1->i; при этом известно, что минимум достигается  на
пути, проходящем только через выделенные города;
     для каждого невыделенного города i хранится наименьшая сто-
имость пути 1->i, в котором в качестве промежуточных используют-
ся только выделенные города.

     Множество  выделенных городов расширяется на основании сле-
дующего замечания: если среди всех  невыделенных  городов  взять
тот,  для которого хранимое число минимально, то это число явля-
ется истинной наименьшей стоимостью. В самом  деле,  пусть  есть
более  короткий  путь.  Рассмотрим  первый невыделенный город на
этом пути - уже до него путь длиннее! (Здесь существенна неотри-
цательность цен.)
     Добавив выбранный город к выделенным, мы должны  скорректи-
ровать информацию, хранимую для невыделенных городов.  При  этом
достаточно учесть лишь пути, в которых новый город является пос-
ледним пунктом пересадки, а это легко сделать, так как минималь-
ную стоимость проезда в новый город мы уже знаем.
     При самом бесхитростном способе хранения множества выделен-
ных городов (в булевском векторе)  добавление  одного  города  к
числу выделенных требует времени O(n).
     Этот алгоритм называют алгоритмом Дейкстры.

     Отыскании кратчайшего пути имеет естественную интерпретацию
в терминах матриц. Пусть A - матрица цен одной аваиакомпании,  а
B  -  матрица цен другой. (Мы считаем, что диагональные элементы
матриц равны 0.) Пусть мы хотим лететь с одной пересадкой,  при-
чем  сначала самолетом компании A, а затем - компании B. Сколько
нам придется заплатить, чтобы попасть из города i в город j?

     9.1.6. Доказать, что эта  матрица  вычисляется  по  обычной
формуле  для произведения матриц, только вместо суммы надо брать
минимум, а вместо умножения - сумму.

     9.1.7. Доказать, что таким образом определенное  произведе-
ние матриц ассоциативно.

     9.1.8. Доказать, что задача о кратчайших путях эквивалентна
вычислению "бесконечной степени" матрицы  цен  A:  в  последова-
тельности  A, A*A, A*A*A,... все элементы, начиная с некоторого,
равны искомой матрице стоимостей кратчайших путей. (Если нет от-
рицательных циклов!)

     9.1.9.  Начиная  с  какого элемента можно гарантировать ра-
венство в предыдущей задаче?

     Обычное  (не  модифицированное) умножение матриц тоже может
оказаться полезным, только матрицы  должны  быть  другие.  Пусть
есть не все рейсы (как в следующем разделе), а только некоторые,
a[i,j]  равно  1,  если рейс есть, и 0, если рейса нет. Возведем
матрицу a (обычным образом) в степень k и посмотрим на ее i-j-ый
элемент.

     9.1.10. Чему он равен?

     Ответ. Числу различных способов попасть  из  i  в  j  за  k
рейсов.

     Случай,  когда есть не все рейсы, можно свести к исходному,
введя фиктивные  рейсы  с  бесконечно  большой  (или  достаточно
большой)  стоимостью. Тем не менее возникает такой вопрос. Число
реальных рейсов может быть существенно меньше n*n, поэтому инте-
ресны алгоритмы, которые работают эффективно в  такой  ситуации.
Исходные  данные  естественно  представлять тогда в такой форме:
для каждого города известно число выходящих из него  рейсов,  их
пункты назначения и цены.

     9.1.11.  Доказать,  что алгоритм Дейкстры можно модифициро-
вать так, чтобы для n городов и k маршрутов он требовал не более
C*(n+k log n) операций.
     Указание. Что надо сделать на каждом шаге? Выбрать  невыде-
ленный город с минимальной стоимостью и скорректировать цены для
всех  городов,  в  которые из него есть маршруты. Если бы кто-то
сообщал нам, для какого города стоимость минимальна, то  хватило
бы C*(n+k) действий. А поддержание сведений о том, какой элемент
в  массиве  минимален  (см. задачу 6.4.1 в главе о типах данных)
обходится еще в множитель log n.

     9.2. Связные компоненты, поиск в глубину и ширину

     Наиболее простой случай задачи о кратчайших  путях  -  если
все цены равны 0 или бесконечны. Другими словами, мы интересуем-
ся  возможностью попасть из i в j, но за ценой не постоим (и она
нас не интересует). В других терминах: мы имеем  ориентированный
граф (картинку из точек, некоторые из которых соединены стрелка-
ми) и нас интересуют вершины, доступные из данной.

     Для  этого  случая  задачи о кратчайших путях приведенные в
предыдущем разделе алгоритмы - не наилучшие. В самом деле, более
быстрая  рекурсивная  программа  решения этой задачи приведена в
главе 7 (Рекурсия), а нерекурсивная - в главе 6  (Типы  данных).
Сейчас  нас  интересует  такая задача: не просто перечислить все
вершины, доступные из данной, но перечислить их  в  определенном
порядке. Два популярных случая - поиск в ширину и в глубину.

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

     9.2.1.  Придумать  алгоритм  решения  этой  задачи с числом
действий не более C*(число ребер, выходящих из интересующих  нас
вершин).

     Решение.  Эта  задача  рассматривалась в главе 6 (Типы дан-
ных), 6.3.7 - 6.3.8. Здесь мы приведём подробное решение.  Пусть
num[i]  -  количество  ребер,  выходящих  из  i,  out[i][1],...,
out[i][num[i]] - вершины, куда ведут ребра. Вот программа,  при-
ведённая ранее:

  procedure Доступные (i: integer);
  |   {напечатать все вершины, доступные из i, включая i}
  | var  X: подмножество 1..n;
  |      P: подмножество 1..n;
  |      q, v, w: 1..n;
  |      k: integer;
  begin
  | ...сделать X, P пустыми;
  | writeln (i);
  | ...добавить i к X, P;
  | {(1) P = множество напечатанных вершин; P содержит i;
  |  (2) напечатаны только доступные из i вершины;
  |  (3) X - подмножество P;
  |  (4) все напечатанные вершины, из которых выходит
  |      ребро в ненапечатанную вершину, принадлежат X}
  | while X непусто do begin
  | | ...взять какой-нибудь элемент X в v;
  | | for k := 1 to num [v] do begin
  | | | w := out [v][k];
  | | | if w не принадлежит P then begin
  | | | | writeln (w);
  | | | | добавить w в P;
  | | | | добавить w в X
  | | | end;
  | | end;
  | end;
  end;

Тогда нам было безразлично, какой именно элемент множества X вы-
бирается. Если мы будем считать X очередью (первым пришел - пер-
мым ушел), то эта программа напечатает все вершины, доступные из
i, в порядке возрастания их расстояния  от  i  (числа  ребер  на
кратчайшем пути из i). Докажем это.

     Обозначим  через V(k) множество всех вершин, расстояние ко-
торых от i (в описанном смысле) равно k. Имеет место такое соот-
ношение:

 V(k+1) = (концы ребер с началами в V(k))-V(0)-V(1)-...-V(k)

(знак "-" обозначает вычитание множеств). Докажем, что для любо-
го k=0,1,2... в ходе работы программы будет такой момент  (после
очередной итерации цикла while), когда

     в очереди стоят все элементы V(k) и только они
     напечатаны все элементы V(1),...,V(k)

(Для  k=0  - это состояние перед циклом.) Рассуждая по индукции,
предположим, что в очереди скопились все элементы V(k). Они  бу-
дут  просматривать  в  цикле,  пока не кончатся (поскольку новые
элементы добавляются в конец, они не перемешаются  со  старыми).
Концы  ведущих из них ребер, если они уже не напечатаны, печата-
ются и ставятся в очередь - то есть всё как  в  записанном  выше
соотношении для V(k+1). Так что когда все старые  элементы  кон-
чатся, в очереди будут стоять все элементы V(k+1).

     Поиск в глубину.

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

     Будем предполагать, что для каждой вершины графа  выходящие
из  нее  ребра  упорядочены (напрмиер, пронумерованы). Тем самым
для каждой вершины дерева выходящие из нее ребра также  упорядо-
чены.  Будем обходить дерево так: сначала корень, а потом подде-
ревья (в порядке  ведущих  в  них  ребер).  Такой  обход  дерева
рассматривался  нами в главе о рекурсии. Ему соответствует обход
графа. Если выкинуть из этого обхода повторные посещения уже по-
сещенных вершин, то получится то, что называется "поиск в глуби-
ну".

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

     9.2.2. Написать программу поиска в глубину.
     Указание.  Возьмем  программу обхода дерева (корень - левое
поддерево - правое поддерево) из главы о рекурсии или  из  гравы
об  устранении  рекурсии  и используем ее применительно к обсто-
ятельствам. Главное изменение: не надо посещать вершины  повтор-
но.  Так что если мы попали в уже посещенную вершину, то можно с
ней ничего не делать. (Если путь не минимален  среди  ведущих  в
данную  вершину,  то  и  все  его продолжения не минимальны - их
просматривать не надо).

     Замечание. Существуют две возможности устранения рекурсии в
программе обхода дерева. Можно хранить в стеке корни  подлежащих
посещению  поддеревьев  (как  это делалось в главе об устранении
рекурсии). А можно применять метод из главы об обходе дерева, то
есть реализовать операции  "вверх_налево",  "вправо"  и  "вниз".
Чтобы их реализовать, необходимо хранить в стеке путь из корня к
текущей  вершине. Оба способа - примерно одинаковой сложности, и
в конкретной ситуации любой из них может оказаться  более  удоб-
ным.

     Поиск в глубину лежит в основе многих алгоритмов на графах,
порой в несколько модифицированном виде.

      9.2.3. Неориентированный граф называется двудольным,  если
его  можно  раскрасить в два цвета так, что концы любого ребра -
разного цвета. Составить алгоритм проверки, является ли заданный
граф двудольным (число действий не провосходит C*(число ребер  +
число вершин).

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

     Замечание.  В  этой задаче безразлично, производить поиск в
ширину или в глубину.

     9.2.4. Составить нерекурсивный алгоритм топологической сор-
тировки  ориентированного  графа без циклов. (См. задачу 7.4.2 в
главе о рекурсии.)

     Решение.  Предположим,  что  граф  имеет вершины с номерами
1..n, для каждой вершины i известно число  num[i]  выходящих  из
нее ребер и номера вершин dest[i][1],..., dest[i][num[i]], в ко-
торые эти ребра ведут. Будем условно считать, что ребра перечис-
лены "слева направо": левее то ребро, у которого  номер  меньше.
Нам надо напечатать все вершины в таком порядке, чтобы конец лю-
бого ребра был напечатан перед его началом. Мы предполагаем, что
в графе нет ориентированных циклов - иначе такое невозможно.
      Для начала добавим к графу вершину 0, из которой ребра ве-
дут в вершины 1,...,n. Если ее удастся напечатать с  соблюдением
правил, то тем самым все вершины будут напечатаны.

      Алгоритм  хранит путь, выходящий из нулевой вершины и иду-
щий по ребрам графа. Переменная l отводится для длины этого  пу-
ти.  Путь  образован  вершинами  vert[1],..., vert[l] и ребрами,
имеющими номера edge[1]...edge[l]. Номер edge[s] относится к ну-
мерации ребер, выходящих из вершины vert[s]. Тем самым для  всех
s должны выполняться неравенство
        edge[s] <= num[vert[s]]
и равенство
        vert[s+1] = dest [vert[s]] [edge[s]]
Впрочем,  для  последнего  ребра мы сделаем исключение, разрешив
ему указывать "в пустоту",  т.е.  разрешим
edge[l] равняться num[vert[l]]+1.

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

(И)     вершины  пути, кроме последней (т.е. vert[1]..vert[l])
        не напечатаны, но свернув с пути налево, мы немедленно
        упираемся в напечатанную вершину

Вот что получается:

        l:=1; vert[1]:=0; edge[1]:=1;
        while not( (l=1) and (edge[1]=n+1)) do begin
        | if edge[l]=num[vert[l]]+1 then begin
        | | {путь кончается в пустоте, поэтому все вершины,
        | |     следующие за vert[l], напечатаны - можно
        | |     печатать vert[l]}
        | | writeln (vert[l]);
        | | l:=l-1; edge[l]:=egde[l]+1;
        | end else begin
        | |  {edge[l] <= num[vert[l]], путь кончается в
        | |     вершине}
        | |  lastvert:= dest[vert[l]][edge[l]]; {последняя}
        | |  if lastvert напечатана then begin
        | |  | edge[l]:=edge[l]+1;
        | |  end else begin
        | |  | l:=l+1; vert[l]:=lastvert; edge[l]:=1;
        | |  end;
        | end;
        end;
        {путь сразу же ведет в пустоту, поэтому все вершины
         левее, то есть 1..n, напечатаны}

     9.2.4. Доказать, что если в графе нет циклов, то этот алго-
ритм заканчивает работу.

     Решение. Пусть это не так. Каждая вершина может  печататься
только  один раз, тако что с некоторого момента вершины не печа-
таются. В графе без циклов длина пути ограничена (вершина не мо-
жет входить дважды), поэтому подождав еще,  мы  можем  дождаться
момента,  после  которого  путь не удлиняется. После этого может
разве что увеличиваться edge[l] - но и это не беспредельно.
     Глава 10. Сопоставление с образцом.

     10.1. Простейший пример.

     10.1.1. Имеется последовательность символов x[1]..x[n]. Оп-
ределить, имеются ли в ней идущие друг за другом символы "abcd".
(Другими словами, требуется выяснить, есть ли в слове x[1]..x[n]
подслово "abcd".)

    Решение. Имеется примерно n (если быть точным, n-3) позиций,
на  которых  может находиться искомое подслово в исходном слове.
Для каждой из позиций можно проверить, действительно ли там  оно
находится, сравнив четыре символа. Однако есть более эффективный
способ. Читая слово x[1]..x[n] слева направо, мы ожидаем появле-
ния  буквы  'a'.  Как только она появилась, мы ждем за ней букву
'b', затем 'c', и, наконец, 'd'. Если наши ожидания оправдывают-
ся, то слово "abcd" обнаружено. Если же какая-то из нужных  букв
не  появляется, мы оказываемся у разбитого корыта и начинаем все
сначала.

     Этот простой алгоритм можно описать в разных терминах.  Ис-
пользуя  терминологию  так  называемых конечных автоматов, можно
сказать, что при чтении слова x слева направо мы в каждый момент
находимся в  одном  из  следующих  состояний:  "начальное"  (0),
"сразу после a" (1), "сразу после ab" (2), "сразу после abc" (3)
и  "сразу после abcd" (4). Читая очередную букву, мы переходим в
следующее состояние по правилу

         Текущее         Очередная      Новое
         состояние       буква          состояние
          0                a             1
          0              кроме a         0
          1                b             2
          1                a             1
          1              кроме a,b       0
          2                c             3
          2                a             1
          2              кроме a,c       0
          3                d             4
          3                a             1
          3              кроме a,d       0

Как только мы попадем в состояние 4,  работа заканчивается.

     Соответствующая программа очевидна:
        i:=1; state:=0;
        {i - первая непрочитанная буква, state - состояние}
        while (i<> n+1) and (state <> 4) do begin
          if state = 0 then begin
            if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 1 then begin
            if x[i] = b then begin
              state:= 2;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 2 then begin
            if x[i] = c then begin
              state:= 3;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end else if state = 3 then begin
            if x[i] = d then begin
              state:= 4;
            end else if x[i] = a then begin
              state:= 1;
            end else begin
              state:= 0;
            end;
          end;
        end;
        answer := (state = 4);

     Иными  словами, мы в каждый момент храним информацию о том,
какое максимальное начало нашего образца "abcd" является  концом
прочитанной  части.  (Его длина и есть то "состояние", о котором
шла речь.)

     Терминология,  нами используемая, такова. Слово - это любая
последовательность символов из некоторого фиксированного  конеч-
ного множества. Это множество называется алфавитом, его элементы
- буквами. Если отбросить несколько букв с конца слова, останет-
ся  другое  слово, называемое началом первого. Любое слово также
считается своим началом. Конец слова - то, что  останется,  если
отбросить  несколько  первых  букв.  Любое слово считается своим
концом. Подслово - то, что останется, если отбросить буквы  и  с
начала, и с конца. (Другими словами, подслова - это концы начал,
или, что то же, начала концов.)

     В  терминах  индуктивных  функций (см. раздел 1.3) ситуацию
можно описать так: рассмотрим функцию на словах, которая  прини-
мает два значения "истина" и "ложь" и истинна на словах, имеющих
"abcd"  своим подсловом. Эта функция не является индуктивной, но
имеет индуктивное расширение

 x ->длина максимального начала слова abcd, являющегося концом x

     10.2. Повторения в образце - источник проблем.

     10.2.1. Можно ли в предыдущих рассуждениях  заменить  слово
"abcd" на произвольное слово?

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

Вот картинка, поясняющая сказанное:

 x   y   z   a   b   a   b   a   b   c   ....  <- входное слово

             a   b   a   b   c       <-  мы ждали образца здесь

                     a   b   a   b   c  <-  а он оказался здесь

Таким образом, к моменту
                           |
 x   y   z   a   b   a   b |             <- входное слово
                           |
             a   b   a   b | c       <-  мы ждали образца здесь
                           |
                     a   b | a   b   c  <-  а он оказался здесь
                           |
есть два возможных положения образца, каждое из которых подлежит
проверке. Тем не менее по-прежнему  возможен  конечный  автомат,
читающий  входное  слово буква за буквой и переходящий из состо-
яния в состояние в зависимости от прочитанных букв.

     10.2.2. Указать состояния соответствующего автомата и  таб-
лицу  перехода (новое состояние в зависимости от старого и чита-
емой буквы).

     Решение. По-прежнему состояния  будут  соответствовать  на-
ибольшему  началу  образца, являющемуся концом прочитанной части
слова. Их будет шесть: 0,  1  ("a"),  2  ("ab"),  3  ("aba"),  4
("abab"), 5 ("ababc"). Таблица перехода:

         Текущее         Очередная      Новое
         состояние       буква          состояние
          0                a             1 (a)
          0              кроме a         0
          1 (a)            b             2 (ab)
          1 (a)            a             1 (a)
          1 (a)          кроме a,b       0
          2 (ab)           a             3 (aba)
          2 (ab)         кроме a         0
          3 (aba)          b             4 (abab)
          3 (aba)          a             1 (a)
          3 (aba)        кроме a,b       0
          4 (abab)         c             5 (ababc)
          4 (abab)         a             3 (aba)
          4 (abab)       кроме a,c       0

Для проверки посмотрим, к примеру, на вторую снизу строку.  Если
прочитанная  часть  кончалась на "abab", а затем появилась буква
"a", то теперь  прочитанная  часть  кончается  на  "ababa".  На-
ибольшее  начало  образца ("ababc"), которое есть ее конец - это
"aba".

     Философский вопрос: мы говорили, что  трудность  состоит  в
том,  что  есть несколько возможных положений образца, каждое из
которых может оказаться истинным. Им соответствуют несколько на-
чал образца, являющихся концами входного слова. Но конечный  ав-
томат помнит лишь самое длинное из них. Как же остальные?

     Философский ответ. Дело в том, что самое длинное из них оп-
ределяет  все остальные - это его концы, одновременно являющиеся
его началами.

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

     10.3. Вспомогательные утверждения

     Для произвольного слова X рассмотрим все его начала, однов-
ременно  являющиеся его концами, и выберем из них самое длинное.
(Не считая, конечно, самого слова X.) Будем обозначать его n(X).

     Примеры: n(aba)=a, n(abab)=ab, n(ababa)=aba, n(abc) =  пус-
тое слово.

     10.3.1. Доказать, что все слова n(X), n(n(X)), n(n(n(X)))
и т.д. являются началами слова X.

     Решение.  Каждое из них (согласно определению) является на-
чалом предыдущего.

     По той же причине все они являются концами слова X.

     10.3.2. Доказать, что последовательность предыдущей  задачи
обрывается (на пустом слове).

     Решение. Каждое слово короче предыдущего.

     Задача.  Доказать, что любое слово, одновременно являющееся
началом и концом слова X (кроме самого X)  входит  в  последова-
тельность n(X), n(n(X)),...

     Решение. Пусть слово Y есть одновременно начало и конец  X.
Слово  n(X)  - самое длинное из таких слов, так что Y не длиннее
n(X). Оба эти слова являются началами X, поэтому более  короткое
из них является началом более длинного: Y есть начало n(X). Ана-
логично, Y есть конец n(X). Рассуждая по индукции, можно предпо-
лагать, что утверждение задачи верно для всех слов короче  X,  в
частности,  для слова n(X). Так что слово Y, являющееся концом и
началом  n(X), либо равно n(X), либо входит в последовательность
n(n(X)), n(n(n(X))), ..., что и требовалось доказать.

     10.4. Алгоритм Кнута - Морриса - Пратта

     Алгоритм Кнута - Морриса - Пратта (КМП)  получает  на  вход
слово

        X = x[1]x[2]...x[n]

и просматривает его слева направо буква за буквой, заполняя  при
этом массив натуральных чисел l[1]..l[n], так что

      l[i] = длина слова n(x[1]...x[i])

(функция  n  определена в предыдущем пункте). Словами: l[i] есть
длина наибольшего начала слова x[1]..x[i], одновременно являюще-
гося его концом.

     10.4.1.  Какое  отношение  все это имеет к поиску подслова?
Другими словами, как использовать алгоритм КМП  для  определения
того, является ли слово A подсловом слова B?

     Решение.  Применим алгоритм КМП к слову A#B, где # - специ-
альная буква, не встречающаяся ни в A, ни в B. Слово A  является
подсловом слова B тогда и только тогда, когда среди чисел в мас-
сиве l будет число, равное длине слова A.

     10.4.2. Описать алгоритм заполнения таблицы l[1]..l[n].

     Решение.  Предположим, что первые i значений l[1]..l[i] уже
найдены. Мы читаем очередную букву слова (т.е. x[i+1]) и  должны
вычислить l[i+1].

     1                                              i   i+1
    --------------------------------------------------------
    |           уже прочитанная часть X                |   |
    --------------------------------------------------------
    \-----------Z-----------/    \------------Z------------/

Другими словами, нас интересуют начала Z слова x[1]..x[i+1], од-
новременно являющиеся его концами - из них нам надо выбрать  са-
мое длинное. Откуда берутся эти начала? Каждое из них получается
из  некоторого слова Z' приписыванием буквы x[i+1]. Слово Z' яв-
ляется началом и концом слова x[1]..x[i]. Однако не любое слово,
являющееся началом и концом слова x[1]..x[i],  годится  -  надо,
чтобы за ним следовала буква x[i+1].

     Получаем такой рецепт отыскания слова Z. Рассмотрим все на-
чала слова x[1]..x[i], являющиеся одновременно его  концами.  Из
них  выберем  подходящие - те, за которыми идет буква x[i+1]. Из
подходящих выберем самое длинное. Приписав в его  конец  x[i+1],
получим искомое слово Z.

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

    i:=1; l[1]:= 0;
    {таблица l[1]..l[i] заполнена правильно}
    while i <> n do begin
    | len := l[i]
    | {len - длина начала слова x[1]..x[i], которое  является
    |    его концом; все более длинные начала оказались
    |    неподходящими}
    | while (x[len+1] <> x[i+1]) and (len > 0) do begin
    | | {начало оказалось неподходящим, применяем к нему n}
    | | len := l[len];
    | end;
    | {нашли подходящее или убедились в отсутствии}
    | if x[len+1] = x[i+1] do begin
    | | {x[1]..x[len] - самое длинное подходящее начало}
    | | l[i+1] := len+1;
    | end else begin
    | | {подходящих нет}
    | | l[i+1] := 0;
    | end;
    | i := i+1;
    end;

     10.4.3. Доказать, что число действий в  приведенном  только
что алгоритме не превосходит Cn для некоторой константы C.

     Решение. Это не вполне очевидно: обработка каждой очередной
буквы может потребовать многих итераций во внутреннем цикле. Од-
нако каждая такая итерация уменьшает len по крайней мере на 1, и
в этом случае l[i+1] окажется заметно меньше l[i]. С другой сто-
роны, при увеличении i на единицу величина l[i]  может  возрасти
не более чем на 1, так что часто и сильно убывать она не может -
иначе убывание не будет скомпенсировано возрастанием.
     Более точно, можно записать неравенство
    l[i+1] <= l[i] - (число итераций на i-м шаге) + 1
или
    (число итераций на i-м шаге) <= l[i] - l[i+1] + 1
и остается сложить эти неравества по всем i  и  получить  оценку
сверху для общего числа итераций.

     10.4.4.  Будем  использовать этот алгоритм, чтобы выяснить,
является ли слово X длины n подсловом слова Y длины m. (Как  это
делать  с помощью специального разделителя #, описано выше.) При
этом число действий будет не более C*(n+m), и  используемая  па-
мять  тоже. Придумать, как обойтись памятью не более Cn (что мо-
жет быть существенно меньше, если искомый  образец  короткий,  а
слово, в котором его ищут - длинное).

     Решение.  Применяем  алгоритм КМП к слову A#B. При этом вы-
числение значений l[1],...,l[n] проводим для слова X длины  m  и
запоминаем  эти  значения. Дальше мы помним только значение l[i]
для текущего i - кроме него и кроме таблицы l[1]..l[n], нам  для
вычислений ничего не нужно.

     На практике слова X и Y могут не находиться подряд, поэтому
просмотр  слова  X и затем слова Y удобно оформить в виде разных
циклов. Это избавляет также от хлопот с разделителем.

     10.4.5. Написать соответствующий алгоритм (проверяющий, яв-
ляется ли слово X=x[1]..x[n] подсловом слова Y=y[1]..y[m]).

     Решение. Сначала вычисляем таблицу l[1]..l[n]  как  раньше.
Затем пишем такую программу:
     j:=0; len:=0
     {len - длина максимального начала слова X, одновременно
            являющегося концом слова y[1]..j[j]}
     while (len <> n) and (j <> m) do begin
     | while (x[len+1] <> y[j+1]) and (len > 0) do begin
     | | {начало оказалось неподходящим, применяем к нему n}
     | | len := l[len];
     | end;
     | {нашли подходящее или убедились в отсутствии}
     | if x[len+1] = y[j+1] do begin
     | | {x[1]..x[len] - самое длинное подходящее начало}
     | | len := len+1;
     | end else begin
     | | {подходящих нет}
     | | len := 0;
     | end;
     | i := i+1;
     end;
     {если len=n, слово X встретилось; иначе мы дошли до конца
        слова Y, так и не встретив X}

     10.5. Алгоритм Бойера - Мура

     Этот алгоритм делает то, что на первый взгляд  кажется  не-
возможным:  в  типичной  ситуации он читает лишь небольшую часть
всех букв слова, в котором ищется заданный образец. Как так  мо-
жет  быть? Идея проста. Пусть, например, мы ищем образец "abcd".
Посмотрим на четвертую букву слова: если, к примеру,  это  буква
"e",  то  нет  никакой необходимости читать первые три буквы. (В
самом деле, в образце буквы "e" нет, поэтому он  может  начаться
не раньше пятой буквы.)

     Мы приведем самую простой вариант этого алгоритма,  который
не  гарантирует быстрой работы во всех случаях. Пусть x[1]..x[n]
- образец, который надо искать. Для каждого символа s найдем са-
мое правое его вхождение в слово X, то есть  наибольшее  k,  при
котором x[k]=s. Эти сведения будем хранить в массиве pos[s]; ес-
ли  символ  s вовсе не встречается, то нам будет удобно положить
pos[s] = 0 (мы увидим дальше, почему).

     10.5.1. Как заполнить массив pos?

     Решение.
        положить все pos[s] равными 0
        for i:=1 to n do begin
          pos[x[i]]:=i;
        end;

В  процессе поиска мы будем хранить в переменной last номер буквы
в слове, против которой последняя буква образца. Вначале last = m
(длине образца), затем постепенно увеличивается.

     last:=m;
     {все предыдущие положения образца уже проверены}
     while last <= m do begin {слово не кончилось}
     | if x[m] <> y[last] then begin {последние буквы разные}
     | | last := last + (m - pos[y[last]]);
     | | {m - pos[y[last]]  - это минимальный сдвиг образца,
     | |    при котором напротив y[last] встанет такая же
     | |    буква в образце. Если такой буквы нет вообще,
     | |    то сдвигаем на всю длину образца}
     | end else begin
     | | если нынешнее положение подходит, т.е. если
     | | x[1]..x[m] = y[last-m+1]..y[last],
     | | то сообщить о совпадении;
     | | last := last+1;
     | end;
     end;

Знатоки рекомендуют проверку совпадения проводить справа налево,
т.е. начиная с последней буквы образца (в которой совпадение за-
ведомо есть). Можно также немного сэкономить, произведы  вычита-
ние заранее и храня не pos[s], а m-pos[s], т.е. число букв в об-
разце справа от последнего вхождения буквы s.

     Возможны разные модификации этого алгоритма. Например, мож-
но строку last:=last+1 заменить на last:=last+(m-u), где u - ко-
ордината второго справа вхождения буквы x[m]  в образец.

     10.5.2. Как проще всего учесть это в программе?

     Решение. При построении таблицы pos написать
        for i:=1 to n-1 do...
в основной программе вместо last:=last+1 написать
        last:= last+m-pos[y[last]];

     Приведенная нами упрощенный вариант алгоритма Бойера - Мура
в некоторых случаях требует существенно больше n действий (число
действий  порядка  mn),  проигрывая  алгоритму Кнута - Морриса -
Пратта.

     10.5.3. Привести пример ситуации, в которой образец не вхо-
дит в слово, но авлгоритму требуется порядка mn действий,  чтобы
это установить.

     Решение. Пусть образец имеет вид  baaa..aa,  а  само  слово
состоит  только  из  букв a. Тогда на каждом шаге несоответствие
выясняется лишь в последний момент.

     Настоящий (не упрощенный) алгоритм Бойера - Мура гарантиру-
ет, что число действий не првосходит C*(m+n) в худшем случае. Он
использует  идеи,  близкие  к  идеям алгоритма Кнута - Морриса -
Пратта. Представим себе, что мы сравнивали  образец  со  входным
словом, идя справа налево. При этом некоторый кусок Z (являющий-
ся  концом образца) совпал, а затем обнаружилось различие: перед
Z в образце стоит не то, что во входном слове. Что можно сказать
в этот момент о входном слове? В нем обнаружен фрагмент,  равный
Z,  а перед ним стоит не та буква, что в образце. Эта информация
может позволить сдвинуть образец на несколько позиций вправо без
риска пропустить его вхождение. Эти сдаиги следует вычислить за-
ранее для каждого конца Z нашего образца. Как  говорят  знатоки,
все  это  (вычисление  таблицы  сдвигов и использовани ее) можно
уложэить в C*(m+n) действий.

     10.6. Алгоритм Рабина

     Этот алгоритм основан на простой идее. Представим себе, что
в слове длины m мы ищем образем длины n. Вырежем окошечко разме-
ра  n  и будем двигать его по входному слову. Нас интересует, не
совпадает ли слово в окошечке с заданным образцом. Сравнивать по
буквам долго. Вместо этого фиксируем некоторую функцию на словах
длины n. Если значения этой функции на слове в окошечке и на об-
разце различны, то совпадения нет. Только если значения одинако-
вы, нужно проверять совпадение по буквам.

     Что мы выигрываем при таком подходе? Казалось бы, ничего  -
ведь  чтобы  вычислить значение функции на слове в окошечке, все
равно нужно прочесть все буквы этого слова. Так уж лучше их сра-
зу сравнить с образцом. Тем не менее выигрыш возможен, и вот  за
счет  чего.  При  сдвиге окошечка слово не меняется полностью, а
лишь добавляется буква в конце и убирается в начале. Хорошо  бы,
чтобы по этим данным можно было бы легко рассчитать, как меняет-
ся функция.

     10.6.1. Привести пример удобной для вычисления функции.

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

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

     10.6.2. Привести пример семейства удобных функций.

     Решение.  Выберем  некоторое  число  p (желательно простое,
смотри далее) и некоторый вычет x по модулю p. Каждое слово дли-
ны n будем рассматривать как последовательность целых чисел (за-
менив буквы кодами). Эти числа будем рассматривать как коэффици-
енты многочлена степени n-1 и вычислим значение этого многочлена
по модулю p в точке x. Это и будет  одна  из  функций  семейства
(для каждой пары p и x получается, таким образом, своя функция).
Сдвиг  окошка на 1 соответствует вычитанию старшего члена, умно-
жению на x и добавлению свободного члена.
     Следующее соображение говорит в пользу того, что совпадения
не слишком вероятны. Пусть число p фиксировано и к тому же прос-
тое,  а  X  и  Y  -  два различных слова длины n. Тогда им соот-
ветствуют различные многочлены (мы предполагаем, что  коды  всех
букв  различны  - это возможно при p, большем числа букв алфави-
та). Совпадение значений функции означает, что в точке x эти два
различных многочлена совпадают, то есть их разность обращается в
0. Разность есть многочлен степени n-1 и имеет не более n-1 кор-
ней. Таким образом, если n много меньше p, то случайному x  мало
шансов попасть в неудачную точку.

     10.7. Более сложные образцы и автоматы

     Мы можем искать не конкретно слово,  а  подслова  заданного
вида.  Например, можно искать слова вида a?b, где вместо ? может
стоять любая буква (иными словами, нас  интересует  буква  b  на
расстоянии 2 после буквы a).

     10.7.1  Указать  конечный  автомат, проверяющий, есть ли во
входном слове фрагмент вида a?b.

     Решение.  Читая  слово, следует помнить, есть ли буква a на
последнем месте и на предпоследнем - пока  не  встретим  искомый
фрагмент. Получаем такой автомат:

    Старое состояние    Очередная буква   Новое состояние

       00                     a                 01
       00                  не a                 01
       01                     a                 11
       01                  не a                 10
       10                     a                 01
       10                     b                 найдено
       10                не a и не b            00
       11                     a                 11
       11                     b                 найдено
       11                не a и не b            10

     Другой стандартный знак в образце - это звездочка  (*),  на
место  которой может быть подставлено любое слово. Например, об-
разец ab*cd означает, что мы ищем подслово ab, за которым следу-
ет что угодно, а затем (на любом расстоянии) следует cd.

     10.7.2. Указать конечный автомат, проверяющий, есть  ли  во
входном слове образец ab*cd (в описанном только что смысле).

     Решение.

    Старое состояние    Очередная буква   Новое состояние

       нач                    a                 a
       нач                 не a                 нач
        a                     b                 ab
        a                     a                 a
        a                  не a и не b          нач
        ab                    c                 abc
        ab                 не c                 ab
        abc                   d                 найдено
        abc                   c                 abc
        abc                не с и не d          ab

     Еще один вид поиска - это поиск любого из слово  некоторого
списка.

     10.7.3.  Дан  список  слов X[1],...,X[k] и слово Y. Опреде-
лить, входит ли хотя бы одно из слов X[i] в слово Y (как подсло-
во). Количество действий не должно превосходить константы, умно-
женной на суммарную длину всех слов (из списка и того, в котором
происходит поиск).

     Решение. Очевидный способ состоит в том, чтобы каждое слово
из  списка проверять отдельно (с помощью одного из рассмотренных
алгоритмов). Однако при этом мы не укладываемся в заданное число
действий (из-за умножения k на длину слова Y).

     Посмотрим на дело с  другой  стороны.  Каждому  образцу  из
списка соответствует конечный автомат с некоторым множество сос-
тояний. Их можно объединить в один автомат, множеством состояний
которого будет произведение множеств состояний всех тех  автома-
тов.  Это  -  очень  большое  множество.  Однако  на  самом деле
большинство его элементов недоступны  (не  могут  появиться  при
чтении входного слова) и за счет этого получается экономия. При-
мерно эту идею (но в измененном виде) мы и будем использовать.

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

     Склеим  все  образцы в дерево, объединив их совпадающие на-
чальные участки. Например, набору образцов

      {aaa, aab, abab}

соответствует дерево

                       a/ *
           a     a    / b
        * --- * --- * --- *
                \b     a     b
                  \ * --- * --- *

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

Читая  входное слово, мы двигаемся по этому дереву: текущая вер-
шина - это наибольшая (самая правая) из вершин, являющихся  кон-
цом  прочитанной части (=наибольший конец прочитанной части, яв-
ляющийся началом одного из образцов).

Определим функцию n, аргументами и значениями  которой  являются
вершины  дерева. Именно, n(P) = наибольшая вершина дерева, явля-
ющаяся концом P. (Напомним, вершины дерева - это слова.) Нам по-
надобится такое утверждение:

     10.7.4. Пусть P - вершина дерева. Докажите,  что  множество
всех вершин, являющихся концами P, равно {n(P), n(n(P)),...}

     Решение.  См.  доказательство  аналогичного утверждения для
алгоритма Кнута - Морриса - Пратта.

     Теперь ясно, что нужно делать, находясь в вершине P и читая
букву y входного слова. Надо просматривать последовательно  вер-
шины  P, n(P), n(n(P)) и т.д., пока не обнаружится такая, из ко-
торой выходит стрелка с буквой y.  Та  вершина,  в  которую  эта
стрелка ведет, и будет нашим следующим положением.

     Остается  понять,  как  для каждой вершины дерева вычислить
указатель на значение функции n в этой вершине. Это делается как
раньше, при этом значения n для более коротких слов используются
при вычислении очередного значения функции n. Это означает,  что
вершины  дерева  следует  просматривать в порядке возрастания их
длины. Нетрудно понять, что все это можно  уложить  в  требуемое
число действий (хотя константа зависит от числа букв  в  алфави-
те).  Относящиеся  к этому подробности см. в главе об алгоритмах
на графах.

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

     Определение.  Пусть  фикисирован конечный алфавит Г, не со-
держащий  символов  'l', 'e', '(', ')', '*' и '|' (они будут ис-
пользоваться для построения регулярных выражений и не должны пе-
ремешиваться с буквами). Регулярные выражения строятся по  таким
правилам:

     (а) буква алфавита Г - регулярное выражение;
     (б) символы 'l', 'e' - регулярные выражения;
     (в)  если A,B,C,..,E - регулярные выражения, то (ABC...E) -
          регулярное выражение.
     (г)   если   A,B,C,..,E   -   регулярные   выражения,    то
          (A|B|C|...|E) - регулярное выражение.
     (д) если A - регулярное выражение, то A* - регулярное выра-
          жение.

Каждое  регулярное  выражение задает множество слов в алфавите Г
по таким правилам:

     (а) букве соответствует одноэлементное множество, состоящее
         из однобуквенного слова, состоящего из этой буквы;
     (б)  символу  'e' соответствует пустое множество, а символу
         'l' - одноэлементное множество, единственным  элементом
         которого является пустое слово;
     (в) регулярному выражению (ABC...E) соответствует множество
         всех слов, которые можно получить, если к  слову  из  A
         приписать слово из B, затем из C,..., затем из E ("кон-
         катенация" множеств);
     (г)   регулярному   выражению  (A|B|C|...|E)  соответствует
         объединение   множеств,   соответствующих    выражениям
         A,B,C,..,E;
     (д) регулярному выражению A* соответствует "итерация"  мно-
         жества, соответствующего выражению A, то есть множество
         всех  слов,  которые  можно так разрезать на куски, что
         каждый кусок  принадлежит  множеству,  соответствующему
         выражению  A.  (В частности, пустое слово всегда содер-
         жится в A*.)

     Примеры

Выражение               Множество

(a|b)*                  все слова из букв a и b
(aa)*                   все слова из четного числа букв a
(l|a|b|aa|ab|ba|bb)     любое слово из не более чем 2 букв a,b

     10.7.5.   Написать  регулярное  выражение,  которому  соот-
ветствует множество всех слов из букв a и  b,  в  которых  число
букв a четно.

     Решение. Выражение b* задает все слова без a, а выражение
               (b* a b* a b*)
- все слова ровно с двумя буквами  a.  Остается  объединить  эти
множества, а потом применить итерацию:
              ((b* a b* a b*) | b*)*

     10.7.6.  Написать регулярное выражение, которое задает мно-
жество всех слов из букв a,b,c, в  которых  слово  bac  является
подсловом.

     Решение. ((a|b|c)* bac (a|b|c)*)

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

     10.7.7. Какие выражения соответствуют образцам a?b и ab*cd,
рассмотренным  ранее? (В образце '*' используется не в том смыс-
ле, что в регулярных выражениях!) Предполается, что алфавит  со-
держит буквы a,b,c,d,e.

     Решение. ((a|b|c|d|e)* a (a|b|c|d|e) b (a|b|c|d|e)*)  и
              ((a|b|c|d|e)* ab (a|b|c|d|e)* cd (a|b|c|d|e)*).

     10.7.8.  Доказать,  что  для  всякого регулярного выражения
можно  построить  конечный  автомат,  который  распознает  соот-
ветствующее этому выражению множество слов.

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

     Будем двигаться различными способами из Н в К, читая  буквы
по  дороге  (на тех стрелках, где они есть). Каждому пути из Н в
К, таким образом, соответствует некоторое слово. А  источнику  в
целом  соответствует  множество  слов  - тех слов, которые можно
прочесть на путях из Н в К.

     Замечание. Если нарисовать состояния конечного  автомата  в
виде  точек,  а переходы при чтении букв изобразить в виде стре-
лок, то станет ясно, что конечный автомат - это  частный  случай
источника.

     Мы  будем строить конечный автомат по регулярному выражению
в два приема.  Сначала  мы  построим  источник,  которому  соот-
ветствует  то  же  самое множество слов. Затем для произвольного
источника построим автомат, который  проверяет,  принадлежит  ли
слово соответствующему множеству.

     10.7.9.  По регулярному выражению построить источник, зада-
ющий то же множество.

     Решение. Индукция по построению регулярного выражения. Бук-
вам соответствуют графы из одной стрелки. Объединение реализует-
ся так:

               |---------|
          ---->|*Н1   К1*|->---
        /      |---------|      \
      /         |---------|       \
    * --------->|*Н2   К2*|--->-----* К
    Н  \        |---------|        /
         \     |---------|       /
           --->|*Н3   К3*|--->--
               |---------|

Нарисована  картинка  для  объединения  трех  множеств,  прямо-
угольники - это источники, соответствующие им; указаны начальные
и конечные вершины. На новых стрелках (их 6) букв не написано.

     Конкатенации соответствует картинка

       |--------|         |--------|          |--------|
 Н*--->|*Н1  К1*|---->----|*Н2  К2*| ---->----|*Н3  К3*|-->--*К
       |--------|         |--------|          |--------|

     Наконец, итерации соответствует картинка

    Н*--------->----------*----------->----------*К
                        /   \
                      /       \
                      |       |
                      V       ^
                      |       |
                    -------------
                    | *Н1   К1* |
                    -------------

     10.7.10. Дан источник. Построить конечный автомат, проверя-
ющий, принадлежит ли входное слово  множеству,  соответствующему
источнику (т.е. можно ли прочесть это слово, идя из Н в К).

     Решение. Состояниями автомата будут множества вершин источ-
ника. Именно, прочтя некоторое начало X входного слова, мы будем
помнить  множество всех вершин источника, в которые можно пройти
из начальной, прочитав на пути слово X.

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

     10.7.11.  Дан источник. Построить регулярное выражение, за-
дающее то же множество, что и этот источник.

     Решение.  (Сообщено  участниками  просеминара  по  логике.)
Пусть источник имеет вершины 1..k. Будем считать, что  1  -  это
начало,  а  k  - конец. Через D[i,j, s] обозначим множество всех
слов, которые можно прочесть на пути из i в j, если  в  качестве
промежуточных  пунктов  разрешается  использовать только вершины
1,...,s. Согласно определению, источнику соответствует множество
D[1,k,k].
     Индукцией  по s будем доказывать регулярность всех множеств
D[i,j,s] при всех i и j. При  s=0  это  очевидно  (промежуточные
вершины  запрещены, поэтому каждое из множеств состоит только из
букв).
     Из чего состоит множество D[i,j,s+1]? Отметим на  пути  мо-
менты, в которых он заходит в s+1-ую вершину. При этом путь раз-
бивается  на  части, каждая из которых уже не заходит в нее. По-
этому легко сообразить, что

 D[i,j,s+1] = (D[i,j,s]| (D[i,s+1,s] D[s+1,s+1,s]* D[s+1,j,s]))

(вольность записи: мы используем для  операций  над  множествами
обозначения  как  для регулярных выражений). Остается воспользо-
ваться предположением индукции.

     10.7.12. Где еще используется то же самое рассуждение?

     Ответ. В алгоритме Флойда вычисления цены кратчайшего пути,
см. главу 9 (Некоторые алгоритмы на графах).

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

     Решение. Для автоматов переход к отрицанию очевиден.

     Замечание.  На  практике важную роль играет число состояний
автомата. Оказывается, что тут все не так просто,  и  переход  о
источника  к автомату требует экспоненциального роста числа сос-
тояний.  Подробное рассмотрение связанных с этим теоретических и
практических вопросов - дело особое.
     Глава 11. Представление множеств. Хеширование.

     11.1. Хеширование с открытой адресацией

     В предыдущей главе было несколько  представлений  для  мно-
жеств,  элементами которых являются целые числа произвольной ве-
личины. Однако в любом из них хотя бы одна из операций  проверки
принадлежности,  добавления  и удаления элемента требовала коли-
чества действий, пропорционального числу элементов множества. На
практике это бывает слишком много. Существуют способы,  позволя-
ющие  получить для всех трех упомянутых операций оценку C*log n.
Один из таких способов мы рассмотрим в следующей главе.  В  этой
главе мы разберем способ, которые хотя и приводит к C*n действи-
ям  в  худшем  случае,  но  зато "в среднем" требует значительно
меньшего их числа. (Мы не будем уточнять слов "в среднем",  хотя
это и можно сделать.) Этот способ называется хешированием.
     Пусть  нам необходимо представлять множества элементов типа
T, причем число элементов заведомо меньше n.  Выберем  некоторую
функцию h, определенную на значениях типа T и принимающую значе-
ния  0..(n-1).  Было  бы  хорошо, чтобы эта функция принимала на
элементах будущего множества по возможности более  разнообразные
значения.  Худший случай - это когда ее значения на всех элемен-
тах хранимого множества одинаковы. Эту  функцию  будем  называть
хеш-функцией.

     Введем два массива

         val:  array [0..n-1] of T;
         used: array [0..n-1] of boolean;

(мы  позволяем  себе писать n-1 в качестве границы в определении
типа, хотя в паскале это не разрешается). В этих массивах  будут
храниться  элементы  множества: оно равно множеству всех val [i]
для тех i, для которых used [i], причем все эти val [i]  различ-
ны.  По  возможности  мы  будем хранить элемент t на месте h(t),
считая это место "исконным" для элемента t.  Однако  может  слу-
читься  так,  что новый элемент, который мы хотим добавить, пре-
тендует на уже занятое место (для которого used истинно). В этом
случае мы отыщем ближайшее справа свободное место и запишем эле-
мент туда. ("Справа" значит  "в  сторону  увеличения  индексов";
дойдя  до  края,  мы  перескакиваем в начало.) По предположению,
число элементов всегда меньше n, так что пустые места гарантиро-
ванно будут.
     Формально говоря, в любой момент должно  соблюдаться  такое
требование:  для любого элемента множества участок справа от его
исконного места до его фактического места полностью заполнен.
     Благодаря этому проверка принадлежности заданного  элемента
t  осуществляется  легко: встав на h(t), двигаемся направо, пока
не дойдем до пустого места или до элемента t.  В  первом  случае
элемент  t отсутствует в множестве, во втором присутствует. Если
элемент отсутствует, то его можно добавить на  найденное  пустое
место.  Если  присутствует, то можно его удалить (положив used =
false).

     11.1.1. В предыдущем  абзаце  есть  ошибка.  Найдите  ее  и
исправьте.

     Решение.  Дело  в  том, что при удалении требуемое свойство
"отсутствия пустот" может нарушиться. Поэтому будем делать  так.
Создав дыру, будем двигаться направо, пока не натолкнемся на еще
одно  пустое место (тогда на этом можно успокоиться) или на эле-
мент, стоящий не на исконном месте. Во втором случае  посмотрим,
не  нужно  ли этот элемент поставить на место дыры. Если нет, то
продолжаем поиск, если да, то затыкаем им старую дыру. При  этом
образуется новая дыра, с которой делаем все то же самое.

     11.1.2.  Написать программы проверки принадлежности, добав-
ления и удаления.

     Решение.
  function принадлежит (t: T): boolean;
  | var i: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | belong := used [i] and (val [i] = t);
  end;

  procedure добавить (t: T);
  | var i: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | if not used [i] then begin
  | | used [i] := true;
  | | val [i] := t;
  | end;
  end;

  procedure исключить (t: T);
  | var i, gap: integer;
  begin
  | i := h (t);
  | while used [i] and (val [i] <> t) do begin
  | | i := (i + 1) mod n;
  | end; {not used [i] or (val [i] = t)}
  | if used [i] and (val [i] = t) then begin
  | | used [i] := false;
  | | gap := i;
  | | i := (i + 1) mod n;
  | | while used [i] do begin
  | | | if i = h (val[i]) then begin
  | | | | i := (i + 1) mod n;
  | | | end else if dist(h(val[i]),i) < dist(gap,i) then begin
  | | | | i := (i + 1) mod n;
  | | | end else begin
  | | | | used [gap] := true;
  | | | | val [gap] := val [i];
  | | | | used [i] := false;
  | | | | gap := i;
  | | | | i := i + 1;
  | | | end;
  | | end;
  | end;
  end;

     Здесь  dist  (a, b) - измеренное по часовой стрелке (слева
направо) расстояние от a до b, т.е.

     dist (a,b) = (b - a + n) mod n.

(Мы прибавили n, так как функция mod правильно работает  только
при положительном делимом.)

     11.1.3. Существует много вариантов хеширования. Один из них
таков: обнаружив, что исконное место (обозначим его  i)  занято,
будем  искать  свободное  не  среди  i+1, i+2,..., а среди r(i),
r(r(i)), r(r(r(i))),..., где r - некоторое отображение 0..n-1  в
себя. Какие при этом будут трудности?

     Ответ. (1) Не гарантируется, что если пустые места есть, то
мы их найдем. (2) При удалении неясно, как заполнять  дыры.  (На
практике во многих случаях удаление не нужно, так что такой спо-
соб  также  применяется.  Считается,  что удачный подбор r может
предотвратить образование "скоплений" занятых ячеек.)

     11.1.4.  Пусть  для  хранения  множества  всех   правильных
русских  слов  в  программе орфографии используется хеширование.
Что нужно добавить, чтобы к тому же  уметь  находить  английский
перевод любого правильного слова?

     Решение.  Помимо  массива  val,  элементы которого являются
русскими словами, нужен параллельный массив их английских  пере-
водов.

     11.2. Хеширование со списками

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

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

     11.2.1. Пусть хеш-функция принимает значения 1..k. Для каж-
дого  значения хеш-функции рассмотрим список всех элементов мно-
жества с данным значением хеш-функции. Будем хранить эти k спис-
ков с помощью переменных

     Содержание: array [1..n] of T;
     Следующий: array [1..n] of 1..n;
     ПервСвоб: 1..n;
     Вершина: array [1..k] of 1..n;

так же, как мы это делали для k  стеков  ограниченной  суммарной
длины.  Напишите  соответствующие программы. (Теперь с удалением
будет меньше проблем.)

     Решение. Перед началом работы  надо  положить  Вершина[i]=0
для  всех  i=1..k,  и  связать  все  места  в  список свободного
пространства,  положив   ПервСвоб=1   и   Следующий[i]=i+1   для
i=1..n-1, а также Следующий[n]=0.

  function принадлежит (t: T): boolean;
  | var i: integer;
  begin
  | | i := Вершина[h(t)];
  | i := Вершина[h(t)];
  | {осталось искать в списке, начиная с i}
  | while (i <> 0) and (Содержание[i] <> t) do begin
  | | i := Следующий[i];
  | end; {(i=0) or (Содержание [i] = t)}
  | belong := Содержание[i]=t;
  end;

  procedure добавить (t: T);
  | var i: integer;
  begin
  | if not принадлежит(t) then begin
  | | i := ПервСвоб;
  | | {ПервСвоб <> 0 - считаем, что не переполняется}
  | | ПервСвоб := Следующий[ПервСвоб]
  | | Содержание[i]:=t;
  | | Следующий[i]:=Вершина[h(t)];
  | | Вершина[h(t)]:=i;
  | end;
  end;

  procedure исключить (t: T);
  | var i, pred: integer;
  begin
  | i := Вершина[h(t)]; pred := 0;
  | {осталось искать в списке, начиная с i;  pred -
  |    предыдущий. если он есть, и 0, если нет}
  | while (i <> 0) and (Содержание[i] <> t) do begin
  | | pred := i; i := Следующий[i];
  | end; {(i=0) or (Содержание [i] = t)}
  | if Содержание[i]=t then begin
  | | {элемент есть, надо удалить}
  | | if pred = 0 then begin
  | | | {элемент оказался первым в списке}
  | | | Вершина[h(t)] := Следующий[i];
  | | end else begin
  | | | Следующий[pred] := Следующий[i]
  | | end;
  | | {осталось вернуть i  в список свободных}
  | | Следующий[i] :=  ПервСвоб;
  | | ПервСвоб:=i;
  | end;
  end;

     11.2.2.   (Для  знакомых  с  теорией  вероятностей.)  Пусть
хеш-функция с m значениями используется для хранения  множества,
в  котором  в данный момент n элементов. Доказать, что математи-
ческое ожидание числа действий в предыдущей задаче не  превосхо-
дит  С*(1+n/m),  если добавляемый (удаляемый, искомый) элемент t
выбран случайно, причем все значения h(t) имеют  равные  вероят-
ности (равные 1/m).

     Решение.   Если   l(i)  -  длина  списка,  соответствующего
хеш-значению i, то число операцией не превосходит C*(1+l(h(i)));
усредняя, получаем искомый ответ, так как сумма всех l(i)  равна
n.

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

     Пусть H - семейство функций, каждая из  которых  отображает
множество T в множество из n элементов (например, 0..n-1). Гово-
рят, что H - универсальное семейство хеш-функций, если для любых
двух различных значений s и t из множества T вероятность события
"h(s)=h(t)"  для  случайной  функции h из семейства H равна 1/n.
(Другими словами, те функции из H, для которых  h(s)=h(t),  сос-
тавляют 1/n-ую часть всех функций в H.)

     Замечание.  Более сильное требование к семейству H могло бы
состоять в том, чтобы для любых двух различных элементов s  и  t
множества  T  значения h(s) и h(t) случайной функции h являются
независимыми случайными величинами,  равномерно  распределенными
на 0..n-1.

     11.2.3. Пусть t[1]..t[u] - произвольная  последовательность
различных элементов множества T. Рассмотрим количество действий,
происходящих при помещении элементов t[1]..t[u] в множество, хе-
шируемое  с помощью функции h из универсального семейства H. До-
казать, что среднее количество действий (усреднение - по всем  h
из H) не превосходит C*u*(1+u/n).

     Решение. Обозначим через m[i] количество элементов последо-
вательности,   для   которых   хеш-функция   равна   i.   (Числа
m[0]..m[n-1] зависят, конечно,  от  выбора  хеш-функции.)  Коли-
чество действий, которое мы хотим оценить, с точностью до посто-
янного множителя равно сумме квадратов чисел m[0]..m[n-1]. (Если
k  чисел попадают в одну хеш-ячейку, то для этого требуется при-
мерно 1+2+...+k действий.) Эту же сумму квадратов можно записать
как число пар <p,q>, для которых h[t[p]]=h[t[q]]. Последнее  ра-
венство,  если его рассматривать как событие при фиксированных p
и q, имеет вероятность 1/n при p<>q,  поэтому  среднее  значение
соответствующего члена суммы равно 1/n, а для всей суммы получа-
ем оценку порядка u*u/n, а точнее u*u/n + u, если учесть члены с
p=q.

   Оценка  этой  задачи  показывает, что в на каждый добавляемый
элемент  приходится  в среднем C*(1+u/n) операций. В этой оценке
дробь u/n имеет смысл "коэффициента заполнения" хеш-таблицы.

     11.2.4. Доказать аналогичное утверждение  для  произвольной
последовательности  операций добавления, поиска и удаления (а не
только для добавления, как в предыдущей задаче).

     Указание. Будем представлять себе, что в ходе  поиска,  до-
бавления  и удаления элемент проталкивается по списку своих кол-
лег с тем же хеш-значением, пока не найдет своего  двойника  или
не  дойдет  до  конца  списка.  Будем называть i-j-столкновением
столкновение t[i] с t[j]. Общее число  действий  примерно  равно
числу всех столкновений плюс число элементов. При t[i]<>t[j] ве-
роятность i-j-столкновения равна  1/n.  Осталось  проследить  за
столкновениями  между  равными  элементами.  Фиксируем некоторое
значение x из множества T и посмотрим на связанные с ним  опера-
ции.  Они  идут по циклу: добавление - проверки - удаление - до-
бавление - проверки - удаление -  ...  Столкновения  между  ними
происходят  между добавляемым элементом и следующими за ним про-
верками (до удаления включительно), поэтому общее  их  число  не
превосходит числа элементов, равных x.

     Теперь приведем примеры универсальных  семейств.  Очевидно,
для  любых конечных множеств A и B семейство всех функций, отоб-
ражающих A в B, является универсальным.  Однако  этот  пример  с
практической  точки зрения бесполезен: для запоминания случайной
функции из этого семейства нужен массив, число элементов в кото-
ром равно числу элементов в множестве A. (А если мы  можем  себе
позволить  такой массив, то никакого хеширования нам не требует-
ся!)

     Более практичные примеры универсальных семейств могут  быть
построены  с помощью несложных алгебраических конструкций. Через
Z[p] мы обозначаем множество вычетов по простому модулю p,  т.е.
{0,1,...,p-1}; арифметические операции в этом множестве выполня-
ются  по модулю p. Универсальное семейство образуют все линейные
функционалы на Z[p] в степени n со значениями в Z[p]. Более под-
робно,  пусть  a[1],...,a[n]  -  произвольные   элементы   Z[p];
рассмотрим отображение

   h: <x[1]...x[n]> |-> a[1]x{1]+...+a{n]z[n]

Мы получаем семейство из (p в степени n) отображений, параметри-
зованное наборами a[1]...a[n].

     11.2.5. Доказать, что это семейство является универсальным.

     Указание. Пусть x и y - различные точки пространства Z[p] в
степени  n.  Какова  вероятность  того, что случайный функционал
принимает на них одинаковые значения?  Другими  словами,  какова
вероятность  того,  что  он равен нулю на их разности x-y? Ответ
дается таким утверждением: пусть u - ненулевой вектор; тогда все
значения случайного функционала на нем равновероятны.

     В  следующей  задаче  множество B={0,1} рассматривается как
множество вычетов по модулю 2.

     11.2.6. Семейство всех линейных отображений из (B в степени
m) в (B в степени n) является универсальным.

     Родственные идеи неожиданно оказываются полезными в  следу-
ющей ситуации (рассказал Д.Варсонофьев). Пусть мы хотим написать
программу, которая обнаруживала (большинство) опечаток в тексте,
но не хотим хранить список всех правильных словоформ.  Предлага-
ется   поступить  так:  выбрать  некоторое  N  и  набор  функций
f[1],...,f[k], отображающих русские слова в 1..N. В массиве из N
битов положим все биты равными нулю, кроме тех, которые являются
значением какой-то функции набора на какой-то правильной  слово-
форме.  Теперь  приближённый тест на правильность словоформы та-
ков: проверить, что значения всех функций набора на этой  слово-
форме попадают на места, занятые единицами.
     Глава 12. Множества и деревья.

     12.1. Представление множеств с помощью деревьев.

     Полное двоичное дерево. T-деревья.

     Нарисуем точку. Из нее проведем две стрелки (влево вверх  и
вправо вверх) в две другие точки. Из каждой из этих точек прове-
дем по две стрелки и так далее. Полученную картинку (в n-ом слое
будет  (2 в степени (n - 1)) точек) называют полным двоичным де-
ревом. Нижнюю точку называют корнем. У каждой вершины  есть  два
сына  (две  вершины, в которые идут стрелки) - левый и правый. У
всякой вершины, кроме корня, есть единственный отец.
     Пусть выбрано некоторое конечное множество  вершин  полного
двоичного  дерева, содержащее вместе с каждой вершиной и всех ее
предков. Пусть на каждой вершине этого множества написано значе-
ние фиксированного типа T (то есть задано отображение  множества
вершин  в  множество  значений типа T). То, что получится, будем
называть T-деревом. Множество всех T-деревьев обозначим Tree(T).
     Рекурсивное определение. Всякое непустое T-дерево  разбива-
ется на три части: корень (несущий пометку из T), левое и правое
поддеревья  (которые  могут быть и пустыми). Это разбиение уста-
навливает взаимно однозначное соответствие между множеством  не-
пустых T-деревьев и произведением T * Tree (T) * Tree (T). Обоз-
начив через empty пустое дерево, можно написать

     Tree (T) = {empty} + T * Tree (T) * Tree (T).

     Поддеревья. Высота.

     Фиксируем  некоторое T-дерево. Для каждой его вершины x оп-
ределено ее левое поддерево (левый сын вершины x и все  его  по-
томки),  правое поддерево (правый сын вершины x и все его потом-
ки) и поддерево с корнем в x (вершина x и все ее потомки). Левое
и правое поддеревья вершины x могут быть пустыми, а поддерево  с
корнем  в x всегда непусто (содержит по крайней мере x). Высотой
поддерева будем считать максимальную длину цепи  y[1]..y[n]  его
вершин, в которой y [i+1] - сын y [i] для всех i. (Высота пусто-
го дерева равна нулю, высота дерева из одного корня - единице.)

     Упорядоченные T-деревья.

     Пусть  на множестве значений типа T фиксирован порядок. На-
зовем T-дерево упорядоченным, если выполнено такое свойство: для
любой вершины x все пометки в ее левом поддереве меньше  пометки
в x, а все пометки в ее правом поддереве больше пометки в x.

     12.1.1.  Доказать,  что  в упорядоченном дереве все пометки
различны.
     Указание. Индукция по высоте дерева.

     Представление множеств с помощью деревьев.

     Каждое дерево будем считать представлением  множества  всех
пометок  на  его вершинах. При этом одно и то же множество может
иметь различные представления.
     Благодаря упорядоченности каждый элемент легко может "найти
свое место" в дереве: придя в какую-то вершину и сравнив себя  с
тем, кто там находится, элемент решает, идти ему налево или нап-
раво. Начав с корня и двигаясь по этому правилу, он либо обнару-
жит, что такой элемент уже есть, либо найдет место, в котором он
должен  быть. Всюду далее мы предполагаем, что на значениях типа
T задан порядок, и рассматриваем только упорядоченные деревья.

     Хранение деревьев в программе.

     Можно было бы сопоставить вершины полного двоичного  дерева
с  числами  1,  2, 3,... (считая, что левый сын (n) = 2n, правый
сын (n) = 2n + 1) и хранить пометки в массиве val [1...]. Однако
этот способ неэкономен, поскольку  тратится  место  на  хранение
пустых вакансий в полном двоичном дереве.

     Более экономен такой способ. Введем три массива

       val: array [1..n] of T;
       left, right: array [1..n] of 0..n;

(n  -  максимальное  возможное число вершин дерева) и переменную
root: 0..n. Каждая вершина хранимого T-дерева будет иметь  номер
- число от 1 до n. Разные вершины будут иметь разные номера. По-
метка  в  вершине  с номером x равна val [x]. Корень имеет номер
root. Если вершина с номером i имеет сыновей, то их номера равны
left [i] и right [i]. Отсутствующим сыновьям соответствует число
0. Аналогичным образом значение root = 0  соответствует  пустому
дереву.
     Для  хранения  дерева  используется лишь часть массива; для
тех i, которые свободны - т.е. не  являются  номерами  вершин  -
значения  val  [i] безразличны. Нам будет удобно, чтобы все сво-
бодные числа были "связаны в список": первое хранится  в  специ-
альное  переменной  free: 0..n, а следующее за i свободное число
хранится в left [i], так что свободны числа

     free, left [free], left [left[free]],...

Для последнего свободного числа i значение left  [i]  =  0.  Ра-
венство  free = 0 означает, что свободных чисел больше нет. (За-
мечание. Мы использовали для связывания свободных вершин  массив
left, но, конечно, с тем же успехом можно было использовать мас-
сив right.)
     Вместо  значения 0 (обозначающего отсутствие вершины) можно
было бы воспользоваться любым другим числом вне 1..n. Чтобы под-
черкнуть это, будем вместо 0 использовать константу null = 0.

     12.1.2. Составить программу,  определяющую,  содержится  ли
элемент  t:  T  в упорядоченном дереве (хранимом так, как только
что описано).

     Решение.

  if root = null then begin
  | ..не принадлежит
  end else begin
  | x := root;
  | {инвариант: остается проверить наличие t в непустом подде-
  |  реве с корнем x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin {left [x] <> null}
  | | | x := left [x];
  | | end else begin {t > val [x], right [x] <> null}
  | | | x := right [x];
  | | end;
  | end;
  | {либо t = val [x], либо t отсутствует в дереве}
  | ..ответ = (t = val [x])
  end;

     12.1.3. Упростить решение, используя следующий трюк. Расши-
рим область определения массива val, добавив  ячейку  с  номером
null и положим val [null] = t.

     Решение.

  val [null] := t;
  x := root;
  while t <> val [x] do begin
  | if t < val [x] then begin
  | | x := left [x];
  | end else begin
  | | x := right [x];
  | end;
  end;
  ..ответ: (x <> null).

     12.1.4.  Составить  программу  добавления элемента t в мно-
жество, представленное упорядоченным деревом (если элемент t уже
есть, ничего делать не надо).

     Решение. Определим процедуру get_free (var i: integer), да-
ющую свободное (не являющееся номером) число i и соответствующим
образом корректирующую список свободных чисел.

  procedure get_free (var i: integer);
  begin
  | {free <> null}
  | i := free;
  | free := left [free];
  end;

С ее использованием программа приобретает вид:

  if root = null then begin
  | get_free (root);
  | left [root] := null; right [root] := null;
  | val [root] := t;
  end else begin
  | x := root;
  | {инвариант: осталось добавить t к непустому поддереву с
  |  корнем в x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | x := right [x];
  | | end;
  | end;
  | if t <> val [x] then begin {t нет в дереве}
  | | get_free (i);
  | | left [i] := null; right [i] := null;
  | | val [i] := t;
  | | if t < val [x] then begin
  | | | left [x] := i;
  | | end else begin {t > val [x]}
  | | | right [x] := i;
  | | end;
  | end;
  end;

     12.1.5. Составить программу удаления  элемента  t  из  мно-
жества, представленного упорядоченным деревом (если его там нет,
ничего делать не надо).

     Решение.

  if root = null then begin
  | {дерево пусто, ничего делать не надо}
  end else begin
  | x := root;
  | {осталось удалить t из поддерева с корнем в x; поскольку
  |  это может потребовать изменений в отце x, введем
  |  переменные  father: 1..n и direction: (l, r);
  |  поддерживаем такой инвариант: если x не корень, то father
  |  - его отец, а direction равно l или r в зависимости от
  |  того, левым или правым сыном является x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | father := x; direction := l;
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | father := x; direction := r;
  | | | x := right [x];
  | | end;
  | end;
  | {t = val [x] или t нет в дереве}
  | if t = val [x] then begin
  | | ..удаление вершины x  с отцом father и направлением
  | |   direction
  | end;
  end;

Удаление  вершины  x происходит по-разному в разных случаях. При
этом используется процедура

  procedure make_free (i: integer);
  begin
  | left [i] := free;
  | free := i;
  end;

она включает число i в список свободных. Различаются 4 случая  в
зависимости от наличия или отсутствия сыновей у удаляемой верши-
ны.

  if (left [x] = null) and (right [x] = null) then begin
  | {x - лист, т.е. не имеет сыновей}
  | make_free (x);
  | if x = root then begin
  | | root := null;
  | end else if direction = l then begin
  | | left [father] := null;
  | end else begin {direction = r}
  | | right [father] := null;
  | end;
  end else if (left[x]=null) and (right[x] <> null) then begin
  | {x удаляется, а right [x] занимает место x}
  | make_free (x);
  | if x = root then begin
  | | root := right [x];
  | end else if direction = l then begin
  | | left [father] := right [x];
  | end else begin {direction = r}
  | | right [father] := right [x];
  | end;
  end else if (left[x] <> null) and (right[x]=null) then begin
  | ..симметрично
  end else begin {left [x] <> null, right [x] <> null}
  | ..удалить вершину с двумя сыновьями
  end;

Удаление вершины с двумя сыновьями нельзя сделать просто так, но
ее  можно предварительно поменять с вершиной, пометка на которой
является непосредственно следующим (в порядке возрастания)  эле-
ментом за пометкой на x.

    y := right [x];
    father := x; direction := r;
    {теперь father и direction относятся к вершине y}
    while left [y] <> null do begin
    | father := y; direction := r;
    | y := left [y];
    end;
    {val [y] - минимальная из пометок, больших val [x],
     y не имеет левого сына}
    val [x] := val [y];
    ..удалить вершину y (как удалять вершину, у которой нет ле-
      вого сына, мы уже знаем)

     12.1.6. Упростить программу удаления, заметив, что  некото-
рые случаи (например, первые два из четырех) можно объединить.

     12.1.7.  Использовать упорядоченные деревья для представле-
ния функций, область определения которых  -  конечные  множества
значений типа T, а значения имеют некоторый тип U. Операции: вы-
числение  значения  на  данном  аргументе, изменение значения на
данном аргументе, доопределение  функции  на  данном  аргументе,
исключение элемента из области определения функции.

     Решение. Делаем как раньше, добавив еще один массив

         func_val: array [1..n] of U;

если val [x] = t, func_val [x] = u, то значение хранимой функции
на t равно u.

     Оценка количества действий.

     Для  каждой из операций (проверки, добавления и исключения)
количество действий не превосходит  C  *  (высота  дерева).  Для
"ровно подстриженного" дерева (когда все листья на одной высоте)
высота  по порядку величины равна логарифму числа вершин. Однако
для кривобокого дерева все может быть гораздо хуже: в  наихудшем
случае  все  вершины  образуют цепь и высота равна числу вершин.
Так случится, если элементы множества добавляются в возрастающем
или убывающем порядке. Можно доказать, однако, что при  добавле-
нии  элементов "в случайном порядке" средняя высота дерева будет
не больше C * (логарифм числа вершин). Если этой оценки "в сред-
нем" мало, необходимы  дополнительные  действия  по  поддержанию
"сбалансированности" дерева. Об этом см. в следующем пункте.

     12.1.8.  Предположим, что необходимо уметь также отыскивать
k-ый элемент множества (в  порядке  возрастания),  причем  коли-
чество  действий  должно  быть не более C*(высота дерева). Какую
дополнительную информацию надо хранить в вершинах дерева?

     Решение. В каждой вершине будем хранить число всех  ее  по-
томков.  Добавление  и исключение вершины требует коррекции лишь
на пути от корня к этой вершине. В процессе поиска k-ой  вершины
поддерживается  такой  инвариант:  искомая вершина является s-ой
вершиной поддерева с корнем в x (здесь s и x - переменные).)

     12.2. Сбалансированные деревья.

     Дерево называется сбалансированным (или АВЛ-деревом в честь
изобретателей этого метода Г.М.Адельсона-Вельского и  Е.М.Ланди-
са),  если  для любой его вершины высоты левого и правого подде-
ревьев этой вершины отличаются не более чем на 1. (В  частности,
когда одного из сыновей нет, другой - если он есть - обязан быть
листом.)

     12.2.1.  Найти  минимальное  и максимальное возможное коли-
чество вершин в сбалансированном дереве высоты n.

     Решение. Максимальное число вершин равно (2 в степени n)  -
1. Если m (n) - минимальное число вершин, то, как легко видеть,
     m (n + 2) = 1 + m (n) + m (n+1),
откуда
     m (n) = fib (n+1) - 1
(fib(n)  -  n-ое число Фибоначчи, fib(0)=1, fib(1)=1, fib(n+2) =
fib(n) + fib(n+1)).

     12.2.2. Доказать, что сбалансированное дерево с n вершинами
имеет высоту не больше C * (log n) для некоторой константы C, не
зависящей от n.

     Решение. Индукцией по n легко доказать, что fib [n+1] >= (a
в степени n), где a - больший корень квадратного уравнения a*a =
1 + a, то есть a = (sqrt(5)  +  1)/2.  Остается  воспользоваться
предыдущей задачей.

     Вращения.

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

     Пусть вершина a имеет правого сына b. Обозначим через P ле-
вое поддерево вершины a, через Q и R - левое и правое поддеревья
вершины b.

     Упорядоченность дерева требует, чтобы P < a < Q  <  b  <  R
(точнее  следовало бы сказать "любая пометка на P меньше пометки
на a", "пометка на a меньше любой пометки на Q" и  т.д.,  но  мы
позволим  себе  этого не делать). Точно того же требует упорядо-
ченность дерева с корнем b, его левым сыном a, в котором P и Q -
левое и правое поддеревья a, R -  правое  поддерево  b.  Поэтому
первое дерево можно преобразовать во второе, не нарушая упорядо-
ченности.  Такое  преобразование  назовем малым правым вращением
(правым - поскольку существует симметричное, левое, малым - пос-
кольку есть и большое, которое мы сейчас опишем).

     Пусть b - правый сын a, c - левый сын b, P -левое поддерево
a, Q и R -левое и правое поддеревья c, S - правое  поддерево  b.
Тогда P < a < Q < c < R < b < S.

Такой же порядок соответствует дереву с корнем c, имеющим левого
сына a и правого сына b, для которого P и Q - поддеревья вершины
a,  а R и S - поддеревья вершины b. Соответствующее преобразова-
ние будем называть большим правым вращением. (Аналогично опреде-
ляется симметричное ему большое левое вращение.)

     12.2.3. Дано дерево, сбалансированное всюду, кроме корня, в
котором разница высот равна 2 (т.е. левое  и  правое  поддеревья
корня сбалансированы и их высоты отличаются на 2). Доказать, что
оно  может  быть  превращено в сбалансированное одним из четырех
описанных преобразований, причем высота  его  останется  прежней
или уменьшится на 1.

     Решение.  Пусть более низким является, например, левое под-
дерево, и его высота равна k.  Тогда  высота  правого  поддерева
равна k+2. Обозначим корень через a, а его правого сына (он обя-
зательно  есть)  через  b.  Рассмотрим левое и правое поддеревья
вершины b. Одно из них обязательно имеет высоту  k+1,  а  другое
может  иметь  высоту  k или k+1 (меньше k быть не может, так как
поддеревья сбалансированы). Если высота левого  поддерева  равна
k+1,  а  правого  - k, до потребуется большое правое вращение; в
остальных случаях помогает малое.

------------------------------------
------------------------------------
------------------------------------

                                        высота уменьшилась на 1

------------------------------------
------------------------------------
------------------------------------

                                         высота не изменилась

   k-1 или k (в одном из случаев k)

------------------------------------
------------------------------------
------------------------------------
                                        высота уменьшилась на 1

        Три случая балансировки дерева.

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

     Решение. Будем доказывать более общий факт:

     Лемма.  Если в сбалансированном дереве X одно из его подде-
ревьев Y заменили на сбалансированное дерево Z, причем высота  Z
отличается  от  высоты  Y не более чем на 1, то полученное такой
"прививкой" дерево можно превратить в сбалансированное  вращени-
ями  (причем количество вращений не превосходит высоты, на кото-
рой делается прививка).
     Частным случаем прививки является замена пустого  поддерева
на лист или наоборот, так что достаточно доказать эту лемму.
     Доказательство  леммы. Индукция по высоте, на которой дела-
ется прививка. Если она происходит в корне (заменяется все дере-
во целиком), то все очевидно ("привой"  сбалансирован  по  усло-
вию). Пусть заменяется некоторое поддерево, например, левое под-
дерево некоторой вершины x. Возможны два случая.
     (1)  После прививки сбалансированность в вершине x не нару-
шилась (хотя, возможно, нарушилась сбалансированность в  предках
x:  высота поддерева с корнем в x могла измениться). Тогда можно
сослаться на предположение индукции, считая,  что  мы  прививали
целиком поддерево с корнем в x.
     (2) Сбалансированность в x нарушилась. При этом разница вы-
сот  равна 2 (больше она быть не может, так как высота Z отлича-
ется от высоты Y не более чем на 1). Разберем два варианта.
    (2а) Выше правое  (не  заменявшееся)  поддерево  вершины  x.
Пусть высота левого (т.е. Z) равна k, правого - k+2. Высота ста-
рого  левого поддерева вершины x (т.е. Y) была равна k+1. Подде-
рево с корнем x имело в исходном дереве высоту k+3, и эта высота
не изменилась после прививки.
     По предыдущей задаче вращение преобразует поддерево с  кор-
нем в x в сбалансированное поддерево высоты k+2 или k+3. То есть
высота  поддерева с корнем x - в сравнении с его прежней высотой
- не изменилась или уменьшилась на 1, и мы можем воспользоваться
предположением индукции.

      -------------                     ----------------
      -------------                     ----------------
      -------------k                    ----------------k
 2а                                 2б

     (2б) Выше левое поддерево вершины x.  Пусть  высота  левого
(т.е. Z) равна k+2, правого - k. Высота старого левого поддерева
(т.е.  Y) была равна k+1. Поддерево с корнем x в исходном дереве
X имело высоту k+2, после прививки она стала  равна  k+3.  После
подходящего  вращения (см. предыдущую задачу) поддерево с корнем
в x станет сбалансированным, его высота будет равна k+2 или k+3,
так что изменение высоты по сравнению с высотой поддерева с кор-
нем x в дереве X не превосходит 1 и можно сослаться на предполо-
жение индукции.

     12.2.5. Составить программы добавления и  удаления  элемен-
тов,  сохраняющие  сбалансированность.  Число действий не должно
превосходить C*(высота дерева). Разрешается хранить  в  вершинах
дерева дополнительную информацию, необходимую при балансировке.

     Решение. Будем хранить для каждой  вершины  разницу  между
высотой ее правого и левого поддеревьев:

  diff [i] = (высота правого поддерева вершины с номером i) -
             (высота левого поддерева вершины с номером i).

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

          Малое правое вращение

          Большое правое вращение

     (2)  После  преобразований  мы  должны также изменить соот-
ветственно значения в массиве diff. Для этого  достаточно  знать
высоты деревьев P, Q, ... с точностью до константы, поэтому мож-
но предполагать, что одна из высот равна нулю.

     Вот процедуры вращений:

  procedure SR (a:integer); {малое правое вращение с корнем a}
  | var b: 1..n; val_a,val_b: T; h_P,h_Q,h_R: integer;
  begin
  | b := right [a]; {b <> null}
  | val_a := val [a]; val_b := val [b];
  | h_Q := 0; h_R := diff[b]; h_P := (max(h_Q,h_R)+1)-diff[a];
  | val [a] := val_b; val [b] := val_a;
  | right [a] := right [b] {поддерево R}
  | right [b] := left [b] {поддерево Q}
  | left [b] := left [a] {поддерево P}
  | left [a] := b;
  | diff [b] := h_Q - h_P;
  | diff [a] := h_R - (max (h_P, h_Q) + 1);
  end;

  procedure BR (a:integer);{большое правое вращение с корнем a}
  | var b,c: 1..n; val_a,val_b,val_c: T;
  |     h_P,h_Q,h_R,h_S: integer;
  begin
  | b := right [a]; c := left [b]; {b,c <> null}
  | val_a := val [a]; val_b := val [b]; val_c := val [c];
  | h_Q := 0; h_R := diff[c]; h_S := (max(h_Q,h_R)+1)+diff[b];
  | h_P := 1 + max (h_S, h_S-diff[b]) - diff [a];
  | val [a] := val_c; val [c] := val_a;
  | left [b] := right [c] {поддерево R}
  | right [c] := left [c] {поддерево Q}
  | left [c] := left [a] {поддерево P}
  | left [a] := c;
  | diff [b] := h_S - h_R;
  | diff [c] := h_Q - h_P;
  | diff [a] := max (h_S, h_R) - max (h_P, h_Q);
  end;

Левые вращения (большое и малое) записываются симметрично.

     Процедуры  добавления  и  удаления  элементов  пишутся  как
раньше, но только добавление и  удаление  должно  сопровождаться
коррекцией  массива  diff  и восстановлением сбалансированности.
При этом используется процедура с такими свойствами:

   дано:  левое и правое поддеревья вершины с номером a сбалан-
       сированы, в самой вершине разница высот не больше  2,  в
       поддереве с корнем a массив diff заполнен правильно;
   надо:  поддерево с корнем a сбалансировано и массив diff со-
       ответственно изменен, d - изменение его высоты (равно  0
       или -1); в остальной части все осталось как было}

  procedure balance (a: integer; var d: integer);
  begin {-2 <= diff[a] <= 2}
  | if diff [a] = 2 then begin
  | | b := right [a];
  | | if diff [b] = -1 then begin
  | | | BR (a); d := -1;
  | | end else if diff [b] = 0 then begin
  | | | SR (a); d := 0;
  | | end else begin {diff [b] = 1}
  | | | SR (a); d := - 1;
  | | end;
  | end else if diff [a] = -2 then begin
  | | b := left [a];
  | | if diff [b] = 1 then begin
  | | | BL (a); d := -1;
  | | end else if diff [b] = 0 then begin
  | | | SL (a); d := 0;
  | | end else begin {diff [b] = -1}
  | | | SL (a); d := - 1;
  | | end;
  | end else begin {-2 < diff [a] < 2, ничего делать не надо}
  | | d := 0;
  | end;
  end;

     Восстановление  сбалансированности  требует   движения   от
листьев  к  корню, поэтому будем хранить в стеке путь от корня к
рассматриваемой в данный момент вершине. Элементами стека  будут
пары (вершина, направление движения из нее), т.е. значения типа

        record
        | vert: 1..n; {вершина}
        | direction : (l, r); {l - левое, r- правое}
        end;

Программа добавления элемента t теперь выглядит так:

  if root = null then begin
  | get_free (root);
  | left [root] := null; right [root] := null; diff[root] := 0;
  | val [root] := t;
  end else begin
  | x := root; ..сделать стек пустым
  | {инвариант: осталось добавить t к непустому поддереву с
  |  корнем в x; стек содержит путь к x}
  | while ((t < val [x]) and (left [x] <> null)) or
  | |     ((t > val [x]) and (right [x] <> null)) do begin
  | | if t < val [x] then begin
  | | | ..добавить в стек пару <x, l>
  | | | x := left [x];
  | | end else begin {t > val [x]}
  | | | ..добавить в стек пару <x, r>
  | | | x := right [x];
  | | end;
  | end;
  | if t <> val [x] then begin {t нет в дереве}
  | | get_free (i); val [i] := t;
  | | left [i] := null; right [i] := null; diff [i] := 0;
  | | if t < val [x] then begin
  | | | ..добавить в стек пару <x, l>
  | | | left [x] := i;
  | | end else begin {t > val [x]}
  | | | ..добавить в стек пару <x, r>
  | | | right [x] := i;
  | | end;
  | | d := 1;
  | | {инвариант: стек содержит путь к изменившемуся поддереву,
  | |  высота  которого увеличилась по сравнению с высотой в
  | |  исходном дереве на d (=0 или 1); это поддерево  сбалан-
  | |  сировано; значения diff для его вершин правильны; в ос-
  | |  тальном дереве  все  осталось  как  было  - в частности,
  | |  значения diff}
  | | while (d <> 0) and ..стек непуст do begin {d = 1}
  | | | ..взять из стека пару в <v, direct>
  | | | if direct = l then begin
  | | | | if diff [v] = 1 then begin
  | | | | | c := 0;
  | | | | end else begin
  | | | | | c := 1;
  | | | | end;
  | | | | diff [v] := diff [v] - 1;
  | | | end else begin {direct = r}
  | | | | if diff [v] = -1 then begin
  | | | | | c := 0;
  | | | | end else begin
  | | | | | c := 1;
  | | | | end;
  | | | | diff [v] := diff [v] + 1;
  | | | end;
  | | | {c = изменение высоты поддерева с корнем в v по сравне-
  | | |  нию с исходным деревом; массив diff содержит правиль-
  | | |  ные значения для этого поддерева; возможно нарушение
  | | |  сбалансированности в v}
  | | | balance (v, d1); d := c + d1;
  | | end;
  | end;
  end;

Легко  проверить, что значение d может быть равно только 0 или 1
(но не -1): если c = 0, то diff [v] = 0 и балансировка не произ-
водится.

     Программа удаления строится аналогично. Ее  основной  фраг-
мент таков:

  {инвариант: стек содержит путь к изменившемуся поддереву,
   высота которого изменилась по сравнению с высотой в
   исходном дереве на d (=0 или -1); это поддерево
   сбалансировано; значения diff для его вершин правильны;
   в остальном дереве все осталось как было -
   в частности, значения diff}
  while (d <> 0) and ..стек непуст do begin
  | {d = -1}
  | ..взять из стека пару в <v, direct>
  | if direct = l then begin
  | | if diff [v] = -1 then begin
  | | | c := -1;
  | | end else begin
  | | | c := 0;
  | | end;
  | | diff [v] := diff [v] + 1;
  | end else begin {direct = r}
  | | if diff [v] = 1 then begin
  | | | c := -1;
  | | end else begin
  | | | c := 0;
  | | end;
  | | diff [v] := diff [v] - 1;
  | end;
  | {c = изменение высоты поддерева с корнем в v по срав-
  |  нению с исходным деревом; массив diff содержит
  |  правильные значения для этого поддерева;
  |  возможно нарушение сбалансированности в v}
  | balance (v, d1);
  | d := c + d1;
  end;

Легко проверить, что значение d может быть равно только 0 или -1
(но  не -2): если c = -1, то diff [v] = 0 и балансировка не про-
изводится.
     Отметим также, что наличие стека делает излишними  перемен-
ные father и direction (их роль теперь играет вершина стека).

     12.2.6. Доказать, что при добавлении элемента
     (а)  второй из трех случаев балансировки (см. рисунок выше)
невозможен;
     (б) полная балансировка требует не  более  одного  вращения
(после чего все дерево становится сбалансированным),
     в  то  время  как  при удалении элемента может понадобиться
много вращений.

     Замечание. Мы старались  записать  программы  добавления  и
удаления  так,  чтобы  они были как можно более похожими друг на
друга. Используя специфику каждой из них,  можно  многое  упрос-
тить.

     Существуют  и другие способы представления множеств, гаран-
тирующие число действий порядка log n на каждую операцию. Опишем
один из них (называемый Б-деревьями).
     До сих пор каждая вершина содержала один элемент  хранимого
множества.  Этот  элемент  служил  границей между левым и правым
поддеревом. Будем теперь хранить в вершине k >= 1 элементов мно-
жества (число k может меняться от вершины к вершине, а также при
добавлении и удалении новых элементов, см. далее). Эти k элемен-
тов служат разделителями для k+1  поддерева.  Пусть  фиксировано
некоторое  число n >= 1. Будем рассматривать деревья, обладающие
такими свойствами:
     (1) Каждая вершина содержит от n до 2n элементов (за исклю-
чением корня, который может содержать любое число элементов от 0
до 2n).
     (2) Вершина с k элементами либо имеет  k+1  сына,  либо  не
имеет сыновей вообще (такие вершины называются листьями).
     (3) Все листья находятся на одной и той же высоте.
     Добавление элемента происходит так. Если лист, в который он
попадает,  неполон  (т.е.  содержит  менее 2n элементов), то нет
проблем. Если он полон, то 2n+1 элемент (все  элементы  листа  и
новый  элемент) разбиваем на два листа по n элементов и разделя-
ющий их серединный элемент. Этот серединный элемент  надо  доба-
вить  в вершину предыдущего уровня. Это возможно, если в ней ме-
нее 2n элементов. Если и она полна, то ее разбивают на две,  вы-
деляют  серединный элемент и т.д. Если в конце концов мы захотим
добавить элемент в корень, а он окажется полным, то корень  рас-
щепляется на две вершины, а высота дерева увеличивается на 1.
     Удаление элемента. Удаление элемента, находящемся не в лис-
те, сводится к удалению непосредственно следующего за ним, кото-
рый находится в листе. Поэтому достаточно научиться удалять эле-
мент  из  листа.  Если лист при этом становится неполным, то его
можно пополнить за счет соседнего листа - если только  и  он  не
имеет  минимально  возможный  размер  n. Если же оба листа имеют
размер n, то на них вместе 2n элементов, вместе с разделителем -
2n+1. После удаления одного элемента остается 2n элементов - как
раз на один лист. Если при этом вершина предыдущего уровня  ста-
новится меньше нормы, процесс повторяется и т.д.

     12.2.7. Реализовать описанную схему хранения множеств, убе-
дившись,  что она также позволяет обойтись C*log(n) действий для
операций включения, исключения и проверки принадлежности.

     12.2.8. Можно определять сбалансированность  дерева  иначе:
требовать, чтобы для каждой вершины ее левое и правое поддеревья
имели не слишком сильно отличающиеся количества вершин. (Преиму-
щество такого определения состоит в том, что при вращениях изме-
няется  сбалансированность  только в одной вершине.) Реализовать
на основе этой  идеи  способ  хранения  множеств,  гарантирующий
оценку  в  C*log(n)  действий для включения, удаления и проверки
принадлежности. (Указание. Он также использует большие  и  малые
вращения.  Подробности см. в книге Рейнгольда, Нивергельта и Део
"Комбинаторные алгоритмы".)
     Глава 13. Контекстно-свободные грамматики.

     13.1. Контекстно-свободные грамматики. Общий алгоритм  раз-
          бора.

     Чтобы  определить  то,  что  называют  контекстно-свободной
грамматикой (КС-грамматикой), надо:
     (а) указать конечное множество A, называемое алфавитом; его
элементы  называют символами; конечные последовательности симво-
лов называют словами (в данном алфавите);
     (б) разделить все символы алфавита A на две группы:  терми-
нальные ("окончательные") и нетерминальные ("промежуточные");
     (в)  выбрать среди нетерминальных символов один, называемый
начальным;
     (г) указать конечное число правил грамматики, каждое из ко-
торых должно иметь вид
     K -> X
где K - некоторый нетерминальный символ, а X - слово (в него мо-
гут входить и терминальные, и нетерминальные символы).

     Пусть  фиксирована  КС-грамматика  (мы часто будем опускать
приставку "КС-", так как других грамматик у нас не будет). Выво-
дом в этой грамматике называется последовательность  слов  X[0],
X[1],..., X[n], в которой X[0] состоит из одного символа, и этот
символ  - начальный, а X[i+1] получается из X[i] заменой некото-
рого нетерминального символа K на слово X по  одному  из  правил
грамматики.  Слово, составленное из терминальных символов, назы-
вается выводимым, если существует вывод, который  им  кончается.
Множество всех выводимых слов (из терминальных символов) называ-
ется языком, порождаемым данной грамматикой.
     В  этой  и следующих главах мы будем ходить вокруг да около
такого вопроса: дана КС-грамматика; построить алгоритм,  который
по любому слову проверяет, выводимо ли оно в этой грамматике.

     Пример 1.   Алфавит:

            ( ) [ ] E

(четыре  терминальных  символа  и один нетерминальный символ E).
Начальный символ: e.
Правила:
                E -> (E)
                E -> [E]
                E -> EE
                E ->

(в последнем правиле справа стоит пустое слово).

     Примеры выводимых слов:

                     (пустое слово)
                ()
                ([])
                ()[([])]
                [()[]()[]]

     Примеры невыводимых слов:

                (
                )(
                (]
                ([)]

Эта грамматика встречалась в разделе 00 (где выводимость  в  ней
проверялась с помощью стека).

     Пример 2. Другая грамматика, порождающая тот же язык:

Алфавит: ( ) [ ] T E

Правила:
           E ->
           E -> TE
           T -> (E)
           T -> [E]

Начальным символом во всех приводимых далее примерах будем  счи-
тать  символ,  стоящий  в  левой части первого правила (в данном
случае это символ T), не оговаривая этого особо.

     Для каждого нетерминального символа можно рассмотреть  мно-
жество всех слов из терминальных символов, которые из него выво-
дятся (аналогично тому, как это сделано для начального символа в
определении выводимости в грамматике). Каждое правило грамматики
можно  рассматривать  как свойство этих множеств. Покажем это на
примере только что приведенной грамматики. Пусть SetT и  SetE  -
множества  слов (из скобок), выводимых из нетерминалов T и E со-
ответственно.  Тогда  правилам  грамматики  соответствуют  такие
свойства:

E ->            SetE содержит пустое слово
E -> TE         если слово A принадлежит SetT,
                слово B принадлежит
                SetE, то слово AB принадлежит SetE
T -> [E]        если A принадлежит
                SetE, то слово [A] принадлежит SetT
T -> (E)        если A принадлежит
                SetE, то слово (A) принадлежит SetT

Сформулированные  свойства множеств SetE, SetT не определяют эти
множества однозначно (например, они остаются верными, если в ка-
честве SetE и SetT взять множество всех слов). Однако можно  до-
казать,  что  множества,  задаваемые грамматикой, являются мини-
мальными среди удовлетворяющих этим условиям.

     13.1.1.  Сформулируйте точно и докажите это утверждение для
произвольной контекстно-свободной грамматики.

     13.1.2. Постройте грамматику, в которой выводимы слова
     (а) 00..0011..11 (число нулей равно числу единиц);
     (б) 00..0011..11 (число нулей вдвое больше числа единиц);
     (в) 00..0011..11 (число нулей больше числа единиц);
(и только они).

     13.1.3.  Доказать, что не существует КС-грамматики, в кото-
рой были бы выводимы слова вида  00..0011..1122..22,  в  которых
числа нулей, единиц и двоек равны, и только они.
     Указание. Докажите следующую лемму о произвольной  КС-грам-
матике:  для  любого  достаточно  длинного слова F, выводимого в
этой грамматике,  существует  такое  его  представление  в  виде
ABCDE,  что  любое  слово  вида AB..BCD..DE, где B и D повторены
одинаковое число раз, также выводимо  в  этой  грамматике.  (Это
можно  установить,  найдя  нетерминальный  символ, оказывающийся
своим собственным "наследником" в процессе вывода.)

     Нетерминальный символ можно рассматривать как "родовое имя"
для выводимых из него слов. В следующем примере для  наглядности
в   качестве   нетерминальных  символов  использованы  фрагменты
русских слов, заключенные в  угловые  скобки.  (С  точки  зрения
грамматики каждое такое слово - один символ!)

     Пример 3. Алфавит:

терминалы: + * ( ) x
нетерминалы: <выр>, <оствыр>, <слаг>, <остслаг>, <множ>
правила:

    <выр>     -> <слаг> <оствыр>
    <оствыр>  -> + <выр>
    <оствыр>  ->
    <слаг>    -> <множ> <остслаг>
    <остслаг> -> * <слаг>
    <остслаг> ->
    <множ>    -> x
    <множ>    -> ( <выр> )

Согласно этой грамматике, выражение  (<выр>)  -  это  последова-
тельность  слагаемых  (<слаг>), разделенных плюсами, слагаемое -
это последовательность множителей (<множ>), разделенных звездоч-
ками (знаками умножения), а множитель - это либо буква  x,  либо
выражение в скобках.

     13.1.4. Приведите пример другой грамматики, задающей тот же
язык.

     Ответ. Вот один из вариантов:
    <выр> -> <выр> + <выр>
    <выр> -> <выр> * <выр>
    <выр> -> x
    <выр> -> ( <выр> )

Эта  грамматика  хоть и проще, но в некоторых отношениях хуже, о
чем мы еще будем говорить.

     13.1.5. Дана произвольная КС-грамматика. Построить алгоритм
проверки принадлежности задаваемому ей языку, работающий полино-
миальное время (т.е. число действий не превосходит  полинома  от
длины проверяемого слова; полином может зависеть от грамматики).

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

     (1) Пусть в грамматике есть нетерминалы K1,...,Kn. Построим
новую грамматику с нетерминалами K1',...,Kn' так, чтобы выполня-
лось такое свойство: из Ki' выводятся (в новой грамматике) те же
слова, что из Ki в старой, за исключением пустого слова, которое
не выводится.
     Чтобы выполнить такое преобразование грамматики, надо выяс-
нить, из каких нетерминалов исходной грамматики выводится пустое
слово,  а  затем каждое правило заменить на совокупность правил,
получающихся, если в правой части опустить какие-либо из  нетер-
миналов, из которых выводится пустое слово, а у остальных поста-
вить штрихи. Например, если в исходной грамматике было правило

     K -> L M N,

причем  из L и N выводится пустое слово, а из M нет, то это пра-
вило надо заменить на правила

     K'-> L'M'N'
     K'->   M'N'
     K'-> L'M'
     K'->   M'

     (2) Итак, мы свели дело к грамматике, где ни из одного  не-
терминала не выводится пустое слово. Теперь устраним "циклы" ви-
да
     K -> L
     L -> M
     M -> N
     N -> K
(в правой части каждого правила один символ, и эти символы обра-
зуют  цикл  произвольной  длины): это легко сделать, отождествив
все входящие в цикл нетерминалы.

     (3) Теперь проверка принадлежности какого-либо слова языку,
порожденному  грамматикой,  может  выполняться  так: для каждого
подслова проверяемого слова и для каждого нетерминала  выясняем,
порождается ли это подслово этим нетерминалом. При этом подслова
проверяются  в порядке возрастания длин, а нетерминалы - в таком
порядке, чтобы при наличии правила K -> L нетерминал L проверял-
ся раньше нетерминала K. (Это возможно в  силу  отсутствия  цик-
лов.) Поясним этот процесс на примере.
     Пусть в грамматике есть правила
        K -> L
        K -> M N L
и других правил, содержащих K в левой части, нет. Мы  хотим  уз-
нать,  выводится  ли  данное слово A из нетерминала K. Это будет
так в одном из случаев: (1) если A выводится из L;  (2)  если  A
можно разбить на непустые слова B, C, D, для которых B выводится
из  M,  C выводится из N, а D выводится из L. Вся эта информация
уже есть (слова B, C, D короче A, а L рассмотрен до K).
     Легко  видеть, что число действий этого алгоритма полиноми-
ально. Степень полинома зависит от числа нетерминалов  в  правых
частях  правил и может быть понижена, если грамматику преобразо-
вать к форме, в которой правая часть каждого правила содержит  1
или  2  нетерминала (это легко сделать, вводя новые нетерминалы:
например, правило K -> LMK можно заменить на K -> LN и N ->  MK,
где N - новый нетерминал).

     13.1.6. Рассмотрим грамматику с  единственным  нетерминалом
K, нетерминалами 1, 2, 3 и правилами

     K -> 0
     K -> 1 K
     K -> 2 K K
     K -> 3 K K K

Как  проверить  выводимость слова в этой грамматике, читая слово
слева направо? (Число действий при прочтении одной буквы  должно
быть ограничено.)

     Решение.  Хранится целая переменная n, инвариант: слово вы-
водимо <-> непрочитанная часть представляет  собой  конкатенацию
(соединение) n выводимых слов.

     13.1.7. Тот же вопрос для грамматики

          K -> 0
          K -> K 1
          K -> K K 2
          K -> K K K 3

     13.2. Метод рекурсивного спуска.

     В отличие от алгоритма предыдущего раздела (представляющего
чисто теоретический интерес), алгоритмы на  основе  рекурсивного
спуска  часто используются на практике. Этот метод применим, од-
нако, далеко не ко всем грамматикам. Мы обсудим необходимые  ог-
раничения позднее.
     Идея  метода рекурсивного спуска такова. Для каждого нетер-
минала K мы строим процедуру ReadK, которая - в применении к лю-
бому входному слову x - делает две вещи:
     (1) находит наибольшее начало z слова x, которое может быть
началом выводимого из K слова;
     (2) сообщает, является ли найденное слово z выводимым из K.

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

     Next: Symbol

дающая  первый  непрочитанный  символ.  Ее значениями могут быть
терминальные символы, а также специальный  символ  EOI  (End  Of
Input  -  конец входа), означающий, что все слово уже прочитано.
Вызов  этой функции, естественно, не сдвигает границы между про-
читанной и непрочитанной частью - для этого есть процедура Move,
которая  сдвигает  границу  на один символ. (Она применима, если
Next <> EOI.) Пусть, наконец, имеется булевская переменная b.

     Теперь мы можем сформулировать наши требования к  процедуре
ReadK. Они состоят в следующем:
     (1)  ReadK  прочитывает  из  оставшейся  части слова макси-
мальное начало A, являющееся началом некоторого слова, выводимо-
го из K;
     (2) значение b становится истинным или ложным в зависимости
от того, является ли A выводимым из K или лишь невыводимым нача-
лом выводимого (из K) слова.

     Для удобства введем такую терминологию: выводимое из K сло-
во будем называть K-словом, а любое начало любого выводимого  из
K  слова - K-началом. Требования (1) и (2) вместе будем выражать
словами "ReadK корректна для K".

     Начнем с рассмотрения частного случая. Пусть правило
        K -> L M
является единственным правилом грамматики, содержащим K в  левой
части, пусть L, M - нетерминалы и ReadL, ReadM - корректные (для
них) процедуры.
     Рассмотрим такую процедуру:

     procedure ReadK;
     begin
     | ReadL;
     | if b then begin
     | | ReadM;
     | end;
     end;

     13.2.1.  Привести  пример, когда эта процедура будет некор-
ректной для K.
     Ответ. Пусть из L выводится любое слово вида 00..00, а из M
выводится лишь слово 01. Тогда из K выводится  слово  00001,  но
процедура ReadK этого не заметит.

     Укажем достаточноые условия корректности  процедуры  ReadK.
Для  этого нам понадобятся некоторые обозначения. Пусть фиксиро-
ваны КС-грамматика и некоторый  нетерминал  N  этой  грамматики.
Рассмотрим  N-слово A, которое имеет собственное начало B, также
являющееся N-словом (если такие есть). Для любой пары таких слов
A и B рассмотрим терминальный символ, идущий в A непосредственно
за B. Множество всех таких терминалов обозначим  Посл(N).  (Если
никакое N-слово не является собственным началом другого N-слова,
то множество Посл(N) пусто.)

     13.2.2. Указать (а) Посл(E) для примера 1;  (б)  Посл(E)  и
Посл(T)  для примера 2; (в) Посл(<слаг>) и Посл(<множ>) для при-
мера 3.
     Ответ.  (а)  Посл(e)  =  {  [, ( }. (б) Посл(e) = { [, ( };
Посл(t) пусто (никакое t-слово не является началом другого). (в)
Посл(<слаг>) = {*}; Посл(<множ>) пусто.

Кроме  того,  для  каждого  нетерминала N обозначим через Нач(N)
множество всех терминалов, являющихся первыми  буквами  непустых
N-слов.  Это  обозначение  - вместе с предыдущим - позволит дать
достаточное условие корректности процедуры ReadK в описанной вы-
ше ситуации.

     13.2.3.  Доказать,  что  если  Посл  (L)  не пересекается с
Нач(M) и множество всех M-слов непусто, то ReadK корректна.

     Решение. Рассмотрим два случая. (1) Пусть после ReadL  зна-
чение  переменной  b  ложно. В этом случае ReadM читает со входа
максимальное M-начало A, не являющееся  M-словом.  Оно  является
K-началом (здесь важно, что множество L-слов непусто.). Будет ли
оно максимальным K-началом среди начал входа? Если нет, то A яв-
ляется  началом  слова BC, где B есть L-слово, C есть M-начало и
BC - более длинное начало входа, чем A. Если B длиннее A, то A -
не максимальное начало входа, являющееся L-началом, что противо-
речит корректности ReadL. Если B = A, то A было бы  L-словом,  а
это  не так. Значит, B короче A, C непусто и первый символ слова
C следует в A за последним символом слова B, т.е. Посл(L)  пере-
секается с Нач(M). Противоречие. Итак, A максимально. Из сказан-
ного  следует  также,  что  A не является K-словом. Корректность
процедуры ReadK в этом случае проверена.
     (2) Пусть после ReadL значение переменной b истинно.  Тогда
прочитанное  процедурой  ReadK  начало входа имеет вид AB, где A
есть L-слово, а B есть M-начало. Тем  самым  AB  есть  K-начало.
Проверим его максимальность. Пусть C есть большее K-начало. Тог-
да  либо  C есть L-начало (что невозможно, так как A было макси-
мальным L-началом), либо C = A'B', где A' - L-слово, B' -  M-на-
чало.  Если  A'  короче A, то B' непусто и начинается с символа,
принадлежащего и Нач(M), и  Посл(L),  что  невозможно.  Если  A'
длиннее  A,  то  это противоречит тому, что A было максимальным.
Итак, A' = A. Но в этом случае B' есть продолжение B, что проти-
воречит корректности ReadM. Итак, AB  -  максимальное  K-начало.
Остается  проверить  правильность  выдаваемого  процедурой ReadK
значения переменной b. Если оно истинно, то это  очевидно.  Если
оно  ложно,  то B не есть M-слово, и надо проверить, что AB - не
K-слово. В самом деле, если бы выполнялось AB = A'B', где  A'  -
L-слово,  B' - M-слово, то A' не может быть длиннее A (ReadL чи-
тает максимальное слово), A' не может быть  равно  A  (тогда  B'
равно  B  и  не  является  M-словом) и A' не может быть короче A
(тогда первый символ B' принадлежит и Нач(M), и Посл(L)). Задача
решена.

     Перейдем теперь к другому частному случаю. Пусть в КС-грам-
матике есть правила
        K -> L
        K -> M
        K -> N
и других правил с левой частью K нет.

     13.2.4.  Считая, что ReadL, ReadM и ReadN корректны (для L,
M и N) и что множества Нач(L), Нач(M) и Нач(N) не  пересекаются,
написать процедуру, корректную для K.

     Решение. Схема процедуры такова:

     procedure ReadK;
     begin
     | if (Next принадлежит Нач(L)) then begin
     | | ReadL;
     | end else if (Next принадлежит Нач(M)) then begin
     | | ReadM;
     | end else if (Next принадлежит Нач(N)) then begin
     | | ReadN;
     | end else begin
     | | b := true или false  в зависимости от того,
     | |      выводимо ли пустое слово из K или нет
     | end;
     end;

Докажем, что ReadK корректно реализует K. Если Next не принадле-
жит ни одному из множеств Нач(L), Нач(M), Нач(N),то пустое слово
является наибольшим началом входа,  являющимся  K-началом.  Если
Next  принадлежит  одному  (и,  следовательно, только одному) из
этих множеств, то максимальное начало входа, являющееся  K-нача-
лом, непусто и читается соответствующей процедурой.

     13.2.5. Используя сказанное, составьте процедуру  распозна-
вания  выражений для грамматики (уже рассматривавшейся в примере
3):

    <выр>     -> <слаг> <оствыр>
    <оствыр>  -> + <выр>
    <оствыр>  ->
    <слаг>    -> <множ> <остслаг>
    <остслаг> -> * <слаг>
    <остслаг> ->
    <множ>    -> x
    <множ>    -> ( <выр> )

     Решение. Эта грамматика не полностью подпадает под рассмот-
ренные частные случаи: в правых частях есть комбинации  термина-
лов и нетерминалов
        + <выр>
и группы из трех символов
        ( <выр> )
В  грамматике есть также несколько правил с одной левой частью и
с правыми частями разного рода, например
    <оствыр>  -> + <выр>
    <оствыр>  ->
Эти ограничения не являются принципиальными. Так, правило типа
        K -> L M N
можно  было бы заменить на два правила K -> LQ и Q -> MN, терми-
нальные символы в правой части - на нетерминалы  (с  едиственным
правилом  замены на соответствующие терминалы). Несколько правил
с одной левой частью и разнородными правыми также можно свести к
уже разобранному случаю: например,

        K -> L M N
        K -> P Q
        K ->

можно заменить на правила

        K  -> K1
        K  -> K2
        K  -> K3
        K1 -> L M N
        K2 -> P Q
        K3 ->

Но  мы  не будем этого делать - а сразу же запишем то, что полу-
чится, если подставить описания процедур для новых  терминальных
символов в места их использования. Например, для правила
        K -> L M N
это дает процедуру

        procedure ReadK;
        begin
        | ReadL;
        | if b then begin ReadM; end;
        | if b then begin ReadN; end;
        end;

Для  ее  корректности  надо,  чтобы  Посл(L)  не  пересекалось с
Нач(MN) (которое равно Нач(M), если из  M  не  выводится  пустое
слово,  и  равно объединению Нач(M) и Нач(N), если выводится), а
также чтобы Посл(M) не пересекалось с Нач(N).
     Аналогичным образом правила
        K -> L M N
        K -> P Q
        K ->
приводят к процедуре

        procedure ReadK;
        begin
        | if (Next принадлежит Нач(LMN)) then begin
        | | ReadB;
        | | if b then begin ReadM; end;
        | | if b then begin ReadN; end;
        | end else if (Next принадлежит Нач(PQ)) then begin
        | | ReadP;
        | | if b then begin ReadQ; end;
        | end else begin
        | | b := true;
        | end;
        end;
Читая  приведенную  далее  программу, полезно иметь в виду соот-
ветствие между русскими и английскими словами:

        ВЫРажение               EXPRession
        ОСТаток ВЫРажения       REST of EXPRession
        СЛАГаемое               ADDitive term
        ОСТаток СЛАГаемого      REST of ADDitive term
        МНОЖитель               MULTiplier

     procedure ReadSymb (c: Symbol);
     | b := (Next = c);
     | if b then begin Move; end;
     end;

     procedure ReadExpr;
     | ReadAdd;
     | if b then begin ReadRestExpr; end;
     end;

     procedure ReadRestExpr;
     | if Next = '+' then begin
     | | ReadSymb ('+');
     | | if b then begin ReadExpr; end;
     | end else begin
     | | b := true;
     | end;
     end;

     procedure ReadAdd;
     | ReadMult;
     | if b then begin ReadRestAdd; end;
     end;

     procedure ReadRestAdd;
     | if Next = '*' then begin
     | | ReadSymb ('*');
     | | if b then begin ReadAdd; end;
     | end else begin
     | | b := true;
     | end;
     end;

     procedure ReadMult;
     | if Next = 'x' then begin
     | | ReadSymb ('x');
     | end else if Next = '(' then begin
     | | ReadSymb ('(');
     | | if b then begin ReadExpr; end;
     | | if b then begin ReadSymb (')'); end;
     | end else begin
     | | b := false;
     | end;
     end;

Осталось  обсудить проблемы, связанные с взаимной рекурсивностью
этих процедур (одна использует другую и наоборот). В паскале это
допускается, только требуется дать предварительное описание про-
цедур  ("forward").  Как всегда для рекурсивных процедур, помимо
доказательства того, что каждая процедура работает  правильно  в
предположении,  что  используемые в ней вызовы процедур работают
правильно, надо доказать отдельно, что работа завершается.  (Это
не  очевидно: если бы в грамматике было правило K -> KK, то из K
ничего не выводится, Посл(K) и Нач(K) пусты,  но  написанная  по
нашим канонам процедура

     procedure ReadK;
     begin
     | ReadK;
     | if b then begin
     | | ReadK;
     | end;
     end;

не заканчивает работы.)
     В   даннном  случае  процедуры  ReadRestExpr,  ReadRestAdd,
ReadMult либо завершаются, либо  уменьшают  длину  непрочитанной
части  входа. Поскольку любой цикл вызовов включает одну из них,
то зацикливание невозможно. Задача решена.

     13.2.6. Пусть в грамматике имеются два правила  с  нетерми-
иналом K в левой части, имеющих вид
        K -> LK
        K ->
по   которым  K-слово  представляет  собой  конечную  последова-
тельность L-слов, причем множества Посл(L) и  Нач(K)  (в  данном
случае  равное Нач(L)) не пересекаются. Используя корректную для
L процедуру ReadL, написать корректную для K процедуру ReadK, не
используя рекурсии. Предполагается, что пустое слово не выводимо
из L.

     Решение. По нашим правилам следовало бы написать

     procedure ReadK;
     begin
     | if (Next принадлежит Нач (L)) then begin
     | | ReadL;
     | | if b then begin ReadK; end;
     | end else begin
     | | b := true;
     | end;
     end;

завершение работы гарантируется тем, что пустое слово не выводи-
мо из L (и, следовательно, перед рекурсивным вызовом длина  неп-
рочитанной части уменьшается).
     Эта рекурсивная процедура эквивалентна нерекурсивной:

     procedure ReadK;
     begin
     | b := true;
     | while b and (Next принадлежит Нач (L)) do begin
     | | ReadL;
     | end;
     end;

Формально можно проверить эту эквивалентность так. Завершаемость
в обоих случаях ясна. Достаточно проверить поэтому, что тело ре-
курсивной  процедуры эквивалентно нерекурсивной в предположении,
что ее рекурсивный вызов эквивалентен вызову нерекурсивной  про-
цедуры. Подставим:

     if (Next принадлежит Нач (K)) then begin
     | ReadL;
     | if b then begin
     | | b := true;
     | | while b and (Next принадлежит Нач (L)) do begin
     | | | ReadL;
     | | end;
     | end;
     end else begin
     | b := true;
     end;

Первую команду b := true можно выкинуть (в этом месте  и  так  b
истинно). Вторую команду можно перенести в начало:

     b := true;
     if (Next принадлежит Нач (K)) then begin
     | ReadL;
     | if b then begin
     | | while b and (Next принадлежит Нач (L)) do begin
     | | | ReadL;
     | | end;
     | end;
     end;

Теперь  внутренний  if  можно выкинуть (если b ложно, цикл while
все равно не выполняется) и добавить в условие внешнего if усло-
вие b (которое все равно истинно).

     b := true;
     if b and (Next принадлежит Нач (L)) then begin
     | ReadL;
     | while b and (Next принадлежит Нач (A)) do begin
     | | ReadL;
     | end;
     end;

что эквивалентно приведенной выше  нерекурсивной  процедуре  (из
которой вынесена первая итерация цикла).

     13.2.7.  Доказать корректность приведенной выше нерекурсив-
ной программы непосредственно, без ссылок на рекурсивную.

     Решение. Рассмотрим  наибольшее  начало  входа,  являющееся
K-началом.  Оно  представляется  в виде конкатенации (последова-
тельного приписывания) нескольких непустых L-слов  и,  возможно,
одного  непустого  L-начала,  не являющегося L-словом. Инвариант
цикла: прочитано несколько из них; b <=> (последнее  прочитанное
является L-словом).
     Сохранение  инварианта:  если осталось последнее слово, это
очевидно; если осталось несколько, то  за  первым  B-словом  (из
числа  оставшихся)  идет  символ из Нач(B), и потому это слово -
максимальным началом входа, являющееся B-началом.

     На  практике  при  записи грамматики используют сокращения.
Если правила для какого-то нетерминала K имеют вид
     K -> L K
     K ->
(т.е. K-слова - это последовательности L-слов), то  этих  правил
не  пишут, а вместо K пишут L в фигурных скобках. Несколько пра-
вил с одной левой частью и разными правыми записывают  как  одно
правило,  разделяя альтернативные правые части вертикальной чер-
той.
     Например, рассмотренная выше  грамматика  для  <выр>  может
быть записана так:

    <выр>     -> <слаг> { + <слаг> }
    <слаг>    -> <множ> { * <множ> }
    <множ>    -> x | ( <выр> )

     13.2.8. Написать процедуру,  корректно  для  <выр>,  следуя
этой  грамматике  и используя цикл вместо рекурсии, где можно.

     Решение.

     procedure ReadSymb (c: Symbol);
     | b := (Next = c);
     | if b then begin Move; end;
     end;

     procedure ReadExpr;
     begin
     | ReadAdd;
     | while b and (Next = '+') do begin
     | | Move;
     | | ReadAdd;
     | end;
     end;

     procedure ReadAdd;
     begin
     | ReadMult;
     | while b and (Next = '*') do begin
     | | Move;
     | | ReadMult;
     | end;
     end;

     procedure ReadMult;
     begin
     | if Next = 'x' do begin
     | | Move;
     | end else if Next = '(' then begin
     | | Move;
     | | ReadExpr;
     | | if b then begin ReadSymb (')'); end;
     | end else begin
     | | b := false;
     | end;
     end;

     13.3. Алгоритм разбора для LL(1)-грамматик.

     В этом разделе мы рассморим еще один метод проверки выводи-
мости в КС-грамматике, называемый  по  традиции  LL(1)-разбором.
Вот его идея в одной фразе: можно считать, что в процессе вывода
мы  всегда  заменяем самый левый нетерминал и нужно лишь выбрать
одно из правил; если нам повезет с грамматикой, то выбрать  пра-
вило  можно, глядя на первый символ выводимого из этого нетерми-
нала слова. Говоря более формально, дадим такое
     Определение. Левым выводом (слова в грамматике)  называется
вывод,  в котором на каждом шаге замене подвергается самый левый
из нетерминалов.

     13.3.1.  Для  каждого  выводимого слова (из терминалов) су-
ществует его левый вывод.

     Решение. Различные нетерминалы заменяются независимо;  если
в  процессе вывода появилось слово ..K..L.., где K, L - нетерми-
налы, то замены K и L можно производить в любом порядке. Поэтому
можно перестроить вывод так, чтобы стоящий левее нетерминал  за-
менялся  раньше. (Формально говоря, надо доказывать индукцией по
длине вывода такой факт: если из некоторого нетерминала K  выво-
дится некоторое
слово A, то существует левый вывод A из K.)

     13.3.2. В грамматике с 4 правилами

        (1) E ->
        (2) E -> T E
        (3) T -> ( E )
        (4) T -> [ E ]

найти  левый  вывод  слова  A  =  [()([])]  и  доказать,  что он
единствен.

     Решение. На первом шаге можно применить только правило (2):
        E -> TE
Что будет дальше с T? Так как слово A начинается на "[", то  мо-
жет примениться только правило (4):
        E -> TE -> [E]E
Первое  E должно замениться на TE (иначе вторым символом была бы
скобка "]"):
        E -> TE -> [E]E -> [TE]E
и T должно заменяться по (3):
        E -> TE -> [E]E -> [TE]E -> [(E)E]E
Далее первое E должно замениться на пустое слово (иначе  третьей
буквой слова будет "(" или "[" - только на эти символы может на-
чинаться слово, выводимое из T):
        E -> TE -> [E]E -> [TE]E -> [(E)E]E -> [()E]E
и далее
  ...  ->  [()TE]E -> [()(E)E]E -> [()(TE)E]E -> [()([E]E)E]E ->
        -> [()([]E)E]E -> [()([])E]E -> [()([])]E -> [()([])].

     Что требуется от грамматики, чтобы такой метод поиска лево-
го вывода был применим? Пусть, например, на очередном шаге самым
левым  нетерминалом  оказался  нетерминал K, т.е. мы имеем слово
вида AKU, где A - слово из терминалов, а U - слово из терминалов
и нетерминалов. Пусть в грамматике есть правила
     K -> L M N
     K -> P Q
     K -> R
Нам надо выбрать одно из них. Мы будем пытаться сделать этот вы-
бор,  глядя  на  первый символ той части входного слова, которая
выводится из KU.
     Рассмотрим множество Нач(LMN) тех терминалов, с которых на-
чинаются непустые слова, выводимые из LMN. (Это множество  равно
Нач(L),  объединенному с Нач(M), если из L выводится пустое сло-
во, а также с Нач(N), если из L и из M выводится пустое  слово.)
Чтобы  описанный  метод  был  применим,  надо,  чтобы  Нач(LMN),
Нач(PQ) и Нач(R) не пересекались. Но этого мало. Ведь может быть
так, например, что из LMN будет выведено пустое слово, а из сло-
ва U будет выведено слово, начинающееся  на  букву  из  Нач(PQ).
Следующие определения учитывают эту проблему.

     Напомним,  что определение выводимости в КС-грамматике было
дано только для слова из терминалов. Оно очевидным образом обоб-
щается на случай слов из терминалов и нетерминалов. Можно  также
говорить о выводимости одного слова (содержащего терминалы и не-
терминалы)  из  другого. (Если говорится о выводимости слова без
указания того, откуда оно выводится, то  всегда  подразумевается
выводимость  в грамматике, т.е. выводимость из начального нетер-
минала.)
     Для каждого слова X  из  терминалов  и  нетерминалов  через
Нач(X) обозначаем множество всех терминалов, с которых начинают-
ся непустые слова из терминалов, выводимые из X. (В случае, если
из  любого  нетерминала выводится хоть одно слово из терминалов,
не играет роли, рассматриваем ли мы при определении Нач(X) слова
только из терминалов или любые слова. Мы будем предполагать  да-
лее, что это условие выполнено.)
    Для каждого нетерминала K  через  Послед(K)  обозначим  мно-
жество  терминалов, которые встречаются в выводимых словах сразу
же за K. Кроме того, в Послед(K) включается символ EOI, если су-
ществует выводимое слово, оканчивающееся на K.
     Для каждого правила
        K -> V
(где  K - нетерминал, V - слово, содержащее терминалы и нетерми-
налы) определим множество "направляющих  терминалов",  обознача-
емое Напр(K->V). По определению оно равно Нач(V), к которому до-
бавлено Послед(K), если из V выводится пустое слово.

     Определение.  Грамматика называется LL(1)-грамматикой, если
для любых правил K->V и K->W с одинаковыми левыми  частями  мно-
жества Напр(K->V) и Напр(K->W) не пересекаются.

     13.3.3. Является ли грамматика
          K -> K #
          K ->
(выводимыми   словами   являются   последовательности    диезов)
LL(1)-грамматикой?

     Решение. Нет: символ # принадлежит множествам  направляющих
символов для обоих правил (для второго - поскольку # принадлежит
Послед(K)).

     13.3.4. Написать LL(1)-грамматику для того же языка.

     Решение.
          K -> # K
          K ->
Как говорят, "леворекурсивное правило" заменено на  "праворекур-
сивное".

     Следующая задача показывает, что для LL(1)-грамматики суще-
ствует не более одного возможного продолжения левого вывода.

     13.3.5. Пусть дано выводимое в LL(1)-грамматике слово X,  в
котором  выделен  самый левый нетерминал К: X=AKS, где A - слово
из терминалов, S - слово из терминалов и нетерминалов. Пусть су-
ществуют два различных правила грамматики с нетерминалом K в ле-
вой части, и мы применили их к выделенному в  X  нетерминалу  K,
затем  продолжили  вывод  и в конце концов получили два слова из
терминалов, начинающихся на A. Доказать, что в  этих  словах  за
началом A идут разные буквы.

     Решение. Эти буквы принадлежат направляющим множествам раз-
личных правил.

     13.3.6. Доказать, что если слово выводимо в LL(1)-граммати-
ке, то его левый вывод единствен.

     Решение.  Предыдущая  задача показывает, что на каждом шаге
левый вывод продолжается однозначно.

     13.3.7.  Грамматика называется леворекурсивной, если из не-
которого нетерминала K выводится слово, начинающееся с K, но  не
совпадающее  с  ним. Доказать, что леворекурсивная грамматика, в
которой из каждого нетерминала выводится хотя бы  одно  непустое
слово  из  терминалов и для каждого нетерминала существует вывод
(начинающийся с начального нетерминала), в котором он встречает-
ся, не является LL(1)-грамматикой.

     Решение. Пусть из K выводится KU, где K - нетерминал, а U -
непустое слово. Можно считать, что это левый вывод  (другие  не-
терминалы  можно не заменять). Рассмотрим вывод K --> KU --> KUU
->... (знак --> обозначает несколько шагов вывода) и левый вывод
K -> A, где A - непустое слово из терминалов. На  каком-то  шаге
второй  вывод отклоняется от первого, а между тем по обоим путям
может быть получено слово, начинающееся на A  (в  первом  случае
это  возможно,  так  как сохраняется нетерминал K, который может
впоследствии быть заменен на A).  Это  противоречит  возможности
однозначного определения правила, применяемого на очередном шаге
поиска  левого вывода. (Oднозначность выполняется для выводов из
начального нетерманала, и надо воспользоваться  тем,  что  K  по
предположению встречается в таком выводе.)

     Таким образом, к леворекурсивным грамматикам (кроме  триви-
альных  случаев) LL(1)-наука неприменима. Их приходится преобра-
зовывать к эквивалентным LL(1)-грамматикам  -  или  пользоваться
другими методами распознавания.

     13.3.8.  Используя  сказанное,  построить алгоритм проверки
выводимости слова из терминалов в LL(1)-грамматике, не являющей-
ся леворекурсивной.

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

    | (1) слово AS выводимо в грамматике;
(И) | (2) любой левый вывод входного слова проходит через стадию
    |     AS

     Вначале A пусто, а S состоит из единственного символа - на-
чального нетерминала.
     Если  в  некоторый  момент S начинается на терминал t и t =
Next, то можно выполнить команду Move и удалить символ t,  явля-
ющийся начальным в S, поскольку при этом AS не меняется.
     Если S начинается на терминал t и t не равно Next, то вход-
ное  слово  невыводимо  -  ибо по условию любой его вывод должен
проходить через AS. (Это же справедливо и в случае Next = EOI.)
     Если S пусто, то из условия (И) следует, что входное  слово
выводимо тогда и только тогда, когда Next = EOI.
     Остается случай, когда S начинается с некоторого нетермина-
ла K. По доказанному выше все левые выводы из  S  слов,  начина-
ющихся на символ Next, начинаются с применения к T одного и того
же  правила  - того, для которого Next принадлежит направляющему
множеству. Если таких правил нет, то входное  слово  невыводимо.
Если такое правило есть, то нужно применить его к первому симво-
лу  слова S - при этом свойство (И) не нарушится. Приходим к та-
кому алгоритму:

   S := пустое слово;
   error := false;
   {error => входное слово невыводимо;}
   {not error => (И)}
   while (not error) and not ((Next=EOI) and (S пусто)) do begin
   | if (S начинается на терминал, равный Next) then begin
   | | Move; удалить из S первый символ;
   | end else if (S начинается на терминал, не равный Next)
   | |           then begin
   | | error := true;
   | end else if (S пусто) and (Next <> EOI) then begin
   | | error := true;
   | end else if (S начинается на нетерминал и Next входит в
   | |    направляющее множество одного из правил для этого
   | |    нетерминала) then begin
   | | применить это правило
   | end else if (S начинается на нетерминал и Next не входит в
   | |    направляющее множество ни одного из правил для этого
   | |    нетерминала) then begin
   | | error := true;
   | end;
   end;
   {входное слово выводимо <=> not error}

Алгоритм заканчивает работу, поскольку при появлении нетерминала
в начале слова S происходит чтение со  входа  или  остановка,  а
бесконечный  цикл сменяющих друг друга терминалов в начале S оз-
начал бы, что грамматика леворекурсивна.

     Замечания.  1.  Приведенный  алгоритм использует S как стек
(все действия производятся с левого конца).
     2. Действия двух последних вариантов внутри цикла не приво-
дят к чтению очередного символа со входа, поэтому их можно зара-
нее предвычислить для  каждого  нетерминала  и  каждого  символа
Next.  После этого на каждом шаге цикла будет читаться очередной
символ входа.
     3. При практической реализации удобно составить таблицу,  в
которой  записаны  варианты  действий  в зависимости от входного
символа и первого символа S, и небольшую программу,  выполняющую
действия в соответствии с этой таблицей.
      Глава 14. Синтаксический разбор слева направо (LR)

      Сейчас мы рассмотрим еще один метод синтаксического разбо-
ра,  называемый LR(1)-разбором, а также некоторые упрощенные его
варианты.

      14.1. LR-процессы

      Два отличия  LR(1)-разбора  от  LL(1)-разбора:  во-первых,
строится  не  левый вывод, а правый, во-вторых, он строится не с
начала, а с конца. (Вывод в КС-грамматике называется правым, ес-
ли на каждом шаге замене подвергается самый правый нетерминал.

     14.1.1. Доказать, что если слово, состоящее из  терминалов,
выводимо, то оно имеет правый вывод.

     Нам  будет удобно смотреть на правый вывод "задом наперед".
Определим понятие LR-процесса над словом A. В этом процессе, по-
мимо A, будет участвовать и другое слово S, которое может содер-
жать как терминалы, так и нетерминалы. Вначале слово S пусто.  В
ходе LR-процесса разрешены два вида действий:
     (1)  можно  перенести  первый  символ слова А (его называют
очередным символом и обозначают Next) в конец  слова  S,  удалив
его из A (это действие называют сдвигом);
     (2) если правая часть одного из правил грамматики оказалась
концом  слова  S, то разрешается заменить ее на нетерминал, сто-
ящий в левой части этого правила; при этом слово A не  меняется.
(Это действие называют сверткой, или приведением.)
     Отметим,  что  LR-процесс  не является детерминированным: в
одной и той же ситуации могут быть разрешены разные действия.
     Говорят, что LR-процесс на слове A успешно завершается, ес-
ли слово A становится пустым, а в слове S остается  единственный
нетерминал - начальный нетерминал грамматики.

     14.1.2.  Доказать,  что  для любого слова A (из терминалов)
успешно завершающийся LR-процесс существует тогда и только  тог-
да,  когда  слово A выводимо в грамматике. В ходе доказательства
установить взаимно однозначное соответствие между правыми  выво-
дами и успешно завершающимися LR-процессами.

     Решение. При сдвиге слово SA не меняется, при свертке слово
SA  подвергается преобразованию, обратному шагу вывода. Этот вы-
вод будет правым, так как сворачивается конец S, а в A все  сим-
волы  -  терминальные.  Таким образом, каждому LR-процессу соот-
ветствует правый вывод. Обратное соответствие: пусть дан  правый
вывод.  Представим  себе,  что за последним нетерминалом в слове
стоит перегородка. Применяя к этому нетерминалу правило  грамма-
тики,  мы  должны  сдвинуть перегородку влево (если правая часть
правила кончается на терминал). Разбивая этот сдвиг на отдельные
шаги, получим процесс, в точности обратный LR-процессу.

     Поскольку в ходе LR-процесса все изменения в слове S проис-
ходят с правого конца, слово S называют стеком LR-процесса.

     Задача построения правого вывода для данного слова  сводит-
ся,  таким образом, к правильному выбору очередного шага LR-про-
цесса. Нам нужно решить, будем ли мы делать сдвиг или свертку, и
если свертку, то по какому правилу - ведь подходящих правил  мо-
жет быть несколько. В LR(1)-алгоритме это решение принимается на
основе  S и первого символа слова A; если используется только S,
то говорят о LR(0)-алгоритме. (Точные определения смотри ниже.)

     Пусть K -> U - одно из правил грамматики (K - нетерминал, U
- слово из терминалов и нетерминалов). Определим множество  слов
(из терминалов и нетерминалов), называемое левым контекстом пра-
вила K -> U. (Обозначение: ЛевКонт(K->U).) По определению в него
входят  все  слова,  которые  являются  содержимым  стека непос-
редственно перед сверткой U в K в ходе некоторого успешно завер-
шающегося LR-процесса.

     14.1.3. Переформулировать это определение на  языке  правых
выводов.

     Решение. Рассмотрим все правые выводы вида
        <начальный нетерминал> --> XKA -> XUA,
где  A - слово из терминалов, X - слово из терминалов и нетерми-
налов. Все возникающие при этом слова XU и образуют  левый  кон-
текст  правила  K->U. Чтобы убедиться в этом, следует вспомнить,
что мы предполагаем, что из любого нетерминала можно вывести ка-
кое-то слово из терминалов (другие грамматики мы не рассматрива-
ем), так что правый вывод слова XUA может быть продолжен до пра-
вого вывода какого-то слова из терминалов.

     14.1.4. Все слова из ЛевКонт(K->U) кончаются, очевидно,  на
U.  Доказать, что если у всех них этот конец U отбросить, то по-
лученное множество слов не зависит от того, какое из правил  для
нетерминала K выбрано. (Это множество обозначается Лев(K).)

     Решение.  Из  предыдущей задачи ясно, что Лев(K) - это все,
что может появиться в правых выводах левее самого правого нетер-
минала K.

     14.1.5. Доказать, что в предыдущей  фразе  можно  отбросить
слова  "самого  правого":  Лев(K)  - это все то, что может появ-
ляться в правых выводах левее любого вхождения нетерминала K.

     Решение. Продолжив построение правого вывода, все  нетерми-
налы  справа  от K можно заменить на терминалы (а слева от K при
этом ничего не изменится).

     14.1.6. Построить грамматику, содержащую для каждого нетер-
минала K исходной граммaтики нетерминал <ЛевK>, причем следующее
свойство должно выполняться для любого  нетерминала  K  исходной
грамматики:  в  новой грамматике из <ЛевK> выводимы все элементы
Лев(K) и только они.

     Решение. Пусть P - начальный нетерминал грамматики. Тогда в
новой грамматике будет правило
     <ЛевP> ->       (пустое слово)
Для каждого правила исходной грамматики, например, правила

      K -> L t M N (L, M, N - нетерминалы, t - терминал),

в новую грамматику мы добавим правила
      <ЛевL> -> <ЛевK>
      <ЛевM> -> <ЛевК> L t
      <ЛевN> -> <ЛевK> L t M
и аналогично поступим с другими правилами.  Смысл  новых  правил
таков: пустое слово может появиться слева от P; если слово X мо-
жет  появиться  слева от K, то X может появиться слева от L, XLt
может появиться слева от M, XLtM - слева от N. Индукцией по дли-
не правого вывода легко проверить, что все, что может  появиться
слева от какого-то нетерминала, появляется в соответствии с эти-
ми правилами.

     14.1.7. Почему в предыдущей задаче важно, что мы рассматри-
ваем только правые выводы?

     Ответ. В противном случае следовало бы учитывать преобразо-
вания, происходящие внутри слова, стоящего слева от K.

     14.1.8.  Для  данной грамматики построить алгоритм, который
по любому слову выясняет, каким из множеств Лев(K) оно принадле-
жит.

     (Замечание для знатоков. Существование такого алгоритма - и
даже конечного автомата, т.е. индуктивного расширения с конечным
числом значений, - вытекает из предыдущей задачи, т.к. построен-
ная в ней грамматика имеет специальный вид: в правых  частях  ее
всего один нетерминал, причем он стоит у левого края. Тем не ме-
нее мы приведем явное построение.)

     Решение. Будем называть ситуацией данной грамматики одно из
ее  правил, в правой части которого отмечена одна из позиций (до
первой буквы, между первой и второй буквой,..., после  последней
буквы). Например, правило
     K -> L t M N   (K, L, M, N - нетерминалы, t - терминал)
порождает пять ситуаций
     К -> _LtMN, K-> L_tMN, K-> Lt_MN, K-> LtM_N, K -> LtMN_.
(позиция указывается знаком подчеркивания).
     Будем говорить, что слово S согласовано с ситуацией K->U_V,
если  S  кончается  на U, то есть S=TU при некотором T, и, кроме
того, T принадлежит Лев(K). (Смысл  этого  определения  примерно
таков:  в  стеке S подготовлена часть U для будущей свертки UV в
K.) В этих терминах ЛевКонт(K->X) -  это  множество  всех  слов,
согласованных  с  ситуацией K->X_, а Лев(К) - это множество всех
слов, согласованных с ситуацией K->_X (где K->_X - любое правило
для нетерминала K).
     Эквивалентное  определение в терминах LR-процесса: S согла-
совано с ситуацией K->U_V, если существует успешный  LR-процесс,
в котором события развиваются так:
     - в ходе процесса в стеке появляется слово S, и оно оканчи-
чивается на U;
     -  некоторое время S не затрагивается, а справа от него по-
является V;
     - UV сворачивается в K;
     - процесс продолжается и успешно завершается.

     14.1.9. Доказать эквивалентность этих определений.

     Указание.  Если S=TU и T принадлежит Лев(K), то можно полу-
чить в стеке сначала T, потом U, потом V, потом свернем UV в K и
затем успешно завершим процесс. (Мы используем несколько раз тот
факт, что из любого нетерминала что-то да  выводится:  благодаря
этому мы можем добавить в стек любое слово.)

     Наша  цель - построение алгоритма, распознающего принадлеж-
ность произвольного слова к Лев(K). Рассмотрим  функцию,  сопос-
тавляющую  с каждым словом S (из терминалов и нетерминалов) мно-
жество всех согласованных с ним ситуаций. Это множество называют
состоянием,  соответствующим  слову  S.  Будем  обозначать   его
Сост(S).  Достаточно  показать,  что функция Сост(S) индуктивна,
т.е. что значение Сост(SJ), где J - терминал или нетерминал, мо-
жет быть вычислено, если известно Сост(S) и символ J. (Мы видели
ранее, как принадлежность к Лев(К) выражается  в  терминах  этой
функции.) Значение Сост(SJ) вычисляется по таким правилам:
     (1)  Если слово S было согласовано с ситуацией K->U_V, при-
чем слово V начиналось на букву J, то есть V=JW, то теперь слово
SJ будет согласовано с ситуацией K->UJ_W.
     Это правило полностью определяет все  ситуации  с  непустой
левой  половиной (то есть не начинающиеся с подчеркивания), сог-
ласованные с SJ. Осталось определить, для каких  нетерминалов  K
слово SJ принадлежит Лев(K). Это делается по двум правилам:
     (2) Если уже выяснено, что ситуация L->U_V согласована с SJ
(по  правилу (1)), а V начинается на нетерминал К, то SJ принад-
лежит Лев(K).
     (3) Если уже выяснено, что SJ входит в Лев(L) для некоторо-
го L, L->V - правило грамматики и V начинается на нетерминал  K,
то SJ принадлежит Лев(K).
     Заметим,  что  правило  (3)  можно рассматривать как аналог
правила (2): в указанных в  (3)  предположениях  ситуация  L->_V
согласована с SJ, а V начинается на нетерминал K.
     Корректность  этих  правил  в общем-то очевидна, если хоро-
шенько подумать. Единственное, что требует некоторых пояснений -
это то, почему с помощью правил (2) и (3) обнаружатся ВСЕ терми-
налы K, для которых SJ принадлежит Лев(K). Попытаемся это объяс-
нить. Рассмотрим правый вывод, в котором SJ стоит  слева  от  K.
Откуда мог взяться в нем нетерминал K? Если правило, которое его
породило,  породило также и конец слова SJ, то принадлежность SJ
к Лев(K) будет обнаружена по правилу (2). Если же K было  первой
буквой  слова, порожденного каким-то другим нетерминалом L, то -
благодаря правилу (3) - достаточно установить принадлежность  SJ
к Лев(L). Осталось применить те же рассуждения к L и т.д.
     В  терминах LR-процесса то же самое можно сказать так. Сна-
чала нетерминал K может участвовать в  нескольких  свертках,  не
затрагивающих  SJ (они соответствуют применению правила (3)), но
затем он обязан подвергнуться свертке, затрагивающей SJ (что со-
ответствует применению правила (2)).
     Осталось выяснить, какие ситуации согласованы с пустым сло-
вом, то есть для каких нетерминалов K пустое  слово  принадлежит
Лев(K).  Это  определяется  по следующим правилам: (1) начальный
нетерминал таков; (2) если K таков и K -> V - правило  граммати-
ки, причем слово V начинается с нетерминала L, то и L таков.

     14.1.10. Проделать описанный анализ для грамматики

        E -> E + T
        E -> T
        T -> T * F
        T -> F
        F -> x
        F -> ( E )

     Решение.

Слово S                    Сост(S)
_________________________________________________

пустое   E->_E+T;E->_T;T->_T*F;T->_F;F->_x;F->_(E)
E        E->E_+T
T        E->T_; T->T_*F;
F        T->F_
x        F->x_
(        F->(_E);E->_E+T;E->_T;T->_T*F;T->_F;F->_x;F->_(E)
E+       E->E+_T;T->_T*F;T->_F;F->_x;F->_(E)
T*       T->T*_F;F->_x;F->_(E)
(E       F->(E_);E->E_+T;
(T       = T
(F       = F
(x       = x
((       = (
E+T      E->E+T_;T->T_*F
E+F      = F
E+x      = x
E+(      = (
T*F      T->T*F_;
T*x      = x
T*(      = (
(E)      F->(E)_
(E+      = E+
E+T*     = T*

Знак равенства означает, что множества ситуаций, являющиеся зна-
чениями  функции  Сост(S)  на  словах, стоящих слева и справа от
знака равенства, одинаковы.
     Правило определения Сост(SJ), если известны Сост(S) и J  (S
-  слово из терминалов и нетерминалов, J - терминал или нетерми-
нал), таково:
     надо найти Сост(S) в правой колонке, взять  соответствующее
ему  слово  T  в  левой колонке, приписать к нему J и взять мно-
жество, стоящее напротив слова ТJ (если слово ТJ в  таблице  от-
сутствует, то Сост(SJ) пусто).

     14.2. LR(0)-грамматики.

     Напомним,  что наша основная цель - это поиск вывода задан-
ного слова, или, другими словами,  поиск  успешного  LR-процесса
над  ним.  Во  всех  рассматриваемых  нами  грамматиках успешный
LR-процесс (над данным словом) единствен. Искать этот единствен-
ный успешный процесс мы будем постепенно:  в  каждый  момент  мы
смотрим,  какой  шаг возможен следующим. Для этого на грамматику
надо  наложить дополнительные требования, и сейчас мы рассмотрим
простейший случай так называемых LR(0)-грамматик.

     Мы уже знаем:
     (1) В успешном LR-процессе возможна свертка по правилу K->U
при содержимом стека S тогда и только тогда, когда S принадлежит
ЛевКонт(K->U)  или, другими словами, когда слово S согласовано с
ситуацией K->U_.
     Аналогичное утверждение про сдвиг гласит:
     (2) В успешном LR-процессе при содержимом стека S
возможен сдвиг с очередным символом a тогда и только тогда, ког-
да S согласовано с некоторой ситуацией K->U_aV.

     14.2.1. Докажите это.
     Указание. Пусть произошел сдвиг и к стеку S добавилась бук-
ва a. Рассмотрите первую свертку, затрагивающую эту букву.

Теперь мы можем дать
     Определение. Рассмотрим некоторую грамматику и произвольное
слово S из терминалов и нетерминалов. Если множество Сост(S) со-
держит  ситуацию, в которой справа от подчеркивания стоит терми-
нал, то говорят, что для слова S возможен сдвиг. Если в  Сост(S)
есть  ситуация, в которой справа от подчеркивания ничего нет, то
говорят, что для слова S возможна свертка  (по  соответствующему
правилу).  Говорят,  что  для  слова  S  возникает конфликт типа
сдвиг/свертка, если возможен и сдвиг, и  свертка.  Говорят,  что
для  слова  S возникает конфликт типа свертка/свертка, если есть
несколько правил, по которым возможна свертка.
     Грамматика  называется  LR(0)-грамматикой,  если  в ней нет
конфликтов типа сдвиг/свертка и свертка/свертка  ни  для  одного
слова S.

     14.2.2. Является ли приведенная выше грамматика LR(0)-грам-
матикой?

     Решение. Нет,  не  является.  Для  слов  T  и  E+T  имеются
конфликты типа сдвиг/свертка.

     14.2.3. Являются ли LR(0)-грамматиками такие:

     (а) T->0
         T->T1
         T->TT2
         T->TTT3

     (б) T->0
         T->1T
         T->2TT
         T->3TTT

     Решение. (а)

Слово S                    Сост(S)
_________________________________________

пустое    Т->_0;T->_T1;T->_TT2;T->_TTT3
0         Т->0_
Т         Т->Т_1;T->T_T2;T->T_TT3;Т->_0;T->_T1;T->_TT2;T->_TTT3
T1        T->T1_
TT        T->TT_2;T->TT_T3;T->T_1;T->T_T2;T->T_TT3;
          T->_0;T->_T1;T->_TT2;T->_TTT3
TT2       T->TT2_
TTT       T->TTT_3;T->TT_2;T->TT_T3;T->T_1;T->T_T2;T->T_TT3
          Т->_0;T->_T1;T->_TT2;T->_TTT3
TT0       = 0
TTT3      T->TTT3_
TTT2      = TT2
TTTT      = TT
TTT0      = 0

Конфликтов нет, это LR(0)-грамматика.

     (б)

Слово S                    Сост(S)
_______________________________________________

пустое    T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
0         Т->0_
1         Т->1_T;T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
2         T->2_TT;T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
3         T->3_TTT;T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
1T        T->1T_
10        = 0
11        = 1
12        = 2
13        = 3
2T        T->2T_T;T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
20        = 0
21        = 1
22        = 2
23        = 3
3T        T->3T_TT;T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
30        = 0
31        = 1
32        = 2
33        = 3
2TT       T->2TT_
2T0       = 0
2T1       = 1
2T2       = 2
2T3       = 3
3TT       T->3TT_T;T->_0;T->_1Т;T->_2ТТ;T->_3ТТТ
3T0       = 0
3T1       = 1
3T2       = 2
3T3       = 3
3TTT      T->3TTT_
3TT0      = 0
3TT1      = 1
3TT2      = 2
3TT3      = 3

Конфликтов нет, это LR(0)-грамматика.

     Эта задача показывает, что LR(0)-грамматики могут быть  как
леворекурсивными, так и праворекурсивными.

     14.2.4. Пусть дана LR(0)-грамматика. Доказать, что у любого
слова существует не более одного правого вывода. Построить алго-
ритм проверки выводимости в LR(0)-грамматике.

     Решение.  Пусть  дано  произвольное  слово A. Будем строить
LR-процесс над A по шагам. Пусть текущее состояние стека LR-про-
цесса равно S. Нам надо решить, делать сдвиг или свертку (и если
сертку, то по какому правилу). Согласно определнию LR(0)-грамма-
тики в нашем состоянии S возможен либо только сдвиг, либо только
свертка (причем лишь по одному правилу).  Таким  образом,  поиск
возможных  продолжений  LR-процесса  происходит детерминированно
(на каждом шаге можно определить, какое действие только  и  воз-
можно).

     14.2.5.  Что  произойдет, если анализируемое слово не имеет
вывода в данной грамматике?

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

     Замечания. 1. При реализации этого алгоритма нет  необходи-
мости каждый раз заново вычислять множество Сост(S) для текущего
значения  S. Эти множества можно также хранить в стеке (в каждый
момент хранятся множества Сост(T) для всех начал текущего  слова
S).
     2. На самом деле само слово S можно не хранить - достаточно
хранить множества ситуаций Сост(T) для всех его начал T.

     В  алгоритме проверки выводимости в LR(0)-грамматике мы ис-
пользуем не всю информацию, которую могли бы. В  этом  алгоритме
для  каждого  состояния  известно  заранее,  что  в нем возможен
только сдвиг или только свертка (причем в последнем  случае  из-
вестно,  по  какому  правилу).  Более изощренный алгоритм мог бы
принимать решение о выборе между сдвигом и  сверткой,  посмотрев
на  очередной  символ (Next). Глядя на состояние, можно сказать,
при каких значениях Next возможен сдвиг (это те терминалы, кото-
рые в ситуациях этого состояния стоят непосреджственно  за  под-
черкиванием). Сложнее воспользоваться информацией о символе Next
для  решения  вопроса о том, возможна ли свертка. Для этого есть
упрощенный метод (грамматики, к которым  он  применим,  называют
SLR(1)-грамматиками [сокращение от Simple LR(1)]) и полный метод
(более  сложный, но использующий всю возможную информацию; грам-
матики, к которым  он  применим,  называют  LR(1)-грамматиками).
Есть и промежуточный класс грамматик, называемый LALR(1).

     14.3. SLR(1)-грамматики

     Напомним,  что  для любого нетерминала K мы определяли мно-
жество Послед(K) тех терминалов,  которые  могут  стоять  непос-
редственно за K в выводимом (из начального нетерминала) слове; в
это  множество  добавляют также специальный символ EOI, если не-
терминал K может стоять в конце выводимого слова.

     14.3.1. Доказать, что если в данный момент LR-процесса пос-
ледний символ стека S равен  K,  причем  процесс  этот  может  в
дальнейшем успешно завершиться, то Next принадлежит Послед(K).

     Решение. Этот факт является непосредственным следствием оп-
ределения   (вспомним  соответствие  между  правыми  выводами  и
LR-процессами).

     Определение. Рассмотрим некоторую грамматику,  произвольное
слово  S  из  терминалов  и нетерминалов и терминал x. Если мно-
жество Сост(S) содержит ситуацию, в которой справа от  подчерки-
вания  стоит терминал x, то говорят, что для пары (S,x) возможен
сдвиг. Если в Сост(S) есть ситуация K->U_, причем x  принадлежит
Послед(K),  то  говорят,  что  для  пары  (S,x)  SLR(1)-возможна
свертка (по правилу K->U). Говорят, что для пары (S,x) возникает
конфликт типа сдвиг/свертка, если возможен и сдвиг,  и  свертка.
Говорят,   что   для   пары   (S,x)   возникает   конфликт  типа
свертка/свертка, если есть несколько правил, по которым возможна
свертка.
     Грамматика называется SLR(1)-грамматикой, если  в  ней  нет
конфликтов типа сдвиг/свертка и свертка/свертка ни для одной па-
ры (S,x).

     14.3.2. Пусть дана SLR(1)-грамматика. Доказать, что у любо-
го  слова  существует  не более одного правого вывода. Построить
алгоритм проверки выводимости в SLR(1)-грамматике.

     Решение. Аналогично случаю LR(0)-грамматик, только при  вы-
боре между сдвигом и сверткой учитывается очередной символ Next.

     14.3.3.  Проверить, является ли приведенная выше грамматика
(с E, T и F) SLR(1)-грамматикой.

     Решение. Да, является, так как оба конфликта,  мешающие  ей
быть LR(0)-грамматикой, разрешаются с учетом очередного символа:
и  для слова T, и для слова E+T сдвиг возможен только при Nеxt =
*,  а  символ  * не принадлежит Послед(E) = {EOI,+,)}, и поэтому
при Next = * свертка невозможна.

     14.4. LR(1)-грамматики, LALR(1)-грамматики

     Описанный выше SLR(1)-подход используют  не  всю  возможную
информацию  при  выяснении того, возможна ли свертка. Именно, он
отдельно проверяет, возможна ли  свертка  при  данном  состоянии
стека  S и отдельно - возможна ли свертка по данному правилу при
данном символе Next. Между тем эти проверки не являются  незави-
симыми:  обе  могут  дать  положительный  ответ, но тем не менее
свертка при стеке S  и  очередном  символе  Next  невозможна.  В
LR(1)-подходе этот недостаток устраняется.

     LR(1)-подход состоит вот в чем: все наши определения и  ут-
верждения  модифицируются  так, чтобы учесть, какой символ стоит
справа от разворачивамого нетерминала (другими словами, чему ра-
вен Next при свертке).

     Пусть K->U - одно из правил грамматики,  а  t  -  некоторый
терминал  или  спецсимвол  EOI  (который  мы домысливаем в конце
входного слова). Определим множество  ЛевКонт(K->U,t)  как  мно-
жество  всех  слов,  которые  являются  содержимым  стека непос-
редственно перед сверткой U в K в  ходе  успешного  LR-процесса,
при условии Next = t (в момент свертки).

     Если отбросить у всех слов из ЛевКонт(K->U) их конец U,  то
получится  множество всех слов, которые могут появиться в правых
выводах перед нетерминалом K, за которым  стоит  символ  t.  Это
множество (не зависящее от того, какое из правил для нетерминала
K выбрано) мы будем обозначать Лев(K,t).

     14.4.1.  Написать  грамматику   для   порождения   множеств
Лев(K,t).

     Решение. Ее нетерминалами будут символы <ЛевKt> для каждого
нетерминала K и для каждого терминала t (и для t=EOI). Ее прави-
ла  таковы. Пусть P - начальный нетерминал исхолной проамматики.
Тогда в новой грамматике будет правило
     <ЛевP EOI> ->      (пустое слово)
Для каждого правила исходной грамматики, например, для правила
     K-> L u M N (L, M, N - нетерминалы, u - терминал)
в новую грамматику мы добавим правила
     <ЛевL u> -> <ЛевK x>  (для всех терминалов x)
     <ЛевM s> -> <ЛевK y> L u
(для всех s, которые могут начинать слова, выводимые из N, и для
всех y, а также для всех s = y, если из N выводимо пустое слово)
     <ЛевN s> -> <ЛевK s> L u M (для всех теминалов s).

     14.4.2. Как меняется определение ситуации?

     Решение. Ситуацией называется пара
         [ситуация в старом смысле, терминал или EOI]

     14.4.3. Как изменится определение согласованности?

     Решение. Cлово S из терминалов и нетерминалов согласованo с
ситуацией  [K->U_V, t] (здесь t - терминал или EOI), если S кон-
чается на  U,  то  есть  S=TU,  и,  кроме  того,  T  принадлежит
Лев(K,t).

     14.4.4. Каковы правила  для  индуктивного  вычисления  мно-
жества Сост(S) ситуаций, согласованных с данным словом S?

     Ответ.  (1)  Если  слово  S  было  согласовано  с ситуацией
[K->U_V, t], причем слово V начиналось на букву J, то есть V=JW,
то теперь слово SJ будет согласовано с ситуацией [K->UJ_W,t].
     Это правило полностью определяет все  ситуации  с  непустой
левой  половиной (то есть не начинающиеся с подчеркивания), сог-
ласованные с SJ. Осталось определить, для каких нетерминалов K и
терминалов t слово SJ принадлежит Лев(K,t). Это делается по двум
правилам:
     (2) Если уже выяснено, что ситуация [L->U_V,t]  согласована
с  SJ  (по  правилу  (1)), а V начинается на нетерминал К, то SJ
принадлежит Лев(K,s) для всех терминалов s, которые могут  начи-
нать слова, выводимые из слова V\K (слово V без первой буквы K),
а также для s=t, если из V\K выводится пустое слово.
    (3) Если уже выяснено, что SJ входит в Лев(L,t) для  некото-
рых L и t, причем L->V - правило грамматики и  V  начинается  на
нетерминал K, то SJ принадлежит Лев(K,s) для всех терминалов  s,
которые могут начинать слова, выводимые из V\K, а также для s=t,
если из V\K выводится пустое слово.

     14.4.5.   Дать   определения   конфликтов  сдвиг/свертка  и
свертка/свертка по аналогии с данными выше..

     Решение.  Пусть дана некоторая грамматика. Пусть S - произ-
вольное слово  из  терминалов  и  нетерминалов.  Если  множество
Сост(S)  содержит  ситуацию,  в  которой справа от подчеркивания
стоит терминал t , то  говорят,  что  для  пары  (S,t)  возможен
сдвиг. (Это определение не изменилось по сравнению с SLR(1)-слу-
чаем - вторые компоненты пар из Сост(S) не учитываются.)
     Если в Сост(S) есть ситуация, в которой справа от подчерки-
вания ничего нет, а вторым членом пары является терминал  t,  то
говорят,  что  для  пары  (S,t) LR(1)-возможна свертка (по соот-
ветствующему правилу). Говорят, что  для  пары  (S,t)  возникает
конфликт  типа  сдвиг/свертка, если возможен и сдвиг, и свертка.
Говорят,  что   для   пары   (S,t)   возникает   конфликт   типа
свертка/свертка, если есть несколько правил, по которым возможна
свертка.
     Грамматика  называется  LR(1)-грамматикой,  если  в ней нет
конфликтов типа сдвиг/свертка и свертка/свертка  ни  для  одной
пары (S,t).

     14.4.6.  Построить  алгоритм  проверки  выводимости слова в
LR(1)-грамматике.

     Решение. Как и раньше, на каждом шаге LR-процесса можно од-
нозначно определить, какой шаг только и может быть следующим.

     Полезно (в частности, для LALR(1)-разбора, смотри ниже) по-
нять,  как  связаны понятия LR(0) и LR(1)-согласованности.

     14.4.7. Сформулировать и доказать соответствующее утвержде-
ние.

     Ответ. Пусть фиксирована некоторая грамматика. Слово  S  из
терминалов  и  нетерминалов является LR(0)-согласованным с ситу-
ацией K->U_V тогда и только тогда, когда оно LR(1)-согласовано с
парой [K->U_V,t] для некоторого терминала t (или для t=EOI).  То
же  самое  другими  словами: Лев(K) есть объединение Лев(K,t) по
всем t. В последней форме это совсем ясно.

     Замечание. Таким образом, функция  Сост(S)  в  LR(1)-смысле
является  расширением  функции  Сост(S) в LR(0)-смысле: Сост1(S)
получается из Сост0(S), если во всех парах выбросить вторые чле-
ны.

    Теперь мы можем дать определение  LALR(1)-грамматики.  Пусть
фиксирована  некоторая  грамматика,  S - слово из нетерминалов и
терминалов, t - некоторый терминал (или  EOI).  Будем  говорить,
что для пары (S,t) LALR(1)-возможна свертка по некоторому прави-
лу, если существует другое слово S1 с Сост0(S)=Сост0(S1), причем
для  пары (S1,t) LR(1)-возможна свертка по рассматриваемому пра-
вилу. Далее определяются  конфликты  (естественным  образом),  и
грамматика называется LALR(1)-грамматикой, если конфликтов нет.

    14.4.8.  Доказать,  что  всякая  SLR(1)-грамматика  является
LALR(1)-грамматикой,  а   всякая   LALR(1)-грамматика   является
LR(1)-грамматикой.
    Указание. Это - простое следствие определений.

    14.4.9.    Построить   алгоритм   проверки   выводимости   в
LALR(1)-грамматике, который хранит в  стеке  меньше  информации,
чем соответствующий LR(1)-алгоритм.
    Указание.  Достаточно  хранить  в  стеке множества Сост0(S),
поскольку согласно определению LALR(1)-возможность  свертки  ими
определяется.  (Так  что  сам  алгоритм  ничем  не отличается от
SLR(1)-случая, кроме таблицы возможных сверток.)

    14.4.10.  Привести  пример LALR(1)-грамматики, не являющейся
SLR(1)-грамматикой.

    14.4.11. Привести  пример  LR(1)-грамматики,  не  являющейся
LALR(1)-грамматикой.

    14.5. Общие замечания о разных методах разбора.

    Применение этих методов на практике имеет  свои  хитрости  и
тонкости,  которых  мы  не  касались. (Например, таблицы следует
хранить по возможности экономно.) Часто оказывается  также,  что
для  некоторого  входного языка наиболее естественная грамматика
не является LL(1)-грамматикой, но является LR(1)-грамматикой,  а
также может быть заменена на LL(1)-грамматику без изменения язы-
ка.  Какой  из  этих  вариантов  выбрать,  не всегда ясно. Диле-
тантский совет: если Вы сами проектируете входной  язык,  то  не
следует  выпендриваться  и  употреблять одни и те же символы для
разных целей - и тогда обычно несложно написать LL(1)-грамматику
или рекурсивный анализатор. Если же входной язык задан заранее с
помощью  LR(1)-грамматики,  не  являющейся LL(1)-грамматикой, то
лучше ее не трогать, а разбирать как есть. При этом  могут  ока-
заться  полезные  средства автоматического порождения анализато-
ров, наиболее известными из которых являются yacc (UNIX) и bison
(GNU).

     Большое  количество полезной и хорошо изложенной информации
о теории и практике синтаксического  разбора  имеется  в  книге:
Alfred  V.  Aho,  Ravi  Sethi,  Jeffrey  D.  Ullman.  Compilers:
principles, techniques and tools. Addison Wesley (1985).


Яндекс цитирования