#L 形式的计数 dp 专题
#L 形式的计数 dp
对于一个判定性问题,如果我们可以在 \(O(\log n)\) 的空间复杂度内解决,那么称这个问题是 L 形式的。
注意,这里的 “对数空间” 是以 bit 记的,那么存储一个 int 就需要 \(\log n\) 个 bit。
对于一个计数问题,如果其对应的判定性问题是 L 形式的,那么我们把这 \(O(\log n)\) 的 bit 记入状态中,dp 的复杂度就为 \(O({\rm poly}(n))\)。
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
Description
给定一张 \(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\in T} G_i\) 中的元素。
我们再设一个 \(v_T=\bigcup_{i\in T} G_i\),则 \(v_T=v_{T\setminus\{lb(T)\}} \cup G_{lb(T)}\),其中 \(lb(T)\) 表示 \(T\) 中最小的元素,也就对应着 \(T\) 的lowbit。
这样直接做,状态数是 \(O(n2^{2^n})\) 的,转移复杂度为 \(O(2^n)\) 的,总复杂度 \(O(2^{2^n+n}n)\),不可接受。
可以发现,对于每一个 \(i\),\(f\) 中有值的位置只有 \(\binom{n}{i}\) 个,状态数降为 \(O(2^{\binom{n}{i}})\),总复杂度降为 \(O(2^{\binom{n}{i}+n})\)。可以通过 Easy Version。
再进一步,直觉告诉我们,\(F\) 中有很多的状态是无法到达的。打表发现有效的状态只有几万个,记忆化搜索即可。可以在 \(400\) ms 内通过 Hard Version。
const ll mod=1e9+7;
int n,p[N][N],AS,id[M],lg[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);}
#define lb(x) (x&-x)
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;
ll v[M]; memset(v,0,sizeof(v));
for(int s=0;s<=AS;s++){
if(s){
int y=lg[lb(s)];
v[s]=v[s^lb(s)]|G[y];
}
Add(res,dfs(x+1,v[s])*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=2;i<=AS;i++) lg[i]=lg[i>>1]+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;
}
Problem C.[AGC013D] Piling Up
Description
一开始有 \(n\) 个颜色为黑白的球,但不知道黑白色分别有多少, \(m\) 次操作,每次先拿出一个球,再放入黑白球各一个,再拿出一个球,最后拿出的球按顺序排列会形成一个颜色序列,求颜色序列有多少种。
\(1\leq n,m \leq 3000\)。
Solution
首先考虑如何判定一个颜色序列合法。
我们先把初始的 \(n\) 个球放在一边。如果在操作的过程中,某一个颜色的球不够用了,就需要从这 \(n\) 个初始球中拿出一个来。一个颜色序列合法,当且仅当消耗的初始球个数 \(\leq n\)。
为了方便计数,我们特判第一步和最后两步操作。无论第一次操作是什么,总会消耗一个初始球;无论最后两步操作是什么,都不会消耗初始球。
所以我们先让 \(n,m\) 都减一,最后答案乘上 \(4\)。现在我们的每一组操作都变为了 “放-拿-拿”。
下面的复杂度分析中,都假定 \(n,m\) 同阶。
$O(n^4) $ Solution
在判定过程中,我们记录三个值 \(x,y,z\),表示现在手中有 \(x\) 个 \(0\),\(y\) 个 \(1\),已经拿了 \(z\) 次初始球。
若我们要拿一个 \(0\),但 \(x=0\),就要让 \(z\) 加一;拿 \(1\) 同理。
所以我们设出 dp 状态 \(f_{i,x,y,z}\),转移是简单的。状态为 \(O(n^4)\),转移 \(O(1)\)。
\(O(n^3)\) Solution I
注意到 \(z\) 在最后一定会等于 \(x+y\)。
我们放球操作,会让 \(x,y\) 都加一;拿球时,如果球够用,\(x\) 或 \(y\) 就会减一。
可以发现,如果球始终够用,那么 \(x+y\) 的值就会一直为 \(0\);每消耗一个初始球,\(x+y\) 就会增加一。
现在我们的状态简化为了 \(f_{i,x,y}\),转移和上面类似。于是状态优化为了 \(O(n^3)\),转移仍是 \(O(1)\)。
\(O(n^3)\) Solution II
我们把一组操作按照拿出什么球分为四类:\(00,01,10,11\)。
我们再把操作刻画为折线:
- \(01,10\) 操作对折线没有影响;
- \(00\) 操作会让 \(x\) 减少 \(1\),\(y\) 增加 \(1\),那么让折线上移一格;
- \(11\) 操作会让 \(y\) 减少 \(1\),\(x\) 增加 \(1\),那么让折线下移一格。
当前折线的位置 \(p\),就表示我们手中现在有 \(p\) 个 \(0\),\(-p\) 个 \(1\)。
设折线到达的所有位置中,最上、最下的位置分别为 \(d_0,d_1\)。于是一个颜色序列合法,当且仅当 \(d_0-d_1\leq n\)。
枚举 \(d_1\)。设 \(f_{i,p,0/1}\) 表示进行了 \(i\) 步操作,当前折线位置为 \(p\),有没有到达过 \(d_1\) 的方案数。
转移过程中要求 \(p-d_1\leq n\)。初始状态为 \(f_{0,0,[d_1=0]}=1\),终止状态为 \(f_{m,p,1}\)。
现在我们做到了状态 \(O(n^2)\),但总复杂度仍然是 \(O(n^3)\)。
\(O(n^2)\) Solution
有了上面的优化,继续做下去也就很简单了。
我们把所有的 \(d_1\) 放在一起 dp。我们不再记录 \(p\),而是记录 \(p-d_1\) 的值。\(p-d_1=0\) 时,折线也就到达了 \(d_1\)。
设 \(f_{i,d,0/1}\) 表示进行了 \(i\) 步操作,当前 \(p-d_1=d\),\(d\) 有没有到达过 \(0\) 的方案数。
初始状态为 \(f_{0,x,[x=0]}=1\),终止状态为 \(f_{m,x,1}\)。
最终总复杂度优化为了 \(O(n^2)\)。
signed main(){
read(n),read(m); n--;
f[0][0][1]=1;
for(int i=1;i<=n;i++) f[0][i][0]=1;
for(int i=1;i<=m;i++){
for(int j=0;j<=n;j++){
for(int l=0;l<=1;l++){
Add(f[i][j][l],Mod(f[i-1][j][l]<<1));
if(j>0) Add(f[i][j-1][l|(j==1)],f[i-1][j][l]);
if(j<n) Add(f[i][j+1][l],f[i-1][j][l]);
}
}
}
ll ans=0;
for(int i=0;i<=n;i++) Add(ans,f[m-1][i][1]*4%mod);
printf("%lld\n",ans);
return 0;
}

浙公网安备 33010602011771号