Основы программирования на языке Visual Prolog

Рекурсия

Разбить на страницы
Показывать лекцию целиком

Рекурсивное программирование является в логических языках основным видом программирования. Эффективность рекурсивных программ повышается с помощью хвостовой рекурсии. В большинстве декларативных языков программирования имеется оптимизация хвостовой рекурсии.

В настоящей главе рассматриваются виды рекурсии и рекурсивные алгоритмы. Кроме этого, вводятся понятие функции и понятие предикатного домена в языке Visual Prolog.

5.1. Рекурсивное определение отношений

Определение рекурсивного предиката состоит в указании базиса рекурсии и шага рекурсии, так что в нем участвуют как рекурсивные правила, в которых предикат описывается через самого себя, так и правила, не являющиеся рекурсивными.

Основным недостатком рекурсивного программирования является большой расход памяти. Для каждого вызова рекурсивного предиката запоминается его состояние, чтобы можно было вернуться к нему впоследствии.

Рассмотрим определение факториала целого неотрицательного числа:

$$n! = 1 \cdot 2 \cdot ... \cdot (n – 1) \cdot n$$.

Очевидно, что $$n! = n \cdot (n – 1)!$$. Соответственно, определим рекурсивно предикат fact/2 следующим образом:

class predicates
    fact: (positive, unsigned64 [out]).
clauses
    fact(0, 1):- !.            % 1 правило
    fact(N, F):- fact(N - 1, X), F = N * X.    % 2 правило

Проследим ход поиска решений для цели

fact(3, R).

Данная цель не унифицируется с заголовком первого правила, но унифицируется с заголовком второго правила, которое имеет вид (напомним, что переменные в правилах автоматически переименовываются):

fact(N1, F1):- fact(N1 - 1, X1), F1 = N1 * X1.
Имеем: N1 = 3, F1 = R. Новая цель имеет вид:
fact(2, X1), R = 3 * X1.

Сначала вызывается подцель

fact(2, X1).

Она также не унифицируется с заголовком первого правила, но унифицируется с заголовком второго:

fact(N2, F2):- fact(N2 - 1, X2), F2 = N2 * X2.

При этом N2 = 2, F2 = X1. Следующая цель имеет вид:

fact(1, X2), X1 = 2 * X2, R = 3 * X1.

Вызывается подцель

fact(1, X2).

Эта подцель также унифицируется с заголовком только второго правила:

fact(N3, F3):- fact(N3 - 1, X3), F3 = N3 * X3.

При этом N3 = 1, F3 = X2. Следующая цель выглядит так:

fact(0, X3), X2 = 1 * X3, X1 = 2 * X2, R = 3 * X1.

Вызывается первая подцель

fact(0, X3).

Она унифицируется с заголовком правила

fact(0, 1):- !.

При этом X3 = 1. В теле этого правила стоит отсечение, поэтому других решений быть не может. Полученное значение передается в оставшуюся цель

X2 = 1 * X3, X1 = 2 * X2, R = 3 * X1.

Из первой подцели находится значение X2 = 1. Оно передается в цель

X1 = 2 * X2, R = 3 * X1.

Из первой подцели следует, что X1 = 2. Это значение передается в цель

R = 3 * X1.

Таким образом, получено решение

R = 6.

Данная рекурсия является нисходящей. Нисходящая рекурсия последовательно разбивает задачу на все более простые, пока не доходит до граничной ситуации, в которой уже не требуется продолжения рекурсии.

В целях повышения эффективности вычислений перейдем к хвостовой рекурсии. Рекурсия — хвостовая, если вызов предиката самого себя идет последним в правиле, при этом до него нет недетерминированных вызовов. Хвостовая рекурсия соответствует итерации в процедурных языках программирования.

Рассмотрим другое определение факториала. В нем используются счетчик C и накопитель для хранения произведения первых C натуральных чисел:

class predicates
    fact: (positive, unsigned64 [out]).
    fact1: (positive, positive, unsigned64, unsigned64 [out]).
clauses
    fact(N, F):- fact1(N, 0, 1, F).

    fact1(N, N, F, F):- !.
    fact1(N, C, X, F):- fact1(N, C + 1, (C + 1) * X, F).

Снова проследим вычисления для цели fact(3, R). Эта цель унифицируется с заголовком правила:

fact(N1, F1):- fact1(N1, 0, 1, F1).

При этом N1 = 3, F1 = R. Далее вызывается цель

fact1(3, 0, 1, R).

Эта цель унифицируется только с заголовком последнего правила:

fact1(N2, C2, X2, F2):- fact1(N2, C2 + 1, (C2 + 1) * X2, F2).

При этом N2 = 3, C2 = 0, X2 = 1, F2 = R. Теперь вызывается цель

fact1(3, 1, 1, R).

Она также унифицируется только с заголовком последнего правила:

fact1(N3, C3, X3, F3):- fact1(N3, C3 + 1, (C3 + 1) * X3, F3).

При этом N3 = 3, C3 = 1, X3 = 1, F3 = R. Вызывается цель:

fact1(3, 2, 2, R).

Она унифицируется с тем же правилом:

fact1(N4, C4, X4, F4):- fact1(N4, C4 + 1, (C4 + 1) * X4, F4).

Теперь N4 = 3, C4 = 2, X4 = 2, F4 = R. Новая цель имеет вид:

fact1(3, 3, 6, R).

Эта цель унифицируется с заголовком правила:

fact1(N5, N5, F5, F5):- !.

При этом N5 = 3, F5 = 6, R = 6. Отсечение предотвращает поиск других решений. Итак, имеем: R = 6.

Это был пример восходящей рекурсии. В восходящей рекурсии промежуточные результаты вычисляются на каждом шаге рекурсии, так что ответ строится постепенно и передается в виде параметра рабочей памяти до тех пор, пока не будет достигнута конечная ситуация. К этому моменту ответ уже будет вычислен.

В языке Visual Prolog имеется оптимизация хвостовой рекурсии. Копии вызываемых предикатов не создаются, а вычисления ведутся в области памяти предиката-родителя. Это позволяет многократно увеличить эффективность вычислений.

Рассмотрим недетерминированный рекурсивный предикат. В следующей программе рекурсивно определяется отношение "предок" как транзитивное замыкание отношения "родитель" (рис. 5.1).

(рис 5.1) Отношение "предок"
class facts - relatives
    parent: (string Родитель, string Ребенок).
clauses
    parent("Иван", "Мария").
    parent("Анна", "Мария").
    parent("Мария", "Павел").
    parent("Мария", "Петр").
    parent("Петр", "Степан").

class predicates
    ancestor: (string Предок, string Потомок) nondeterm (o,o) (i,o).
clauses
    ancestor(X, Y):-
        parent(X, Y).
    ancestor(X, Y):-
        parent(X, Z),
        ancestor(Z, Y).

    run():-
        ancestor(X, Y),
            write(X, " - ", Y), nl,
        fail;
        _ = readLine().

Упражнение 1.

  • Постройте дерево поиска для цели

    ancestor("Мария", D).
    
  • Определите отношение "потомок" как транзитивное замыкание отношения "родитель".
  • 5.2. Функции

    В языке Visual Prolog наряду с предикатным стилем можно использовать функции и функциональный стиль программирования. Функции объявляются следующим образом:

    class predicates
        имя_функции: (домен1, домен2, …) -> домен_значений.
    

    Например:

    class predicates
        f: (real, real) -> real.
    clauses
        f(X, Y) = X + Y.
    

    После знака -> стоит имя домена, которому принадлежит значение функции. В правиле это значение определяется после знака равенства. Аргументы функции могут быть как входными, так и выходными. Функция может иметь любой режим детерминизма.

    Аналогичное определение функции f из приведенного выше примера в предикатном стиле имеет вид:

    class predicates
        f: (real, real, real [out]).
    clauses
        f(X, Y, X + Y).
    

    Предикатный домен — это домен, элементами которого являются предикаты. Имена этих предикатов могут быть аргументами предикатов высшего порядка. В объявлении предикатных доменов указываются типы предикатов вместе с режимами детерминизма и потоками параметров. Предикатный домен функций объявляется и используется, например, следующим образом:

    domains
        fun = (real, real) -> real.
    
    class predicates
        f : fun.
        g : fun.
    clauses
        f(X, Y) = X + Y.
        g(X, Y) = X * Y.
    
        run():-
            (F = f; F = g),
                R = F(2, 3),
                write(R), nl,
            fail;
            _ = readLine().
    

    В следующих двух программах генерируются натуральные числа в порядке убывания, от заданного числа n до единицы включительно. Например, цель

    number(3, X)
    
    

    имеет следующий набор решений:

    X = 3
    X = 2
    X = 1
    

    Используется сначала предикатный стиль, а затем, для сравнения, функциональный.

    class predicates
        number: (positive Граница, positive Число) multi (i,o).
    clauses
        number(X, X).
        number(N, X):-
            N > 1,
            number(N - 1, X).
    
        run():-
            number(3, X),
                write(X), nl,
            fail;
            _ = readLine().
    
    class predicates
        number: (positive) -> positive multi.
    clauses
        number(N) = N.
        number(N) = number(N - 1):-
            N > 1.
    
        run():-
            write(number(3)), nl,
            fail;
            _ = readLine().
    

    Упражнение 2. Напишите генератор натуральных чисел по возрастанию от 1 до заданного числа N.

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

    class predicates
        fact: (positive) -> unsigned64.
        fact: (positive, unsigned64) -> unsigned64.
    clauses
        fact(N) = fact(N, 1).
    
        fact(0, X) = X:- !.
        fact(N, X) = fact(N - 1, N * X).
    
        run():-
            write(fact(5)),
            _ = readLine().
    

    Используем функции для вычисления чисел Фибоначчи. Последовательность Фибоначчи — это последовательность натуральных чисел, которая определяется следующим образом:

    $$f(1) = f(2) = 1, f(n) = f(n – 1) + f(n – 2)$$ для $$n > 2$$.

    Это определение можно буквально перенести в программу:

    class predicates 
        f: (positive) -> unsigned64.
    clauses
        f(N) = 1:- N < 3, !.
        f(N) = f(N - 1) + f(N - 2).
    

    Но эта программа уже при n = 40 требует для поиска решения нескольких секунд. Поэтому используем для вычисления n-го числа Фибоначчи хвостовую рекурсию. Для этого будем хранить на каждом шаге рекурсии два соседних элемента последовательности.

    class predicates
        fib: (positive) -> unsigned64.
        fib: (positive, unsigned64, unsigned64) -> unsigned64.
    clauses
        fib(0) = 0:- !.
        fib(N) = fib(N, 0, 1).
    
        fib(1, _, Y) = Y:- !.
        fib(N, X, Y) = fib(N - 1, Y, X + Y).
    
        run():-
            write(fib(70)),
            _ = readLine().
    

    Упражнение 2. Напишите программу, которая по заданному натуральному числу определяет номер наибольшего элемента последовательности Фибоначчи, не превосходящего этого числа.

    5.3. Рекурсивный алгоритм

    Рассмотрим задачу о ханойской башне. Эту задачу придумал Эдуард Люка в XIX веке. Задача заключается в следующем. Имеется три стержня — левый, средний и правый. На левом стержне находятся n дисков, диаметры которых попарно различны (рис. 5.2 (а)). Диски упорядочены по размеру диаметра, сверху лежит наименьший, снизу — наибольший. Требуется перенести диски с левого стержня на правый, используя средний стержень как вспомогательный. Переносить можно только по одному диску, при этом нельзя класть диск большего диаметра на диск меньшего диаметра.

    (рис 5.2) Ханойская башня

    Очевидно, что для того чтобы перенести нижний диск на правый стержень, нужно предварительно перенести башню, состоящую из n – 1 дисков, с левого стержня на средний стержень (рис. 5.2 (b)). После того, как нижний диск окажется на правом стержне (рис. 5.2 (c)), останется перенести башню со среднего стержня на правый стержень (рис. 5.2 (d)). Нетрудно показать, что оптимальное решение задачи с n дисками состоит из 2n – 1 перемещений дисков.

    class predicates
        hanoi: (positive N, string Откуда, string ВспСтерж, string Куда).
    clauses
        hanoi(0, _, _, _):- !.
        hanoi(N, A, B, C):-
            % перенос с A на B с помощью С
            hanoi(N - 1, A, C, B),
            writef("Перенести диск % с % на %\n", N, A, C),
            % перенос с B на C с помощью A
            hanoi(N - 1, B, A, C).
    
        run():-
            hanoi(3, "A", "B", "C"),
            _ = readLine().
    

    Упражнения

  • Определите на множестве кубиков отношение "не ниже", которое является рефлексивно-транзитивным замыканием отношения наверху. Пара кубиков принадлежит отношению наверху, если первый из них стоит на втором.
  • Сгенерируйте подмножество целых чисел от числа m до числа n включительно с шагом s
  • по возрастанию;
  • по убыванию.
  • По заданному натуральному числу n найдите два соседних элемента последовательности Фибоначчи с номерами n и n + 1.
  • Вычислите значение n–й частичной суммы разложения функции $$y=\sin x$$ в ряд по степеням x в заданной точке.
  • Вычислите с заданной погрешностью значение функции $$y=\sin x$$ в заданной точке с помощью разложения в ряд по степеням x.
  • Для заданного натурального n вычислите значение 1/sin 1 + 1/(sin 1 + sin 2) + … + 1/(sin 1 + … + sin n).
  • По двум точкам, заданным координатами в консоли, постройте соединяющий их "отрезок" в виде набора точек. Отобразите "отрезок" в окне консоли.
  • Для представления неотрицательных целых чисел в виде термов o, s(o), s(s(o)), s(s(s(o))), …, определите операции сложения и умножения, а также функцию вычисления факториала.
  • Страницы:

    Рекурсивное программирование является в логических языках основным видом программирования. Эффективность рекурсивных программ повышается с помощью хвостовой рекурсии. В большинстве декларативных языков программирования имеется оптимизация хвостовой рекурсии.

    В настоящей главе рассматриваются виды рекурсии и рекурсивные алгоритмы. Кроме этого, вводятся понятие функции и понятие предикатного домена в языке Visual Prolog.

    5.1. Рекурсивное определение отношений

    Определение рекурсивного предиката состоит в указании базиса рекурсии и шага рекурсии, так что в нем участвуют как рекурсивные правила, в которых предикат описывается через самого себя, так и правила, не являющиеся рекурсивными.

    Основным недостатком рекурсивного программирования является большой расход памяти. Для каждого вызова рекурсивного предиката запоминается его состояние, чтобы можно было вернуться к нему впоследствии.

    Рассмотрим определение факториала целого неотрицательного числа:

    $$n! = 1 \cdot 2 \cdot ... \cdot (n – 1) \cdot n$$.

    Очевидно, что $$n! = n \cdot (n – 1)!$$. Соответственно, определим рекурсивно предикат fact/2 следующим образом:

    class predicates
        fact: (positive, unsigned64 [out]).
    clauses
        fact(0, 1):- !.            % 1 правило
        fact(N, F):- fact(N - 1, X), F = N * X.    % 2 правило
    

    Проследим ход поиска решений для цели

    fact(3, R).
    

    Данная цель не унифицируется с заголовком первого правила, но унифицируется с заголовком второго правила, которое имеет вид (напомним, что переменные в правилах автоматически переименовываются):

    fact(N1, F1):- fact(N1 - 1, X1), F1 = N1 * X1.
    Имеем: N1 = 3, F1 = R. Новая цель имеет вид:
    fact(2, X1), R = 3 * X1.
    

    Сначала вызывается подцель

    fact(2, X1).
    

    Она также не унифицируется с заголовком первого правила, но унифицируется с заголовком второго:

    fact(N2, F2):- fact(N2 - 1, X2), F2 = N2 * X2.
    

    При этом N2 = 2, F2 = X1. Следующая цель имеет вид:

    fact(1, X2), X1 = 2 * X2, R = 3 * X1.
    

    Вызывается подцель

    fact(1, X2).
    

    Эта подцель также унифицируется с заголовком только второго правила:

    fact(N3, F3):- fact(N3 - 1, X3), F3 = N3 * X3.
    

    При этом N3 = 1, F3 = X2. Следующая цель выглядит так:

    fact(0, X3), X2 = 1 * X3, X1 = 2 * X2, R = 3 * X1.
    

    Вызывается первая подцель

    fact(0, X3).
    

    Она унифицируется с заголовком правила

    fact(0, 1):- !.

    При этом X3 = 1. В теле этого правила стоит отсечение, поэтому других решений быть не может. Полученное значение передается в оставшуюся цель

    X2 = 1 * X3, X1 = 2 * X2, R = 3 * X1.
    

    Из первой подцели находится значение X2 = 1. Оно передается в цель

    X1 = 2 * X2, R = 3 * X1.
    

    Из первой подцели следует, что X1 = 2. Это значение передается в цель

    R = 3 * X1.
    

    Таким образом, получено решение

    R = 6.
    

    Данная рекурсия является нисходящей. Нисходящая рекурсия последовательно разбивает задачу на все более простые, пока не доходит до граничной ситуации, в которой уже не требуется продолжения рекурсии.

    В целях повышения эффективности вычислений перейдем к хвостовой рекурсии. Рекурсия — хвостовая, если вызов предиката самого себя идет последним в правиле, при этом до него нет недетерминированных вызовов. Хвостовая рекурсия соответствует итерации в процедурных языках программирования.

    Рассмотрим другое определение факториала. В нем используются счетчик C и накопитель для хранения произведения первых C натуральных чисел:

    class predicates
        fact: (positive, unsigned64 [out]).
        fact1: (positive, positive, unsigned64, unsigned64 [out]).
    clauses
        fact(N, F):- fact1(N, 0, 1, F).
    
        fact1(N, N, F, F):- !.
        fact1(N, C, X, F):- fact1(N, C + 1, (C + 1) * X, F).
    

    Снова проследим вычисления для цели fact(3, R). Эта цель унифицируется с заголовком правила:

    fact(N1, F1):- fact1(N1, 0, 1, F1).
    

    При этом N1 = 3, F1 = R. Далее вызывается цель

    fact1(3, 0, 1, R).
    

    Эта цель унифицируется только с заголовком последнего правила:

    fact1(N2, C2, X2, F2):- fact1(N2, C2 + 1, (C2 + 1) * X2, F2).
    

    При этом N2 = 3, C2 = 0, X2 = 1, F2 = R. Теперь вызывается цель

    fact1(3, 1, 1, R).
    

    Она также унифицируется только с заголовком последнего правила:

    fact1(N3, C3, X3, F3):- fact1(N3, C3 + 1, (C3 + 1) * X3, F3).
    

    При этом N3 = 3, C3 = 1, X3 = 1, F3 = R. Вызывается цель:

    fact1(3, 2, 2, R).
    
    

    Она унифицируется с тем же правилом:

    fact1(N4, C4, X4, F4):- fact1(N4, C4 + 1, (C4 + 1) * X4, F4).
    
    

    Теперь N4 = 3, C4 = 2, X4 = 2, F4 = R. Новая цель имеет вид:

    fact1(3, 3, 6, R).
    
    

    Эта цель унифицируется с заголовком правила:

    fact1(N5, N5, F5, F5):- !.
    
    

    При этом N5 = 3, F5 = 6, R = 6. Отсечение предотвращает поиск других решений. Итак, имеем: R = 6.

    Это был пример восходящей рекурсии. В восходящей рекурсии промежуточные результаты вычисляются на каждом шаге рекурсии, так что ответ строится постепенно и передается в виде параметра рабочей памяти до тех пор, пока не будет достигнута конечная ситуация. К этому моменту ответ уже будет вычислен.

    В языке Visual Prolog имеется оптимизация хвостовой рекурсии. Копии вызываемых предикатов не создаются, а вычисления ведутся в области памяти предиката-родителя. Это позволяет многократно увеличить эффективность вычислений.

    Рассмотрим недетерминированный рекурсивный предикат. В следующей программе рекурсивно определяется отношение "предок" как транзитивное замыкание отношения "родитель" (рис. 5.1).

    (рис 5.1) Отношение "предок"
    class facts - relatives
        parent: (string Родитель, string Ребенок).
    clauses
        parent("Иван", "Мария").
        parent("Анна", "Мария").
        parent("Мария", "Павел").
        parent("Мария", "Петр").
        parent("Петр", "Степан").
    
    class predicates
        ancestor: (string Предок, string Потомок) nondeterm (o,o) (i,o).
    clauses
        ancestor(X, Y):-
            parent(X, Y).
        ancestor(X, Y):-
            parent(X, Z),
            ancestor(Z, Y).
    
        run():-
            ancestor(X, Y),
                write(X, " - ", Y), nl,
            fail;
            _ = readLine().
    

    Упражнение 1.

  • Постройте дерево поиска для цели

    ancestor("Мария", D).
    
  • Определите отношение "потомок" как транзитивное замыкание отношения "родитель".
  • 5.2. Функции

    В языке Visual Prolog наряду с предикатным стилем можно использовать функции и функциональный стиль программирования. Функции объявляются следующим образом:

    class predicates
        имя_функции: (домен1, домен2, …) -> домен_значений.
    

    Например:

    class predicates
        f: (real, real) -> real.
    clauses
        f(X, Y) = X + Y.
    

    После знака -> стоит имя домена, которому принадлежит значение функции. В правиле это значение определяется после знака равенства. Аргументы функции могут быть как входными, так и выходными. Функция может иметь любой режим детерминизма.

    Аналогичное определение функции f из приведенного выше примера в предикатном стиле имеет вид:

    class predicates
        f: (real, real, real [out]).
    clauses
        f(X, Y, X + Y).
    

    Предикатный домен — это домен, элементами которого являются предикаты. Имена этих предикатов могут быть аргументами предикатов высшего порядка. В объявлении предикатных доменов указываются типы предикатов вместе с режимами детерминизма и потоками параметров. Предикатный домен функций объявляется и используется, например, следующим образом:

    domains
        fun = (real, real) -> real.
    
    class predicates
        f : fun.
        g : fun.
    clauses
        f(X, Y) = X + Y.
        g(X, Y) = X * Y.
    
        run():-
            (F = f; F = g),
                R = F(2, 3),
                write(R), nl,
            fail;
            _ = readLine().
    

    В следующих двух программах генерируются натуральные числа в порядке убывания, от заданного числа n до единицы включительно. Например, цель

    number(3, X)
    
    

    имеет следующий набор решений:

    X = 3
    X = 2
    X = 1
    

    Используется сначала предикатный стиль, а затем, для сравнения, функциональный.

    class predicates
        number: (positive Граница, positive Число) multi (i,o).
    clauses
        number(X, X).
        number(N, X):-
            N > 1,
            number(N - 1, X).
    
        run():-
            number(3, X),
                write(X), nl,
            fail;
            _ = readLine().
    
    class predicates
        number: (positive) -> positive multi.
    clauses
        number(N) = N.
        number(N) = number(N - 1):-
            N > 1.
    
        run():-
            write(number(3)), nl,
            fail;
            _ = readLine().
    

    Упражнение 2. Напишите генератор натуральных чисел по возрастанию от 1 до заданного числа N.

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

    class predicates
        fact: (positive) -> unsigned64.
        fact: (positive, unsigned64) -> unsigned64.
    clauses
        fact(N) = fact(N, 1).
    
        fact(0, X) = X:- !.
        fact(N, X) = fact(N - 1, N * X).
    
        run():-
            write(fact(5)),
            _ = readLine().
    

    Используем функции для вычисления чисел Фибоначчи. Последовательность Фибоначчи — это последовательность натуральных чисел, которая определяется следующим образом:

    $$f(1) = f(2) = 1, f(n) = f(n – 1) + f(n – 2)$$ для $$n > 2$$.

    Это определение можно буквально перенести в программу:

    class predicates 
        f: (positive) -> unsigned64.
    clauses
        f(N) = 1:- N < 3, !.
        f(N) = f(N - 1) + f(N - 2).
    

    Но эта программа уже при n = 40 требует для поиска решения нескольких секунд. Поэтому используем для вычисления n-го числа Фибоначчи хвостовую рекурсию. Для этого будем хранить на каждом шаге рекурсии два соседних элемента последовательности.

    class predicates
        fib: (positive) -> unsigned64.
        fib: (positive, unsigned64, unsigned64) -> unsigned64.
    clauses
        fib(0) = 0:- !.
        fib(N) = fib(N, 0, 1).
    
        fib(1, _, Y) = Y:- !.
        fib(N, X, Y) = fib(N - 1, Y, X + Y).
    
        run():-
            write(fib(70)),
            _ = readLine().
    

    Упражнение 2. Напишите программу, которая по заданному натуральному числу определяет номер наибольшего элемента последовательности Фибоначчи, не превосходящего этого числа.

    5.3. Рекурсивный алгоритм

    Рассмотрим задачу о ханойской башне. Эту задачу придумал Эдуард Люка в XIX веке. Задача заключается в следующем. Имеется три стержня — левый, средний и правый. На левом стержне находятся n дисков, диаметры которых попарно различны (рис. 5.2 (а)). Диски упорядочены по размеру диаметра, сверху лежит наименьший, снизу — наибольший. Требуется перенести диски с левого стержня на правый, используя средний стержень как вспомогательный. Переносить можно только по одному диску, при этом нельзя класть диск большего диаметра на диск меньшего диаметра.

    (рис 5.2) Ханойская башня

    Очевидно, что для того чтобы перенести нижний диск на правый стержень, нужно предварительно перенести башню, состоящую из n – 1 дисков, с левого стержня на средний стержень (рис. 5.2 (b)). После того, как нижний диск окажется на правом стержне (рис. 5.2 (c)), останется перенести башню со среднего стержня на правый стержень (рис. 5.2 (d)). Нетрудно показать, что оптимальное решение задачи с n дисками состоит из 2n – 1 перемещений дисков.

    class predicates
        hanoi: (positive N, string Откуда, string ВспСтерж, string Куда).
    clauses
        hanoi(0, _, _, _):- !.
        hanoi(N, A, B, C):-
            % перенос с A на B с помощью С
            hanoi(N - 1, A, C, B),
            writef("Перенести диск % с % на %\n", N, A, C),
            % перенос с B на C с помощью A
            hanoi(N - 1, B, A, C).
    
        run():-
            hanoi(3, "A", "B", "C"),
            _ = readLine().
    

    Упражнения

  • Определите на множестве кубиков отношение "не ниже", которое является рефлексивно-транзитивным замыканием отношения наверху. Пара кубиков принадлежит отношению наверху, если первый из них стоит на втором.
  • Сгенерируйте подмножество целых чисел от числа m до числа n включительно с шагом s
  • по возрастанию;
  • по убыванию.
  • По заданному натуральному числу n найдите два соседних элемента последовательности Фибоначчи с номерами n и n + 1.
  • Вычислите значение n–й частичной суммы разложения функции $$y=\sin x$$ в ряд по степеням x в заданной точке.
  • Вычислите с заданной погрешностью значение функции $$y=\sin x$$ в заданной точке с помощью разложения в ряд по степеням x.
  • Для заданного натурального n вычислите значение 1/sin 1 + 1/(sin 1 + sin 2) + … + 1/(sin 1 + … + sin n).
  • По двум точкам, заданным координатами в консоли, постройте соединяющий их "отрезок" в виде набора точек. Отобразите "отрезок" в окне консоли.
  • Для представления неотрицательных целых чисел в виде термов o, s(o), s(s(o)), s(s(s(o))), …, определите операции сложения и умножения, а также функцию вычисления факториала.
  • Вернуться к учебному плану