【总结】数位DP
填坑进度(3/4)
边听歌边写博客,我可真是个天才。
\(\texttt{I'm lost inside your deep blue.}\)
数位DP
简称暴搜(误
前置芝士
暴搜,记忆化搜索
导入
众所周知,我们可以用 \(dfs\) 搜每一个数位来遍历 \(1\sim99999\) 之间的每一个数,其方法如下:
#include<cstdio>
int temp[15];
void dfs(int x){
//x为当前数字位数,x越小,位数越低(也就是说x是该数从右向左数的结果)
//后面说的位数都默认为从右向左数
if(!x){
for(int i=5;i;--i) //从高位到低位输出
printf("%d",temp[i]);//不要在意前导0那些无关紧要的细节
putchar('\n');
return;
}
for(int i=0;i<=9;++i){
temp[x]=i;//记录答案
dfs(x-1);
}
return;
}
那么,如果不是 \(1\sim 99999\),而是 \(1\sim 114514\) 呢?
\(\texttt{For example:}\) 如果第这个时候第 \(1,2,3\) 位分别为 \(1,1,4\),第 \(4\) 位就不能为 \(0\sim 9\) 了,而是 \(0\sim5\).
形象地说,在 \(1\sim 114514\) 这个范围内,有 \(110999,111999,112999,113999\) ,但是没有 \(114999\)。
所以,我们在每一位分两种情况考虑:前面的每一位都顶格了,那么这一位最多只能顶格;如果前面几位有任何一位没有顶格,这一位 \(0\sim9\) 随便选。
注:因为 \(114514=0114514,0=0\) ,所以我们认为第 \(6\) 位的前面顶格了,第 \(6\) 位最多也只能顶格。
对于每一位,我们分两种情况讨论:
打 √ 的是指 这一位以后 的所有位数都可以为 \(0\sim9\) 之间的任意数字。
那么,就可以愉快地按字典序遍历 \(1\sim 114514\) 之间的每一个数了。
我们先用一个数组 \(a\) 存下 \(114514\) 的每一位数字,以便顶格时判断当前位的取值范围。
在 \(dfs\) 中,我们用int
参数 \(x\) 表示当前位数, bool
参数 \(v\) 表示前面的每一位是不是都顶格了,如果 v==true
,当前最大取值就是 a[x]
,否则便是 \(9\)。
那么代码就很容易惹:
#include<cstdio>
int cnt;
int temp[15],a[15];
void dfs(int x,bool v){
if(!x){
for(int i=6;i;--i)
printf("%d",temp[i]);
putchar('\n');
return;
}
int u=v?a[x]:9;//u为上限,若前面每一位都顶格了,这位最多只能顶格
for(int i=0;i<=9;++i){
temp[x]=i;
dfs(x-1,v&&(i==a[x]));//将v向下传
//若前面有任何一位没有顶格,v即false,任何数&false=false
//若i没有顶格,也就是下一位的前一位没有顶格
//只有当前面每一位和当前位都顶格了,下一位的最高才能是a[x-1]
}
return;
}
int main(){
int n=114514;
while(n){
//cnt表示位数
a[++cnt]=n%10;
n/=10;//倒着取走114514的每一位
}
dfs(cnt,1);//从最高位搜起
return 0;
}
那么,大家可能有疑惑了,为什么要用这玩意儿?直接 \(\operatorname{for}\) 循环他不香嘛?常数也比这个 \(dfs\) 小,对吧?
氮素,众所周知,\(dfs\) 是珂以记忆化的!
例题一:不要62
Solution
【XSC062因言语过激被提出直播间】
咦?这个咋整?
我们只要在 \(dfs\) 的时候再设置一个参数 \(f\),表示这一位的前一位数。
如果f==6
且循环时的i==2
,就会出现一个连续的 \(62\),不应该继续 \(dfs\) 下去了。
如果i==4
,那么最终的数里就会出现 \(4\),也不应该继续 \(dfs\) 了。
所以,我们珂以像酱紫写粗来!
#include<cstdio>
#define int long long
int a[15];
int l,r,cnt,la,ra;
int dfs(int x,int f,bool v){
if(!x)return 1;
int u=v?a[x]:9;
int ans=0;
for(int i=0;i<=u;++i){
if(i==4||(i==2&&f==6))
continue;
ans+=dfs(x-1,i,v&&(i==a[x]));
}
return ans;
}
//简单易懂,老少皆宜,不写注释了。
咦咦咦?但这个并不是 \(1\sim r\),而是 \(l\sim r\) 呀!
像用前缀和求 \(l\sim r\) 的和一样,用 sum[r]-sum[l-1]
不就行了?
但是。。。第一位的 \(f\) 应该是多少呢?
本来,我们只要保证 f!=6
,不影响答案,不过为了保险,我们设置一个不是 \(0\sim 9\) 之间的正整数。
主函数:
signed main(){
while((~scanf("%lld%lld",&l,&r))&&(l||r)){
cnt=0;
while(r){
a[++cnt]=r%10;
r/=10;
}
ra=dfs(cnt,10,1);//记录 "sum[r]"
cnt=0;
l--;
while(l){
a[++cnt]=l%10;
l/=10;
}
la=dfs(cnt,10,1);//记录 "sum[l-1]"
printf("%lld\n",ra-la);//相减的结果就是 "l+(l+1)+(l+2)+...+r"
}
return 0;
}
然后你就可以在 \(\texttt{loj}\) 上A了。
还记得我前面说的那句话吗?
众所周知,\(dfs\) 是珂以记忆化的!
我们完全珂以用记忆化来优化这个 \(dfs\) 吖!
就像前面举的那个 \(114514\) 的例子,\(112\ \_\ \_\ \_\,,113\ \_ \ \_\ \_\) 已经确定的位中,倒数第一位(也就是 \(2\) 和 \(3\))不是 \(6\),后三位最多都是 \(999\),后三位的答案总数一定是一样的。
所以如果两个数的答案一样,就必须要满足上面的三个条件,任意一个条件不成立,两个数的答案就不相同。
下文中,“不行”指与 \(112\ \_\ \_\ \_\,,113\ \_ \ \_\ \_\) 后二位的答案不相同。
- \(114\ \_ \ \_\ \_\) 不行,因为后三位最多只能是 \(514\)。(而且还出现 \(4\) 了好伐?),违反了第二条。
- \(106\ \_ \ \_\ \_\) 不行,因为如果下一位是 \(2\) ,那么这个答案就不符合题意,答案总数会减少,违反了第一条。
综上,我们使用 s[x][f]
表示上一位为 \(f\) ,还剩下 \(x\) 位时的记忆化的答案,如果v==0
就珂以使用或记录。
欸?有个小问题,本题是多组输入,记忆化数组需不需要清空呢?
本题的答案是不需要。因为题目限制是没有 \(4\) 或 \(62\) ,任何数都可以使用之前记忆化的结果。
因为每道题的题目要求不同,所以每做一道题都要考虑一下这个问题。
Code
#include<cstdio>
#define int long long
int a[15];
int s[15][15];
int l,r,cnt,la,ra;
int dfs(int x,int f,bool v){
if(!x)return 1;
if(!v&&s[x][f])
return s[x][f];
int u=v?a[x]:9;
int ans=0;
for(int i=0;i<=u;++i){
if(i==4||(i==2&&f==6))
continue;
ans+=dfs(x-1,i,v&&(i==a[x]));
}
if(!v)s[x][f]=ans;
return ans;
}
signed main(){
while((~scanf("%lld%lld",&l,&r))&&(l||r)){
cnt=0;
while(r){
a[++cnt]=r%10;
r/=10;
}
ra=dfs(cnt,10,1);
cnt=0;
l--;
while(l){
a[++cnt]=l%10;
l/=10;
}
la=dfs(cnt,10,1);
printf("%lld\n",ra-la);
}
return 0;
}
练习1-1:数字游戏
练习1-2:Bomb
<法一>用所有情况减去“不要49”的情况
<法二>等会再讲
例题二:windy数
我去小伙子,没看出来呀,你居然是一道蓝题
Solution
不含前导0?咋搞?
我们想,如果第 \(x\) 位前面每一个数位上的数都是 \(0\) 的话:
- 如果第 \(x\) 位不是 \(0\)
好了,从第 \(x\) 位开始,后面每一位都不再有是前导 \(0\) 的可能 - 如果第 \(x\) 位是 \(0\)
那么 \(x\) 也算在前导 \(0\) 内,后面的数位还有是前导 \(0\) 的可能
所以我们再来一个参数 \(q\),记录 \(x\) 之前是不是都是 \(0\) 呗!q==true
时表示这一位不可能是前导 \(0\),否则表示这一位可能是前导 \(0\),也就是说,前面的每一位都是 \(0\)。
因为题目要求每一位都要和上一位相差 \(2\),当q==false
时,可以无视此要求。
快速传 \(q\) 值:q||i
- \(i\) 不为 \(0\),\(q\) 为
true
,理所当然下一位不可能是前导 \(0\)。 - \(i\) 为 \(0\),\(q\) 为
true
,说明 \(i\) 只是数中间的一个正常的 \(0\)。 - \(i\) 不为 \(0\),\(q\) 为
false
,因为 \(i\) 已经将最高位占领了,下一位不可能是前导 \(0\)。 - \(i\) 为 \(0\),\(q\) 为
false
,\(i\) 也算在前导 \(0\) 内,下一位可能是前导 \(0\)。
还记得我们的记忆化吗?用s[x][f]
来表示。如果 \(f\) 碰巧为 \(0\),那就会分为 \(f\) 是前导 \(0\) 和 \(f\) 不是前导 \(0\) 的情况,我们记录的自然是 \(f\) 不是前导 \(0\) 的答案,当v==false
时不能直接使用。
Code
#include<cstdio>
#define int long long
int a[25];
int l,r,cnt;
int s[25][15];
int abs(int x){return x<0?-x:x;}
int dfs(int x,int f,bool v,bool q){
if(!x)return 1;
if(!v&&q&&s[x][f])
return s[x][f];
int u=v?a[x]:9;
int ans=0;
for(int i=0;i<=u;++i){
if(!q||abs(i-f)>=2)
ans+=dfs(x-1,i,v&&(i==a[x]),q||i);
}
if(!v&&q)s[x][f]=ans;
return ans;
}
signed main(){
scanf("%lld%lld",&l,&r);
l--;
cnt=0;
while(r){
a[++cnt]=r%10;
r/=10;
}
int ra=dfs(cnt,0,1,0);
cnt=0;
while(l){
a[++cnt]=l%10;
l/=10;
}
int la=dfs(cnt,0,1,0);
printf("%lld\n",ra-la);
return 0;
}
例题三:数字计数
Solution
想自己为什么能对比想题怎么做想得还久系列
既然每一个数都要统计,那么记忆化就必须每算一个数出现的次数清空一次。
那不是很好写嘛。由于需要统计 \(0\) 的个数,我们也不能算上前导 \(0\) 。
在 \(dfs\) 中加上两个参数 \(num\) 和 \(sum\) ,\(num\) 表示当前正在搜的数,\(sum\) 表示这个数已经出现的次数。
记忆化数组呢,
我们设 s[x][sum]
为当前 \(dfs\) 的 \(num\) 在前 \(x\) 位出现了 \(sum\) 次,后面的结果。
为毛这样设?跟 \(sum\) 有毛关系?直接 s[x]
他不香嘛!
没错,以上就是我花一个下午思考的问题,翻遍全网都没有大佬讲过这个问题。。。
经过暴力程序一顿搞+肉眼查错之后发现,会发生酱紫的情况:
输入 \(1,105\) ,输出 \(0\) 出现的次数为 \(16\)(和暴力程序对得上)
输入 \(1,110\) ,输出 \(0\) 出现的次数为 \(11\)( 。。。)
WWWW...What?发生了什么?
事情是这样的,我们这个方法还不够严谨。
比如求 \(1\sim110\) 中 \(0\) 出现的次数,程序计算得 \(10\sim19\),也就是s[1]
的值为 \(1\) (因为只有 \(10\) 中有一个 \(0\))
但是,我们计算 \(100\sim109\) 时程序会使用s[1]
的值(此时满足使用条件)
But,As we all know,\(100\sim109\) 中有整整 \(10\) 个 \(0\),而程序只计算了 \(100\) 当中的一个 \(0\)。
所以我们需要往记忆化里再添一维,保证不会发生上面那一种情况。
好了,以上是我口胡的理由,总而言之,我们加一维 \(sum\) ,刚好珂以解决上面那个问题。
那么,这个问题就处理完了。。。(【笑容逐渐变态】)
然后这个问题处理完就没问题了。
Code
#include<cstdio>
#include<cstring>
#define int long long
int l,r,cnt;
int s[25][25];
int ans[25],a[25];
int dfs(int x,int sum,int num,bool v,bool q){
//q => 无前导0
//!q => 有前导0
//num => 当前查询的数
//sum => 当前查询的数的总个数
if(!x)return sum;
if(!v&&q&&~s[x][sum])
return s[x][sum];
int u=v?a[x]:9;
int ans=0;
for(int i=0;i<=u;++i){
if(i||q)
ans+=dfs(x-1,sum+(i==num),num,v&&(i==a[x]),1);
else ans+=dfs(x-1,sum,num,v&&(i==a[x]),0);
}
if(!v&&q)s[x][sum]=ans;
return ans;
}
int solve(int x,int w){
cnt=0;
memset(s,-1,sizeof(s));
while(x){
a[++cnt]=x%10;
x/=10;
}
return dfs(cnt,0,w,1,0);
}
signed main(){
scanf("%lld%lld",&l,&r);
for(int i=0;i<=9;++i)
printf("%lld ",solve(r,i)-solve(l-1,i));
return 0;
}
例题四:B-number
题面翻译成人话:
\(B\) 数的定义:是 \(13\) 的倍数,且包含连续的 \(\texttt{"13"}\) 的数。
例如:\(130\) 和 \(2613\) 是 \(B\) 数,但是 \(143\) (不包含连续 \(\texttt{"13"}\)) 和 \(131\) (不是 \(13\) 的倍数) 不是 \(B\) 数。
计算 \(1\)到 \(n\) 之间有多少个 \(B\) 数。
温故而知新
在 练习1-2 处,我们提到了 Boob 的另外一种做法。
我们考虑使用二维记忆化数组 s[maxn][3]
来进行记忆化。
s[x][0]
表示搜到第 \(x\) 位,前面没有出现连续的 \(49\),并且上一位不是 \(4\) 的所有情况。s[x][1]
表示搜到第 \(x\) 位,前面没有出现连续的 \(49\),并且上一位是 \(4\) 的所有情况。s[x][2]
表示搜到第 \(x\) 位,前面已经出现了连续的 \(49\) 的所有情况。
定义参数 \(f\) 为数组的第二维。\(dfs\) 向下延伸时
- 若
f==2
,也就是前面已经出现过 \(49\) ,那么向下拓展时理所应当 \(f\) 也是 \(2\)。 - 若
f==1&&i==9
,也就是上一位是 \(4\) ,这一位是 \(9\) ,刚好凑成一个 \(49\) ,递归时将 \(f\) 传为 \(2\)。 - 若
f!=2&&i==4
,将 \(f\) 向下传为 \(1\)。 - 否则 \(f\) 为 \(0\)。
最后一个数搜完时,只有前面出现了 \(49\) ,也就是 f==2
时才会计算这个数。
完整代码:
#include<cstdio>
#include<cstring>
#define int long long
int a[25];
int x,T,n,cnt;
int s[25][3];
int dfs(int x,int f,bool v){
if(!x)return f==2;
if(!v&&(~s[x][f]))
return s[x][f];
int u=v?a[x]:9;
int ans=0;
for(int i=0;i<=u;++i){
if(f==2||(i==9&&f==1))
ans+=dfs(x-1,2,v&&(a[x]==i));
else if(f!=2&&i==4)
ans+=dfs(x-1,1,v&&(a[x]==i));
else ans+=dfs(x-1,0,v&&(a[x]==i));
}
if(!v)s[x][f]=ans;
return ans;
}
signed main(){
scanf("%lld",&T);
while(T--){
scanf("%lld",&n);
memset(s,-1,sizeof(s));
int tmp=n;
cnt=0;
while(n){
a[++cnt]=n%10;
n/=10;
}
printf("%lld\n",dfs(cnt,0,1));
}
return 0;
}
我们等一会儿会尝试将这种新方法应用到本题中来。
那么,取模,咋整?依次把每个搜到的数记录下来最后再取模?
假设有一个数 \(\overline{x_1x_2x_3\cdots x_n}\bmod13\),可以变形为 \((x_1\times 10^n+x_2\times 10^{n-1}+x_3\times10^{n-2}+\cdots+x_n\times10^0)\bmod13\),再变一下,就是:
在 \(dfs\) 时,我们增加一个参数 \(m\) ,表示当前已经搜完的前几位数 \(\bmod 13\) 的值,向后搜时,我们传(m*10+i)%13
。
搜完最后一个数时,只有同时满足 f==2
和 m==0
时,也就是这个数包含连续的 \(\texttt{"13"}\) ,并且这个数被 \(13\) 整除才计算这个数。
Code
#include<cstdio>
#include<cstring>
#define int long long
int a[25];
int x,n,cnt;
int s[25][15][3];
int dfs(int x,int f,bool v,int m){
if(!x)return f==2&&!m;
if(!v&&(~s[x][m][f]))
return s[x][m][f];
int u=v?a[x]:9;
int ans=0;
for(int i=0;i<=u;++i){
if(f==2||(i==3&&f==1))ans+=dfs(x-1,2,v&&(a[x]==i),(m*10+i)%13);
else if(i==1)ans+=dfs(x-1,1,v&&(a[x]==i),(m*10+i)%13);
else ans+=dfs(x-1,0,v&&(a[x]==i),(m*10+i)%13);
}
if(!v)s[x][m][f]=ans;
return ans;
}
signed main(){
while(~scanf("%lld",&n)){
memset(s,-1,sizeof(s));
int tmp=n;
cnt=0;
while(n){
a[++cnt]=n%10;
n/=10;
}
printf("%lld\n",dfs(cnt,0,1,0));
}
return 0;
}
练习4-1
注意清空记忆化数组。
注意是数字和取模,而不是数字本身。
例题五:起床困难综合征
Solution
位,,位运算???
预处理
提供一个思路:一个数的二进制位只可能是 \(0\) 或 \(1\)。
并且这个数每一位要做的操作已知。
那我们挨个挨个,假设每一位是 \(0\) 或 \(1\) 不就行了?
我们用 number[i][0]
表示初始攻击力的第 \(i\) 位为 \(0\) 时的操作后的结果,number[i][1]
表示初始攻击力的第 \(i\) 位为 \(1\) 时操作后的结果。
把这一位是 \(0\) 时操作的答案记录一下,这一位是 \(1\) 时操作的答案记录一下,再用数位DP找最大的答案。
数位DP
整个 \(dfs\) 共计 \(3\) 个参数:位数 \(x\) ,当前搜到的最大答案对应的初始攻击力 \(c\) ,最大答案 \(w\)。
如果 \(c>m\),不满足题意,return;
搜完了,将最大答案 \(ans\) 和 \(w\) 比 \(\max\) 。
向下拓展时,因为没有了 \(v\) 的限制,枚举第 \(i\) 位为 \(0\) 或 \(1\) 就行了。
然后因为这是数位,所以,假设第 \(x\) 位数为 \(i\),整个数就应该增加 \(i\times 2^x\) ,也就是 i<<x
才对。对应的,因为 number[x][i]
知识当前位的结果,答案 \(w\) 也要加上 number[x][i]<<x
。
所以数位DP的代码长这样:
void dfs(int x,int c,int w){
if(c>m)return;
if(x==-1){
ans=max(ans,w);
return;
}
for(int i=0;i<=1;++i)
dfs(x-1,c+(i<<x),w+(number[x][i]<<x));
return;
}
然而这样会 T 。
所以我们需要剪枝。
我们想,如果 \(w<ans\) ,可能是发生了什么?
差不多就是 \(ans\) 被 \(11\ \_ \ \_\) 更新,而此时的 \(w\) 是 \(10\ \_ \ \_\) 。
\(ans\) 最少是 \(1100\),\(w\) 最多是 \(1011\),\(\max\{w\}<\min\{ans\}\),\(ans\) 永远不会被 \(w\) 更新。
所以当 \(w<ans\) 时,return
。
Code
#include<cstdio>
#define int long long
int a[35];
char op[5];
int number[35][2];
int n,m,cnt,t,ans;
void dfs(int x,int c,int w){
if(c>m||w<ans)return;
if(x==-1){
ans=w;
return;
}
for(int i=0;i<=1;++i)
dfs(x-1,c+(i<<x),w+(number[x][i]<<x));
return;
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=0;i<31;++i)
number[i][1]=1;
for(int i=1;i<=n;++i){
scanf("%s%lld",op,&t);
for(int j=0;j<31;++j){
bool c=(t>>j)&1;
if(op[0]=='A'){
number[j][0]&=c;
number[j][1]&=c;
}
else if(op[0]=='O'){
number[j][0]|=c;
number[j][1]|=c;
}
else{
number[j][0]^=c;
number[j][1]^=c;
}
}
}
dfs(30,0,0);
printf("%lld",ans);
return 0;
}
没有练习,咕咕。
end.
—— · EOF · ——
真的什么也不剩啦 😖