На этом шаге мы рассмотрим построение алгоритма с возвратом.
Опишем, следуя монографии [1], общий метод, позволяющий значительно сократить число шагов в алгоритмах типа полного перебора всех возможностей. Чтобы применить этот метод, искомое решение должно иметь вид последовательности (x1, x2, ...,xn).
Основная идея метода состоит в том, что мы строим решение последовательно, начиная с пустой последовательности e (длины 0). Вообще, имея данное частичное решение (x1, x2, ...,xi), мы стараемся найти такое допустимое значение xi+1, относительно которого мы не можем сразу заключить, что (x1, x2, ...,xi+1) можно расширить до некоторого решения (либо (x1, x2, ...,xi+1) уже является решением). Если такое предполагаемое, но еще не использованное решение xi+1 существует, то мы добавляем его к нашему частичному решению и продолжаем процесс для последовательности (x1, x2, ...,xi+1). Если его не существует, то мы возвращаемся к нашему частичному решению (x1, x2, ...,xi-1) и продолжаем наш процесс, отыскивая новое, еще не использованное допустимое значение xi' - отсюда название "алгоритм с возвратом" (англ. Backtracking) [1, с.110].
Точнее говоря, мы предполагаем, что для каждого k>0 существует некоторое множество Ak, из которого мы будем выбирать кандидатов для k-й координаты частичного решения. Очевидно, что множества Ak должны быть определены таким образом, что для каждого k<=n множество Ak содержало элемент xk (на практике мы не можем вообще исключить ситуацию, когда множество Ak содержит некоторые "лишние" элементы, не появляющиеся в k-й координате ни одного целочисленного решения).
Мы предполагаем также, что существует некоторая простая функция, которая произвольному частичному решению (x1, x2, ...,xi) ставит в соответствие значение P(x1, x2, ...,xi) (истина либо ложь) таким образом, что если P(x1, x2, ...,xi) = ложь, то последовательность (x1, x2, ...,xi) несомненно нельзя расширить до решения.
Если P(x1, x2, ...,xi) = истина, то мы говорим, что значение xi допустимо (для частичного решения (x1, x2, ...,xi-1), но это отнюдь не означает, что (x1, x2, ...,xi-1) обязательно расширяется до полного решения. Этот процесс можно записать в виде следующей схемы [1, с.111]:
{ k = 1; while ( k>0 ) if ( существует еще неиспользованный элемент yпринадлежащий Ak, такой что P(X[1],X[2],...,X[k-1],y) { X[k] = y; //Элемент y использован. if ((X[1],X[2],...,X[k]) является целочисленным решением) { cout << (X[1],X[2],...,X[k]); k++; } } else //Возврат на более короткое частичное решение; все эле- //менты множества Ak вновь становятся неиспользованными. k -= 1; }
Этот алгоритм находит все решения в предположении, что множества Ak конечные и что существует n такое, что P(x1, x2, ...,xn) = ложь для всех x1, принадлежащем A1, x2, принадлежащем A2, ...,xn, принадлежащем An (последнее условие означает, что все решения имеют длину меньше n).
Приведем рекурсивный вариант схемы алгоритма с возвратом [1, с.112]:
AP (k); //Генерирование всех решений, являющихся расширением //последовательности X[1],X[2],...,X[k-1]. //Массив X - глобальный. { for (y, принадлежащего Ak, такого, что P(X[1],X[2],...,X[k-1])) { X[k] = y; if (X[1],X[2],...,X[k] есть целочисленное решение) { cout << X[1],X[2],...,X[k]; AP(k+1); } }
Генерирование всех целочисленных решений можно вызвать вызовом AP(1). Представление алгоритма с возвратом мы начали с несколько более сложного нерекурсивного варианта только потому, что в рекурсивном варианте "возврат" не появляется в явном виде, будучи частью реализации механизма рекурсии.
#include <iostream.h> #define TRUE 1 #define FALSE 0 #define Node 20 //Максимальное количество вершин в графе. typedef int Boolean; typedef struct L *Lref; // Тип: указатель на заголовочный узел. typedef struct T *Tref; // Тип: указатель на дуговой узел. //Описание типа заголовочного узла. typedef struct L { int Key; //Имя заголовочного узла. int Count; //Количество предшественников. Boolean Flag; //Флаг посещения узла при обходе. Tref Trail; //Указатель на список смежности. Lref Next; //Указатель на следующий узел в списке заголовочных узлов. }; //Описание типа дугового узла. typedef struct T { Lref Id; Tref Next; }; class Spisok { private: Lref Head; //Указатель на голову списка заголовочных узлов. Lref Tail; //Указатель на фиктивный элемент // в конце списка заголовочных узлов. int X[Node+1]; void SearchGraph (int, Lref *); public: Spisok() {//Инициализация списка заголовочных узлов. Head = Tail = new (L); } Lref GetHead() { return Head; } Lref GetTail() { return Tail; } void MakeGraph (); void PrintGraph (); void Summa (int, int, int); void X1 (Lref t) {X[1] = t->Key;}; }; void main () { Spisok A; Lref t; //Рабочий указатель для перемещения // по списку заголовочных звеньев. Lref t1; int n=0,B; //Построение графа и вывод его структуры Вирта. A.MakeGraph (); A.PrintGraph (); cout<<endl; //Подсчет n - количества вершин в графе Head. t = A.GetHead(); while (t!=A.GetTail()) { n++; t = (*t).Next; } // ------------------------------------ //Обнаружение множества индексов. cout << "Введите число B: "; cin >> B; t = A.GetHead(); while (t!=A.GetTail()) { //Инициализация. t1 = A.GetHead(); while (t1!=A.GetTail()) { t1->Flag = TRUE; t1 = t1->Next; } A.X1(t); t->Flag = FALSE; A.Summa (2,B,n); t = t->Next; } } void Spisok::SearchGraph (int w, Lref *h) //Функция возвращает указатель на заголовочный узел //с ключом w в графе, заданном структурой Вирта с указателем Head. { *h = Head; (*Tail).Key = w; while ((**h).Key!=w) *h = (**h).Next; if (*h==Tail) //В списке заголовочных узлов нет узла с ключом w. //Поместим его в конец списка Head. { Tail = new (L); (**h).Count = 0; (**h).Trail = NULL; (**h).Next = Tail; } } void Spisok::MakeGraph () //Функция возвращает указатель Head на структуру //Вирта, соответствующую ориентированному графу. { int x,y; Lref p,q; //Рабочие указатели. Tref t,r; //Рабочие указатели. Boolean Res; //Флаг наличия дуги. cout<<"Вводите начальную вершину дуги: "; cin>>x; while (x!=0) { cout<<"Вводите конечную вершину дуги: "; cin>>y; //Определим, существует ли в графе дуга (x,y)? SearchGraph (x, &p); SearchGraph (y,&q); r = (*p).Trail; Res = FALSE; while ((r!=NULL)&&(!Res)) if ((*r).Id==q) Res = TRUE; else r = (*r).Next; if (!Res) //Если дуга отсутствует, то поместим её в граф. { t = new (T); (*t).Id = q; (*t).Next = (*p).Trail; (*p).Trail = t; (*q).Count++; } cout<<"Вводите начальную вершину дуги: "; cin>>x; } } void Spisok::PrintGraph () //Вывод структуры Вирта, заданной указателем //Head и соответствующей ориентированному графу. { Lref p; //Рабочий указатель. Tref q; //Рабочий указатель. p = Head; while (p!=Tail) { cout<< (*p).Key << "("; q = (*p).Trail; while (q!=NULL) { cout<<(*(*q).Id).Key; q = (*q).Next; } cout<<")"; p = (*p).Next; cout<<" "; } } void Spisok::Summa (int k, int B, int n) //Нахождение множества вершин в графе, заданном //структурой Вирта с указателем Head. { int i; //Параметр цикла. Lref t; //Указатель на k-ю вершину частичного решения. Tref r; int S; int Set1[256]; //Вспомогательное множество. int m; //Количество элементов в Set1. SearchGraph (X[k-1], &t); r = t->Trail; while ( r != NULL ) { X[k] = r->Id->Key; //Построение вспомогательного множества Set1 для //проверки, все ли элементы массива X различны. for (int j=0;j<256;Set1[j++]=0); for (i=1;i<=k;i++) Set1[X[i]]=1; //Подсчет m - количества элементов в множестве Set1. m = 0; for(i=1;i<=Node;i++) if (Set1[i]==1) m++; // ------------------------------- S = 0; for (i=1;i<=k;i++) S += X[i]; // ---------------------------- if (k<n+1 && S==B && m==k) { //Вывод множества индексов на экран дисплея. for (i=1;i<=k;i++) cout << X[i] << " "; cout << endl; } else if (r->Id->Flag) { X[k] = r->Id->Key; r->Id->Flag = FALSE; Summa (k+1,B,n); r->Id->Flag = TRUE; } r = r->Next; } }
На следующем шаге мы познакомимся с гамильтоновыми циклами.