原题链接 https://www.luogu.com.cn/problem/P4127
题解
浅谈数位$dp$
昨天通过网课复习了一下数位$dp$,然后来做几道数位 $dp$ 的题来练练手。
经典的数位$dp$ 是要求统计符合限制的数字的个数。
一般的形式是:求区间 $[ n , m ]$ 满足限制 $f ( 1 ) 、f ( 2 ) 、f ( 3 )$ 等等的数字的数量是多少。条件 $f ( i )$ 一般与数的大小无关,而与数的组成有关。
善用不同进制来处理,一般问题都是十进制和二进制的数位$dp$。
数位$dp$ 的部分一般都是很套路的,但是有些题目在数位$dp$ 外面套了一个华丽的外衣,有时我们难以看出来。
数位$dp$ 一般用记忆化搜索来实现。
数位$dp$ 的时间复杂度与位数有关。
回到本题
一个很自然的想法是在搜索过程中维护当前已经选的数的大小和各个位的数字之和,当枚举完所有位之后判断模数是否为 $0$ 即可;
想法固然不错,但是数实在太大,没法记忆化,会超时,不可取!
考虑到我们其实并不关心这个数本身是几,只需关心这个数模各个位的数字之和是几,而模数 ( 各个位的数字之和 ) 是小于 $9*18$ 的,所以余数也是小于 $9*18$ 的,可行。
但是在选数的过程中,模数也是变化的,所以我们无法维护余数的大小,怎么办呢?
我们让模数不变!
我们可以去枚举 $[ 1 , x ]$ 范围内的所有数的各个位的数字之和是几,等到选完所有位之后,再判断你选完的数的各个位的数字之和与你一开始枚举的是否相同即可,不相同即不合法状态;
剪枝
想出正解的你开开心心地把代码交上去,然后 $TLE$ 了......
考虑怎么剪枝,记忆化我们都加上了,看来只能在各个位的数字之和上面动点手脚了$qwq$。
考虑到判断一个状态合法的前提是你选出的各个位的数字之和要等于你一开始枚举的那个数,由此我们可以想到两个可行性剪枝:
①. 如果当前你选的各个位的数字之和 $sum$ 已经大于了你一开始枚举的那个数,那么之后的状态一定是不合法的,直接返回 $0$ 即可;
②. 如果你剩下的所有位都选了 $9$,但是加起来的 $sum$ 还是比你一开始枚举的那个数小,那么之后再怎么选都是不合法的,直接返回 $0$ 即可;
$Code$:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; long long l,r; int c[100]; long long dp[100][200][200]; long long dfs(int k,int Mod,int mod,int sum,bool limit) //当前正在选第k位,模数是Mod,余数是mod,选的所有位的数字之和为sum,是否顶上界 { if(sum>Mod||sum+9*(k+1)<Mod) return 0; //如果当前选的各个数的数字之和已经大于Mod了,或者之后的位数都选9还是小于Mod,那么不用搜了,肯定是不合法的 if(k<0) //枚举完所有位数了 { if(sum==Mod&&mod==0) return 1; //必须要保证你选的各个位的数字之和等于Mod,且余数mod==0才是符合要求的状态 return 0; } if(!limit&&dp[k][mod][sum]!=-1) return dp[k][mod][sum]; //记忆化 int up=9; if(limit) up=c[k]; //up是当前位所能填的最大数字 long long ans=0; for(int i=0;i<=up;i++) { ans+=dfs(k-1,Mod,(mod*10+i)%Mod,sum+i,limit&&i==up);//转移到下一位 } if(!limit) dp[k][mod][sum]=ans; return ans; } long long solve(long long x) //求[1,x]内符合条件的数的个数 { memset(c,0,sizeof(c)); int len=0; while(x) //将x一位一位地拆开 { c[len++]=x%10; x/=10; } long long ans=0; for(int i=1;i<=9*len;i++) //枚举所有数的各位数字之和,最大是9*len { memset(dp,-1,sizeof(dp)); //注意每次搜索前都要memset ans+=dfs(len-1,i,0,0,1); //现在在枚举第len-1位,模数是i,余数是0,选的各个位的数字之和为0,顶上界 } return ans; } int main() { scanf("%lld %lld",&l,&r); printf("%lld ",solve(r)-solve(l-1)); //前缀和的思想 return 0; }
总结
$10$ 进制数位$dp$ 的基本最简单的形式。
记忆化搜索处理数位$dp$ 的代码实现,数位$dp$ 一般都用记忆化搜索来做。
考察思维的数位$dp$ 往往会和其他如枚举算法结合,或作为原问题的子问题。
除了十进制,二进制的数位$dp$ 也是常见的,此外 $K$ 进制的也是可以的。