计数 dp 专题 1
Problem A. P5074 Eat the Trees
Description
给出 \(n \times m\) 的方格,有些格子不能铺线,其它格子必须铺,可以形成多个闭合回路。问有多少种铺法?
\((2 \le n,m \le 12)\)。
Solution
首先考虑如何判定一个铺线的方案合法。
我们把每一个格子看成一个点,线看成节点之间的边。那么一种铺线方案合法,当且仅当每个标记为 \(1\) 的节点的度数都是 \(2\),每个标记为 \(0\) 的节点的度数都是 \(0\)。
我们设函数 \(g(E) \to \{0,1\}\),来判断 \(E\) 这张图是否合法。我们接下来要尽可能减少 \(g\) 中需要保留的信息,以保证接下来的 dp 复杂度不会爆炸。
如果我们在 \(g\) 中保存下所有的边,那么我们的时间复杂度为 \(O(2^{nm}nm)\approx 2.4\times 10^9\),无法接受。
我们从上到下一行一行地检查每一个点,那么我们不必一次性读入所有的边,每检查到一个 \((i,j)\) 再把边 \([(i,j),(i+1,j)]\) 与 \([(i,j),(i,j+1)]\) 读入即可。进一步地,我们可以发现,对于一条边,如果它的两个端点都被检查过了,那么这条边就没有用了。
假如我们接下来要检查的点是 \((i,j)\)。可以发现,当前时刻仍需要保留的边有:
- \([(i,k),(i+1,k)],1\leq k< j\)。
- \([(i-1,k),(i,k)],j\leq k \leq m\)。
- \([(i,j-1),(i,j)]\)。
画图可知,这些边形成了一个类似于 “轮廓线” 的形状。
然后删除 \([(i,j-1),(i,j)]\) 与 \([(i-1,j),(i,j)]\) 两条边,读入 \([(i,j),(i,j+1)]\) 与 \([(i,j),(i+1,j)]\) 两条边。
这样,我们在任意时刻,都保证了保留的边的数量 \(\leq m+1\)。
那么我们就可以进行状压 dp 了。设 \(f_{i,j,S}\) 表示已经检查完了 \((i,j)\) 点,要保存的边的选取情况为 \(S\),检查过的点都合法的方案数。转移枚举 \((i,j)\) 的两条新边选不选即可。
对于单组数据,时间复杂度为 \(O(2^{m}nm)\)。
int T,n,m,a[N][N],AS;
ll f[N][N][M];void Solve(){memset(f,0,sizeof(f));f[1][0][0]=1; AS=(1<<(m+1))-1;for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){for(int s=0;s<=AS;s++){if(!f[i][j-1][s]) continue;int p=(s>>(j-1))&1,q=(s>>j&1);int tx=(i<n)&(a[i][j]);int ty=(j<m)&(a[i][j]);for(int x=0;x<=tx;x++){for(int y=0;y<=ty;y++){if((p+q+x+y!=2)&&a[i][j]) continue;if((p+q+x+y!=0)&&(!a[i][j])) continue;int t=s^((x!=p)*(1<<(j-1)))^((y!=q)*(1<<j));f[i][j][t]+=f[i][j-1][s];}}}for(int s=0;s<=AS;s++){if(!f[i][j][s]) continue;if(j==m) f[i+1][0][s<<1]+=f[i][j][s];}}}ll ans=0;for(int s=0;s<=AS;s++) ans+=f[n][m][s];printf("%lld\n",ans);
}signed main(){read(T);while(T--){read(n),read(m);for(int i=1;i<=n;i++){for(int j=1;j<=m;j++)read(a[i][j]);}Solve();}return 0;
}
Problem B. CF1229E2 Marek and Matching
Descreption
给定一张 \(2n\) 个点的二分图,\(l_i,r_j\) 之间有边的概率为 \(p_{i,j}\),求这张图有完美匹配的概率。
\(1 \leq n \leq 7\)。
Solution
非常暴力的一个数据范围。
我们还是设函数 \(g(E)\to \{0,1\}\),来判断一张二分图是否有完美匹配。
由于数据范围非常小,我们可以设 \(f_{i,S}\) 表示考虑了左边的 \(1\sim i\) 号点,能否匹配右边的 \(S\)。转移显然。
进一步地,我们发现有用的 \(f_{i,S}\) 一定满足 \(|S|=i\)。那么可以直接扔掉第一维,只需要记 \(2^n\) 个 bit 了。
于是我们可以设另一个 dp:\(F_{i,f'}\) 表示考虑了 \(1\sim i\) 号点,使得当前的 \(f\) 数组为 \(f'\) 的概率。
考虑枚举 \(i+1\) 号点连向的点集 \(T\),首先处理出新得到的 \(f\) 数组,然后乘上相应的概率转移。但这样每个 \(T\) 都要做一遍,效率未免太低。
我们首先对于右边每个点 \(i\) 处理出一个 \(G_i\),表示 \(f_{S\setminus\{i\}}=1\) 的 \(S\) 的集合。
那么对于一个 \(T\),新的 \(f\) 中有值的位置就是 \(\bigcup_{i=1}^n G_i\) 中的元素。
这样直接做,状态数是 \(O(n2^{2^n})\) 的,转移复杂度为 \(O(2^nn)\) 的,总复杂度 \(O(2^{2^n+n}n^2)\),不可接受。
可以发现,对于每一个 \(i\),\(f\) 中有值的位置只有 \(\binom{n}{i}\) 个,总复杂度降为 \(O(2^{\binom{n}{i}}n^2)\)。可以通过 Easy Version。
再进一步,直觉告诉我们,\(F\) 中有很多的状态是无法到达的。打表发现有效的状态只有几万个,记忆化搜索即可。可以通过 Hard Version。
const ll mod=1e9+7;int n,p[N][N],AS,id[M];
ll e[N][N],INV,h[N][M];
vector<int> num[N];unordered_map<ll,ll> f[N];ll QuickPow(ll x,ll y){ll res=1;while(y){if(y&1) res=res*x%mod;x=x*x%mod; y>>=1;}return res;
}inline ll Mod(ll x){return (x>=mod)?(x-mod):(x);}inline void Add(ll &x,ll y){x=Mod(x+y);}ll dfs(int x,ll y){if(x==n) return y==1; if(f[x].count(y)) return f[x][y];ll G[N]; memset(G,0,sizeof(G));for(int s:num[x]){if(!(y>>id[s]&1)) continue;for(int i=0;i<n;i++)if(!(s>>i&1)) G[i]|=(1ll<<id[s|(1<<i)]);}ll res=0;for(int s=0;s<=AS;s++){ll v=0;for(int i=0;i<n;i++)if(s>>i&1) v|=G[i];Add(res,dfs(x+1,v)*h[x][s]%mod);}return f[x][y]=res;
}signed main(){read(n); INV=QuickPow(100,mod-2);for(int i=0;i<n;i++){for(int j=0;j<n;j++){read(p[i][j]);e[i][j]=p[i][j]*INV%mod;}}AS=(1<<n)-1;for(int i=0;i<n;i++){for(int s=0;s<=AS;s++){h[i][s]=1;for(int j=0;j<n;j++){if(s>>j&1) (h[i][s]*=e[i][j])%=mod;else (h[i][s]*=Mod(1-e[i][j]+mod))%=mod;}}}for(int i=0;i<=AS;i++){int x=__builtin_popcount(i);num[x].push_back(i);id[i]=(signed)num[x].size()-1;}ll ans=dfs(0,1);printf("%lld\n",ans);return 0;
}