Шаг 111.
Построение алгоритмов с возвратом

    На этом шаге мы рассмотрим построение алгоритма с возвратом.

    Опишем, следуя монографии [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, принадлежащем A1x2, принадлежащем 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). Представление алгоритма с возвратом мы начали с несколько более сложного нерекурсивного варианта только потому, что в рекурсивном варианте "возврат" не появляется в явном виде, будучи частью реализации механизма рекурсии.


    Пример [1,c.118]. Для данных целых чисел a1, a2, ...,an нахождение множества индексов J, принадлежащего {1, 2, ..., n}, такого, что сумма aj равна b при j, принадлежащих J, если такое множество существует.
#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;
  }
}
Текст этой программы можно взять здесь.

   


(1) Липский В. Комбинаторика для программистов: Пер. с польск. - М.: Мир, 1988. - 213 с.

    На следующем шаге мы познакомимся с гамильтоновыми циклами.




Предыдущий шаг Содержание Следующий шаг