Полиномиальный алгоритм решения задачи изоморфизма графов, O(N^5)

Полиномиальный алгоритм решения задачи изоморфизма графов, O(N^5)

Рогач Сергей

Дата последнего входа: 30.01.2012 22:58:18
Дата регистрации: 30.01.2012 21:04:40
Пол: Мужской
День рождения: 3 октября
Научное направление: Технические науки
Специальность: Искусственный Интеллект
Ученая степень: Нет
Ученое звание: нет
Член-корреспондент: нет
Академик: нет

Полиномиальный алгоритм решения задачи изоморфизма графов, O(N^5)

Полиномиальное решение задачи изоморфизма графов
В этом докладе я предлагаю алгоритм решения задачи изоморфизма графов, который имеет полиномиальную оценку вычислительной сложности для худшего случая. Из существования такого алгоритма следует что задача изоморфизма графов принадлежит к классу вычислительной сложности P.
Оригинальная публикация, детали и обсуждения здесь:
http://www.researchgate.net/profile/Sarge_Rogatch/blog/44638_Polynomial-time_algorithm_for_Graph_Isomorphism_problem
http://www.researchgate.net/topic/Bioinformatics_and_Computational_Biology/post/Poly­nomial-time_algorithm_for_Graph_Isomorphism_problem2

O(N^5) алгоритм определения одинаковости графов
Приведённый ниже код программы на языке С++ решает задачу изоморфизма графов, выполняя O(N^5) операций в худшем случае.  Очевидно, что графы с разным количеством вершин неизоморфны. Поэтому мы рассматриваем только графы с одинаковым количеством вершин.
Для двух графов, G1 и G2, входной файл “input.txt” должен содержать число N - количество вершин в каждом из графов, и матрицы смежности обоих графов, сначала G1, потом G2. Генератор тестов из последующей секции создаёт входной файл в описанном формате.
Если графы изоморфны, выходной файл “output.txt” в первой строке будет содержать число N (количество вершин в каждом из графов), а во второй строке будет содержать перестановку вершин G2 достаточную для того, чтобы граф G2 совпал с G1, т.е. матрицы смежности стали идентичны при перестановке строк и столбцов матрицы смежности G2 указанным образом.
// @Sarge Rogatch, September 8, 2011
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <map>
#include <set>
#include <time.h>
using namespace std;
const int MAXN = 100;
typedef enum {
 cpv_impossible = 0,
 cpv_possible = 1,
 cpv_constructed = 2
} corPosVals;
typedef long long cellVal;
typedef map<cellVal, set<int> > samePathLen;
FILE *fpin, *fpout;
int N;
int lastPow; // the last power of matrices (max. path len) taken into account
char A[2][MAXN+2][MAXN+2]; // G1 and G2 adjacency matrices
char B[MAXN+2][MAXN+2]; // correspondence possibilities implied from the built part
char C[MAXN+2][MAXN+2]; // experimental correspondence possibilities (for finding contradictions)
// I expect overflow in the cells of these matrices, and C#(decimal) or long-arithmetic would
//   alleviate or eliminate the overflow, however, I expect little adverse effects of overflow
//   on the efficiency of the search of contradictions
cellVal S[2][MAXN+2][MAXN+2][MAXN+2];
// oriG2v[j] contains the original number of vertex from G2 which currently (due to row exchanges)
//  stands in j-th row
int oriG2v[MAXN+2];
int builtF[MAXN+2]; // the result: builtF[i] is the vertex in G2 which corresponds to vertex [i] in G1
inline bool must(bool c) {
 if( !c ) {
  printf(" #invariant_failed ");
 }
 return c;
}
void pathNumOverflow(void) {
 static int happened = 0;
 if( !happened ) {
  printf(" #overflow ");
  happened = 1;
 }
}
inline cellVal checkProduct(cellVal a, cellVal b) {
 static const cellVal maxCV = (((cellVal)1)<<(sizeof(cellVal)*8-1)) - 1;
 //TODO: look in Debug whether maxCV gets the proper value
 if( b != 0 ) {
  if( maxCV / b < a ) {
   pathNumOverflow();
  }
 }
 return a*b;
}
template <class T> inline void xchg(T& a, T&b) {
 T t = a;
 a = b;
 b = t;
}
int main(void) {
 double tmPrev = clock() / (double)CLOCKS_PER_SEC;
 fpin = fopen("input.txt", "rt");
 fpout = fopen("output.txt", "wt");
 ///// Input
 // the number of vertices
 fscanf(fpin, "%d", &N);
 // then two adjacency matrices follow
 for(int g=0; g<=1; g++) {
  for(int i=0; i<N; i++) {
   for(int j=0; j<N; j++) {
    int t;
    fscanf(fpin, "%d", &t);
    must( t == 1 || t == 0 );
    A[g][i][j] = t;
    S[g][1][i][j] = t; // power 1 of the adjacency matrices
   }
  }
 }
 //// Initialization
 lastPow = N; // regard powers 1...N of the adjacency matrices
 // Precompute the powers of the matrices
 for(int g=0; g<=1; g++) { // for both matrices
  for(int p=1; p<=lastPow-1; p++) { // in each step, compute (p+1)-th power from p-th
   // Use the simplest matrix multiplication algorithm O(N^3), which can be improved to
   //   about O(N^2.5) as a matter of craft
   for(int i=0; i<N; i++) {
    for(int j=0; j<N; j++) {
     S[g][p+1][i][j] = 0;
     for(int k=0; k<N; k++) {
cellVal inc = checkProduct(S[g][p][i][k], S[g][p][k][j]);
S[g][p+1][i][j] += inc;
     }
    }
   }
  }
 }
 printf("Matrix multiplication done in %.3lf sec.\n", clock()/(double)CLOCKS_PER_SEC-tmPrev);
 tmPrev = clock()/(double)CLOCKS_PER_SEC;
 // Fill the 1's the whole correspondence possibility matrix,
 //   because there are no restrictions untill anything is built
 for(int i=0; i<N; i++) {
  for(int j=0; j<N; j++) {
   B[i][j] = cpv_possible;
  }
 }
 // Initially, vertices in rows of G2 matrices stand "as is"
 for(int j=0; j<N; j++) {
  oriG2v[j] = j;
 }
 //// Build the non-contradicting solution step by step
 for(int i=0; i<N; i++) {
  // All the previous vertices 0...i-1 are fixed. Iterate [j] starting with [i]
  int j;
  for(j=i; j<N; j++) {
   // A case when the impossibility of f(i)=j mapping was proved during construction of the former pieces
   if( B[i][j] == cpv_impossible ) continue;
   //// Copy the bottom-right square of the built-part matrix of correspondence possibilities
   ////   into the experimental matrix of correspondence possibilities
   //NOTE: I'm not sure whether it's sufficient to start from k=i
   for(int k=i; k<N; k++) {
    for(int m=i; m<N; m++) {
     C[k][m] = B[k][m];
    }
   }
   //// Search for contradictions to f(i) = j, i.e. the correspondence of v[i] from G1 to v[j] from G2
   // First, search only in the rows&columns of the mapping upon test: f(i) = j
   //   It may be sufficient to search just in them, but I don't know how to prove this yet.
   for(int k=0; k<N; k++) {
    for(int m=0; m<N; m++) {
     // No need to look again at a known to be impossible option
     if( C[k][m] == cpv_impossible ) continue;
     for(int p=1; p<=lastPow; p++) {
if( (S[0][p][i][k] != S[1][p][j][m]) // mismatch on the number of outgoing paths, disproving f(k)=m
|| (S[0][p][k][i] != S[1][p][m][j]) // mismatch on the number of incoming paths, disproving f(k)=m
) {
// C[k][m] must become 0, but first test whether it's in the unconstructed range.
// If it's in the constructed range, we've got a contradiction to the non-contradictory built solution,
// thus there is no point in considering f(i)==j any more, we can discard any further testing of
// f(i)==j and proceed with f(i) == (j+1)
if( (k == m && k < i) // a contradiction to the formerly built non-contradicting part
  || (k == i && m == j) // f(i)==j proved itself impossible
 )
{
  goto mapping_i_to_j_failed;
}
C[k][m] = cpv_impossible;
break; // for(p), as we've disproved mapping f(k)=m
}
     }
    }
   }
  
   // check that for each [k] there is at least one [m], which is a mapping possibility, i.e. f(k) may be [m]
   for(int k=i; k<N; k++) {
    int nOptions = 0;
    for(int m=i; m<N; m++) {
     if( C[k][m] != cpv_impossible ) {
nOptions++;
//TODO: later, we can exit the cycle here, or use in-line statistics for the number of options
     }
    }
    if( nOptions == 0 ) {
     goto mapping_i_to_j_failed;
    }
   }
   // check that there is a reverse mapping possibility, i.e. for each [m] there exist [k] so that f(k) may be m
   for(int m=i; m<N; m++) {
    int nOptions = 0;
    for(int k=i; k<N; k++) {
     if( C[k][m] != cpv_impossible ) {
nOptions++;
     }
    }
    if( nOptions == 0 ) {
     goto mapping_i_to_j_failed;
    }
   }
   // if we come to this point, it means that no contradiction was found for f(i)=j
   break;
mapping_i_to_j_failed:
   // We've tried the possibility f(i)=j in the this iteration over [j],
    //  and as we're here (still searching for a correspondence to [i], knowing that
    //  f(i)=i, f(i)=i+1, ..., f(i)=j do not work), we must record this fact in B.
    //  P.S. "we" - the program and I.
    B[i][j] = cpv_impossible;
  }
  if( j >= N ) {
   //No possible [j] is found, thus halt with "not isomorphic" result
   goto not_isomorphic;
  }
  //// A non-contradicting [j] is found, so that f(i)=j . Record the constructed piece.
  builtF[i] = oriG2v[j];
  // Copy from matrix C into B
  for(int k=i; k<N; k++) {
   for(int m=i; m<N; m++) {
    B[k][m] = C[k][m];
   }
  }
  //// Exchange in all the matrices of G2 rows [i] and [j], and/or columns [i] and [j],
  //  and exchange items [i] and [j] of oriG2v array
  if( i != j ) {
   // oriG2v
   xchg(oriG2v[i], oriG2v[j]);
   // B, and C (cause part of C is not going to be overwritten in the next iteration)
   // In B and C matrices, exchange only columns, because
   //   columns stand for G2 vertices, while rows stand for G1 vertices)
   for(int k=0; k<N; k++) {
    xchg(B[k][i], B[k][j]);
    xchg(C[k][i], C[k][j]);
   }
   // S[1]
   for(int p=1; p<=lastPow; p++) {
    for(int k=0; k<N; k++) {
     xchg( S[1][p][i][k], S[1][p][j][k] );
    }
    for(int k=0; k<N; k++) {
     xchg( S[1][p][k][i], S[1][p][k][j] );
    }
   }
  }
 }
 printf("Solution found in %.3lf sec.\n", clock()/(double)CLOCKS_PER_SEC-tmPrev);
 // Output the built mapping
 fprintf(fpout, "%d\n", N);
 // i-th number is the vertex of G2 that corresponds to vertex [i] of G1
 for(int i=0; i<N; i++) {
  fprintf(fpout, "%s%d", (i>0) ? " " : "",
   builtF[i]+1 // 1-based vertex numbering in the output (but internal 0-bazed numbering)
  );
 }
 fprintf(fpout, "\n");
 return 0;
not_isomorphic:
 fprintf(fpout, "0\n");
 printf("Non-isomorphism identified in %.3lf sec.\n", clock()/(double)CLOCKS_PER_SEC-tmPrev);
 return 0;
}



Генератор тестов
Приведённая ниже программа на языке C++ генерирует два графа, которые гарантированно изоморфны, и поэтому отличаются разве что нумерацией вершин. Граф G1 получается из G2 путём перенумерации вершин G2. Это значит что  матрица смежности G1 получается соответствующей перестановкой строк и столбцов матрицы смежности G2. Эта случайная перестановка генерируется и записывается во второй строке файла “answer.txt”. В первой строке файла “answer.txt” записывается количество вершин в графах G1 и G2, каждое из которых равно N.

//@Sarge Rogatch, September 7-8, 2011

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>

#include <stdlib.h>

#include <algorithm>

#include <time.h>

 

const int MAXN = 100;

 

//// Inputs

int N, chanceP, chanceQ;

//// Own reference

int perm[MAXN+2]; // the permutation of vertices

//// Outputs

char A1[MAXN+2][MAXN+2];

char A2[MAXN+2][MAXN+2];

 

int main(void) {

FILE *fptask = fopen("input.txt", "wt");

FILE *fpans = fopen("answer.txt", "wt");

 

//// hard-coded inputs for now

N = 100;

chanceP = 14;

chanceQ = 17;

srand( (unsigned)time(NULL) );

//// generate the permutation

for(int i=0; i<N; i++) {

  perm[i] = i;

}

for(int i=N-1; i>=2; i--) {

  int pos = rand()%i;

  std::swap( perm[i], perm[pos] );

}

for(int i=0; i<N; i++) {

  for(int j=0; j<N; j++) {

   if( i == j ) continue;

   if( rand()%chanceQ >= chanceP ) continue;

   A1[i][j] = 1;

   A2[perm[i]][perm[j]] = 1;

  }

}

//

fprintf(fptask, "%d\n", N);

for(int i=0; i<N; i++) {

  for(int j=0; j<N; j++) {

   fprintf( fptask, "%s%d", (j>0) ? " " : "", (int)A1[i][j] );

  }

  fprintf(fptask, "\n");

}

fprintf(fptask, "\n"); // separate the graphs

for(int i=0; i<N; i++) {

  for(int j=0; j<N; j++) {

   fprintf( fptask, "%s%d", (j>0) ? " " : "", (int)A2[i][j] );

  }

  fprintf(fptask, "\n");

}

fprintf(fpans, "%d\n", N);

for(int i=0; i<N; i++) {

  fprintf(fpans, "%s%d", (i>0) ? " " : "", (int)perm[i]+1); // 1-based indexing

}

fprintf(fpans, "\n");

return 0;

}