Given a simple graph, output the number of simple cycles in it. A simple cycle is a cycle with no repeated vertices or edges.
The first line of input contains two integers n and m (1 ≤ n ≤ 19, 0 ≤ m) – respectively the number of vertices and edges of the graph. Each of the subsequent m lines contains two integers a and b, (1 ≤ a, b ≤ n, a ≠ b) indicating that vertices a and b are connected by an undirected edge. There is no more than one edge connecting any pair of vertices.
Output the number of cycles in the given graph.
4 6 1 2 1 3 1 4 2 3 2 4 3 4
7
The example graph is a clique and contains four cycles of length 3 and three cycles of length 4.
题解:
对于样例我们得到如图
对于该图我们得到7个简单回路
构思
由于n的数字很小,用比较tricky的思维来看,应该找不到一个多项式算法,因此我们可以设计一个阶层或指数级的算法。有兴趣的同学可以证明该问题是个NP问题。
一个环是由若干个节点以及节点的顺序决定的。若用最暴力的方法统计n个节点的无向图中环的个数,则根据圆排列公式需要枚举个环,每个环用O(n)的时间复杂度检查每个环是否真的存在,因此,总的时间复杂度则为O(n!)。由于该问题的n最大值是19,而19!是一个庞大的数字,所以阶层复杂度的算法是不能够被接受的。
分析重复计算之处
如图所示的情况,节点s到节点j有一条边,节点i到节点j有一条边。
假设我们已经计算出节点s到节点i有3条简单路径。
接下来,我们要计算节点s到节点j的简单环有几条。根据前面阶层级的枚举算法,我们还要重新计算出节点s到节点i的3条简单路径,然后加上节点i到节点j的1条边,再加上节点s到节点j的1条边,构成3个简单环。
实际上,我们并不关心节点s到节点i的简单路径是怎样的,我们只关心节点s到节点i的简单路径的条数,就可以计算出节点s到节点j的简单环的个数。
算法设计
为了消除重复计算的部分,就很容易想到动态规划了。我们设计一个状态{[s][SET][i]}来记录起点s到终点i的简单路径的条数,其中SET表示经过的节点的集合。但由于圆排列的性质,这样的状态是有重复的。我们通过指定起点为SET中的最小序号点来消除圆排列带来的重复,状态变为{[SET][i]}。还要注意,即使这样定义状态,计算简单环个数的时候仍会将2个节点一条单边的情况当成环,也会将长度大于2的环正向计算一遍,反向计算一遍。所以我们还要进行后处理。
前向的状态转移方程可以写作:
dp[SET][j]=∑(i∈SET,i−j)dp[SET][i]
但这样的方程并不利于统计简单环的个数。
后向的状态转移方程可以写作:
设简单环的个数用统计量cnt表示。对于dp[SET][i]状态,i的每一条边eij,j∈neighbor(i):若j∈SET则说明遇到了环,dp[SET][i]贡献给cnt;若j∉SET则说明获得一条简单路径,dp[SET][i]贡献给dp[SET′][j]。
后处理:
由于长度为2的简单环被统计了进去,所以cnt−m;又由于长度大于2的简单环被统计了2遍,所以(cnt−m)/2。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 20;
int maze[maxn][maxn];
// dp[s][i] : 表示s状态下以 s 状态的最小顶点和顶点i构成的简单环 (这样仍然会重复计算2次)
ll dp[1<<maxn][maxn];
int n,m;
int main()
{
while(~scanf("%d%d",&n,&m))
{
memset(dp,0,sizeof(dp));
memset(maze,0,sizeof(maze));
for(int i=0,u,v;i<m;i++) {
scanf("%d%d",&u,&v);
u--,v--;
maze[u][v] = maze[v][u] = 1;
}
ll ans = 0;
for(int i=0;i<n;i++) dp[1<<i][i] = 1;
for(int s=1;s<(1<<n);s++) {
int pre = log2(s & -s);
for(int i=pre;i<n;i++) if(dp[s][i]){ /// 枚举结尾的顶点
for(int j = pre;j<n;j++) if(maze[i][j]){ /// 枚举接下来要连接的顶点
if(s & (1<<j)) { /// 节点j在当前状态中
if (((1<<i)|(1<<j))==s) continue; /// 排除两个节点成环的情况
if(j == pre) ans += dp[s][i];
}
else { /// 节点j不在当前状态中
dp[s|(1<<j)][j] += dp[s][i];
}
}
}
}
printf("%lld\n",ans/2);
}
return 0;
}