数位dp

前言

感觉数位dp就是基本思路非常好理解,但是写起来细节很多的一个东西,可能很快就听懂了,但是如果想做到能应用到实践,可能还是需要下一点功夫的。

正文

应用场景

感觉主要其实就是应用在当询问的数非常的大,然后问的是类似于满足条件的数的个数一类的问题,应用一般似乎都十分明显,但是很显然你知道了也写不出来。
对于数位dp的满足条件,一般都和每个数位上的数字或者不同数位上数字和有关系。

思路

我们考虑,既然他问的就是当某些数位上满足...条件时的方案数,那我们就按位考虑,先忽略掉上界和下界的限制,考虑该数字第 \(i\) 位置填 \(j\) 的方案数。
提前处理出来这个东西之后,我们开一个ask函数,ask(x) 计算从 \(1\)\(x\) 的所有方案数,然后算两次,相减即可。

给点例题吧

windy数

求区间 \([l,r]\) 中满足相邻两个数位之间的数值之差大于等于 \(2\) 的个数。

很经典一道数位dp题,我当年第一次学数位dp第一道就是这个,今年重新学了一遍,感觉思路清晰多了。

对于每一个数位,我们考虑预处理出 \(dp_{i,j}\) 表示第 \(i\) 位的值为 \(j\) 时的方案数,统计答案的时候,位数小于 \(x\) 的没有限制,随便加,对于位数等于 \(x\) 的,每次都卡边向下走,每次加上不卡边的代价,直到无法卡边界了为止。
代码写起来也是非常之小清新啊。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int a,b;
int dp[110][110];
int t[40];
int read(){int x;cin>>x;return x;}
int ask(int x) {
	if(x==0) return 0;
	memset(t,0,sizeof(t));
	int cnt=0,ans=0;
	while(x) t[++cnt]=x%10,x/=10;
	t[cnt+1]=-2;
	// cerr<<cnt<<endl;
	for(int i=cnt;i>=1;i--) {
		for(int j=((i==cnt)?(1):0);j<t[i];j++) {
			if(abs(j-t[i+1])>=2) ans=ans+dp[i][j];
		}
		if(abs(t[i]-t[i+1])<2) break;
		if(i==1) {
			// cerr<<"GG"<<endl;
			ans++;
		}
	}
	// cerr<<ans<<endl;
	for(int i=1;i<cnt;i++) {
		for(int j=1;j<=9;j++) {
			ans=ans+dp[i][j];
		}
	}
	return ans;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	a=read();b=read();
	for(int i=0;i<=9;i++) {
		dp[1][i]=1;
	}
	for(int i=2;i<20;i++) {
		for(int j=0;j<=9;j++) {
			for(int k=0;k<=9;k++) {
				if(abs(j-k)>=2) dp[i][j]+=dp[i-1][k];
			}
		}
	}
	cout<<ask(b)-ask(a-1)<<endl;
	// cerr<<ask(b)<<" "<<ask(a-1)<<endl;
	return 0;
}

电话号码

定义一个长为11位的电话号码是好的,当且仅当满足以下条件:

  • 有连续的三个数字是相同的。
  • 没有同时出现 \(4\)\(8\)

给定 \(l,r\),求满足条件的电话号码个数。

刚拿到这道题的时候,其实思路是很混乱的,然后写了一堆非常无用的状态,判断是否满足条件也写的非常抽象,调了很久,最后交上去直接全T了,后来静下心,仔细想了想,思路清晰了之后,直接把之前的代码删了重构了一遍,就写得很顺畅了。

其实就是裸的数位dp板子,你只需要枚举每一位上都填了点什么,每次往下递归,但是参数确实要传很多东西。
对于记搜中的参数,\(x\) 表示当前枚举到了哪一位,\(a,b\) 分别表示 \(x-1,x-2\) 位置上都填的是哪些数,后面是一堆 Flag 维护当前是否存在满足条件一的,当前是否卡上界了,当前是否卡下界了,当前有没有出现过 \(4\),当前有没有出现过 \(8\)。对于转移,直接暴力判断一下是否满足以上条件,然后往下走就好。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int L,R;
int l[20],r[20],now[20];
struct node {
	int a[20];
};
int cnt=0;
int dp[13][10][10][2][2][2][2][2];
int read(){int x;cin>>x;return x;}
int check(int l,int r) {
	bool flag1=0,flag2=0,flag3=0;
	for(int i=l;i<=r;i++) {
		if(i>=3) {
			if(now[i]==now[i-1]&&now[i-1]==now[i-2]) flag3=1;
		}
		if(now[i]==4) flag1=1;
		if(now[i]==8) flag2=1;
	}
	if(flag1&flag2) return 0;
	if(flag3) return 2;
	return 1;
}
int ask(int x,int a,int b,bool flag1,bool flag2,bool flag3,bool flag4,bool flag5) {
	//当前到第几位了,前两位分别是啥,前面有没有连续3个一样的,卡没卡上界,卡没卡下界,有没有8,有没有4
	if(dp[x][a][b][flag1][flag2][flag3][flag4][flag5]>0) return dp[x][a][b][flag1][flag2][flag3][flag4][flag5];
	if(x==0) {
		int res=0;
		if(flag1) res=1;
		return dp[x][a][b][flag1][flag2][flag3][flag4][flag5]=res;
	}
	int res=0;
	int beg=0,end=9;
	if(flag2) beg=l[x];
	if(flag3) end=r[x];
	for(int i=beg;i<=end;i++) {
		if(flag4&&(i==4)) continue;
		if(flag5&&(i==8)) continue;
		res+=ask(x-1,i,a,flag1|((i==a)&&(a==b)),flag2&(i==beg),flag3&&(i==end),flag4|(i==8),flag5|(i==4));
	}
	return dp[x][a][b][flag1][flag2][flag3][flag4][flag5]=res;
}
signed main() {
	memset(dp,-0x3f,sizeof(dp));
	L=read();R=read();
	R=min(R,99999999999ll);
	for(int i=1;i<=11;i++) {
		l[i]=L%10;
		L/=10;
		r[i]=R%10;
		R/=10;
	}
	cout<<ask(11,0,0,0,1,1,0,0);
	return 0;
}

P6218 [USACO06NOV] Round Numbers S

定义一个数是圆数当且仅当这个数满足二进制下 \(0\) 的位置比 \(1\) 多。给定 \(l,r\),求 \(l\sim r\) 内满足条件的数的个数。

依然是很经典的数位dp板子题,我们容易发现,对于前 \(i\) 位,要想有正好 \(j\)\(1\) 的方案数是 \(C(i,j)\),所以直接写一个ask询问即可,对于卡上界的情况,如果当前这位为 \(1\),则加上当前位选 \(0\),后面随便选,保证 \(1\) 不超过 \(0\) 即可。至于不卡上界的情况,我们考虑,每次强制令最高位选 \(1\),剩下的随便选就好。

#include<bits/stdc++.h>
#define int long long 
using namespace std;
int l,r;
const int mod=1e9+7;
int dp[40][40];
int a[40],lx[50],inv[50];
int ask(int x) {
	int cnt=0;
	if(x==1) return 0;
	if(x==0) return 0;
	while(x) {
		a[++cnt]=x%2;
		x>>=1;
	}
	int ans=0,num=0;
	for(int i=cnt;i>=1;i--) {
		if(!a[i]) continue;
		if(i==cnt) {
			num++;
			continue;
		}
		for(int j=0;j<=(cnt/2-num);j++) {
			ans=ans+dp[i-1][j];
			// cerr<<j<<" "<<i<<" "<<cnt<<" "<<num<<endl;
		}
		num++;
		if(num==cnt/2) {
			ans++;
			break;
		}
	}
	if(num<cnt/2) ans++;
	// cerr<<ans<<endl;
	for(int i=2;i<cnt;i++) {
		// cerr<<i<<endl;
		for(int j=0;j<=((i)/2-1);j++) {
			ans+=dp[i-1][j];
			// cerr<<i<<" "<<j<<" "<<dp[i-1][j]<<endl;
		}
	}
	return ans;
}
int read(){int x;cin>>x;return x;}
int C(int x,int y) {
	return lx[x]*inv[y]%mod*inv[x-y]%mod;
}
int ksm(int x,int y) {
	int t=1;
	while(y) {
		if(y&1) t=t*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return t;
}
signed main() {
	l=read();r=read();
	memset(dp,0,sizeof(dp));
	dp[0][0]=1;
	lx[0]=1;
    for(int i=1;i<=40;i++)
        lx[i]=lx[i-1]*i%mod;
    inv[40]=ksm(lx[40],mod-2);
    for(int i=39;i>=0;i--)
        inv[i]=inv[i+1]*(i+1)%mod;
	for(int i=1;i<=32;i++) {
		for(int j=0;j<=i;j++) {
			dp[i][j]=C(i,j);
			dp[i][j]=(dp[i][j]+mod)%mod;
		}
	}
	cout<<ask(r)-ask(l-1)<<endl;
	return 0;
}

CF1036C

板子+1,没费什么精力,直接就暴力跑,传4个参数分别表示当前到第几位了,有几个数在 \(1\sim 9\) 之间,卡没卡上下界就结束了。

posted @ 2025-06-28 19:45  wjx_2010  阅读(29)  评论(0)    收藏  举报