Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Костюк - Основы программирования

.pdf
Скачиваний:
134
Добавлен:
30.05.2015
Размер:
1.3 Mб
Скачать

81

горитм сортировки, основанный на слиянии отдельных упорядоченных ранее фраг­ ментов массива.

Предположим, что алгоритм слияния (3.13) оформлен в виде процедуры S со следующим заголовком:

procedure S(var X,Y:mas;b1,e1,e2:integer);

(4.5)

Тип mas описан в программе как массив с подходящим типом элементов и раз­ мером, необходимым для решения задачи. Процедура S переписывает со слиянием

два

подряд расположенных фрагмента в массиве

X, имеющие номера элементов от

b1

до e1 для первого фрагмента и от e1+1 до

e2 для второго фрагмента, в мас­

сив Y с номерами элементов от b1 до e2.

 

Идея алгоритма сортировки слиянием основывается на методе математической индукции.

Базис. Размер N упорядочиваемого фрагмента в массиве X равен 1. Алгоритм

ничего не делает.

Предположение. Пусть алгоритм умеет упорядочивать фрагменты в массиве X

размером от 1 до N = k ≥ 1 элементов.

Индукция. Пусть фрагмент в массиве X имеет размер N = k + 1 ≥ 2 элементов. Тогда алгоритм: 1) делит фрагмент на две равные (или почти равные) части; 2) каж­ дую из частей (размером не более k элементов) рекурсивно упорядочивает; 3) вы­ полняет слияние двух упорядоченных частей в один фрагмент массива Y; 4) копирует фрагмент массива Y в массив X на место исходного фрагмента.

Пример 4.5. Рекурсивный алгоритм сортировки слиянием:

procedure sort(var X,Y:mas;b,e:integer); var c:integer;

begin

if b<e then begin c:=(b+e)div2;

sort(X,Y,b,c); sort(X,Y,c+1,e); (4.6)

S(X,Y,b,c,e); for i:=b to e do

end X[i]:=Y[i] end;

Вызов процедуры sort для упорядочения массива X из n элементов (при ис­ пользовании вспомогательного массива Y) должен быть таким:

sort(X,Y,1,n);

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

82

чиваемого массива 2m – 1 < n ≤ 2m, то не более чем за m последовательных делений размер фрагмента массива станет равным 1. Отсюда следует, что глубина рекурсии также не превысит m, т.е. élog2 nù , что намного меньше размера массивов X и Y.

Трудоемкость T (n) алгоритма (4.6), в котором вначале упорядочиваются две по­ ловинки фрагмента массива, а затем они сливаются во второй массив и копируются

обратно, определяется рекуррентным соотношением

 

 

ìT (1) = 1,

 

 

(4.8)

í

+ cn + d,

n = 2, 3, ...,

îT (n) = 2T (n / 2)

 

где c, d – константы.

Полагая 2m – 1 < n ≤ 2m и пренебрегая константой d (d << c n), из (4.8) получим: T (n) = 2 T (n / 2) + c n = 22 T (n / 4) + c n + c n = … ≤ c n m = c n élog2 nù . (4.9)

Конец примера.

Трудоемкость алгоритма сортировки слиянием оказалась линейно-логарифмиче­ ской. Это намного лучше, чем квадратичная трудоемкость рассматривавшихся ранее алгоритмов упорядочения, см. табл. 2.1.

Алгоритм сортировки слиянием можно реализовать и в нерекурсивном виде, при этом он будет выглядеть сложнее, чем алгоритм (4.6), но ему будет не нужен стек, а вместо рекурсивных вызовов будет выполняться цикл. В нерекурсивном варианте можно сэкономить на памяти для стека (но это всего élog2 nù ) и на времени самих ре­

курсивных вызовов. Рекуррентное соотношение для количества рекурсивных вызо­

вов Rn процедуры (4.6) при n = 2m:

 

 

 

ìR1

= 1,

2

 

(4.10)

í

= 2Rn / 2 + 1, n = 2, 2

, ...,

îRn

 

 

Из соотношения (4.10), аналогичного соотношению (4.1), получим: Rn = 2∙2m – 1 ≈ 2 n.

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

Элементы списков также можно сортировать рекурсивным алгоритмом. При этом списки должны состоять из элементов, тип которых описан аналогично приме­ ру 3.20 (см. алгоритм (3.21)).

Пример 4.6. Процедура sortlist реализует рекурсивный алгоритм сортировки

списков методом слияния (4.7).

Первый параметр в процедуре sortlist является указателем на начало списка, который перед работой процедуры неупорядочен, а после работы становится упорядоченным. Второй параметр задает количество элементов в списке.

83

Впроцедуре sortlist вызывается процедура slist, описанная в примере

3.23(см. алгоритм (3.24)).

 

 

procedure sortlist(var p:pel;n:integer);

 

var p1,p2:pel;

 

 

begin k,i:integer;

 

 

if n>1 then begin

 

 

k:=n div 2; p1:=p;

 

 

for i:=1 to k-1 do p1:=p1^.p;

(4.7)

p2:=p1^.p; p1^.p:=nil; p1:=p;

{список разделен на две почти одинаковые части}

 

sortlist(p1,k);

sortlist(p2,n-k);

 

{обе части списка рекурсивно отсортированы}

 

slist(p1,p2,p)

 

 

{обе части списка объединены слиянием}

 

end

 

 

end;

 

 

Доказательство правильности и анализ трудоемкости алгоритма (4.7) полностью аналогичен доказательству и анализу алгоритма (4.6), реализующего сортировку сли­ янием массивов.

Конец примера.

4.3 Бэктрекинг

Существует ряд задач, которые без рекурсии решить весьма сложно. К ним отно­ сятся, в частности, комбинаторные задачи, в которых ищется решение, состоящее из комбинации значений некоторых переменных. Иногда требуется найти не одно, а несколько, быть может, даже все возможные решения. В качестве примера рассмот­ рим задачу нахождения всех перестановок натуральных чисел от 1 до n. Задачу будем решать следующим образом:

1)в качестве первого числа выбираем любое из чисел 1, …, n;

2)в качестве второго числа выбираем любое из чисел 1, …, n, кроме того чис­ ла, которое выбрано первым;

3)третьим числом выбираем одно из чисел, которое не выбрано первым или вто­ рым и т.д.

Этот процесс продолжаем до тех пор, пока не выберем последнее, n–е число для перестановки. Таким образом, для первого числа выбираются n вариантов, для вто­

84

рого n – 1 вариант и т.д., для последнего, n–го – 1 вариант. Всего получается n (n – 1) … 2∙1 = n! вариантов перестановок.

Чтобы получить все возможные перестановки, надо на каждом этапе выбора k– го числа последовательно перебирать все допустимые числа, однако перейти к следу­ ющему варианту можно, только если найдены полные перестановки для всех преды­ дущих вариантов. Такой процесс можно представить схемой, изображенной на рис. 4.2 для n = 3. В кружках на схеме записаны выбираемые на очередном шаге числа, стрелками – переход к выбору очередного числа.

 

1

 

2

3

 

2

3

1

3

1

2

3

2

3

1

2

1

 

 

Рис. 4.2

 

 

В соответствии со схемой вначале будет выбрана перестановка 1–2–3, затем 1–

3–2, 2–1–3

и т.д. При выборе очередного числа движение по схеме идет по стрелке

вниз, а при

отказе от этого выбора (чтобы выбрать другое число) – обратное движе­

ние (бэктрекинг). Этот процесс легко реализовать рекурсивным алгоритмом. Перестановки будем генерировать в глобальном массиве P, содержащем n эле­

ментов. Массив R (также из

n элементов) содержит признаки включения чисел в

перестановку: если R[i]=1,

то число i включено в перестановку, а если R[i]=0,

то нет.

 

Пример 4.7. Рекурсивный алгоритм генерации перестановок.

procedure per(k:integer); var i:integer;

begin

for i:=1 to n do if R[i]=0 then

begin P[k]:=i; R[i]:=1; (4.8) if k=n then ВЫВОД

else per(k+1);

R[i]:=0 end

85

end;

Рекурсивная процедура per(k) выбирает для перестановок все оставшиеся числа, начиная с k–й позиции. Вспомогательная процедура ВЫВОД выводит n эле­ ментов массива P. Вначале в программе всем элементам массива R присваивается нулевое значение, после чего вызывается per(1).

Правильность алгоритма можно доказать методом математической индукции с

параметром индукции k для любого фиксированного n. Заметим лишь, что присваи­ вание R[i]:=0 после рекурсивного вызова обеспечивает обратный ход – бэктре­

кинг для поиска другого решения.

Глубина рекурсии алгоритма равна n, так как при каждом вложенном рекурсив­ ном вызове параметр k увеличивается на 1, а возврат из рекурсии происходит при k=n. При этом величина n на практике не может быть большой из-за огромного ко­ личества перестановок (например, 10! = 3628800).

Трудоемкость алгоритма можно оценить общим количеством выполнений цикла во всех рекурсивных вызовах. Если бы в цикле не было проверки оператором if, то

получилось бы T1(n) = nn выполнений. С другой стороны, если бы не было лишних

выполнений цикла, т.е. цикл выполнялся бы не n раз, а всего лишь

n k + 1 раз,

то, как можно увидеть из рис. 4.2, общее количество T2(n)

выполнений цикла было

бы равно

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

n!

 

n!

æ

 

1

 

1

ö

 

(4.11)

T

(n) = n + n(n -1) + ...+

 

 

+

 

 

+ n!= n!ç1

+1+

 

+ ...+

 

÷

< e × n!

 

 

 

 

 

 

2

 

2!

 

1!

 

ç

 

2!

 

(n -1)!

÷

 

 

 

 

 

 

è

 

 

ø

 

 

Величины T1(n) и T2(n) определяют соответственно верхнюю и нижнюю гра­ ницы количества выполнений цикла. Точное значение этой величины подсчитать до­ вольно сложно, оно примерно в n раз больше, чем T2(n). Учитывая, что на вывод каждой перестановки тратится n шагов, в результате чего общая трудоемкость вы­ вода равна nn!, можно сделать заключение, что в целом алгоритм (4.8) делает отно­ сительно немного лишней работы.

Конец примера.

На основе алгоритма (4.8) генерации перестановок можно разработать множе­ ство других алгоритмов решения комбинаторных задач.

Пример 4.8. Генерация сочетаний из n чисел по m.

Как и в случае перестановок рассматриваются числа 1, 2, …, n. Из этих чисел необходимо сгенерировать все возможные группы чисел по m чисел, причем в од­ ной группе все числа должны быть различными. Если в процедуре per в операторе

if сравнение k=n заменить сравнением

k=m, то будут генерироваться все пере­

становки таких групп,

т.е. размещения

Am . При этом

будет сгенерировано

 

 

n

 

n (n – 1) … (n m + 1) размещений. Число сочетаний из n по

m, равное биномиаль­

ному коэффициенту Cm ,

в m! раз меньше, чем число размещений Am .

n

 

 

n

86

Чтобы не делать лишнюю работу, будем в каждой такой группе генерировать

числа в возрастающем порядке, помещая их в массив C из

m элементов. Тогда на

первом месте может располагаться число C1

в диапазоне от

1 до

n m + 1, на вто­

ром месте – число C2

в диапазоне от C1 + 1

до n m + 2, …, на

m–м месте – чис­

ло Cm в диапазоне от

Cm – 1 + 1 до n. В этом случае нет необходимости использова­

ния массива R, но для того, чтобы алгоритм выглядел одинаково как при генерации

первого числа, так и остальных чисел, массив

C должен содержать дополнительный

нулевой элемент с предварительно присвоенным ему значением 0. Рекурсивный алгоритм генерации сочетаний:

procedure comb(k:integer); var i:integer;

begin

for i:=C[k-1]+1 to n-m+k do

begin C[k]:=i; (4.9) if k=m then ВЫВОД

else comb(k+1); end;end

Правильность алгоритма (4.9) доказывается аналогично алгоритму (4.8). В алго­ ритме (4.9) глубина рекурсии равна m. Что касается трудоемкости, то алгоритм (4.9),

в отличие от алгоритма (4.8), не делает никакой лишней работы.

Для вычисления сочетаний процедура comb вызывается оператором comb(1);

Конец примера.

Пример 4.9. Рассмотрим еще одну комбинаторную задачу – головоломку «маги­

ческие числовые квадраты». В квадратной таблице

n × n

требуется так расставить

числа 1, 2, …, n2,

чтобы суммы по всем строкам, столбцам и главным диагоналям

были одинаковы. На рис. 4.3

приведены примеры магических квадратов для n = 3,

n = 4 и n = 5. Задача имеет решение лишь для n ≥ 3.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1

2

 

13

24

25

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

9

1

8

16

 

 

5

23

 

22

7

8

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2

7

6

 

4

12

5

13

 

 

20

19

 

11

12

3

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

9

5

1

 

14

6

11

3

 

 

21

4

 

9

16

15

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

4

3

8

 

7

15

10

2

 

 

18

17

 

10

6

14

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 4.3

87

Будем искать все возможные магические квадраты для заданного n. Квадрат­ ную таблицу вытянем в одну строку с нумерацией ее элементов от 1 до n2. Тогда решением будет такая перестановка чисел 1, 2, …, n2, которая удовлетворяет услови­ ям магического квадрата. Поэтому за основу алгоритма можно взять алгоритм (4.7) генерации перестановок, дополнив его проверкой условий.

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

Сумма S

всех чисел квадрата равна S = 1 + 2 + … + n2 = n2 (n2 + 1) / 2. Эта сумма де­

лится на

n одинаковых частей (по количеству строк в квадрате). Таким образом, ис­

комая сумма S0 = n∙(n2 + 1)/2.

В отличие от перестановок в алгоритме (4.8) магическими квадратами будет лишь небольшая часть всех перестановок. Так, для n = 3 только 8 перестановок из 9! = 362880, а для n = 4 всего лишь 7040 из 16! = 20922789888000. Пусть, напри­ мер, компьютер способен за 1 секунду сгенерировать и проверить 106 квадратов. То­ гда для генерации всех магических квадратов при n = 4 ему потребуется почти год!

Чтобы уменьшить лишнюю работу, в алгоритме необходимо организовать отсе­ чение тех вариантов квадрата, которые заведомо не будут магическими. Пример ал­

горитма генерации магических квадратов с отсечениями:

 

 

 

 

 

 

procedure sq(k:integer);

 

 

 

var i:integer;

 

 

 

begin

 

 

 

if k mod n=0 then begin

{последний столбец}

 

 

i:=s0-sumH(k);

 

 

 

if (i>=1)and(i<=n2)and(R[i]=0) then begin

 

 

R[i]:=1; T[k]:=i;

 

 

 

if k=n2 then ВЫВОД

 

 

 

else sq(k+1);

 

 

 

R[i]:=0

 

 

 

end

 

 

 

end

 

 

 

else if k>n2-n then begin

{последняя строка}

 

 

i:=s0-sumV(k);

 

 

 

if (i>=1)and(i<=n2)and(R[i]=0) then begin

(4.10)

 

R[i]:=1; T[k]:=i;

 

 

 

sq(k+1);

 

 

 

R[i]:=0

 

 

 

end

 

 

 

end

 

 

 

else begin

{другие элементы}

 

 

for i:=1 to n2 do

 

 

 

if R[i]=0 then begin

 

 

 

R[i]:=1; T[k]:=i;

 

 

 

sq(k+1);

 

 

88

R[i]:=0 end end

end;

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

Отсечение реализовано следующим образом. Пусть k – порядковый номер гене­ рируемого элемента в массиве T, который представляет собой вытянутый в одну ли­ нию квадрат. Если номер k соответствует последнему столбцу, то можно сразу определить то число i, которое должно быть на k–м месте. Число i равно S0 за

вычетом суммы предыдущих элементов квадрата в текущей строке (при этом должно выполняться R[i]=0). Если, к тому же, номер k соответствует последнему эле­

менту квадрата, то нужно дополнительно проверить сумму последнего столбца и суммы двух диагоналей. Если номер k соответствует последней строке, то очеред­

ное число можно определить аналогичным образом.

Переменная s0 в алгоритме (4.10) содержит значение S0 (сумму одной строки или столбца в магическом квадрате), n2=n*n. Вспомогательная функция sumH(k) вычисляет сумму элементов в текущей строке квадрата, расположенных в массиве T перед k–м элементом, а функция sumV(k) – элементов в текущем столбце квад­ рата, расположенных выше k–го элемента. Функция ВЫВОД осуществляет про­ верку последнего столбца и двух диагоналей квадрата, и при положительном исходе проверок выводит сформированный магический квадрат. Благодаря отсечениям алго­ ритм (4.10) находит все магические квадраты при n = 4 за 2–3 часа вместо года ра­ боты.

Для еще большего ускорения в алгоритме (4.10) можно дополнительно преду­ смотреть отсечение на более ранних этапах генерации квадрата: когда номер k со­ ответствует предпоследнему столбцу, и когда номер k соответствует предпоследней строке квадрата. Если реализовать все эти усовершенствования, можно уменьшить время работы алгоритма при n = 4 до нескольких минут.

Конец примера.

А теперь кратко рассмотрим общие принципы метода бэктрекинга для решения задач комбинаторного характера:

1)вначале следует определить такие структуры данных, с помощью которых можно перечислить все возможные варианты решения задачи;

2)затем необходимо записать основную рекурсивную процедуру с циклом, в ко­ тором производится перебор вариантов движения вниз на одну ступень по схеме из

89

текущего состояния (с помощью рекурсивного вызова), либо выдачу полностью сге­ нерированного очередного варианта;

3)в процедуре следует предусмотреть проверку корректности полного сгенери­ рованного варианта;

4)следует предусмотреть проверку бесперспективности частично сгенерирован­ ных вариантов и их отсечения, для чего при отрицательной проверке не следует вы­ полнять рекурсивный вызов.

Вопросы и задания

1.Как необходимо записывать рекурсивный алгоритм, чтобы гарантировать его заверши­ мость?

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

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

4.Записать в рекурсивном виде алгоритм поиска элемента в неупорядоченном массиве и до­ казать его правильность.

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

6.Написать и отладить программу, использующую алгоритм (4.2), которая вводит число пе­ рекладываемых дисков в задаче «Ханойские башни» и выводит сообщения о перекладыва­ емых дисках.

7.Написать и отладить программу, использующую алгоритм (4.4), которая вводит номер ва­ рианта подынтегральной функции и интервал интегрирования, после чего вычисляет ин­ теграл с точностью 10–6. В качестве подынтегральных функций использовать такие эле­ ментарные функции, как sin x, cos x, ln x и др. Сравнить получившиеся значения с точ­ ными.

8.Написать и отладить программу, которая вводит размер n (n≤30000) массива X, выделя­

ет для него и других необходимых массивов динамическую память, генерирует случай­ ные целочисленные значения для массива X. После этого выполняет алгоритм простей­ шей сортировки (1.8), обменной сортировки (2.8) и сортировки слиянием (4.6), каждый раз предварительно копируя исходные данные в другой массив. До и после выполнения

каждого алгоритма сортировки программа должна сделать замеры текущего времени (процедурой gettime), вычислить и вывести время работы каждого из алгоритмов. На­ рисовать графики зависимости времени работы алгоритмов от n.

9.Провести полное доказательство правильности алгоритма генерации перестановок (4.8) методом математической индукции.

10.Провести полное доказательство правильности алгоритма генерации сочетаний (4.9) мето­ дом математической индукции.

11.На основе алгоритма генерации перестановок (4.8), записать алгоритм генерации разме­ щений из n по m и доказать его правильность.

90

12.Усовершенствовать алгоритм (4.10), предусмотрев отсечение на более ранних этапах гене­ рации квадрата. Написать программу, которая вводит размер квадрата n и количество ге­ нерируемых квадратов L. После этого программа должна генерировать и выводить L магических квадратов (если при заданном n их оказалось меньше L, то все сгенериро­ ванные квадраты).