关于状压DP
ABC411_g
给个图 \(20\) 个点 \(2e5\) 条边没有自环,可以有重边问环的个数
显然是状压 \(dp\) 可以 \(dp[S][i][j]\) 表示当前选择的边为集合 \(S\) 起点为 \(i\) 终点为 \(j\)
如何状态转移?
求环的时候,肯定是枚举一个起点 \(i\)
然后枚举当前半环子集中的另外一个点 \(j\) 表示断点
最后再枚举一个不在当前集合中的点 \(k\) 最后判断 \(j\) 和 \(k\) 之间有没有连边就行了
最后算答案的时候枚举 \(i,j,S\) 然后再把 \(dp[s][i][j]\times E[i][j]\) 表示 \(i,j\) 之间连的边的数量就行了
这样做的时间复杂度是 \(O(2^n\times n^3)\) 超时
不难发现有很多环被算了很多次
具体来说就是一个包含 \(m\) 个点的环被算了 \(m\) 次
显然没必要
为了省时间,我可以只在一个环的最小点的位置来枚举这个环
然后你就发现我根本没有必要枚举起点了直接在 \(S\) 里面找就行 可以用 \(lowbit\) 快速查找
这样子 状态转移就变成了这个
\(dp[S][i]\) 表示当前链的点集为 \(S\) 其中一个起点是 \(i\) 另一个起点是 \(lowbit(S)\)
然后只需要再枚举一个点 \(k\) 满足 \(k\notin S\) 然后 \(dp[S][k] \times= dp[S][i]\times E[i][k]\)
#include<bits/stdc++.h>
#define debug(...) fprintf(stderr,##__VA_ARGS__)
#define int long long
#define endl "\n"
using namespace std;
bool Men;
const int N=21;
const int mod=998244353;
inline int read() {int x=0,f=1; char ch=getchar();while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar();}while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar();return x*f;}
void add(int &x,int y){x=(x+y)%mod;}
int ksc(int m,int n,int p){int ans=0;while(n){if(n%2==1){n=n-1;ans=(ans+m)%p;}n=n/2;m=(m+m)%p;}return ans;}
int ksm(int base,int power,int p){int result=1;while(power>0){if(power&1){result=result*base%p;}power>>=1;base=(base*base)%p;}return result;}
int mod_inverse(int a){return ksm(a,mod-2,mod);}
int inv(int numerator,int denominator){int inverse_denominator = mod_inverse(denominator);int result=(1LL*numerator*inverse_denominator)%mod;return result;}
int n,m,dp[(1<<N)+5][N+5];
int d[N][N];
bool Mbe;
signed main(){
debug("%.8lfMB\n",(&Men-&Mbe)/1048576.0);
n=read(),m=read();
for(int i=0;i<m;i++){
int u=read()-1,v=read()-1;
d[u][v]++,d[v][u]++;
}
for(int i=0;i<n;i++){
dp[1<<i][i]=1;
}
int ans=0;
for(int S=1;S<(1<<n);S++){
int low=__builtin_ctz(S);
for(int j=0;j<n;j++){
if(!(S&(1<<j))) continue;
if(dp[S][j]==0) continue;
for(int k=low+1;k<n;k++){
if(S&(1<<k)) continue;
if(d[j][k]==0) continue;
dp[S|(1<<k)][k]=(dp[S|(1<<k)][k]+dp[S][j]*d[j][k]%mod)%mod;
}
if(j!=low&&d[j][low]>0){
add(ans,(dp[S][j]*d[j][low]%mod));
}
}
}
cout<<((ans-m+mod)%mod*inv(1,2))%mod<<endl;
debug("%.8lfms\n",1e3*clock()/CLOCKS_PER_SEC);
return 0;
}
总结
现在想想这题其实也挺套路的 好 trick!
当需要状态压缩的时候,为了节省 空间/时间,可以把一个(可以静止的)条件放到当前状态中,比如图的环就可以用状态的最小点来表示环的起点之类的

浙公网安备 33010602011771号