状态压缩/Bitmask
在动态规划问题中,我们会遇到需要记录一个节点是否被占用/是否到达过的情况。而对于一个节点数有多个甚至十几个的问题,开一个巨型的[0/1]数组显然不现实。于是就引入了状态压缩,用一个整数的不同二进制位来表示该节点的状态。
Description
- 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&Output
Input
- 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
- Output the number of cycles in the given graph.
Sample
Input
4 6
1 2
1 3
1 4
2 3
2 4
3 4
Output
7
Solution
- 大意是求简单无向图的环数,暴搜遍历必然会TLE,重复环的处理也十分复杂。
- 考虑状态压缩,用二进制位来表示当前状态是否经过了特定的点。为了减轻重复环的处理难度,我们约定只计算起点序小于当前节点的状态(在代码中会有解释)。若节点i与当前节点y之间有边,状态的转移有以下几种条件:
- 若当前状态的起点序大于当前节点 (k&-k>(1<<y)) ,不转移。
- 若当前状态经过了当前节点 (k&(1<<y)) ,判断起点是否就是当前节点,若是,意味着我们找到了环,更新答案。
- 若当前状态没有经过当前节点,则更新经过当前节点的状态 f[k|(1<<y)][y] ,由 f[k][i] 贡献。
- 遍历以每个节点为起点的所有状态,我们可以得到一个ans。但需要注意的是,这种计算方式会将两点间连一条边的路径(为什么?)和一个环的双向都计算在内,输出时需要将答案减去边数再除以2.
细节与边界处理 - 由于二进制位需要从第0位开始,我们不妨在建图时同一将点的编号减1,方便计算。节点的遍历也要从0到n-1。
- 初始状态下,以节点i为起点,只经过i的状态,f值为1。
代码如下:
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxn 20 #define maxe 400 using namespace std; typedef long long ll; struct edge{ int to,nxt; }e[maxe]; int n,m,x,y,edgenum,lnk[maxn]; ll ans,f[1<<maxn][maxn]; void add(int bgn,int end)//事实上,节点比较少,邻接矩阵也可以存下 { edgenum++; e[edgenum].to=end; e[edgenum].nxt=lnk[bgn]; lnk[bgn]=edgenum; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;++i) { scanf("%d%d",&x,&y); add(x-1,y-1); add(y-1,x-1); } for(int i=0;i<n;++i)f[1<<i][i]=1; for(int k=0;k<(1<<n);++k){ for(int i=0;i<n;++i){ if(!f[k][i])continue; for(int p=lnk[i];p;p=e[p].nxt){ int y=e[p].to; if((k&-k)>(1<<y))continue;//判断起点序 if(k&(1<<y)){ if((k&-k)==(1<<y))//判断环 ans+=f[k][i]; } else f[k|(1<<y)][y]+=f[k][i]; } } } ans=(ans-m)/2; printf("%I64d",ans); return 0; }