Открыть Электронные книги
Категории
Открыть Аудиокниги
Категории
Открыть Журналы
Категории
Открыть Документы
Категории
Ю. Л. Костюк
ЛЕКЦИИ ПО ОСНОВАМ
ПРОГРАММИРОВАНИЯ
Учебное пособие
Издательство
2019
УДК 510.5
ББК 22.18.73
ISBN
Рецензенты:
заведующий кафедрой автоматизированных систем управления Том-
ского государственного университета систем управления и радиоэлек-
троники, доктор технических наук, профессор А. М. Кориков;
профессор кафедры программирования Томского государственного
университета, доктор технических наук А. Ю. Матросова
Рис. 1.1
Входные
данные
Компьютер
Рис. 1.2
1.2. Тестирование
Компьютер, исполняя программу-транслятор, действует по строго
формальным правилам: он не способен вникнуть в замысел программиста
и понять идею алгоритма, положенного в основу анализируемой програм-
мы. Из этого следует, что транслируемая программа не должна содержать
ни одной синтаксической ошибки, т.е. программа должна строго соот-
ветствовать правилам языка программирования. Но так как программу
пишет человек (а человеку свойственно ошибаться!), то синтаксические
ошибки почти всегда присутствуют. В процессе трансляции транслятор
выявляет такие ошибки и выдает о них сообщения. Программист должен
проанализировать сообщения транслятора и исправить ошибки. Следует
заметить, что процесс исправления не всегда прост, тем более что сообще-
ния транслятора относятся к тому участку текста программы, в котором
была обнаружена синтаксическая ошибка, но не к первоначальной
причине ошибки. И даже если программа транслировалась успешно, это
совсем не означает, что она безупречна: в ней вполне могут быть смысло-
вые, т.е. семантические ошибки.
Компьютер является идеальным исполнителем алгоритмов: на то, что
современным компьютером выполняется за одну секунду, человеку может
потребоваться несколько лет. Однако чтобы программу можно было ис-
пользовать по ее прямому назначению, в ней не должно быть ошибок: при
ее исполнении для любых допустимых входных данных она каждый раз
должна выдавать правильные выходные данные. К сожалению, как пока-
зывает практика, никакой сверхгениальный программист не может напи-
сать достаточно большую программу сразу без единой семантической
ошибки.
Алгоритмы и программы. Тестирование. Аналитическая верификация 9
A B
Рис. 1.3
то
{P} А; В {R},
т. е. постусловие для А есть предусловие для В.
Последовательность может состоять из любого количества операторов.
Если последовательность взять в «операторные скобки» begin и end, то ее
можно считать одним оператором.
Алгоритмы и программы. Тестирование. Аналитическая верификация 15
да
A
B
нет
Рис. 1.4
да
A
E
нет
Рис. 1.5
2n 3 3n 2 n 2(n 1) 3 3(n 1) 2 (n 1)
S n1 (n 1) 2 .
6 6
Конец примера.
Проиллюстрируем применение метода математической индукции на
примере программы вычисления факториала.
Пример 1.7. Пусть выполнено предусловие n≥1. Требуется доказать,
что после выполнения программы
f:=1; i:=2;
while i<=n do
begin f:=f*i;
i:=i+1
end;
будет справедливо постусловие:
f=1*2*...*n, i=n+1.
Доказательство. Завершимость очевидна, так как переменной i пе-
ред выполнением цикла присваивается 2, а при каждом исполнении цикла
она увеличивается на 1. Методом математической индукции легко дока-
зать, что после n-1 исполнений цикла переменная i будет равна n+1
и условие в заголовке цикла станет ложным. Справедливость постусловия
также докажем этим методом.
Базис. Если n=1, то f=1, i=2, так как цикл ни разу не будет выпол-
няться. Таким образом, базис справедлив.
Предположение. Предполагаем, что при n=k, k≥1, справедливо по-
стусловие f=1*2*...*k, i=k+1.
Индуктивный вывод. Все отличие выполнения программы при n=k+1
от выполнения при n=k состоит в том, что вначале программа выполнит-
ся в точности так же, как при n=k, а затем цикл будет исполнен еще один
раз для i=k+1. Так как, по предположению, перед последним выполне-
нием цикла f=1*2*...*k, то после последнего выполнения цикла (по-
сле исполнения операторов f:=f*i; i:=i+1) будет верно:
f=1*2*...*k*(k+1)=1*2*...*n, i=k+2=M+1,
что и требовалось доказать.
Конец примера.
Алгоритмы и программы. Тестирование. Аналитическая верификация 21
Постусловие: i=n+1.
Инвариант: «вычислены все простые числа от 2 до i-1.
Доказательство для внутреннего цикла.
Предусловие для внутреннего цикла: j=2,p=0.
Постусловие: j=n,p=0, тогда число i простое,
или: j<n,p=1, тогда число i не простое.
Инвариант: «число i не делится на числа от 2 до j-1
и: (число не делится на j,p=0)
или: (число делится на j,p=1)».
Конец примера.
Метод абстракции применяется также при разработке сложных про-
грамм. В этом случае он называется методом поэтапной разработки или
методом сверху-вниз и реализуется следующим образом:
- вначале создаётся общая программа (на первом уровне), внутри кото-
рой отдельные операторы – это программы второго уровня;
- затем создаются программы второго уровня, внутри которых отдель-
ные операторы – это программы третьего уровня и т.д.
Наибольший положительный эффект достигается, если в качестве от-
дельных операторов удается применить ранее созданные программы. Для
удобства их можно оформить в виде процедур или функций, как типовые
программы. При этом часто такие типовые программы приходится моди-
фицировать, подгонять под конкретные условия.
Таким образом, чтобы успешно разрабатывать сложные программы,
программист должен:
1) знать и понимать большое количество типовых программ;
2) уметь модифицировать типовые программы;
3) уметь из типовых программ собирать, как из «кирпичиков», более
сложные программы.
На практике аналитическая верификация не заменяет, а дополняет
компьютерное тестирование. Имеется ряд причин, по которым нельзя
ограничиться только аналитическим доказательством.
Во-первых, очень трудно учесть все ограничения, связанные с кон-
кретным представлением данных (особенно целых и вещественных чисел)
в компьютере.
28 Лекция 1
1 2 3 ... n n n .
Таким образом, порядок трудоёмкости O(n3/2).
Конец примера.
Алгоритмы и программы. Тестирование. Аналитическая верификация 31
Вопросы и задания
1. Какими свойствами должен обладать алгоритм? В чем смысл этих свойств?
2. Какими свойствами обладает алгоритм, если у него нет входных данных?
3. Для чего необходим специальный язык программирования?
4. Что такое синтаксическая и что такое семантическая ошибка в программе? Как и
когда они обнаруживаются?
5. В чем состоит цель тестирования?
6. Почему невозможно, как правило, исчерпывающее тестирование?
7. Привести пример алгоритма, для которого возможно исчерпывающее тестирование.
8. Почему тестированием нельзя доказать отсутствие ошибок в программе?
9. Написать программу, которая вводит числа a и b, и вычисляет x согласно уравне-
нию: a∙x + b = 0. Привести примеры тестов по методу черного и белого ящика для
неё. В каком случае выполнение программы будет невозможно?
10. Программа возводит целое число n в целую степень m и выдает результат, при-
сваиваемый переменной типа integer. Разработать тесты по методу черного ящи-
ка. Каков будет результат при n = 8 и m = 40?
11. Как выполнять пошаговую отладку, используя какой-либо транслятор для Паскаля?
12. Написать программу, которая вводит три вещественных числа, проверяет, может ли
существовать треугольник со сторонами, равными этим числам. Если не может, то
выдает сообщение об этом и просит ввести данные повторно. Если может, то вы-
числяет и выводит площадь треугольника. Разработать тесты по методу черного
ящика и по методу белого ящика, протестировать программу на объединенных те-
стах.
13. Как выбирать тесты по методу белого ящика для последовательности операторов,
условного оператора и цикла?
14. Доказать методом математической индукции, что 1 + 2 + … + n = n (n + 1)/2 .
15. Доказать методом математической индукции, что 1 + 3 + … + (2n – 1) = n2 .
32 Лекция 1
16. В чем состоит доказательство свойств программы? Что значит доказать заверши-
мость программы?
17. Что такое предусловие и постусловие, как их задавать?
18. Какую структуру имеют программы, при доказательстве свойств которых применя-
ется последовательное перечисление выполняемых действий?
19. Какую структуру имеют программы, при доказательстве свойств которых применя-
ется перечисление вариантов?
20. Какую структуру имеют программы, при доказательстве свойств которых применя-
ется метод математической индукции?
21. Какую структуру имеют программы, при доказательстве свойств которых применя-
ется метод инварианта?
22. Какую структуру имеют программы, при доказательстве свойств которых применя-
ется метод абстракции?
23. В чем состоит доказательство методом эквивалентов?
24. Написать программу, которая вычисляет максимальное значение из трёх введённых
чисел. Разработать для неё тесты по методу черного и белого ящика. Доказать пра-
вильность программы.
25. Написать программу, не использующую массивы, которая вводит n, затем n целых
чисел и выводит два наибольших из введённых чисел. Разработать для неё тесты по
методу черного и белого ящика. Доказать правильность программы. Какова трудо-
ёмкость программы?
26. Дан целочисленный двумерный массив из n строк и m столбцов. Написать про-
грамму вычисления сумм его элементов во всех столбцах. Доказать ее корректность
и вывести трудоёмкость.
27. Программу из примера 1.18 можно ещё больше ускорить, если во внутреннем цикле
проверять делимость числа i не подряд на все числа, которые не больше √i, а только
на ранее вычисленные простые числа, не большие √i. Вывести формулу трудоёмко-
сти, учитывая, что при больших n количество простых чисел, не больших n, стре-
мится к величине n/ln n.
Лекция 2.
Рекуррентные алгоритмы
x 2k 3
f k 1 (1) k 2 ,
(2k 3)!
получим рекуррентное соотношение для элементов суммы:
f1 x,
f f x2
k k 1 , k 2, 3, ...
(2k 1)(2k 2)
Программа вычисления суммы с заданной точностью eps:
S:=x; f:=x; k:=2;
while abs(f)>=eps do
begin
f:=-f*x*x/((2*k-1)*(2*k-2));
S:=S+f;
k:=k+1
end
Завершимость программы следует из того, что fk → 0 при k → ∞.
Здесь инвариант такой же, как в примере 2.2:
f = f k-1, S = Sk-1,
Трудоёмкость алгоритма (количество k выполнений цикла) можно
определить из формулы:
2 k 1
x
fk ,
(2k 1)!
1 a a ( a 1) 0 ,
2
1
e1 s1 a
2 2
и тогда
1 ek21 1
ek 0, ek ek 1 , k 2, ...
2 a ek 1 2
fn . (*)
5 2 2
Доказательство.
Базис. При n = 0 и n = 1 проверяем простой подстановкой в форму-
лу (*).
Предположение. Пусть при n ≥ 0 формула (*) верна.
40 Лекция 2
1 1 5 1 5 1 1 5 1 5
n n n 1 n 1
f n1
5 2 2 5 2 2
n 1
1 5
n 1
1 1 5
,
5 2 2
учитывая, что:
2
1 5
1 1 5 .
2 2
Число lim ( f k 1 / f k ) (1 5 ) / 2 1,61803398875, входящее в фор-
k
мулу (*), называют золотым сечением. Заметим, что эту формулу целе-
сообразно применять при достаточно больших n. При этом вычисления
необходимо выполнять над вещественными, т.е. приближенными числами,
а результат округлять до целого. Так как абсолютное значение второго
слагаемого в формуле (*) меньше 0.5, то им можно пренебречь.
Конец примера.
Пример 2.9. Алгоритм Евклида, вычисляющий наибольший общий де-
литель НОД(a, b) двух целых чисел a ≥ 0, b ≥ 0, основан на следующих
инвариантных соотношениях:
1) НОД(a, 0) = a;
2) НОД(a, b) = НОД(b, a);
3) НОД(a, b) = НОД(a mod b, b), a ≥ b > 0.
Первые два соотношения очевидны. Докажем третье. Во-первых, заме-
тим, что операция r = a mod b эквивалентна многократному вычитанию b
из a до тех пор, пока не будет выполняться 0 ≤ r < b. Поэтому достаточно
доказать, что
НОД(a, b) = НОД(a – b, b), a ≥ b > 0.
Пусть верно противоположное утверждение, а именно:
НОД(a, b) = d, НОД(a – b, b) = c, причем c ≠ d.
Рекуррентные алгоритмы 41
ab a b ab b
Но тогда , при этом , а также – оба целые, в то вре-
c c c c c
a
мя как может быть целым лишь в случае, если c = d. Это противоречие
c
и доказывает третье соотношение.
Из этих соотношений можно построить такую последовательность пар
{xi, yi}, что НОД(xi, yi) = НОД(a, b), max(xi, yi) > max(xi + 1, yi + 1):
x0 a, y0 b,
xi 1 xi mod yi , yi 1 yi , при xi yi 0,
y y mod x , x x , при yi xi 0.
i 1 i i i 1 i
где (1 5 ) / 2 .
42 Лекция 2
Вопросы и задания
b:=1; e:=n;
while b<e do
begin
c:=(b+e)div 2;
if A[In[c]]<p then b:=c+1
else e:=c
end;
if A[In[b]]=p then i:=b else i:=0
Конец примера.
Вопросы и задания
ве А. Создать тесты для программы методом чёрного и белого ящика и провести те-
стирование.
16. Написать программу, которая вводит числа n, d, r и строку символов S c именем для
файла, затем переназначает стандартный вывод на этот файл. После этого выводит
в файл число n, генерирует и записывает в файл n случайных чисел из диапазона от
–d до +d при заданном начальном значении r. Создать тесты для программы мето-
дом чёрного и белого ящика и провести тестирование.
17. Написать программу, которая вводит строку символов S c именем для входного
файла (который записан программой из предыдущего задания), затем переназначает
стандартный ввод на этот файл. Из файла считывает число n, выделяет память для n
элементов массива А и вводит n элементов в массив А. После этого косвенно упоря-
дочивает его алгоритмом сортировки выбором и затем выводит его элементы в
упорядоченном виде без повторений. Создать тесты для программы методом чёрно-
го и белого ящика и провести тестирование.
Лекция 4.
Рекурсия
n fact(n); n fact(n);
3 3 6
2 2 2
1 1 1
0 0 1
Рис. 4.1
Конец примера.
При каждом вызове в момент входа в процедуру или функцию выделя-
ется память для аргументов и для всех внутренних переменных, описанных
внутри процедуры или функции. Поэтому максимальная глубина рекурсии
определяет максимальный размер выделяемой памяти, которую надо учи-
тывать при анализе рекурсивных процедур и функций. При этом особое
внимание следует обращать на то, как описаны способы подстановки фор-
мальных параметров – по значению или по ссылке. При подстановке по
значению в стеке выделяется память для значения этого параметра в мо-
мент вызова (если параметр массив, то выделяется память для всех эле-
ментов массива, т.е. создается его копия, и все действия с массивом внутри
процедуры или функции будут выполняться с этой копией). При подста-
новке по ссылке выделяется память для адреса в памяти, где расположено
значение фактического параметра (так, для массива копия не создаётся, все
действия с массивом будут выполняться с подставленным при вызове
функции массивом).
Пример 4.4. Задача «Ханойские башни». Имеется три колышка, обо-
значенные буквами: a,b,c. На колышке a нанизано n дисков в виде
башни, как в детской пирамидке.
Задача: переложить всю башню на колышек c, перекладывая по одному
диску так, чтобы любой диск большего размера не лежал на меньшем дис-
ке для любого из колышков.
Доказательство.
Базис. Число дисков n=1. Переложить диск с колышка a на
колышек c.
Рекурсия 65
Рис. 4.2
66 Лекция 4
Из него получаем:
H(n) = 2H(n – 1) + 1 = 22H(n – 2) + 2 + 1 = 2n – 1 + 2n – 2 + … + 2 + 1 = 2n – 1.
Таким образом, трудоёмкость алгоритма O(2n), и это минимально воз-
можная трудоёмкость решения задачи. Глубина рекурсии: n.
Если на перекладывание одного диска тратить 1 секунду, то при n = 64
всю работу можно завершить за 264 с ≈ 580 млрд. лет.
Конец примера.
Пример 4.5. Рекурсивное вычисление минимального значения среди
элементов массива A[0],...,A[n]:
Рекурсия 67
function fib(n:integer):integer;
begin
if n=0 then begin F[0]:=0; fib:=0 end
else if n=1 then
begin F[1]:=1; fib:=1 end
else if F[n]>0 then fib:=F[n] end
else begin
F[n]:=fib(n-1)+fib(n-2); fib:=F[n];
end;
end;
Числа Фибоначчи запоминаются в массиве F с нумерацией от 0 до n.
Перед вызовом элементы этого массива надо обнулить:
for i:=0 to n do F[i]:=0;
x:=fib(n);
Этот вариант алгоритма имеет трудоёмкость и глубину рекурсии O(n),
так как каждое из чисел Фибоначчи вычисляется только один раз, однако
трудоёмкость его в несколько раз больше, чем при вычислении чисел
Фибоначчи в цикле.
Конец примера.
Любой алгоритм, вычисляющий рекуррентное соотношение, можно
реализовать в рекурсивном виде. Однако из рассмотрения последних двух
примеров можно сделать вывод о том, что если рекуррентную последова-
тельность можно вычислять с помощью цикла, то её и следует вычис-
лять с помощью цикла, а не рекурсии!
R1 1,
Rn 2 Rn / 2 1, n 2, 2 , ...
2
Отсюда получим:
Rn = 2Rn/2 + 1 = 4Rn/4 + 2 + 1 = . . . = 2∙2m – 1 ≈ 2 n.
Таким образом, количество всех рекурсивных вызовов гораздо мень-
ше, чем количество других действий при выполнении алгоритма (сравне-
ний и копирований).
В целом трудоёмкость T (n) имеет порядок: O(n log n).
Конец примера.
В таблице 4.1 приведены значения таких функций, как log 2 n ,
n log 2 n , n ,n , n , 2 , которые характеризуют трудоёмкость ряда типо-
3/2 2 3 n
вых алгоритмов.
Таблица 4.1
log 2 n n log 2 n
3/2 2 3
n n n n 2n
4 2 8 8 16 64 16
10 4 40 31 100 1000 1024
20 5 100 100 400 8000 ≈106
40 6 240 280 1600 64000 ≈1012
100 7 700 1000 10000 106 ≈1030
1000 10 10000 31000 106 109 ≈10300
106 20 20∙106 109 1012 1018 ≈10300000
Вопросы и задания
5 10 5 10
p1 p2 p1 p2
Рис. 5.1
p1^.s:=5; p1^.p:=p2;
В первом элементе списка содержимое 5, указатель указывает на 2-й
элемент. Здесь p1^.s – это поле s элемента списка, на который указывает
указатель p1, а p1^.p – это поле p элемента списка, на который указывает
указатель p1.
p2^.s:=10; p2^.p:=nil;
Во втором элементе списка содержимое 10, указатель равен nil (осо-
бое пустое значение, на рисунке перечеркнутый прямоугольник, означает
конец списка). В результате получился простой линейный список. Указа-
тель p1 указывает на начало списка, указатель p2 – на конец списка. Если
последнее присваивание заменить на:
p2^.p:=p1;
то во втором элементе списка указатель будет указывать на начальный
элемент списка. В результате получится циклический список, в котором из
последнего элемента можно по ссылке перейти к первому элементу.
Список может содержать произвольное число элементов, даже ни од-
ного, тогда указатель на список равен nil.
Если какой-либо элемент списка больше не нужен, его можно удалить
стандартной процедурой dispose, аргумент которой – переменная-
указатель, ссылающаяся на удаляемый элемент списка, например:
dispose(p1);
После удаления доступ к элементу становится невозможным, а указа-
телю присваивается значение nil.
78 Лекция 5
p3:=p1;
while (p3<>nil)and(p3^.s<S0) do
p3:=p3^.p;
if (p3<>nil)and(p3^.s>S0) then
p3:=nil;
Инвариант цикла здесь такой же, как в примере 5.5.
Постусловие цикла: p3=nil или p3^.s≥S0.
Трудоёмкость O(n). В отличие от примера 5.5 цикл может закончиться
раньше как при успешном, так и при безуспешном поиске.
Конец примера.
Пример 5.7. Вставка элемента со значением S0 в упорядоченный по
возрастанию список. Указатель p1 – начало списка, p2 – конец списка:
new(p3); p3^.s:=S0;
p4:=nil; p5:=p1;
while (p5<>nil)and(p5^.s<S0) do
begin p4:=p5; p5:=p5^.p end;
if p4=nil then p1:=p3{вставка в начало списка}
else p4^.p:=p3;{вставка в список между p4 и p5}
p3^.p:=p5;
if p5=nil then p2:=p3;{вставка в конец списка}
Вначале в цикле выполняется поиск места для вставки нового элемента
(с указателем p3). Варианты постусловия цикла:
1) p5=nil, p4=nil – список был пустой, создаётся из нового элемен-
та;
2) p5=nil, p4≠nil – просмотрен весь список, p4 указывает на по-
следний элемент списка, новый элемент присоединяется в конец списка;
3) p5≠nil, p5^.s≥S0, p4=nil – цикл ни разу не выполнялся, новый
элемент присоединяется в начало списка;
4) p5≠nil, p5^.s≥S0, p4≠nil – новый элемент вставляется между
элементами списка с указателями p4 и p5.
Конец примера.
Пример 5.9. Удаление элемента со значением S0 из упорядоченного по
возрастанию списка. Указатель p1 – начало списка, p2 – конец списка:
82 Лекция 5
p4:=nil; p5:=p1;
while (p5<>nil)and(p5^.s<S0) do
begin p4:=p5; p5:=p5^.p end;
if (p5<>nil)and(p5^.s=S0) then begin
{p5 указывает на удаляемый элемент}
if p4<>nil then p4^.p:=p5^.p
else p1:= p5^.p; {удаляемый элемент 1-й}
if p5^.p=nil then p2:=p4;
{удаляемый элемент последний}
dispose(p5);
end;
Вначале в цикле выполняется поиск удаляемого элемента. Варианты
постусловия цикла:
1) p5=nil – список не содержит удаляемого элемента;
2) p5≠nil, p5^.s≥S0, p4=nil – цикл ни разу не выполнялся, удаля-
емый элемент 1-й;
3) p5≠nil, p5^.s≥S0, p4≠nil – удаляемый элемент находится меж-
ду элементами списка с указателями p4 и p5.
Конец примера.
Рассмотренные программы выполняют поиск с помощью цикла. С дру-
гой стороны, рекурсивные алгоритмы поиска обычно более простые.
Пример 5.10. Рекурсивный поиск значения S0 в неупорядоченном ли-
нейном списке:
function flist(p:pel,S0:integer):pel;
begin
if p=nil then flist:=nil
else if p^.s=S0 then flist:=p
else flist:=flist(p^.p,S0)
end;
Выход из рекурсии: либо возвращаемый указатель указывает на эле-
мент с содержимым, равным S0, либо он равен nil, тогда искомого зна-
чения в списке нет. Пример вызова:
p3:=flist(p1,d);
Конец примера.
Списочные структуры 83
Вопросы и задания
1 2 3
2 3 1 3 1 2
3 2 3 1 2 1
Рис. 6.1
else
for i:=1 to n do
if R[i]=0 then begin
P[k]:=i; R[i]:=1;
if k=n then ВЫВОД
else pers(k+1,s+i);
R[i]:=0
end
end;
Параметры процедуры:
k – номер элемента массива P, в котором будут запоминаться очеред-
ные генерируемые числа;
s – сумма чисел в ранее вычисленной части генерируемой перестанов-
ки, s=P[1]+...+P[k-1].
Вызов процедуры генерации:
for i:=1 to n do R[i]:=0;
pers(1,0);
Алгоритм процедуры pers построен на основе алгоритма per. Отли-
чие в том, что в процедуре pers проверка того, что генерируемая переста-
новка чисел будет удовлетворять требуемому условию, выполняется при
генерации числа номер n1 в перестановке. Такое число может не суще-
ствовать, и тогда производится выход из процедуры, т.е. отсечение всех
продолжений вариантов генерации.
Конец примера.
Пример 6.8. Задача о ферзях. Пусть на шахматной доске клетки нуме-
руются двумя числами – номером строки и номером столбца. Фигура
ферзь, стоящая на клетке (i, j), держит под ударом клетки шахматной доски
в строке i, столбце j, а также клетки двух диагоналей, проходящих через
клетку (i, j). Будем рассматривать «шахматную» доску размером n x n. Тре-
буется на доске расставить n ферзей таким образом, чтобы они не «били»
друг друга. Расстановка записывается в виде n чисел, число i на j–м месте
задаёт ферзя на клетке (i, j). Например, для доски 4х4 возможна расстанов-
ка (2, 4, 1, 3), показанная на рис. 6.1.
Бэктрекинг 97
Ф
Ф
Ф
Ф
Рис. 6.1
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
Рис. 6.3
Вопросы и задания
for i:=1 to N do
if (A[i]=1)xor(B[i]=1) then C[i]:=1
else C[i]:=0;
Дополнение множества A (\ A) – в множество C входят те объекты,
которые отсутствуют в множестве A (но есть в универсуме):
for i:=1 to N do
C[i]:=1-A[i];
Конец примера.
Пример 7.3. Операции над множествами, представленными логиче-
скими массивами (тип boolean).
Проверка принадлежности объекта множеству М:
if M[i] then {объект номер i принадлежит множеству}.
Добавление объекта номер i в множество М:
M[i]:= true;
Удаление объекта номер i из множества М:
M[i]:= false;
Вычисление мощности K множества М:
K:=0;
for i:=1 to N do
if M[i] then K:=K+1;
Объединение (A U B):
for i:=1 to N do C[i]:= A[i] or B[i];
Пересечение (A ∩ B) :
for i:=1 to N do C[i]:= A[i] and B[i];
Разность (A \ B):
for i:=1 to N do C[i]:= A[i] and not B[i];
Симметрическая разность (A B):
for i:=1 to N do C[i]:= A[i] xor B[i];
106 Лекция 7
P3:=p1;
while (p3<>nil)and(p3^.s<>i) do p3:=p3^.p;
if (p3<>nil)and(p3^.s=i)then НАЙДЕН else НЕТ;
P3:=p1;
while (p3<>nil)and(p3^.s<>i) do p3:=p3^.p;
if (p3=nil) then begin
new(p3); p3^.s:=i; p3^.p:=p1; p1:=p3;
end;
Удаление объекта номер i из множества:
p3:=nil; p4:=p1;
while (p4<>nil)and(p4^.s<>i) do
begin p3:=p4; p4:=p4^.p end;
if (p4<>nil)and(p4^.s=i) then begin
if p3<>nil then p3^.p:=p4^.p
else p1:=p1^.p;
dispose(p4)
end;
Сначала проверяется, принадлежит ли объект номер i множеству.
Постусловие для цикла:
1) (p4=nil)or(p4^.s=i) – объект номер i не принадлежит мно-
жеству;
2) (p4^.s=i)and(p3<>nil) – объект номер i находится в сере-
дине или в конце списка;
3) (p4^.s=i)and(p3=nil) – объект номер i находится в начале
списка;
Вычисление мощности множества:
K:=0; p3=p1;
while (p3<>nil) do
begin K:=K+1; p3:=p3^.p end;
Трудоёмкость каждого из рассмотренных действий – O(K), где K – ко-
личество объектов в множестве.
Конец примера.
Пример 7.9. Операции над двумя множествами, заданными неупоря-
доченными списками. Указатель p1 указывает на начало 1-го списка, ука-
затель p2 – на начало 2-го списка. В результате создаётся 3-й список с ука-
зателем p3.
Множества 115
Объединение множеств:
new(p3); p4:=p3; p5:=p1;
while p5<>nil do begin
new(p6); p4^.p:=p6; p4:=p6;
p6^.s:=p5^.s; p5:=p5^.p;
end;
p7:=p2;
while p7<>nil do begin
p5:=p1;
while(p5<>nil)and(p7^.s<>p5^.s)do p5:=p5^.p;
if p5=nil then begin
new(p6); p4^.p:=p6; p4:=p6; p6^.s:=p7^.s;
end;
p7:=p7^.p;
end;
p4^.p:=nil; p6:=p3; p3:=p3^.p; dispose(p6);
Сначала создается фиктивный элемент 3-го списка (с указателем p3), к
нему далее в 1-м цикле присоединяются копии элементов 1-го списка. Ука-
затель p4 указывает на последний элемент в 3-м списке.
Затем в цикле просматриваются все элементы 2-го списка, и для каж-
дого из них во вложенном цикле проверяется совпадение с каким-либо из
элементов 1-го списка. Если совпадения не было, то к 3-му списку присо-
единяется копия элемента из 1-го списка.
В завершение уничтожается фиктивный элемент в начале 3-го списка.
Пересечение множеств:
new(p3); p4:=p3; p7:=p2;
while p7<>nil do begin
p5:=p1;
while(p5<>nil)and(p7^.s<>p5^.s)do p5:=p5^.p;
if p5<>nil then begin
new(p6); p4^.p:=p6; p4:=p6; p6^.s:=p7^.s;
end;
p7:=p7^.p;
end;
p4^.p:=nil; p6:=p3; p3:=p3^.p; dispose(p6);
116 Лекция 7
end;
while (p11<>nil) do begin
new(p5); p4^.p:=p5; p4:=p5;
p5^.s:=p11^.s; p11:=p11^.p
end;
while (p22<>nil) do begin
new(p5); p4^.p:=p5; p4:=p5;
p5^.s:=p22^.s; p22:=p22^.p
end;
p4^.p:=nil; p5:=p3; p3:=p3^.p;
dispose(p5); {удален фиктивный элемент}
Операция отличается от обычного слияния тем, что в первом цикле ко-
пируются только неравные очередные элементы из 1-го и 2-го списков.
Конец примера.
Вопросы и задания
ства, если известно, что мощность множества не превышает L. Вычислить при этом
мощность множества K.
7. Написать программу, которая преобразует представление множества из упорядо-
ченного списка номеров объектов множества в двоичный массив А длиной n эле-
ментов, если известно, что мощность множества равна K. Какова её трудоёмкость?
8. Написать программу, которая преобразует представление множества из неупорядо-
ченного списка номеров объектов множества в двоичный массив А длиной n эле-
ментов, если известно, что мощность множества равна K. Какова её трудоёмкость?
9. Доказать корректность программы, добавляющей объект в множество, представ-
ленное целочисленным упорядоченным массивом. Какова её трудоёмкость?
10. Доказать корректность программы, удаляющей объект из множества, представлен-
ного целочисленным упорядоченным массивом. Какова её трудоёмкость?
11. Доказать корректность программы, вычисляющей объединение множеств, заданных
целочисленными неупорядоченными массивами. Какова её трудоёмкость?
12. Доказать корректность программы, вычисляющей пересечение множеств, заданных
целочисленными неупорядоченными массивами. Какова её трудоёмкость?
13. Написать программу вычисления симметрической разности множеств, заданных
целочисленными неупорядоченными массивами. Доказать её корректность. Какова
её трудоёмкость?
14. Доказать корректность программы, вычисляющей объединение множеств, заданных
целочисленными упорядоченными массивами. Какова её трудоёмкость?
15. Доказать корректность программы, вычисляющей пересечение множеств, заданных
целочисленными упорядоченными массивами. Какова её трудоёмкость?
16. Написать программу вычисления разности разности множеств, заданных целочис-
ленными упорядоченными массивами. Доказать её корректность. Какова её трудо-
ёмкость?
17. Доказать корректность программы, удаляющей объект из множества, представлен-
ного упорядоченным списком. Какова её трудоёмкость?
18. Доказать корректность программы, вычисляющей объединение множеств, заданных
неупорядоченными списками. Какова её трудоёмкость?
19. Доказать корректность программы, вычисляющей пересечение множеств, заданных
неупорядоченными списками. Какова её трудоёмкость?
20. Написать программу вычисления разности множеств, заданных неупорядоченными
списками. Доказать корректность программы. Какова её трудоёмкость?
21. Доказать корректность программы, вычисляющей объединение множеств, заданных
упорядоченными списками. Какова её трудоёмкость?
22. Доказать корректность программы, вычисляющей пересечение множеств, заданных
упорядоченными списками. Какова её трудоёмкость?
23. Написать программу вычисления разности множеств, заданных упорядоченными
списками. Доказать корректность программы. Какова её трудоёмкость?
Лекция 8.
Символьные строки и таблицы
Рис. 8.1
80 90 A0 B0 C0 D0 E0 F0
0 А Р а ░ └ ╨ р Ё
1 Б С б ▒ ┴ ╤ с ё
2 В Т в ▓ ┬ ╥ т Є
3 Г У г │ ├ ╙ у є
4 Д Ф д ┤ ─ ╘ ф Ї
5 Е Х е ╡ ┼ ╒ х ї
6 Ж Ц ж ╢ ╞ ╓ ц Ў
7 З Ч з ╖ ╟ ╫ ч ў
8 И Ш и ╕ ╚ ╪ ш º
9 Й Щ й ╣ ╔ ┘ щ •
A К Ъ к ║ ╩ ┌ ъ ∙
B Л Ы л ╗ ╦ █ ы √
C М Ь м ╝ ╠ ▄ ь №
D Н Э н ╜ ═ ▌ э ¤
E О Ю о ╛ ╬ ▐ ю ■
F П Я п ┐ ╧ ▀ я
Рис. 8.2
Символьные строки. Из символов, также как из других типов дан-
ных, можно создавать массивы. Кроме того, можно описывать символьные
строки, как особые массивы. Пример описания:
var s1, s2: string[20];
Описаны две символьные строки, для каждой строки выделена память
размером 20 символов, это значит, что в строке может быть записано от 0
до 20 символов. Если в описании отсутствует число в квадратных скобках,
то по умолчанию выделяется память размером 255 символов.
Действия с символьными строками:
- присваивание, например:
s1:=’’; {текущая длина строки 0}
s1:=’abcd’; {текущая длина строки 4}
- сложение (конкатенация, склеивание), например:
Символьные строки и таблицы 125
1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,
0,0,0);
Пример 8.6. Контекстный поиск слова d (первого совпадения) в сим-
вольном массиве S длиной n с учетом перекодировки символов:
m:=length(d); p:=0; j:=1;
while (p<2)and(j<=n) do
begin c:=S[j];
if p=0 then begin
if Y[ord(c)]=1 then {начало слова}
begin d1:=c; p:=1; i:=j end
end
else begin
if Y[ord(c)]=1 then d1:=d1+c
{слово продолжается}
else if scomp(d,d1)=0 then p:=2
{слово закончилось и совпало с образцом}
else p:=0
{слово закончилось но не совпало с образцом}
end;
j:=j+1
end;
if p=0 then i:=0
else if (p=1)and(scomp(d,d1)<>0) then i:=0;
В переменной d1 накапливается очередное слово из массива S. Когда
встречается символ не буква при p=1, очередное слово полностью сфор-
мировано в d1, и оно сравнивается с образцом d с помощью функции
сравнения с переколировкой scomp. При совпадении цикл завершается.
После завершения цикла последнее выделенное слово в d1 также сравни-
вается с образцом d.
132 Лекция 8
Фамилия F Оценка R
sort2(F,R,n);
S:=0; k:=0; m:=0;
for i:=1 to n do
begin S:=S+R[i]; k:=k+1;
if (i=n)or(F[i]<>F[i+1]) then begin
m:=m+1;
FS[m]:=F[i]; KS[m]:=k; RS[m]:=S/k
S:=0; k:=0
end
end;
Процедура sort2 упорядочивает символьные строки массива, задан-
ного как первый параметр, с одновременной перестановкой элементов
числового массива – второго параметра. После этого оценки для каждого
учащегося будут располагаться подряд в массиве R.
Далее в цикле вычисляется сумма оценок и их количество для одного
учащегося. В операторе if условие будет истинным, когда обнаружена
группа записей с одинаковыми фамилиями.
Трудоёмкость всей программы определяется трудоёмкостью использу-
емого алгоритма сортировки и может оцениваться как O(n log n) .
Конец примера.
Вопросы и задания
Оператор do:
do {<список операторов>} while (<выражение>);
реализует циклический алгоритм с проверкой условия в конце. При его ис-
полнении вначале выполняется <список операторов>, после этого вычисля-
ется <выражение>. Если оно равно 0, то выполнение цикла заканчивается,
если нет, то снова выполняется <список операторов>, вычисляется <выра-
жение> и т.д.
Оператор return:
return <выражение>;
является последним выполняемым оператором внутри описания функции.
После него программа возобновляет выполнение после вызова этой функ-
ции. При этом вычисленное <выражение> – результат функции. <Выраже-
ние> в записи оператора может отсутствовать, если тип значения, выраба-
тываемого функцией, описан как void.
Пример 9.1. Программа ввода чисел, их упорядочения и вывода:
#include ”stdafx.h” /* или <stdio.h> 1*/
void main(void) /*2*/
{int i, n, z, X[1000]; /*3*/
scanf(”%d”,&n); /*4*/
for(i=0;i<n;i++) scanf(”%d”,&X[i]); /*5*/
i=0; /*6*/
while(i<n-1) /*7*/
if(X[i]<=X[i+1]) i++; /*8*/
else /*9*/
{z=X[i];X[i]=X[i+1];X[i+1]=z; /*10*/
if(i>0)i--; /*11*/
} /*12*/
for(i=0;i<n;i++) printf(”%8d”,X[i]); /*13*/
printf(”\n”); /*14*/
} /*15*/
Номера строк в программе записаны в виде комментариев. 1-я строка –
директива препроцессора, задающая вставку текста из файла (его имя взя-
то в двойные кавычки или в угловые скобки). Этот файл содержит описа-
ния стандартных функций ввода/вывода. 2-я строчка – заголовок главной
Язык Си. Базовые понятия 149
9.2. Функции
X
a b
Рис. 9.1.
Вопросы и задания
1. Имеется описание: int *x,y; Какие действия правильные, и что они значат в
операторах: x=y; y=5; *x=10; x=*y; *y=20;
2. В каком порядке будут выполняться операции и что они значат в операторе:
x+=(b*c+d)*2<i&(y=a==b);
3. Для чего нужно описание заголовка функции без самого алгоритма функции?
4. Написать на Си и отладить программу, которая вводит три числа, проверяет, можно
ли построить треугольник со сторонами такой длинв, и если можно, то вычисляет
его площадь. Создать тесты для программы методом чёрного и белого ящика и про-
вести тестирование.
5. Написать на Си описание функции с двумя параметрами: имя массива веществен-
ных элементов и его длина, которая возвращает вещественное значение, и которая
вычисляет сумму элементов массива. Написать пример вызова этой функции.
6. Написать на Си описание функции с тремя параметрами: имя вещественного мас-
сива, его длина, ссылка на вещественное значение (результат вычислений), и кото-
рая вычисляет сумму элементов массива. Написать пример вызова этой функции.
7. Написать на Си и отладить программу, которая содержит описание функции, реали-
зующей сортировку вещественного массива методом вставки. Программа должна
вводить размер массива, выделять ему память, вводить элементы массива, вызывать
функцию, выводить массив после сортировки, проверять, что массив стал упорядо-
ченным. Создать тесты для программы методом чёрного и белого ящика и провести
тестирование.
8. Написать на Си описание функции, генерирующей все сочетания чисел из n по m.
Написать пример вызова этой функции.
9. Написать на Си описание функции Sum, возвращающей вещественное, имеющей
два параметра: 1) ссылка на функцию f с одним целочисленным параметром, воз-
вращающей вещественное; 2) количество суммируемых элементов n. Функция Sum
должна вычислять сумму: Sum=f(1)+f(2)+. . .+f(n). Написать примеры
вызова функции Sum для функций f: 1/n, 1/n2, n, n2.
Лекция 10.
Язык Си. Продолжение
131,132,133,134,135,136,137,138,139,140,141,142,
143,176,177,178,179,180,181,182,183,184,185,186,
187,188,189,190,191,192,193,194,195,196,197,198,
199,200,201,202,203,204,205,206,207,208,209,210,
211,212,213,214,215,216,217,218,219,220,221,222,
223,144,145,146,147,148,149,150,151,152,153,154,
155,156,157,158,159,133,133,242,243,244,245,246,
247,248,249,250,251,252,253,254,255};
Таблица соответствует кодировке ASCII, CP866. Функция scomp лек-
сикографического сравнения двух строк с перекодировкой:
inline int ord(char c) /*тип char -> int*/
{return c<0 ? c+256 : c;}
int scomp(char *s1, char *s2)
{int i=0,r=0;
while(s1[i]!=0 && s2[i]!=0 &&
T[ord(s1[i])]==T[ord(s2[i])]) i++;
if(s1[i]!=0 && s2[i]!=0)
{if(T[ord(s1[i])]<T[ord(s2[i])])r=-1;
else r=1;
}
else if(s1[i]!=0) r=1;
else if(s2[i]!=0) r=-1;
return r;
}
Здесь используется вспомогательная функция ord, которая аргумент
типа char (как число длиной 1 байт со знаком) преобразует в тип int из
диапазона от 0 до 255. Функция ord имеет описатель inline, это означа-
ет, что при трансляции программы вызов такой функции заменяется пря-
мой текстовой вставкой алгоритма функции. В этой функции использована
тернарная операция «условное выражение», обозначаемая двумя символа-
ми: вопросительным знаком и двоеточием. Всё это необходимо для более
эффективной трансляции программы в машинные команды.
Функция scomp имеет два параметра – указатели на символьные стро-
ки s1 и s2. Конец символьных строк определяется нулевым байтом. Ре-
Язык Си. Продолжение 167
зультат сравнения имеет тип int: –1, если s1<s2, +1, если s1>s2, и 0,
если s1=s2.
Конец примера.
Пример 10.6. Составление словаря. Задача состоит в том, что из запи-
санного в файле текста, который содержит слова из букв, отделенных друг
от друга другими символами (не буквами), необходимо выделить все слова
и записать их в массив символьных строк без повторений. При этом следу-
ет отождествить заглавные и строчные буквы. Результат (последо-
вательность слов) вывести.
Таблица для эффективного распознавания букв задана в виде массива
констант (код ASCII, CP866):
const char Y[]=
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,
0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,
0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
Если Y[i]=1, то символ с кодом i буква, а если Y[i]=0, то символ
не буква. Программа составления словаря описана с параметрами q и S,
она может после трансляции вызываться в командной строке как функция
с одним параметром – символьной строкой, в которой записано имя файла.
При этом значение параметра q должно быть равно 2. В операторе if про-
веряется, что q равно 2, после чего файл F1 с именем S[1] открывается.
168 Лекция 10
#include <string.h>
void main(int q, char *S[])
{FILE *F1;
if (q<2 || (F1=fopen(S[1],"rb"))==NULL)
printf("ERROR\n");
else
{char d1[21],*D[100],c;
int q=1,p=0,n=0,k1,i;
while(q)
{if (!feof(F1)) i=fgetc(F1);
else {i=' '; q=0;}
c=i;
if (p==0) {if (Y[i]) {d1[0]=c; k1=1; p=1;}}
else if (Y[i]) {d1[k1]=c; k1++;}
else
{d1[k1]=0; p=0; i=0;
while (i<n && scomp(d1,D[i])!=0) i++;
if (i==n)
{D[n]=new char[k1+1];
strcpy(D[n],d1); n++;
}
}
}
fclose(F1);
for(i=0;i<n;i++) printf(”%s\n”,D[i]);
}
}
Переменная d1 описана как массив типа char длиной 21 элемент, т.е.
строка d1 может содержать до 20 символов. Переменная D описана как
массив указателей на тип char длиной 100 элементов.
Если файл F1 удалось открыть, то в цикле while на каждом шаге в
переменную i функцией fgetc считывается очередной символ из файла,
если же функцией feof обнаружен конец файла, то переменной i присва-
ивается код символа пробел.
Дальнейшие действия определяются значением переменной p и симво-
лом c. Если p=0, то буква ранее ещё не была обнаружена, и если при этом
Y[i]=1, то прочитанный символ – первая буква очередного слова, она
Язык Си. Продолжение 169
запоминается в строке d1. Если же p=1, и если при этом Y[i]=1, то про-
читанный символ добавляется к строке d1. Если p=1 и Y[i]=0, то в стро-
ку d1 добавляется нулевой символ – признак конца строки, после чего в
массиве ищется совпадение слова в строке d1 с ранее помещёнными в
массив D словами. Если совпадения не обнаружено, то n увеличивается на
1, для n–го элемента массива D выделяется память, в которую копируется
d1.
После завершения цикла while закрывается файл F1 и выводятся n
строк массива D, каждая строка с новой строчки.
Конец примера.
10.3. Синтаксис Си
Вопросы и задания
элеменов типа float. После этого в двойном цикле вводится n*k значе-
ний для всех элементов M[i][j].
В конце программы, когда массивы становятся не нужными, выделен-
ную память для них можно освободить:
for (i=0; i<n; i++)
delete [] M[i];
delete [] M;
delete [] X;
Конец примера.
Операции над векторами и матрицами. Сложение и различные виды
умножения векторов и матриц реализуются непосредственно по определе-
нию соответствующих операций.
Пример 11.2. Вычисление суммы векторов (массивов) A и B размер-
ности n. Результат – вектор (массив) C:
for (i=0; i<0=n; i++)
C[i]=A[i]+B[i];
Конец примера.
Пример 11.3. Вычисление суммы матриц (двумерных массивов) A и B
размерности n×m. Результат – матрица (двумерный массив) C размерно-
стью n×m:
for (i=0; i<n; i++)
for (j=0; j<m; j++)
C[i][j]=A[i][j]+B[i][j];
Конец примера.
Пример 11.4. Вычисление скалярного произведения векторов (масси-
вов) A и B размерности n. Результат – переменная d.
d=0;
for (i=0; i<n; i++)
d+=A[i]*B[i];
Конец примера.
Алгоритмы линейной алгебры. Векторы и матрицы 175
все элементы, лежащие ниже главной диагонали, равны нулю. Это дела-
ется путем сложения одного из уравнений с другим, умноженным на спе-
циально подобранный коэффициент, в результате коэффициент при одной
из переменных становится нулевым, и этот процесс (прямой ход) продол-
жается до получения треугольной матрицы. Далее тем же способом обну-
ляются элементы, лежащие выше главной диагонали (этот этап называют
обратным ходом). В результате матрица становится диагональной, по ней
неизвестные вычисляются путем деления правой части на соответствую-
щий диагональный коэффициент в левой части уравнения.
Рассмотрим промежуточный этап прямого хода, когда требуется обну-
лить коэффициенты ai + 1,i , ai + 2,i , …, an,i (в формуле i = 3):
a11 a12 a13 ... a1n x1 b1
0 a a 23 ... a 2n x 2 b2
22
0 0 a33 ... a 3n x3 b3
0 0 a n3 ... a nn x n bn
Для этого из k-го уравнения, k = i + 1, …, n, необходимо вычесть i-е
уравнение, умноженное на коэффициент c ak ,i / ai ,i . При этом возникает
следующая проблема: делитель ai,i может оказаться равным нулю, и тогда
коэффициент c вычислить невозможно. Более того, если этот коэффици-
ент не равен, но близок к нулю, то результат (неизвестные x1, ..., xn) будет
вычислен с большими ошибками. Дело в том, что вычисления над веще-
ственными числами в компьютере выполняются приближенно, т.е. с
ошибками округления, а в этом случае ошибки возрастают во много раз.
Для решения этой проблемы в методе Гаусса выбирают ведущий (имею-
щий максимальное абсолютное значение) элемент среди еще не обрабо-
танных коэффициентов матрицы. Проще всего его выбирать из ai,i , ai + 1,i ,
…, an,i . Чтобы решение системы уравнений из-за этого не изменилось,
необходимо строку с выбранным элементом поменять местами с i-й стро-
кой матрицы (обменяв также соответствующие коэффициенты вектора b ).
Пример 11.10. Программа реализует метод Гаусса с выбором ведуще-
го элемента в столбце. Массив A размерностью n×(n+1) в первых n
столбцах содержит коэффициенты при неизвестных, а в последнем столб-
180 Лекция 11
Вопросы и задания
}
/*вычитание строк матрицы*/
for (k=i+1; k<n; k++)
{c=A[k][i]/A[i][i];
for (j=i; j<n; j++)
A[k][j]-=c*A[i][j];
}
i++;
}
}
/*вычисление определителя*/
if (r<n) X=0;
else
{X=p*A[0,0];
for (i=1; i<n; i++) X*=A[i][i];
}
Массив A размерностью n×n содержит коэффициенты матрицы. Ре-
зультат вычисляется в переменной X.
Прямой ход реализуется при выполнении цикла while с параметром
i, изменяющемся от 0 до r=n, если матрицу удаётся преобразовать к тре-
угольному виду, когда на всех шагах цикла ведущий элемент больше (по
абсолютной величине) eps. Если же это не так, то цикл преждевременно
заканчивается, при этом r=i<n, что означает равенство нулю определите-
ля. Переменная p, задающая знак определителя, при каждой перестановке
строк матрицы изменяется с 1 до –1 или обратно.
Нетрудно видеть, что общее количество исполнений внутреннего цик-
ла на этапе вычитания уравнений (самом трудоёмком этапе) приближенно
равно n3/3, как в программе из примера 11.10.
Конец примера.
Вычисление обратной матрицы. Согласно определению, обратная
матрица должна удовлетворять равенству:
A∙D = I , (*)
где A – исходная квадратная матрица; D – обратная матрица; I – еди-
ничная матрица. Это равенство можно представить в виде:
A D j I j , j 1,..., n ,
186 Лекция 12
где D j – j-й столбец обратной матрицы; I j – j-й столбец единичной мат-
рицы.
Таким образом, для вычисления обратной матрицы необходимо ре-
шить n систем уравнений с одной и той же квадратной матрицей коэффи-
циентов, но с различными правыми частями. Трудоёмкость последова-
тельного решения n систем уравнений – O(n4). Более эффективно решать
все n систем уравнений одновременно, преобразуя матрицу A в единич-
ную, и тогда единичная матрица в правой части уравнения (*) преобразу-
ется в обратную:
I ∙D = D.
При этом следует учесть случай, когда определитель системы равен
нулю и тогда обратной матрицы не существует.
Пример 12.2. Программа вычисляет обратную матрицу D к исходной
матрице A методом Гаусса:
float eps=0.000001;
/*задание единичной матрицы D*/
i=0; r=n;
while (i<r)
{/*выбор ведущего элемента A[v,i]*/
if (abs(A[v][i])<eps) r=i;
else
{/*перестановка строк матриц*/
/*деление i-й строки на A[i,i]*/
/*вычитание строк матриц*/
i++;
}
}
Массив A размерностью n×n содержит коэффициенты матрицы.
Комментариями здесь обозначены отдельные действия в программе. Зада-
ние единичной матрицы D:
for (i=0; i<n; i++)
for (j=0; j<n; j++)
if (i==j) D[i][j]=1; else D[i][j]=0;
Алгоритмы линейной алгебры. Продолжение 187
aii= A[i][i];
for (j=i; j<=n; j++) A[i][j]/=aii;
for (j=1; j<=n; j++) D[i][j]/=aii;
Вычитание строк матриц:
for (k=0; k<n; k++)
if (k!=i)
{c=A[k,i];
for (j=i; j<n; j++) A[k][j]-=c*A[i][j];
for (j=0; j<n; j++) D[k][j]-=c*D[i][j];
}
Если после исполнения программы величина r оказалась меньше чем
n, то это означает, что обратная матрица не существует.
Благодаря тому, что перед вычитанием уравнений выполняется деле-
ние строк матриц A и D на диагональный элемент матрицы A, диагональ-
ные элементы матрицы A становятся единичными. Кроме того, в отличие
от программы из примера 11.10, здесь вычитание строк матриц реализова-
но так, что одновременно выполняется прямой и обратный ход, в котором
участвуют обе матрицы – A и D.
Трудоёмкость программы, так же как программы из примера 11.10,
определяется количеством повторений в циклах при вычитании уравнений.
Из-за совмещентя прямого и обратного хода это количество для матрицы A
188 Лекция 12
равно n3/2, а для матрицы D – n3, т.е. общее количество – 1,5∙n3. Если реа-
лизовать прямой и обратный ход раздельно, то трудоёмкость можно
уменьшить до величины n3/3 + n3 ≈ 1,33∙n3 .
Конец примера.
Вопросы и задания
1 (1,2) 1 2 3 4 5 6 7
(3,1) 1 1 1 2 3
3
2 (2,3) 2 1 1 1 3
4 (4,3) 3 1 1 1 1 2 4
4 1 3
5 6 (5,6)
5 1 6
6 1 5
7 7
Рис. 13.1
1 2 3 4 5 6 7 8 9 10
L S D 2 3 1 3 1 2 4 3 6 5
2 1
2 3
3 5
1 8
1 9
1 10
0 11
Рис. 13.2
Здесь все списки размещены в целочисленном массиве D, размер кото-
рого для ориентированного графа равен числу ребер, а для неориентиро-
ванного – удвоенному числу рёбер. Эти списки могут быть упорядоченны-
ми для ускорения в них поиска. В массиве S размещены для каждого из
списков индексы начала, а в массиве L – длины списков. Длина массивов S
и L равна количеству вершин графа. Если граф взвешенный, то необходим
еще один массив (такой же длины, что и массив D) с весами ребер.
Чтобы определить, имеется ли ребро (дуга), соединяющее вершины i
и j, необходимо среди элементов i-го списка:
D[S[i]], ..., D[S[i]+L[i]-1]
найти элемент, равный j. При упорядоченности списков можно использо-
вать алгоритм дихотомического поиска.
Чтобы просмотреть все вершины, смежные по ребрам с вершиной i
(или вершины, в которые идут дуги из i), необходимо перебрать все эле-
менты i-го списка. А для просмотра вершин, из которых идут дуги в вер-
шину i, необходимо перебрать все списки, т.е. все элементы массива D.
200 Лекция 13
void cdeеp(int k)
{int i, j;
for (i=S[k];i<S[k]+L[k];i++)
{j=D[i];
if (C[j]==0) {C[j]=q; cdeеp(j);}
}
}
Нетрудно видеть, что общая трудоёмкость программы, включая время
выполнения всех вызовов функции cdeеp, имеет порядок O(n + m).
Конец примера.
При просмотре вширь заданной начальной вершине приписывается
уровень 1. Затем в цикле ищутся все непросмотренные вершины, смежные
с текущей, и им приписывается уровень, на 1 больший, чем уровень теку-
щей вершины. Далее текущей становится очередная вершина среди после-
довательности просмотренных вершин и т.д. В результате всем вершинам,
достижимым из начальной, приписывается уровень, на 1 больший, чем
кратчайшее расстояние от начальной вершины, измеряемое числом прой-
денных ребер.
Пример 13.7. Программа просмотра вширь графа из n вершин, задан-
ного в виде массива номеров смежных вершин:
P[0]=a; r=0; t=0; //очередь из одной вершины a
for (i=0;i<n;i++) V[i]=0;
V[a]=1; //уровень для вершины a
while (t<=r)
{k=P[t]; q=V[k]+1;
for (i=S[k];i<=S[k]+L[k]-1;i++)
{j=D[i];
if (V[j]==0) {V[j]=q; r++; P[r]=j;}
}
t++;
}
В массиве V записываются уровни для просмотренных вершин, в мас-
сиве P – очередь из номеров просмотренных вершин. Размер этих масси-
вов – n. Первоначально элементу P[0] присваивается номер начальной
вершины a, элементу V[a] – единица, а всем остальным элементам мас-
Графы. Начало 205
Вопросы и задания
# # # # # # # #
Н # # # 1 # # #
# # # # 2 # # # 11 #
# # 3 4 5 # 9 10 #
# 4 5 6 7 8 9 #
# # # # 5 # # 8 # 10 #
# К # 6 # 10 9 10 11 #
# # # # # # # #
Рис. 14.1
графа имеет ребра теми из четырёх соседних вершин, которые также со-
держат 0. Для упрощения проверок того, что при перемещении по лаби-
ринту не произойдёт выход за его пределы, лабиринт со всех сторон можно
окружить стенами, как показано справа на рис. 14.1. Решение всей задачи
можно выполнить за 4 этапа: 1) ввод данных и формирование структуры
лабиринта; 2) разметка лабиринта; 3) отслеживание кратчайшего пути по
разметке; 4) вывод результата.
Пример 14.1. Ввод данных и формирование структуры лабиринта:
int L[22][22],i,j,n,i0,j0,ik,jk; char S[21];
scanf("%d\n",&n);
for (i=1;i<=n;i++)
{S=gets();
for (j=1;j<=n;j++)
if (S[j-1]==’-’) L[i][j]=0;
else L[i][j]=1000;
}
for (j=0;j<=n+1;j++)
{L[0][j]=1000; L[n+1][j]=1000;}
for (i=1;i<=n;i++)
{L[i][0]=1000; L[i][n+1]=1000;}
scanf("%d%d\n",&i0,&j0);
scanf("%d%d\n",&ik,&jk);
Максимальные размеры лабиринта – 20×20. Вначале вводится размер
лабиринта n, затем построчно (в строку символов S) сам лабиринт, после
этого – строка i0 и столбец j0 начала пути, а также строка ik и столбец
jk конца пути в лабиринте. Вводимые данные для лабиринта на рис. 14.1:
6
-#---#
-##-#-
---#--
------
-##-#-
-#----
1 1
6 6
210 Лекция 14
while (t<k)
{i=P[t];
for (j=0;j<n;j++)
{if (M[i][j]==1)
{R[j]--;
if (R[j]==0) {k++; P[k]=j;}
}
}
t++;
}
Трудоёмкость программы O(n2), так как каждая вершина просматрива-
ется один раз, и при этом просматриваются все выходящие из нее дуги.
Конец примера.
Результатом топологической сортировки является последовательность
номеров вершин в массиве P. Если же для вершины графа необходимо
найти ее порядковый номер в массиве P, то следует вычислить обратную
к P перестановку. Обратная перестановка определяется следующим обра-
зом. Заполним элементы в массиве Q числами 0, …, n-1 так, что Q[i]=i.
Тогда на i-м месте в массиве топологической сортировки P находится
вершина P[Q[i]] графа. Если отсортировать массив P с одновремен-
ной перестановкой элементов массива Q, то Q и будет массивом обратной
перестановки. На самом деле ничего сортировать не нужно: существует
гораздо более простой алгоритм.
Пример 14.6. Вычисление обратной перестановки. Задан массив P, n
элементов которого образует некоторую перестановку чисел 0, …, n-1.
Вычисление в массиве Q обратной перестановки чисел:
for (i=0;i<n;i++) Q[P[i]]=i;
Пример перестановки чисел от 0 до 5 (массив P):
2, 3, 0, 5, 1, 4
Обратная перестановка (массив Q):
2, 4, 0, 1, 5, 3
Конец примера.
Графы. Простые алгоритмы 217
Рис. 14.1
0 1 2 3 4
– 7 5 4 11
Рис. 14.2
0 1 2 3 4
– 6 5 4 8
Рис. 14.3
0 1 2 3 4
– 6 5 4 7
Рис. 14.4
0 1 2 3 4
– 2 0 0 1
Рис. 14.5
По такому массиву можно восстановить (в обратном порядке) каждый
кратчайший путь из вершины 0. Например, кратчайший путь от вершины 0
до вершины 4:
0→2→1→4
Пример 14.7. Вычисление кратчайших расстояний. В программе ис-
пользуются массивы R, P, Q длиной n. Элемент R[i] содержит вычислен-
Графы. Простые алгоритмы 219
Вопросы и задания
сты для программы методом чёрного и белого ящика и провести тестирование. Ка-
кова её трудоёмкость и почему?
4. Написать программу, которая вводит данные ориентированного графа и представ-
ляет его в виде матрицы смежности, после чего вычисляет топологическую сорти-
ровку и определяет, является ли граф ациклическим. Если граф ациклический, то
вычисляет также обратную перестановку номеров вершин. Создать тесты для про-
граммы методом чёрного и белого ящика и провести тестирование. Какова её тру-
доёмкость и почему?
5. Написать программу, которая для ориентированного графа представленного в виде
массива списков номеров вершин вычисляет топологическую сортировку и опреде-
ляет, является ли граф ациклическим. Какова её трудоёмкость и почему?
6. Доказать корректность программы вычисления обратной перестановки.
7. Написать программу, которая вводит матрицу расстояний между вершинами графа
и номер начальной вершины, после чего вычисляет и выводит все кратчайшие рас-
стояния и все пути от начальной вершины до всех остальных. Создать тесты для
программы методом чёрного и белого ящика и провести тестирование. Какова её
трудоёмкость и почему?
8. Написать программу, которая вводит матрицу расстояний между вершинами графа
и номера начальной и конечной вершины, после чего вычисляет и выводит крат-
чайшее расстояние и путь от начальной вершины до конечной. Создать тесты для
программы методом чёрного и белого ящика и провести тестирование. Какова её
трудоёмкость и почему?
Лекция 15.
Циклы и пути в графах
1 i k 1
pb p
i1 i
1 i k 1
pb p
Рис. 15.1
s2=0;
for (j=0;j<n;j++) if (M[j][i]) s2++;
if (s1!=s2) q=0; //если не совпадает количество
} //входящих и исходящих дуг
if (q) {существует} else {не существует}
Трудоёмкость обоих проверок – O(n2), так как просматриваются все n2
элементов матрицы смежности M.
Конец примера.
Пример 15.4. Вычисление эйлерова цикла в неориентированном графе
из n вершин. Граф задан матрицей смежности M:
p=new struct el;
pb=p; p->s=0; p->p=NULL;
for (i=0;i<n) U[i]=0;
while (p!=NULL)
{j=p->s; k=U[j];
while ((k<n) && (M[j][k]==0)) k++;
U[j]=k;
if (k<n)
{p1=p->p; i0=p->s; i=-1; p0=p;
while (i!=i0)
{p2=new struct el;
p->p=p2; i=p->s; k=U[i];
while (M[i][k]==0) k++;
U[i]=k; M[i][k]=0;
M[k][i]=0; //для орграфа удалить это присваивание
i=k; p2->s=k; p=p2;
}
p->p=p1; p=p0;
}
else p=p->p;
}
В отличие от программы из примера 15.1 здесь используется вспомога-
тельный массив U, элемент U[i] указывает на элемент матрицы
M[i][k], с которого следует продолжать просмотр i-й строки матрицы
для обнаружения очередной вершины, смежной с i-й вершиной. При об-
226 Лекция 15
Трудоёмкость вычисления всего пути – O(n + m), такая же, как при по-
строении эйлерова цикла.
Конец примера.
Пример 15.7. Проверка существования эйлерова пути или цикла. Граф
задан матрицей смежности M. Если граф неориентированный:
q=0;
for (i=0;(i<n)&&(q<3);i++)
{s=0;
for (j=0;j<n;j++) if (M[i][j]) s++;
if (s&1) {q++; ib=ik; ik=i;}
}
if (q==0) {существует цикл}
else if (q==2) {существует путь}
else {не существует}
Если граф ориентированный:
q=0;
for (i=0;(i<n)&&(q<3);i++)
{s1=0;
for (j=0;j<n;j++) if (M[i][j]) s1++;
s2=0;
for (j=0;j<n;j++) if (M[j][i]) s2++;
if (s1!=s2)
{q++;
if(s1==s2-1) ib=i;
else if(s1==s2+1) ik=i;
else q=3;
}
}
if (q==0) {существует цикл}
else if (q==2) {существует путь}
else {не существует}
Аналогично программе из примера 15.5, в переменной q подсчитыва-
ется, сколько вершин имеют дисбаланс по рёбрам. В переменной ib вы-
числяется начало, а в переменной ik – конец пути, при этом для неориен-
тированного графа начало и конец можно обменять между собой.
Циклы и пути в графах 229
{P[k]=j; R[j]=1;
if (k==n-1) {ВЫВОД ПУТИ; z=1;}
else hamilton1(k+1);
R[j]=0;
}
}
Вызов функции hamiltonp1:
z=0;
for (i=0;(i<n)&&(z==0); i++)
{P[0]=i; R[i]=1; hamiltonp1(1); R[i]=0;}
Трудоёмкость программы в худшем случае такая же, как у программы
вычисления всех гамильтоновых путей, т.е. O(n∙(n – 1)!) = O(n!).
Конец примера.
Пример 15.13. Вычисление всех гамильтоновых циклов в графе из n
вершин. Граф задан массивом D номеров смежных вершин:
void hamiltons(int k)
{int i,j,q,r;
i=P[k-1];
for (j=S[i]; j<S[i]+L[i]; j++)
{q=D[j];
if (R[q]==0)
{P[k]=q; R[q]=1;
if (k==n-1)
{r=S[q];
while (r<S[q]+L[q])
if (D[r]==0)
{ВЫВОД ЦИКЛА; r=S[q]+L[q];}
else r++;
}
else hamiltons(k+1);
R[q]=0;
}
}
}
234 Лекция 15
1 – 8 12 3 15
2 6 – 9 7 11
3 10 4 – 16 8
4 3 12 15 – 7
5 13 9 5 6 –
Рис. 15.2
236 Лекция 15
Вопросы и задания
программа
1
1 2 3 программный
программа продукт
Рис.16.1
Программа
Выбор
ведущего Переста- Вычитание Вычисление
новка строк строк определителя
элемента
Рис. 16.2
Конец примера.
Несмотря на всю привлекательность метода проектирования сверху-
вниз, иногда приходится сознательно отходить от него. Примером служит
разработка системы, в которой на нижних уровнях многие из модулей по-
вторяют идентичные действия, возможно, с небольшими вариациями. В
этом случае такие одинаковые модули желательно спроектировать один
раз и, реализовав их в виде процедур или функций, просто вызывать в
нужном месте. По-существу, проектирование набора таких универсальных
(для данной системы) модулей не отличается от проектирования комплекса
программ.
жен быть его заместитель, который, так же как главный программист, вла-
деет всей наиболее существенной информацией по проекту, и в случае
необходимости может его заменить. Кроме того, некоторые обязанности
распределяются между отдельными программистами в бригаде. Архивари-
ус отвечает за сохранность всех копий написанных компонентов системы и
документации, причем все копии должны регулярно обновляться. Инстру-
ментальщик отвечает за подготовку и освоение вспомогательных про-
граммных инструментов, необходимых всем другим членам бригады. Те-
стировщик отвечает за тестирование системы и ее модулей. Литературный
редактор отвечает за подготовку документации.
При использовании нисходящей технологии проектирования можно
уже на первых этапах проведения разработки более-менее равномерно за-
грузить работой всех членов бригады, чтобы вовремя завершить проект. В
целом бригада многократно умножает способности главного программи-
ста, от которого в наибольшей степени зависит успех работы.
При планировании проведения работ по разработке системы следует
учесть и другие законы технологии программирования.
«Первая система (её 1-й вариант) всегда создается на выброс». Дело
в том, что заказчик полностью осознает то, что ему необходимо в системе,
как правило, только после того, как увидит ее в действии. Поэтому, как бы
разработчик тщательно и аккуратно не старался реализовать систему, её
все равно придется переделывать, возможно, с самого начала. Отсюда вы-
вод: первую систему следует создавать так, чтобы было не жалко
выбрасывать. Т.е. вначале целесообразно создавать действующий макет
системы, в котором реализованы основные функции, но в упрощенном ви-
де с самыми простыми вариантами алгоритмов и т.п. После того, как за-
казчик или пользователь изучит разработанную систему, определит, что в
ней его не устраивает, можно разрабатывать проект второго варианта си-
стемы.
«Вторая система (её 2-й вариант) обладает эффектом второй си-
стемы». При создании второго варианта системы легко впасть в край-
ность и реализовать излишние функции в системе, которые на самом деле
не нужны пользователю, сделать архитектуру системы чрезмерно громозд-
кой и неуклюжей, из-за чего она снова будет мало устраивать заказчика.
Отсюда вывод: вторую систему не имеет смысла «доводить до блеска»,
после ее испытания и опробования надо быстрее переходить к созданию 3-
го варианта системы.
Технология программирования 253
Вопросы и задания