Шаг 113.
Клики

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

    Понятие клики используется в различных социологических теориях (вопросы, связанные с голосованием, альянсами и т.п.), а также в теории игр.

    Вначале напомним Вам, что:

Определение [1, с.34]
Клика графа - это любой максимальный полный подграф.

    Не требует никакой изобретательности следующий процесс нахождения клики.

    Пусть вершины графа L=(X,U) пронумерованы: X={x1, x2, ..., xn}. Выберем вершину x1, затем первую смежную с ней xi, потом вторую xj, смежную с обеими (1<i<j) и т.д. пока возможно, и пусть p - число вершин выявленной таким образом максимальной (по включению) клики. Попытаемся увеличить это число, рассматривая другой вариант процесса: вместо вершины xk, добавленной к некоторой клике на последнем шаге, добавляем другую вершину xl, l>k, тоже смежную со всеми вершинами этой клики (если такая xl есть), в надежде, что продолжение процесса по новой ветви окажется более успешным и даст хотя бы (p+1)-клику; если ни одна замена вершины xk не приводит к цели, возвращаемся еще на один шаг и т.д.


    Пример.
//Нахождение всех клик в графе, заданном структурой Вирта.
#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[20]; //Результат работы программы.
    void SearchGraph (int, Lref *);
  public:
    Spisok() {//Инициализация списка заголовочных узлов.
              Head = Tail =  new (L); }
    Lref GetHead() { return Head; }
    Lref GetTail() { return Tail; }
    void MakeGraph ();
    void PrintGraph ();
    void Clique (int, int);
    void X1 (Lref t) {X[1] = t->Key;};
};

void main ()
{
  Spisok A;
  Lref t; //Рабочий указатель для перемещения 
          // по списку заголовочных звеньев.
  Lref t1;
  int n=0;

  //Построение графа и вывод его структуры Вирта.
  A.MakeGraph ();
  A.PrintGraph (); cout<<endl;
  //Инициализация и подсчет количества вершин графа.
  t = A.GetHead();
  while (t!=A.GetTail())
    { n++; t = (*t).Next; }
  // ------------------------------------ 
  //Нахождение всех клик в графе Head.
  for (int i=3;i<=n;i++)
  {
    //i - количество вершин в клике (i<=3).
    cout << "Перечислим все клики длины " << i << endl;
    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;
       //Отыскание клики с i вершинами, "начинающейся" в вершине t.
       A.Clique (2,i); 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::Clique (int k, int m)
//Нахождение всех клик, содержащих m вершин, в графе,
//заданном структурой Вирта с указателем Head,
//k - количество вершин в частичном решении.
{
  Tref r,r1;   //Рабочие указатели.
  Lref p,q;    //Рабочие указатели.
  Lref t;      //Указатель на k-ю вершину частичного решения.
  int v;       //Вершина - кандидат на дополнение к частичному решению.
  Boolean Res; //Флаг клики.
  Boolean Res1;//Флаг существования ребра.
  int i;       //Параметр цикла.

  SearchGraph (X[k-1], &t);
  r = t->Trail;
  while ( r != NULL )
  {
    v = r->Id->Key;
    //Проверим, смежна ли вершина v с вершинами X[1],X[2],...,X[k-1].
    Res = TRUE;
    for (i=1;i<=k-1;i++)
    {
      //Cуществует ли в графе ребро (X[i],v)?
      SearchGraph (v, &p);
      SearchGraph (X[i], &q);
      r1 = p->Trail;
      Res1 = FALSE;
      while  (r1 != NULL && !Res1)
       if ( r1->Id==q ) Res1 = TRUE;
       else  r1 = r1->Next;
      Res = (Res && Res1);
    }
    if (!Res) r->Id->Flag = FALSE;
    // -------------------------- 
    if  (k==m && Res)
      //Количество вершин в графе равно m, и 
      //вершины X[1],X[2],...,X[k] образуют клику.
      {
        //Вывод клики на экран дисплея.
        for (i=1;i<=k-1;i++) cout << X[i] << " ";
        cout << v << endl;
      }
    else  
      if ( r->Id->Flag )
      {
        X[k] = r->Id->Key;
        r->Id->Flag = FALSE;
        Clique (k+1,m);
        r->Id->Flag = TRUE;
      }
    r = r->Next;
  }
}
Текст этой программы можно взять здесь.

    Мы рассмотрели лишь естественный поиск клик с возвращением, т.е. поиск, в котором не делается никаких попыток упростить дерево поиска. Каждый узел в дереве поиска соответствует полному подграфу графа, и каждое ребро соответствует вершине графа. Заметим, что каждая клика порождается много раз: в общем случае клика размера k порождается k! раз.

    От общего числа шагов порядка n! могут спасти (не всегда, но часто) следующие два обстоятельства [2, с.56].

  1. Если когда-то уже была найдена p-клика, а затем на некоторой ветви процесса образовалась q-клика от присоединения (к какой-то другой, вообще говоря, клике) вершины xj с номером j>=0n-p+q, то развивать эту ветвь не имеет смысла, ибо ни одной клики с числом вершин более p она все равно дать не может.

  2. Пусть из некоторой сформированной клики F мы удаляем последнюю присоединенную вершину xk и вместо нее собираемся добавить xl с l>k; это заведомо не имеет смысла делать, если все вершины клики F\xk, смежные с xl, смежны также с xk.

    Весь процесс с учетом этих двух обстоятельств представляет собой частный случай хорошо известного метода ветвей и границ, предлагался в разное время (устно и письменно) многими авторами, и установить приоритет здесь затруднительно.

    Ссылка на вариант технического воплощения этого процесса, называемый одесским алгоритмом есть в монографии [2, с.56].


    Замечания.
  1. В монографии [3, с.118] кликой называется произвольное подмножество вершин, в котором каждая пара различных вершин соединена ребром графа.

  2. Кликовое число графа (густота или плотность) - это максимальное число вершин в кликах данного графа [4, с.46]. Тогда, образно говоря, чем более "плотен" граф, тем будет больше кликовое число.

  3. Матрицей клик графа называется матрица инциденций вершин графа его кликам, т.е. aij=1, если вершина j принадлежит некоторой клике Ki, и aij=0 в противном случае.

  4. В монографии [5, с.389-396] приведен очень сложный алгоритм порождения всех клик графа.

  5. Мун и Мозер (1965 г.) доказали (см. ссылку в [4, с.70]), что наибольшее число клик, которые могут встретиться в графе с n вершинами, дается соотношениями:
        3n/3      , если n=0(mod 3)
        4*3(n-4)/3, если n=1(mod 3)
        2*3(n-2)/3, если n=2(mod 3)
    

        Этот результат впервые был получен в 1960 г. Р.Миллером и Д.Малером, но не был опубликован.


   


(1) Харари Ф. Теория графов. - М.: Мир, 1973. - 300 с.
(2) Зыков А.А. Основы теории графов. М.: Наука, 1987. - 384 с.
(3) Липский В. Комбинаторика для программистов: Пер. с польск. - М.: Мир, 1988. - 213 с.
(4) Кристофидес Н. Теория графов. Алгоритмический подход. - М.: Мир, 1978. - 432 с.
(5) Рейнгольд Э., Hивергельт Ю., Део H. Комбинаторные алгоритмы. Теория и практика. - М.: Мир, 1980. - 476 с.

    На следующем шаге мы рассмотрим независимые множества вершин графа.




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