Жанр: Учеба
Программирование в теоремах и задачах
.... (Пример: при
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
...Закладка в соц.сетях