| | | | 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= 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]