На этом шаге мы рассмотрим понятие и реализацию кликов.
Понятие клики используется в различных социологических теориях (вопросы, связанные с голосованием, альянсами и т.п.), а также в теории игр.
Вначале напомним Вам, что:
Не требует никакой изобретательности следующий процесс нахождения клики.
Пусть вершины графа 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].
Весь процесс с учетом этих двух обстоятельств представляет собой частный случай хорошо известного метода ветвей и границ, предлагался в разное время (устно и письменно) многими авторами, и установить приоритет здесь затруднительно.
Ссылка на вариант технического воплощения этого процесса, называемый одесским алгоритмом есть в монографии [2, с.56].
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 г. Р.Миллером и Д.Малером, но не был опубликован.
На следующем шаге мы рассмотрим независимые множества вершин графа.