#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)\)。可以发现,当前时刻仍需要保留的边有:

  1. \([(i,k),(i+1,k)],1\leq k< j\)
  2. \([(i-1,k),(i,k)],j\leq k \leq m\)
  3. \([(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\)

我们再把操作刻画为折线:

  1. \(01,10\) 操作对折线没有影响;
  2. \(00\) 操作会让 \(x\) 减少 \(1\)\(y\) 增加 \(1\),那么让折线上移一格;
  3. \(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;
}
posted @ 2025-03-13 18:48  XP3301_Pipi  阅读(55)  评论(0)    收藏  举报
Title