Главная страница
Навигация по странице:

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

  • А. шень Издание шестое, дополненное Москва


    Скачать 1.62 Mb.
    НазваниеА. шень Издание шестое, дополненное Москва
    Дата17.09.2022
    Размер1.62 Mb.
    Формат файлаpdf
    Имя файлаshen-progbook.pdf
    ТипКнига
    #681926
    страница8 из 19
    1   ...   4   5   6   7   8   9   10   11   ...   19
    Почему? Как изменится спецификация процедуры?
    Решение. Спецификацию можно выбрать такой:
    дано: напечатанное корректно надо: напечатанное корректно и включает вершину i;
    все вновь напечатанные вершины доступны из i.
    

    130 7. Рекурсия

    7.4.4. Где использован тот факт, что граф не имеет циклов?
    Решение. Мы опустили доказательство конечности глубины рекур- сии. Для каждой вершины рассмотрим её «глубину» | максимальную длину пути по стрелкам, из неё выходящего. Условие отсутствия циклов гарантирует, что эта величина конечна. Из вершины нулевой глубины стрелок не выходит. Глубина конца стрелки по крайней мере на 1 мень- ше, чем глубина начала. При работе процедуры add(i) все рекурсивные вызовы add(j) относятся к вершинам меньшей глубины.
    
    Вернёмся к оценке времени работы. Сколько вызовов add(i) воз- можно для какого-то фиксированного i? Прежде всего ясно, что пер- вый из них печатает i, остальные сведутся к проверке того, что i уже напечатано. Ясно также, что вызовы add(i) индуцируются «печата- ющими» (первыми) вызовами add(j) для тех j, из которых в i ведёт ребро. Следовательно, число вызовов add(i) равно числу входящих в i рёбер (стрелок). При этом все вызовы, кроме первого, требуют 𝑂(1)
    операций, а первый требует времени, пропорционального числу исходя- щих из i стрелок. (Не считая времени, уходящего на выполнение add(j)
    для концов j выходящих рёбер.) Отсюда видно, что общее время про- порционально числу рёбер (плюс число вершин).
    
    Связная компонента графа. Неориентированный граф | набор то- чек (вершин), некоторые из которых соединены линиями (рёбрами).
    Неориентированный граф можно считать частным случаем ориенти- рованного графа, в котором для каждой стрелки есть обратная.
    Связной компонентой вершины i называется множество всех тех вершин, в которые можно попасть из i, идя по рёбрам графа. (По- скольку граф неориентированный, отношение «j принадлежит связной компоненте i» является отношением эквивалентности.)
    7.4.5. Дан неориентированный граф (для каждой вершины указано число соседей и массив номеров соседей, как в задаче о топологической сортировке). Составьте алгоритм, который по заданному i печатает все вершины связной компоненты i по одному разу (и только их). Чи- сло действий не должно превосходить 𝐶 · (общее число вершин и рёбер в связной компоненте).
    Решение. Программа в процессе работы будет «закрашивать» неко- торые вершины графа. Незакрашенной частью графа будем называть то, что останется, если выбросить все закрашенные вершины и веду- щие в них рёбра. Процедура add(i) закрашивает связную компоненту i в незакрашенной части графа (и не делает ничего, если вершина i уже закрашена).

    7.4. Другие применения рекурсии
    131
    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. Решите ту же задачу для ориентированного графа (напе- чатать все вершины, доступные из данной по стрелкам; граф может содержать циклы).
    Ответ. Годится по существу та же программа (строку «для всех соседей» надо заменить на «для всех вершин, куда ведут стрелки»). 

    132 7. Рекурсия
    Следующий вариант задачи о связной компоненте имеет скорее те- оретическое значение (и называется теоремой Сэвича).
    7.4.7. Ориентированный граф имеет 2
    𝑛
    вершин (двоичные слова длины 𝑛) и задан в виде функции есть ребро, которая по двум верши- нам 𝑥 и 𝑦 сообщает, есть ли в графе ребро из 𝑥 в 𝑦. Составьте алгоритм,
    который для данной пары вершин 𝑢 и 𝑣 определяет, есть ли путь (по рёбрам) из 𝑢 в 𝑣, используя память, ограниченную многочленом от 𝑛.
    (Время при этом может быть | и будет | очень большим.)
    [Указание. Используйте рекурсивную процедуру, выясняющую, су- ществует ли путь из 𝑥 в 𝑦 длины не более 2
    𝑘
    (и вызывающую себя с уменьшенным на единицу значением 𝑘).]
    
    Быстрая сортировка Хоара. В заключение приведём рекурсивный алгоритм сортировки массива, который на практике является одним из самых быстрых. Пусть дан массив 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
    , с.
    36
    (это можно сде- лать за время, пропорциональное длине участка). Конечность глубины рекурсии гарантируется тем, что длина сортируемого участка на ка- ждом уровне рекурсии уменьшается хотя бы на 1.
    7.4.8. (Для знакомых с основами теории вероятностей). Докажите,
    что математическое ожидание числа операций при работе этого ал-

    7.4. Другие применения рекурсии
    133
    горитма не превосходит 𝐶𝑛 log 𝑛, причём константа 𝐶 не зависит от сортируемого массива.
    [Указание. Пусть 𝑇 (𝑛) | максимум математического ожидания чи- сла операций для всех входов длины 𝑛. Из текста процедуры вытекает такое неравенство:
    𝑇 (𝑛) 6 𝐶𝑛 +
    1
    𝑛
    ∑︁
    𝑘+𝑙=𝑛−1
    (︀
    𝑇 (𝑘) + 𝑇 (𝑙))︀
    Первый член соответствует распределению элементов на меньшие, рав- ные и большие. Второй член | это среднее математическое ожидание для всех вариантов случайного выбора. (Строго говоря, поскольку сре- ди элементов могут быть равные, в правой части вместо 𝑇 (𝑘) и 𝑇 (𝑙)
    должны стоять максимумы 𝑇 (𝑥) по всем 𝑥, не превосходящим 𝑘 или 𝑙, но это не мешает дальнейшим рассуждениям.) Далее индукцией по 𝑛 нуж- но доказывать оценку 𝑇 (𝑛) 6 𝐶

    𝑛 ln 𝑛. При этом для вычисления средне- го значения 𝑥 ln 𝑥 по всем 𝑥 = 1, . . . , 𝑛 − 1 нужно вычислять ∫︀
    𝑛
    1
    𝑥 ln 𝑥 𝑑𝑥
    по частям как ∫︀ ln 𝑥 𝑑(𝑥
    2
    ). При достаточно большом 𝐶

    член 𝐶𝑛 в пра- вой части перевешивается за счёт интеграла ∫︀ 𝑥
    2
    𝑑 ln 𝑥, и индуктивный шаг проходит.]
    
    7.4.9. Имеется массив из 𝑛 различных целых чисел и число 𝑘. Требу- ется найти 𝑘-е по величине число в этом массиве, сделав не более 𝐶𝑛 дей- ствий, где 𝐶 | некоторая константа, не зависящая от 𝑘 и 𝑛.
    Замечание. Сортировка позволяет очевидным образом сделать это за 𝐶𝑛 log 𝑛 действий. Очевидный способ: найти наименьший элемент,
    затем найти второй, затем третий, . . . , 𝑘-й требует порядка 𝑘𝑛 дей- ствий, то есть не годится (константа при 𝑛 зависит от 𝑘).
    [Указание. Изящный (хотя практически и бесполезный | константы слишком велики) способ сделать это таков:
    А. Разобьём наш массив на 𝑛/5 групп, в каждой из которых по 5 эле- ментов. Каждую группу упорядочим.
    Б. Рассмотрим средние элементы всех групп и перепишем их в мас- сив из 𝑛/5 элементов. С помощью рекурсивного вызова найдём средний по величине элемент этого массива.
    В. Сравним этот элемент со всеми элементами исходного массива:
    они разделятся на большие его и меньшие его (и один равный ему).
    Подсчитав количество тех и других, мы узнаем, в какой из этих частей должен находится искомый (𝑘-й) элемент и каков он там по порядку.
    Г. Применим рекурсивно наш алгоритм к выбранной части.

    134 7. Рекурсия
    Пусть 𝑇 (𝑛) | максимально возможное число действий, если этот способ применять к массивам из не более чем 𝑛 элементов (𝑘 может быть каким угодно). Имеем оценку:
    𝑇 (𝑛) 6 𝐶𝑛 + 𝑇 (𝑛/5) + 𝑇 (примерно 0,7𝑛).
    Последнее слагаемое объясняется так: при разбиении на части каждая часть содержит не менее 0,3𝑛 элементов. В самом деле, если 𝑥 | сред- ний из средних, то примерно половина всех средних меньше 𝑥. А если в пятёрке средний элемент меньше 𝑥, то ещё два заведомо меньше 𝑥.
    Тем самым по крайней мере 3/5 от половины элементов меньше 𝑥.
    Теперь по индукции можно доказать оценку 𝑇 (𝑛) 6 𝐶𝑛 (решающую роль при этом играет то обстоятельство, что 1/5 + 0,7 < 1).]
    

    8. КАК ОБОЙТИСЬ
    БЕЗ РЕКУРСИИ
    Для универсальных языков программирования (каковым является паскаль) рекурсия не даёт ничего нового: для всякой рекурсивной про- граммы можно написать эквивалентную программу без рекурсии. Мы не будем доказывать этого, а продемонстрируем некоторые приёмы,
    позволяющие избавиться от рекурсии в конкретных ситуациях.
    Зачем это нужно? Ответ прагматика мог бы быть таким: во мно- гих компьютерах (в том числе, к сожалению, и в современных, исполь- зующих так называемые RISC-процессоры), рекурсивные программы в несколько раз медленнее соответствующих нерекурсивных программ.
    Ещё один возможный ответ: в некоторых языках программирования ре- курсивные программы запрещены. А главное, при удалении рекурсии возникают изящные и поучительные конструкции.
    8.1. Таблица значений (динамическое программирование)
    8.1.1. Следующая рекурсивная процедура вычисляет числа сочета- ний (биномиальные коэффициенты). Напишите эквивалентную нере- курсивную программу.
    function C(n,k: integer):integer;
    {n >= 0; 0 <= k <=n}
    begin if (k = 0) or (k = n) then begin
    C:=1;
    end else begin {0C:= C(n-1,k-1)+C(n-1,k)
    end;
    end;

    136 8. Как обойтись без рекурсии
    Замечание. 𝐶
    𝑘
    𝑛
    | число 𝑘-элементных подмножеств 𝑛-элементного множества. Соотношение 𝐶
    𝑘
    𝑛
    = 𝐶
    𝑘−1
    𝑛−1
    + 𝐶
    𝑘
    𝑛−1
    получится, если мы фикси- руем некоторый элемент 𝑛-элементного множества и отдельно подсчи- таем 𝑘-элементные подмножества, включающие и не включающие этот элемент. Таблица значений 𝐶
    𝑘
    𝑛
    1 1
    1 1
    2 1
    1 3
    3 1
    называется треугольником Паскаля (того самого). В нём каждый эле- мент, кроме крайних единиц, равен сумме двух стоящих над ним.
    Решение. Можно воспользоваться формулой
    𝐶
    𝑘
    𝑛
    =
    𝑛!
    𝑘! (𝑛 − 𝑘)!
    Мы, однако, не будем этого делать, так как хотим продемонстрировать более общие приёмы устранения рекурсии. Вместо этого составим та- блицу значений функции C(n,k) = 𝐶
    𝑘
    𝑛
    , заполняя её для 𝑛 = 0, 1, 2, . . .,
    пока не дойдём до интересующего нас элемента.
    
    8.1.2. Что можно сказать о времени работы рекурсивной и нере- курсивной версий в предыдущей задаче? Тот же вопрос о памяти.
    Решение. Таблица занимает место порядка 𝑛
    2
    , его можно сократить до 𝑛, если заметить, что для вычисления следующей строки треуголь- ника Паскаля нужна только предыдущая. Время работы остаётся по- рядка 𝑛
    2
    . Рекурсивная программа требует существенно большего вре- мени: вызов C(n,k) сводится к двум вызовам для C(n-1,...), те |
    к четырём вызовам для C(n-2,...) и так далее. Таким образом, время оказывается экспоненциальным (порядка 2
    𝑛
    ). Используемая рекурсив- ной версией память пропорциональна 𝑛 | умножаем глубину рекурсии
    (𝑛) на количество памяти, используемое одним экземпляром процедуры
    (константа).
    
    Кардинальный выигрыш во времени при переходе от рекурсивной версии к нерекурсивной связан с тем, что в рекурсивном варианте одни и те же вычисления происходят много раз. Например, вызов C(5,3)

    8.1. Таблица значений (динамическое программирование)
    137
    в конечном счёте порождает два вызова C(3,2):
    C(5,3)


    C(4,2)
    C(4,3)




    C(3,1)
    C(3,2)
    C(3,3)
    Заполняя таблицу, мы каждую клетку заполняем только однажды |
    отсюда и экономия. Этот приём называется динамическим программи- рованием, и применим в тех случаях, когда объём хранимой в таблице информации оказывается не слишком большим.
    8.1.3. Поговорите на ту же тему на примере рекурсивной и (про- стейшей) нерекурсивной программ для вычисления чисел Фибоначчи,
    заданных соотношением
    ˘
    1
    = ˘
    2
    = 1; ˘
    𝑛
    = ˘
    𝑛−1
    + ˘
    𝑛−2
    (𝑛 > 2).
    
    8.1.4. Дан выпуклый 𝑛-угольник (заданный координатами своих вершин в порядке обхода). Его разрезают на треугольники диагоналя- ми, для чего необходимо 𝑛 − 2 диагонали (это можно доказать индукци- ей по 𝑛). Стоимостью разрезания назовём сумму длин всех использован- ных диагоналей. Найдите минимальную стоимость разрезания. Число действий должно быть ограничено некоторым многочленом от 𝑛. (Пере- бор не подходит, так как число вариантов не ограничено многочленом.)
    Решение. Будем считать, что вершины пронумерованы от 1 до 𝑛
    и идут по часовой стрелке. Пусть 𝑘, 𝑙 | номера вершин, причём 𝑙 > 𝑘.
    Через 𝐴(𝑘, 𝑙) обозначим многоугольник, отрезаемый от нашего хор- дой 𝑘 { 𝑙. (Эта хорда разрезает многоугольник на два, один из которых включает сторону 1 { 𝑛; через 𝐴(𝑘, 𝑙) мы обозначаем другой.) Исходный многоугольник естественно обозначить 𝐴(1, 𝑛). При 𝑙 = 𝑘 + 1 получа- ется «двуугольник» с совпадающими сторонами.
    q q
    
    
    
    
    q q
    q
    A
    A
    A
    A
    q q
    
    
    
    
    q
    
    
    
    
    q
    A
    A
    A
    A
    1
    𝑘
    𝑙
    𝑛
    𝐴(𝑘, 𝑙)

    138 8. Как обойтись без рекурсии
    Через 𝑎(𝑘, 𝑙) обозначим стоимость разрезания многоугольника 𝐴(𝑘, 𝑙)
    диагоналями на треугольники. Напишем рекуррентную формулу для
    𝑎(𝑘, 𝑙). При 𝑙 = 𝑘 + 1 получается двуугольник, и мы полагаем 𝑎(𝑘, 𝑙) = 0.
    При 𝑙 = 𝑘 + 2 получается треугольник, и в этом случае также 𝑎(𝑘, 𝑙) = 0.
    Пусть 𝑙 > 𝑘 + 2.
    q q
    A
    A
    A
    A
    q q
    
    
    
    
    q
    
    
    
    
    q
    𝑘
    𝑙
    𝑖
    @
    @
    @
    @
    @
    @
    Хорда 𝑘 { 𝑙 является стороной многоугольника 𝐴(𝑘, 𝑙) и, следовательно,
    стороной одного из треугольников, на которые он разрезан. Противо- положной вершиной 𝑖 этого треугольника может быть любая из вершин
    𝑘 + 1, . . . , 𝑙 − 1, и минимальная стоимость разрезания может быть вы- числена как min{(длина хорды 𝑘 { 𝑖) + (длина хорды 𝑖 { 𝑙) + 𝑎(𝑘, 𝑖) + 𝑎(𝑖, 𝑙)}
    по всем 𝑖 = 𝑘 + 1, . . . , 𝑙 − 1. При этом надо учесть, что при 𝑞 = 𝑝 + 1
    хорда 𝑝 { 𝑞 | не хорда, а сторона, и её длину надо считать равной 0
    (по стороне разрез не проводится).
    Составив таблицу для 𝑎(𝑘, 𝑙) и заполняя её в порядке возрастания числа вершин (равного 𝑙 − 𝑘 + 1), мы получаем программу, использую- щую память порядка 𝑛
    2
    и время порядка 𝑛
    3
    (однократное применение рекуррентной формулы требует выбора минимума из не более чем 𝑛 чи- сел).
    
    8.1.5. Матрицей размера 𝑚 × 𝑛 называется прямоугольная таблица из 𝑚 строк и 𝑛 столбцов, заполненная числами. Матрицу размера 𝑚 × 𝑛
    можно умножить на матрицу размера 𝑛 × 𝑘 (ширина левого сомножи- теля должна равняться высоте правого), и получается матрица разме- ром 𝑚 × 𝑘. Ценой такого умножения будем считать произведение 𝑚𝑛𝑘
    (таково число умножений, которые нужно выполнить при стандартном способе умножения | но сейчас это нам не важно). Умножение матриц ассоциативно, поэтому произведение 𝑠 матриц можно вычислять в раз- ном порядке. Для каждого порядка подсчитаем суммарную цену всех матричных умножений. Найдите минимальную цену вычисления произ- ведения, если известны размеры всех матриц. Число действий должно быть ограничено многочленом от числа матриц.

    8.1. Таблица значений (динамическое программирование)
    139
    Пример. Матрицы размером 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], . . . , 𝑠-я | на отрезке [𝑠 − 1, 𝑠]. Матри- цы на отрезках [𝑖 − 1, 𝑖] и [𝑖, 𝑖 + 1] имеют общий размер, позволяющий их перемножить. Обозначим его через 𝑑[𝑖]. Таким образом, исходным данным в задаче является массив 𝑑[0] . . . 𝑑[𝑠].
    Через 𝑎(𝑖, 𝑗) обозначим минимальную цену вычисления произведе- ния матриц на участке [𝑖, 𝑗] (при 0 6 𝑖 < 𝑗 6 𝑠). Искомая величина равна
    𝑎(0, 𝑠). Величины 𝑎(𝑖, 𝑖 + 1) равны нулю (матрица одна и перемножать ничего не надо). Рекуррентная формула будет такой:
    𝑎(𝑖, 𝑗) = min{𝑎(𝑖, 𝑘) + 𝑎(𝑘, 𝑗) + 𝑑[𝑖]𝑑[𝑘]𝑑[𝑗]}
    где минимум берётся по всем возможных местам последнего умноже- ния, то есть по всем 𝑘 = 𝑖 + 1, . . . , 𝑗 − 1. В самом деле, произведение матриц на отрезке [𝑖, 𝑘] есть матрица размера 𝑑[𝑖]𝑑[𝑘], произведение матриц на отрезке [𝑘, 𝑗] имеет размер 𝑑[𝑘]𝑑[𝑗], и цена вычисления их произведения равна 𝑑[𝑖]𝑑[𝑘]𝑑[𝑗].
    
    Замечание. Две последние задачи похожи. Это сходство станет яс- нее, если написать матрицы-множители на сторонах 1 { 2, 2 { 3, . . . ,
    (𝑠 − 1) { 𝑠 многоугольника, а на каждой хорде 𝑖 { 𝑗 написать произве- дение всех матриц, стягиваемых этой хордой.
    8.1.6. Железная дорога с односторонним движением имеет 𝑛 стан- ций. Известны цены билетов от 𝑖-й станции до 𝑗-й (при 𝑖 < 𝑗 | в обрат- ную сторону проезда нет). Найдите минимальную стоимость проез- да от начала до конца (с учётом возможной экономии за счёт переса- док).
    
    8.1.7. Задано конечное множество с бинарной операцией (вообще говоря, не коммутативной и даже не ассоциативной). Имеется 𝑛 эле- ментов 𝑎
    1
    , . . . , 𝑎
    𝑛
    этого множества и ещё один элемент 𝑥. Проверьте,
    можно ли так расставить скобки в произведении 𝑎
    1
    ×
    . . . × 𝑎
    𝑛
    , чтобы в результате получился 𝑥. Число операций должно не превосходить 𝐶𝑛
    3
    для некоторой константы 𝐶 (зависящей от числа элементов в выбран- ном конечном множестве).
    Решение. Заполняем таблицу, в которой для каждого участка
    𝑎
    𝑖
    . . . 𝑎
    𝑗
    нашего произведения хранится список всех возможных его зна- чений (при разной расстановке скобок).
    

    140 8. Как обойтись без рекурсии
    По существу этот же приём применяется в полиномиальном алго- ритме проверки принадлежности слова произвольному контекстно-сво- бодному языку (см. главу
    15
    ).
    Следующая задача (задача о рюкзаке) уже упоминалась в главе
    3 8.1.8. Имеется 𝑛 положительных целых чисел 𝑥
    1
    , . . . , 𝑥
    𝑛
    и число 𝑁.
    Выясните, можно ли получить 𝑁, складывая некоторые из чисел 𝑥
    1
    , . . .
    . . . , 𝑥
    𝑛
    . Число действий должно быть порядка 𝑁𝑛.
    [Указание. После 𝑖 шагов хранится множество тех чисел на отрезке
    0 . . . 𝑁, которые представимы в виде суммы некоторых из 𝑥
    1
    . . . 𝑥
    𝑖
    .] 
    Замечание. Мы видели, что замена рекурсивной программы на за- полнение таблицы значений иногда позволяет уменьшить число дей- ствий. Примерно того же эффекта можно добиться иначе: оставить программу рекурсивной, но в ходе вычислений запоминать уже вычи- сленные значения, а перед очередным вычислением проверять, нет ли уже готового значения.
    8.2. Стек отложенных заданий
    Другой приём устранения рекурсии продемонстрируем на примере задачи о ханойских башнях.
    8.2.1. Напишите нерекурсивную программу для нахождения после- довательности перемещений колец в задаче о ханойских башнях.
    Решение. Вспомним рекурсивную программу, перекладывающую 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 верхних дисков с m-го стержня на n-й»
    сводится к трём задачам того же типа: двум задачам с i-1 дисками

    8.2. Стек отложенных заданий
    141
    и к одной задаче с единственным диском. Занимаясь этими задачами,
    важно не позабыть, что ещё осталось сделать.
    Для этой цели заведём стек отложенных заданий, элементами ко- торого будут тройки ⟨i, m, n⟩. Каждая такая тройка интерпретируется как заказ «переложить i верхних дисков с m-го стержня на n-й». Зака- зы упорядочены в соответствии с требуемым порядком их выполнения:
    самый срочный | вершина стека. Получаем такую программу:
    procedure move(i,m,n: integer);
    begin сделать стек заказов пустым положить в стек тройку
    {инвариант: осталось выполнить заказы в стеке}
    while стек непуст do begin удалить верхний элемент, переложив его в
    if j = 1 then begin writeln (’сделать ход’, p, ’->’, q);
    end else begin s:=6-p-q;
    {s - третий стержень: сумма номеров равна 6}
    положить в стек тройки , <1,p,q>,
    end;
    end;
    end;
    (Заметим, что первой в стек кладётся тройка, которую надо выполнять последней.) Стек троек может быть реализован как три отдельных сте- ка. (Кроме того, в паскале есть специальный тип, называемый «запись»
    (record), который может быть применён.)
    
    8.2.2. (Сообщил А. К. Звонкин со ссылкой на Анджея Лисовского.)
    Для задачи о ханойских башнях есть и другие нерекурсивные алгорит- мы. Вот один из них: простаивающим стержнем (не тем, с которого переносят, и не тем, на который переносят) должны быть все стерж- ни по очереди. Другое правило: поочерёдно перемещать наименьшее кольцо и не наименьшее кольцо, причём наименьшее | по кругу.
    
    8.2.3. Используйте замену рекурсии стеком отложенных заданий в рекурсивной программе печати десятичной записи целого числа.
    Решение. Цифры добываются с конца и закладываются в стек, а за- тем печатаются в обратном порядке.
    
    8.2.4. Напишите нерекурсивную программу, печатающую все вер- шины двоичного дерева.

    142 8. Как обойтись без рекурсии
    Решение. В этом случае стек отложенных заданий будет содержать заказы двух сортов: «напечатать данную вершину» и «напечатать все вершины поддерева с данным корнем» (при этом nil считается корнем пустого дерева). Таким образом, элемент стека есть пара: ⟨тип заказа,
    номер вершины⟩.
    Вынимая элемент из стека, мы либо сразу исполняем его (если это заказ первого типа), либо помещаем в стек три порождённых им зака- за | в одном из шести возможных порядков.
    

    1   ...   4   5   6   7   8   9   10   11   ...   19


    написать администратору сайта