DP

数位 DP

P13085

介绍一下数位 dp 的模板:

click here
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=something;
int dp[N][][][][][][],digit[N];
int dfs(int pos,int limit,int lead0,something){
	if(!pos)
		return something;
	if(!limit&&!lead0&&dp[pos][last]!=-1)
		return dp[pos][last];
	int res=0; 
	for(int i=0;i<=(limit?digit[pos]:9);i++){
		if(lead0&&i==0)
			res+=dfs(pos-1,limit&&i==(limit?digit[pos]:9),1,11);
		do something
	}
	if(!limit&&!lead0)
		dp[pos][last]=res;
	return res;
}
int sol(int x){
	int cnt=0;
	while(x){
		digit[++cnt]=x%10,x/=10;
	}
	return dfs(cnt,1,1,something);
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	memset(dp,-1,sizeof dp);
	do something
	return 0;
}

然后这题照着写即可。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=20;
int l,r;
int dp[N][N],digit[N];

int dfs(int pos,int limit,int lead0,int last){
	if(!pos)
		return 1;
	if(!limit&&!lead0&&dp[pos][last]!=-1)
		return dp[pos][last];
	int res=0; 
	for(int i=0;i<=(limit?digit[pos]:9);i++){
		if(lead0&&i==0)
			res+=dfs(pos-1,limit&&i==(limit?digit[pos]:9),1,11);
		else if(abs(last-i)>=2)
			res+=dfs(pos-1,limit&&i==(limit?digit[pos]:9),0,i);
	}
	if(!limit&&!lead0)
		dp[pos][last]=res;
	return res;
}
int sol(int x){
	int cnt=0;
	while(x){
		digit[++cnt]=x%10,x/=10;
	}
	return dfs(cnt,1,1,11);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	memset(dp,-1,sizeof dp);
	cin>>l>>r;
	cout<<sol(r)-sol(l-1);
	return 0;
}

注意,数位 dp 有个很好的性质,就是可以方便地枚举比某个数小的数。

树形 DP

P12382

tj.

P12238

tj.

状压 DP

P5911

只有区区 \(16\) 人,很明显的状压。

\(dp_i\) 表示过桥状态为 \(i\) 时的最少总时间。

初始 \(dp_0=0\),其余极大;答案就是 \(dp_{2^n-1}\)

然后考虑转移。因为我并不知道上最后一组的状态是什么,但一定是目前状态的一个子集。所以这里需要枚举一下子集,并且预处理出每组状态的总重量和总时间才方便转移。具体见代码。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=1e5+5;
int maxw,n;
int w[N],W[N],t[N],T[N];
int dp[N];

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>maxw>>n;
	for(int i=0;i<n;i++)
		cin>>t[i]>>w[i];
	for(int i=0;i<(1ll<<n);i++){
		for(int j=0;j<n;j++){
			if((i>>j)&1){
				W[i]+=w[j],T[i]=max(T[i],t[j]);
			}
		}
	}
	memset(dp,0x3f,sizeof dp);
	dp[0]=0;
	for(int i=0;i<(1<<n);i++){
		for(int j=i;j!=0;j=i&(j-1)){
			if(W[j]<=maxw){
				dp[i]=min(dp[i],dp[i^j]+T[j]);
			}
		}
	}
	cout<<dp[(1<<n)-1];
	return 0;
}

总结:枚举子集的技巧。

P2396

又是一道明显的状压。

\(dp_i\) 表示选卡片的状态为 \(i\) 时的方案数,答案 \(dp_{2^n-1}\),初始 \(dp_0=1\),其余为 \(0\)

考虑转移。这时,我们发现一个问题。因为厄运数字的位置不能走,所以我必须知道当前的位置才能转移。于是我们需要一个辅助状态 \(pos\) 记录,但如何快速得到 \(pos\)?显然,\(pos\) 需要从当前状态中的一个 \(1\) 中转移而来,于是想到从 lowbit 转移过来即可。同理,dp 转移的时候,也要从上个状态转移而来,而如何快速枚举上一个状态?显然,上一个状态只会比当前状态少走一步,即少一个 \(1\),于是也可以使用 lowbit 优化。

也许你会问,为什么不枚举子集呢?很简单,因为会 T 飞。

实现
#include<bits/stdc++.h>
//#define int long long
using namespace std;

const int N=24;
const int MOD=1e9+7;
int n,m;
int pos[1<<N],dp[1<<N],b[3];

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>n;
	for(int i=0;i<n;i++)
		cin>>pos[1<<i];
	cin>>m;
	for(int i=1;i<=m;i++)
		cin>>b[i];
	dp[0]=1;
	for(int i=1;i<(1<<n);i++){
		int j=i&-i;
		pos[i]=pos[i^j]+pos[j];
		bool f=1;
		for(int p=1;p<=m;p++){
			if(b[p]==pos[i]){
				f=0; break;
			}
		}
		if(f){
			int t=i;
			while(t){
				int cur=t&-t;
				t^=cur;
				dp[i]=(dp[i]+dp[i^cur])%MOD;
			}
		}
		else{
			dp[i]=0;
		}
	}
	cout<<dp[(1<<n)-1];
	return 0;
}

总结:lowbit 优化状压 dp 的技巧。

P2167

仍然考虑状压 dp,令 \(dp_{i,j}\) 表示第 \(i\)\(n\) 个字符串的匹配情况为 \(j\) 时的方案数。

容易发现越到后面能匹配上 \(T\) 的串会越来越少,所以我写的是扩散形转移,初始 \(dp_{0,2^n-1}=1\),答案 \(dp_{len,j}\),其中 \(j\) 满足二进制下恰有 \(k\)\(1\)

转移的话,第一条要求不用管,重点考虑第二条。我如何知道前一个状态满足的字符串到当前状态还是否满足呢?这启发我们设计辅助状态 \(sta_{i,j}\) 表示 \(n\) 个字符串第 \(i\) 位上有 \(j\) 字符的状态,这个是很容易预处理出来的。有了这个辅助状态,我们仅需和前一个状态与一下就能得到当前状态了。

实现
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=16,M=2e2+5,K=55;
const int MOD=1000003;
int _,n,m,k;
string s[N];
int dp[K][1<<N],sta[K][M];

void sol(){
	memset(dp,0,sizeof dp);
	memset(sta,0,sizeof sta); 
	cin>>n>>k;
	for(int i=0;i<n;i++)
		cin>>s[i];
	m=s[1].size();
	for(int j=0;j<m;j++){
		for(char c='a';c<='z';c++){
			for(int i=0;i<n;i++){
				if(s[i][j]=='?'||s[i][j]==c)
					sta[j][c]|=(1<<i);
			}
		}
	}
	dp[0][(1<<n)-1]=1;
	for(int i=0;i<m;i++){
		for(int j=0;j<(1<<n);j++){
			for(char c='a';c<='z';c++)
				dp[i+1][sta[i][c]&j]=(dp[i+1][sta[i][c]&j]+dp[i][j])%MOD;
		}
	}
	int ans=0;
	for(int i=0;i<(1<<n);i++)
		if(__builtin_popcount(i)==k)
			ans=(ans+dp[m][i])%MOD;
	cout<<ans<<'\n';
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin>>_;
	while(_--)
		sol();
	return 0;
}

总结:设计辅助状态的思想。

posted @ 2025-08-05 13:12  _KidA  阅读(94)  评论(0)    收藏  举报