
- •7. Списки в Прологе
- •Определение и унификация
- •Упражнения на унификацию
- •Рассмотрим ещё несколько примеров
- •7.2 Операции на списках
- •7.2.1 Поиск элемента в списке
- •7.2.2 Создание списков
- •7.2.2.1 Простейший случай
- •7.2.2.2 Получение списка из двух существующих списков
- •7.2.2.3 Получение списка по условию
- •7.2.2.4 Вычеркивание элементов списка
- •7.2.2.5 Замена элементов списка
- •7.2.2.6 Проверка, является ли аргумент списком.
- •7.2.2.7 Подсчет количества элементов в списке.
- •7.2.2.9 Вывод списка на дисплей.
- •7.3 Задачи на списках
- •7.3.1 Нахождение путей в связанном неориентированном графе
- •7.3.2 Синтаксический анализатор на дка
7.2.2.4 Вычеркивание элементов списка
Напишем программу удаления всех вхождений указанного элемента из заданного списка. Она должна иметь три параметра: Входной список, элемент для удаления и результирующий список.
Все должно работать так
? - delete_all([a, b, a, c, a, d], a, Результат).
Результат = [b, c, d]
? - delete_all([a, b, a, c, a, d], b, Результат).
Результат = [a, a, c, a, d]
? - delete_all([a, b, a, c, a, d], prolog, Результат).
Результат = [a, b, a, c, a, d]
Вот три предложения, реализующие действия этой программы
delete_all([],Item,[]).
delete_all([Item|After],Item, R):- delete_all(After,Item,R).
delete_all([NoItem|After],Item,[NoItem|R]):-
delete_all(After,Item,R).
7.2.2.5 Замена элементов списка
Напишем программу, заменяющую все вхождения одного элемента в списке на другой элемент.
Она должна иметь четыре параметра: исходный список, элемент для поиска, элемент чтобы его заменить и список результата.
Вот - некоторые примеры ее работы
? - replace_all([a,b,a,c,a,d], a, яша, Result).
Result = [яша, b, яша, c, яша, d].
? - replace_all([a, b, a, c, a, d], b, foo, Result).
Result = [a, foo, a, c, a, d]
? - replace_all([a,b,a,c,a,d],prolog,logic,Result).
Result = [a, b, a, c, a, d]
Вот текст процедуры.
replace_all([],Item,ReplItem,[]).
replace_all([Item|After],Item,ReplItem,[ReplItem|R]):-
replace_all(After,Item,ReplItem,R).
replace_all([NoItem|After],Item,ReplItem,[NoItem|R]):-
replace_all(After,Item,ReplItem,R).
7.2.2.6 Проверка, является ли аргумент списком.
is_list33([]). is_list33([_|X]) :- is_list33(X).
Успешен, если аргумент - список, то есть заканчивается пустым списком [].
?- is_list33([карась, сладкий, маленькая]).
true.
?- is_list33([карась | сладкий]).
false.
7.2.2.7 Подсчет количества элементов в списке.
length33(List,N) возвращает в N количество элементов из которых состоит список List.
Это правило может выглядеть так:
length33([], 0).
length33([_| Lt], N) :- length33(Lt, M), N is 1+M.
Пример его работы на SWI-Прологе.
?- length33([p,q,r,s,t],N).
N = 5.
7.2.2.9 Вывод списка на дисплей.
writelist_v([]).
writelist_v([H|Lt]):- write(H), nl, writelist_v(Lt).
Таким образом список выдается вертикально.
?- writelist_v([красный, синий, голубой]).
красный
синий
голубой
true.
Чтобы вывести его горизонтально, надо в приведенном правиле заменить nl на write(' ').
writelist_h([]).
writelist_h([H|Lt]):- write(H), write(' '), writelist_h(Lt).
?- writelist_h([красный, синий, голубой]).
красный синий голубой
true.
7.3 Задачи на списках
7.3.1 Нахождение путей в связанном неориентированном графе
Рассмотрим сначала следующий связанный ориентированный граф:
Дугу, ведущую из вершины X в вершину Y, представим фактом: edge(X,Y).
Тогда весь (ориентированный) граф запишется так:
e
dge(1,2).
edge(1,4).
edge(1,3).
edge(2,3).
edge(2,5).
edge(3,4).
edge(3,5).
edge(4,5).
Это будут наши факты. Но чтобы задать, неориентированный граф надо дополнительно задать те же дуги, но строго в обратном направлении. Для чего мы могли бы добавить еще восемь предложений для edge. К примеру, наряду с уже написанным edge(1,2) для вершин 1 и 2 добавить еще и edge(2,1).
Но лучше ввести новый предикат и для него использовать правила для двух встречных соединений, для которого не важен порядок соединения, а лишь только его наличие:
connected(X,Y) :- edge(X,Y).
connected(X,Y) :- edge(Y,X).
А если обратить внимание на использование операции дизъюнкции ';' при написании правил Пролога, то наш предикат получится короче:
connected(X,Y) :- edge(X,Y) ; edge(Y,X). (1)
Итак, построение исходного графа закончено.
Обратимся к самой программе.
Мы задались целью разработать программу Пролога, при обращении к которой получим все пути между двумя любыми вершинами неориентированного графа.
Нам нужно написать программу path(S,F,P), где
S – стартовая вершина, F – вершина, оканчивающая путь, а переманная P – это сам путь, который разумно представить в виде списка проходимых вершин.
Например, для вершин 1 и 5 должен получиться результат:
?- path(1,5,P).
P = [1,2,5] ;
P = [1,2,3,5] ;
P = [1,2,3,4,5] ;
P = [1,4,5] ;
P = [1,4,3,5] ;
P = [1,4,3,2,5] ;
P = [1,3,5] ;
P = [1,3,4,5] ;
P = [1,3,2,5] ;
false.
Создадим первое правило, которое будет гласить: «путешествие от A до B по пути P получается, если A и B связаны дугой».
travel(A,B,P,[B|P]) :- connected(A,B). (2)
Форма [B|P] означает, что построение результирующего пути (списка) получается добавлением к уже существующему пути P последней вершины B, причём она должна добавляется спереди, потому что один элемент в список по-другому добавит сложно.
Поскольку ответом должен быть именно найденный путь, скажем, Path, то наша процедура, лучше будет выглядеть так:
path(A,B,Path) :- travel(A,B,[A],Path). (3)
Форма [A] необходима из-за желания получить в ответе именно список, а A - это первый, обязательный его элемент, поскольку с него должен начинаться путь.
Проба программы для соседних вершин:
[trace] 4 ?- path(1,4,Path).
Call: (6) path(1, 4, _G468) ? creep
Call: (7) travel(1, 4, [1], _G468) ? creep
Call: (8) connected(1, 4) ? creep
Call: (9) edge(1, 4) ? creep
Exit: (9) edge(1, 4) ? creep
Exit: (8) connected(1, 4) ? creep
Exit: (7) travel(1, 4, [1], [4, 1]) ? creep
Exit: (6) path(1, 4, [4, 1]) ? creep
Path = [4, 1] ;
Redo: (9) edge(1, 4) ? creep
Fail: (9) edge(1, 4) ? creep
Redo: (8) connected(1, 4) ? creep
Call: (9) edge(4, 1) ? creep
Fail: (9) edge(4, 1) ? creep
Fail: (8) connected(1, 4) ? creep
Fail: (7) travel(1, 4, [1], _G468) ? creep
Fail: (6) path(1, 4, _G468) ? creep
false.
Или без trace
?- path(4,5,P).
P = [5, 4] ;
false.
Получается, что путь ведёт в обратную сторону. Значит, надо добавить “переворачивающий” предикат reverse, и изменить правило (3):
path(A,B,Path) :-
travel(A,B,[A],Q), (4)
reverse(Q,Path).
Теперь верно, что
?- path(1,2,P).
P = [1, 2] ;
false.
?- path(4,5,P).
P = [4, 5] ;
false.
Перейдем к рекурсии по графу и допишем ещё одно утверждение для случая несмежных вершин:
travel(A,B,Visited,Path):- connected(A,C),
C \== B,
\+ member(C,Visited),
travel(C,B,[C|Visited],Path).
Третий параметр - переменная Visited скрывает за собой список вершин того пути, который уже пройден по графу
Смысл добавленного правила таков
Переход от А до B, может быть получен при условии,
(далее комментарий по каждой строке)
что А связана с некой вершиной C,
причем такой вершиной С, которая не совпадает с вершиной B,
и С не находится в уже пройденной части пути,
и при этом от С можно продолжить поиск пути до конечной B.
Выбрасывание пройденных вершин гарантирует, что программа не будет зацикливаться.
Таким образом, наша программа для конкретно заданного графа в итоге получает такой вид:
edge(1,2).
edge(1,4).
edge(1,3).
edge(2,3).
edge(2,5).
edge(3,4).
edge(3,5).
edge(4,5).
connected(X,Y) :- edge(X,Y) ; edge(Y,X).
travel(A,B,P,[B|P]) :- connected(A,B).
path(A,B,Path) :-
travel(A,B,[A],Q),
reverse(Q,Path).
travel(A,B,Visited,Path):- connected(A,C),
C \== B,
\+ member(C,Visited),
travel(C,B,[C|Visited],Path).
Программа её была уже показана в начале (!!!)