数位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\) 之间,卡没卡上下界就结束了。

浙公网安备 33010602011771号