备注:作者非常菜,有不足之处请发表于讨论区,谢谢!
求出在给定区间 [A,B] 内,符合条件 f(i) 的数 i 的个数。条件 f(i) 一般与数的大小无关,而与数的组成有关
由于数是按位$dp$,数的大小对复杂度的影响很小
这里我们使用记忆化搜索实现数位$dp$。本质上记搜其实就是$dp$
$[l,r]$
对于 $[l,r]$ 区间问题,我们一般把他转化为两次数位dp,即找 $[0,r]$ 和 $[0,l-1]$ 两段,再将结果相减就得到了我们需要的 $[l,r]$
模板(摘自大佬的博客)
一、前导0标记 $ ext{lead}$
由于我们要搜的数可能很长,所以我们的直接最高位搜起
举个例子:假如我们要从 $[0,1000]$ 找任意相邻两数相等的数
显然 $111,222,888$等等是符合题意的数
但是我们发现右端点 $1000$ 是四位数
因此我们搜索的起点是 $0000$ ,而三位数的记录都是 $0111,0222,0888$ 等等
而这种情况下如果我们直接找相邻位相等则 $0000$ 符合题意而 $0111,0222,0888$ 都不符合题意了
所以我们要加一个前导$0$标记
如果当前位 $lead=1$ 而且当前位也是0,那么当前位也是前导0, $pos+1$ 继续搜;
如果当前位 $lead=1$ 但当前位不是0,则本位作为当前数的最高位, $pos+1$ 继续搜;(注意这次根据题意st或其他参数可能发生变化)
当然前导 $0$ 有时候是不需要判断的,上述的例子是一个有关数字结构上的性质,0会影响数字的结构,所以必须判断前导0;而如果我们研究的是数字的组成(例如这个数字有多少个 $1$ 之类的问题),0并不影响我们的判断,这样就不需要前导0标记了。总之,这个因题而异,并不是必须要标记(当然记了肯定是不会出错的)
二、最高位标记 $ ext{limit}$
我们知道在搜索的数位搜索范围可能发生变化;
举个例子:我们在搜索 $[0,555]$ 的数时,显然最高位搜索范围是 $0$ ~ $5$ ,而后面的位数的取值范围会根据上一位发生变化:
当最高位是 $1$ ~ $4$ 时,第二位取值为 $[0,9]$ ;
当最高位是 $5$ 时,第二位取值为 $[0,5]$ (再往上取就超出右端点范围了)
为了分清这两种情况,我们引入了 $ ext{limit}$ 标记:
若当前位 $limit=1$ 而且已经取到了能取到的最高位时,下一位 $limit=1$ ;
若当前位 $limit=1$ 但是没有取到能取到的最高位时,下一位 $limit=0$ ;
若当前位 $limit=0$ 时,下一位 $limit=0$ 。
我们设这一位的标记为 $limit$ ,这一位能取到的最大值为 $res$ ,则下一位的标记就是 $i==res$ && $limit$ ( $i$ 枚举这一位填的数)
三、 $ ext{dp}$ 值的记录和使用
最后我们考虑dp数组下标记录的值
本文介绍数位dp是在记忆化搜索的框架下进行的,每当找到一种情况我们就可以这种情况记录下来,等到搜到后面遇到相同的情况时直接使用当前记录的值。
dp数组的下标表示的是一种状态,只要当前的状态和之前搜过的某个状态完全一样,我们就可以直接返回原来已经记录下来的dp值。
再举个例子
假如我们找 $[0,123456]$ 中符合某些条件的数
假如当我们搜到 $1000$ 时,dfs从下返上来的数值就是当前位是第 $5$ 位,前一位是 $0$ 时的方案种数,搜完这位会向上反,这是我们可以记录一下:当前位第 $5$ 位,前一位是 $0$ 时,有这么多种方案种数
当我们继续搜到 1010??1010?? 时,我们发现当前状态又是搜到了第 55 位,并且上一位也是 00 ,这与我们之前记录的情况相同,这样我们就可以不继续向下搜,直接把上次的dp值返回就行了。
注意,我们返回的dp值必须和当前处于完全一样的状态,这就是为什么dp数组下标要记录 $pos,pre$ 等参量了。
最重要的来了————————————————————
接着上面的例子,范围 $[0,123456]$
如果我们搜到了 $1234$ ,我们能不能直接返回之前记录的:当前第 $5$ 位,前一位是 $4$ 时的dp值?
答案是否定的
我们发现,这个状态的dp值被记录时,当前位也就是第 $5$ 位的取值是 $[0,9]$ ,而这次当前位的取值是 $[0,5]$ ,方案数一定比之前记录的dp值要小。
当前位的取值范围为什么会和原来不一样呢?
如果你联想到了之前所讲的知识,你会发现:现在的 $limit=1$ ,最高位有取值的限制。
因此我们可以得到一个结论:当 $limit=1$ 时,不能记录和取用dp值!
类似上述的分析过程,我们也可以得出:当 $lead=1$ 时,也不能记录和取用dp值!
p.s.当然没有这么绝对的说……因题而异的说……
以上就是计划搜索的完整步骤。
ll dfs(int pos,int pre,int st,……,int lead,int limit)//记搜 { if(pos>len) return st;//剪枝 if((dp[pos][pre][st]……[……]!=-1&&(!limit)&&(!lead))) return dp[pos][pre][st]……[……];//记录当前值 ll ret=0;//暂时记录当前方案数 int res=limit?a[len-pos+1]:9;//res当前位能取到的最大值 for(int i=0;i<=res;i++) { //有前导0并且当前位也是前导0 if((!i)&&lead) ret+=dfs(……,……,……,i==res&&limit); //有前导0但当前位不是前导0,当前位就是最高位 else if(i&&lead) ret+=dfs(……,……,……,i==res&&limit); else if(根据题意而定的判断) ret+=dfs(……,……,……,i==res&&limit); } if(!limit&&!lead) dp[pos][pre][st]……[……]=ret;//当前状态方案数记录 return ret; } ll part(ll x)//把数按位拆分 { len=0; while(x) a[++len]=x%10,x/=10; memset(dp,-1,sizeof dp);//初始化-1(因为有可能某些情况下的方案数是0) return dfs(……,……,……,……);//进入记搜 } int main() { scanf("%d",&T); while(T--) { scanf("%lld%lld",&l,&r); if(l) printf("%lld",part(r)-part(l-1));//[l,r](l!=0) else printf("%lld",part(r)-part(l));//从0开始要特判 } return 0; }
下面讲几道例题:
P4127 [AHOI2009]同类分布
给出两个数$a,b$,求出$[a,b]$中各位数字之和能整除原数的数的个数。
$1 ≤ a ≤ b ≤ 10^{18}$
本题记录前面的数字的和,同时还要知道最后产生的数字是否整除和。
• 记录各位数字的和比较容易,共9*18个状态。关键是如何知道已经产生 的数位构成的数字是否整除最后的总和,比较麻烦。考虑求模,整除的 模数为0,可以记录已经产生的数字的模数,但是又不知道最后的数字总 和是多少,这个模数怎么记录?可以这样定义:
• F[20][200][200][200]->f[i][sum][m][mod]表示到第i位,前面数字总和为sum ,%mod的余数为m,然后枚举mod即可计算。
• 这样可以解决问题,但是计算内存发现:需要1G的内存,只好想办法压 缩空间,因为mod是要在程序中枚举的,所以不必记录这一维状态,这 样空间就足够了
• F[20][200][200]->f[i][sum][m]表示到第i位,前面数字总和为sum,%mod的 余数为m,然后枚举mod即可计算
见代码
// luogu-judger-enable-o2 #include <cstdio> #include <algorithm> #include <cstring> using namespace std; typedef long long LL; LL f[20][205][205]; LL l,r; int a[205]; //sum 数字和 //mo %MOD后的值 LL dfs(int MOD,int len,int sum,int mo,int shangjie){ if (len==0){ if (MOD==sum&&mo==0) return 1; return 0; } if (shangjie==0&&f[len][sum][mo]!=-1) return f[len][sum][mo]; int maxn=(shangjie)?a[len]:9; LL ans=0; for (int i=0;i<=maxn;++i) ans+=dfs(MOD,len-1,sum+i,(mo*10+i)%MOD,shangjie&&(i==maxn)); if (shangjie==0) return f[len][sum][mo]=ans; return ans; } LL calc(LL x){ int ln=0; while (x){ a[++ln]=x%10; x/=10; } LL ans=0; for (int i=1;i<=(ln*9);++i){ memset(f,-1,sizeof(f)); ans+=dfs(i,ln,0,0,1); } return ans; } int main(){ scanf("%lld%lld",&l,&r); printf("%lld ",calc(r)-calc(l-1)); return 0; }
CF908G New Year and Original Order
给$n<=10^{700}$
,问1到n中每个数在各数位排序后得到的数的和。
考虑到原问题很难做,我们转换一下问题
举个例子:假设第$i$位填$3$,那么他的贡献的$3*10^i$,考虑把这个贡献拆开
$3ge3,3ge2,3ge1$我们在这一位填$1,2,3$时都记上$10^i$的贡献
那么我们就只要记这一位填大于等于某个数的方案数就好了
考虑按照套路设$dp[i][j][k][l]$表示前$i$位有$j$位的数字大小大于等于$k$,是否严格小于$n$的方案数
枚举第$i+1$位填$p$
$f[i+1][j+(p>=k)][k][l|(p<a_{i+1})]=sum f[i][j][k][l]$
然后实际上假设前$n$位我们$j$位数字大于等于$k$的方案数是$sum=f[n][j][k][0]+f[n][j][k][1]$
这个对答案的贡献的是$sum*underbrace{111ldots11}_{j ext{个}1}$
#include <cstdio> #include <algorithm> #include <cstring> #define P 1000000007 using namespace std; char st[1000]; int loop[1000],a[1000],dp[705][705][10][2]; void Add(int &x,int y){ x=x+y; x-=(x>=P)?P:0; } int main(){ scanf("%s",st+1); int n=strlen(st+1); loop[1]=1; for (int i=2;i<=(n+1);++i) loop[i]=(10ll*loop[i-1]+1)%P; for (int i=1;i<=n;++i) a[i]=st[i]-'0'; for (int i=0;i<=9;++i) dp[0][0][i][0]=1; for (int i=0;i<n;++i) for (int j=0;j<=i;++j) for (int k=1;k<=9;++k){ int l=0; for (int p=0;p<=a[i+1];++p) Add(dp[i+1][j+(p>=k)][k][l|(p<a[i+1])],dp[i][j][k][l]); l=1; for (int p=0;p<=9;++p) Add(dp[i+1][j+(p>=k)][k][l|(p<a[i+1])],dp[i][j][k][l]); } int ans=0; for (int j=1;j<=9;++j) for (int i=1;i<=n;++i) Add(ans,(1LL*dp[n][i][j][0]*loop[i])%P), Add(ans,(1LL*dp[n][i][j][1]*loop[i])%P); printf("%d ",ans); return 0; }
P3413 SAC#1 - 萌数
辣鸡蒟蒻SOL是一个傻逼,他居然觉得数很萌!
好在在他眼里,并不是所有数都是萌的。只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。
现在SOL想知道从l到r的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。
只需对例如101,11的回文数列分开讨论即可,其余此题与之前类似
#include <cstdio> #include <algorithm> #include <cstring> #define P 1000000007 using namespace std; typedef long long LL; const int N=1005; int dp[N][10][10][2]; int a[N]; char aa[N],bb[N]; LL dfs(int len,int last,int last_last,bool yes,bool shangjie,bool qian0){ if (len==0) return yes; if (last!=-1&&last_last!=-1&&shangjie==0&&qian0==0&&dp[len][last][last_last][yes]!=-1) return dp[len][last][last_last][yes]; int maxn=shangjie?a[len]:9; LL ans=0; for (int i=0;i<=maxn;++i) ans=(ans+ dfs( len-1,i,qian0?-1:last, yes||( (i==last&&qian0==0)||(i==last_last&&qian0==0) ), shangjie&&(i==maxn),qian0&&(i==0) ) )%P; if (last!=-1&&last_last!=-1&&shangjie==0&&qian0==0) dp[len][last][last_last][yes]=ans; return ans; } LL calc(char *s,bool flag){ int len=strlen(s+1); for (int i=len;i;--i) a[len-i+1]=s[i]-'0'; if (flag){ --a[1]; int i=1; while (a[i]<0) a[i]+=10,a[i+1]--,++i; } return dfs(len,-1,-1,0,1,1); } int main(){ memset(dp,-1,sizeof(dp)); scanf("%s",aa+1); scanf("%s",bb+1); printf("%lld ",((calc(bb,0)-calc(aa,1))%P+P)%P); return 0; }