Codeforces 11D A Simple Task 统计简单无向图中简单环的个数

D. A Simple Task
time limit per test
2 seconds
memory limit per test
256 megabytes
input
standard input
output
standard output

Given a simple graph, output the number of simple cycles in it. A simple cycle is a cycle with no repeated vertices or edges.

Input

The first line of input contains two integers n and m (1 ≤ n ≤ 190 ≤ 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 ≤ na ≠ 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

Output the number of cycles in the given graph.

Examples
input
Copy
4 6
1 2
1 3
1 4
2 3
2 4
3 4
output
Copy
7
Note

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]=∑(iSET,ij)dp[SET][i]

但这样的方程并不利于统计简单环的个数。

后向的状态转移方程可以写作: 
设简单环的个数用统计量cnt表示。对于dp[SET][i]状态,i的每一条边eij,jneighbor(i):若jSET则说明遇到了环,dp[SET][i]贡献给cnt;若jSET则说明获得一条简单路径,dp[SET][i]贡献给dp[SET′][j]。

后处理: 
由于长度为2的简单环被统计了进去,所以cntm;又由于长度大于2的简单环被统计了2遍,所以(cntm)/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;
}

猜你喜欢

转载自blog.csdn.net/m0_38013346/article/details/80635177