一、关于数位 dp

有时候我们会遇到某类问题,它所统计的对象具有某些性质,答案在限制/贡献上与统计对象的数位之间有着密切的关系,有可能是数位之间联系的形式,也有可能是数位之间相互独立的形式。(如求满足条件的第 K 小的数是多少,或者求在区间 [L,R] 内有多少个满足限制条件的数等)

常见的在 dp 状态中需要记的信息:当前位数、与上界之间的关系(从高到低做这个信息为 0/1,即当前与上界相等/小于上界。往往数位 dp 的对象是 0 到某个上界 R,为了统计这个范围的信息,我们需要保证从高位往低位做的过程中,这个数始终是小于等于这个上界的。从低到高做这个信息为 0/1/2),是否处于前导零状态等,更多的是跟题目条件有关的信息(包括题目的限制/贡献)。

写法:

  • 每一维的信息用循环遍历到,转移时判每一种合法/不合法的情况。(缺点: 容易漏情况)
  • 手动转移。手展合法的情况。(缺点: 难写难查)

二、例题

1. HDU3652 B-Number

题目大意:问 1~N 中所有含有 13 并且能被 13 整除的数的个数。N≤109

Solution:“含有 13”,即符合条件的数有相邻的两个数位:1、3;“能被 13 整除”这个信息可以通过从高到低按数位计算。因此,在这道题中的两个限制,都和数位之间有着密切的关系。

设 dp[i][j][k][t=0/1] 表示当前第 i 位,上一位(即第 i+1 位)是 j,对 13 取模余数为 k,小于/等于上界(t 表示与上界之间的关系)的方案数。(针对前缀的个数,对 13 取模也是对前缀 13 取模)

我们需要考虑 i-1 位的数位,这个数位枚举的范围由 t 决定。若 t=0,即此时它小于上界,则当前这个数位枚举的范围为 0~9;若 t=1,即此时它等于上界,则当前这个数位枚举的范围为 0~N 的当前数位。设枚举到的这个数位为 c,按照定义,则可以从 dp[i-1][c][(k*10+c)%13][t=1&&c=N 的当前位] 转移到 dp[i][j][k][t]。

由于统计的是数的个数,因此转移为: dp[i][j][k][t]+=dp[i-1][c][(k*10+c)%13][t=1&&c=N 的当前位]。

以上的 dp 状态对于这道题还有遗漏的地方,还需再开一维状态记录目前是否出现了 13。此时还可以缩减状态:用 0 表示之前什么都没有出现过,1 表示上一位以 1 结尾,2 表示已出现了 13。

Code:

数位 dp 的第一种写法:

  1. void solve(){
  2. memset(dp,0,sizeof(dp));
  3. dp[0][0][1][0]=1;
  4. for(int i=0;i<len;i++) //数位
  5. for(int j=0;j<=12;j++) //mod 13 的值
  6. for(int k=0;k<=1;k++) //与上界之间的关系
  7. for(int m=0;m<=2;m++) //处理是否出现了 13。0 表示之前什么都没有出现过,1 表示上一位以 1 结尾,2 表示已出现了 13。
  8. if(dp[i][j][k][m]!=0){
  9. int end=(k==1)?s[i]-'0':9; //数位上界
  10. for(int x=0;x<=end;x++) //当前数位
  11. dp[i+1][(j*10+x)%13][k==1&x==r][(x==1&&m!=2)?1:((m==2||(x==3&&m==1))?2:0)]+=dp[i][j][k][m];
  12. }
  13. } //ans=sigma dp[len][0][0/1][2]

数位 dp 的另一种写法:(记忆化搜索)

  1. #include<bits/stdc++.h>
  2. #define int long long
  3. using namespace std;
  4. const int N=15;
  5. int n,f[N][N][N][2][2],a[20];
  6. int dfs(int i,int last,int p,bool have13,bool less){ //i:数位 last:上一个数位 p:mod 13的值 have13:是否含有13 less:与上界之间的关系
  7. if(!i) return have13&&(p==0);
  8. if(!less&&f[i][last][p][have13][less]!=-1) return f[i][last][p][have13][less];
  9. int end=less?a[i]:9,ans=0;
  10. for(int j=0;j<=end;j++) //当前数位
  11. ans+=dfs(i-1,j,(p*10+j)%13,have13||(last==1&&j==3),less&&j==end);
  12. if(!less) f[i][last][p][have13][less]=ans;
  13. return ans;
  14. }
  15. int calc(int x){
  16. int n=0;
  17. while(x) a[++n]=x%10,x/=10;
  18. return dfs(n,0,0,0,1);
  19. }
  20. signed main(){
  21. while(~scanf("%lld",&n)){
  22. memset(f,-1,sizeof(f));
  23. printf("%lld\n",calc(n));
  24. }
  25. return 0;
  26. }

2. Luogu P4317 花神的数论题

题目大意:问 1~N 中所有数转化为二进制后数位中 1 的个数之积。N≤1015

Solution:

令 dp[i][j][k] 表示 dp 到第 i 位,数位中有 j 个 1,跟上界 N 之间的大小关系为 k(0相等,1小于)的方案数。

第 i 位取 0:dp[i-1][j][k]→dp[i][j][k|N(i)]

(若 N(i) 为 0,则与上界的大小关系不变;若 N(i) 为1,而第 i 位取了 0,则已经小于上界)

第 i 位取 1:dp[i-1][j][k]*max(k,N(i))→dp[i][j+1][k]

(在这种情况下,若 k=N(i)=0是不合法的,也就是说之前已经达到上界了,并且在这一位是0,但第 i 位取了1,就超过了上界,所以要乘以 max(k,N(i)))

其中 N(i) 表示上界 N 在二进制下第 i 位的取值。

最终的答案为:对于任意 1≤j≤n,jdp[n][j][0]+dp[n][j][1] 的乘积。

Code:

写法1:

  1. #include<bits/stdc++.h>
  2. #define int long long
  3. using namespace std;
  4. const int N=60,mod=1e7+7;
  5. int n,f[N][N][2],ans=1,cnt,a[N];
  6. int mul(int x,int a,int mod){ //快速幂
  7. int ans=mod!=1;
  8. for(x%=mod;a;a>>=1,x=x*x%mod)
  9. if(a&1) ans=ans*x%mod;
  10. return ans;
  11. }
  12. signed main(){
  13. scanf("%lld",&n);
  14. while(n) a[++cnt]=n%2,n/=2;
  15. reverse(a+1,a+1+cnt),f[0][0][0]=1;
  16. for(int i=1;i<=cnt;i++)
  17. for(int j=0;j<i;j++)
  18. for(int k=0;k<=1;k++){
  19. f[i][j][k|a[i]]+=f[i-1][j][k];
  20. f[i][j+1][k]+=f[i-1][j][k]*max(k,a[i]);
  21. }
  22. for(int j=1;j<=cnt;j++)
  23. ans=ans*mul(j,f[cnt][j][0]+f[cnt][j][1],mod)%mod;
  24. printf("%lld\n",ans);
  25. return 0;
  26. }

写法2:

  1. #include<bits/stdc++.h>
  2. #define int long long
  3. using namespace std;
  4. const int N=60,mod=1e7+7;
  5. int n,f[N][N][2],a[N],t;
  6. int mul(int x,int n,int mod){ //快速幂
  7. int ans=mod!=1;
  8. for(x%=mod;n;n>>=1,x=x*x%mod)
  9. if(n&1) ans=ans*x%mod;
  10. return ans;
  11. }
  12. int dfs(int i,int cnt,bool less){ //i:数位 cnt:1的个数 less:与上界之间的关系
  13. if(!i) return t==cnt;
  14. if(!less&&f[i][cnt][less]!=-1) return f[i][cnt][less];
  15. int end=less?a[i]:1,ans=0;
  16. for(int j=0;j<=end;j++) //当前数位
  17. ans+=dfs(i-1,cnt+(j==1),less&&j==end);
  18. if(!less) f[i][cnt][less]=ans;
  19. return ans;
  20. }
  21. int calc(int x){
  22. int n=0;
  23. while(x) a[++n]=x%2,x/=2;
  24. int ans=1;
  25. for(int i=1;i<=n;i++){ //枚举1的个数
  26. memset(f,-1,sizeof(f));
  27. t=i,ans=ans*mul(i,dfs(n,0,1),mod)%mod;
  28. }
  29. return ans;
  30. }
  31. signed main(){
  32. scanf("%lld",&n);
  33. printf("%lld\n",calc(n));
  34. return 0;
  35. }

3. Luogu P2602 数字计数

题目大意:给定两个正整数 a 和 b,求在 [a,b] 中的所有整数中,每个数码(digit)各出现了多少次。a≤b≤1012

Solution:

做法一:预处理出所有 0~10x-1 的答案,将询问中给出的 [a, b] 转化为 [0, b] 的答案减去 [0,a-1] 的答案,对于最高位上界为 R 的任务,可以利用预处理出的结果统计最高位在 0~R-1 范围内的答案;将最高位独自的贡献统计了之后就可只考虑其他数位,问题转化到了一个结构相同但数位减少了一的任务上去。

做法二:

令 f[i][0/1] 表示已经做到了第 i 位,与上界之间的大小关系(0相等,1小于)时的答案总和。由于在后续的统计当中单个数位贡献的权还跟满足该种状态的数字个数有关,因此我们需要额外记录一下数字个数 g[i][0/1]。(在本题中这个值可以直接计算)

4. HDU4734 f(x)

题目大意:定义一个十进制数 N 的权值 F(N) 为其各个数位乘上 2 的(后面位数)次幂之和。给出 A,B。问 0~B 中权值不超过 F(A) 的数个数。A,B≤109

Solution:

Sum 的范围大约在 5k 的样子,直接存下来即可。

令 dp[i][j][0/1] 表示 dp 到数字的第 i 位,每个数位对 F 值贡献之和为 j 的方案数。

  1. #include<bits/stdc++.h>
  2. #define int long long
  3. using namespace std;
  4. const int N=15,M=2e4+5;
  5. int t,A,B,f[N][M][2],a[N];
  6. int F(int x){ //计算 F(x)
  7. int cnt=0,y=1;
  8. while(x) cnt+=x%10*y,x/=10,y<<=1;
  9. return cnt;
  10. }
  11. int dfs(int i,int sum,bool less){ //i:数位 j:每个数位对 F 值贡献之和 less:与上界之间的关系
  12. if(!i) return sum>=0;
  13. if(sum<0) return 0;
  14. if(!less&&f[i][sum][less]!=-1) return f[i][sum][less];
  15. int end=less?a[i]:9,ans=0;
  16. for(int j=0;j<=end;j++) //当前数位
  17. ans+=dfs(i-1,sum-j*(1<<(i-1)),less&&j==end);
  18. if(!less) f[i][sum][less]=ans;
  19. return ans;
  20. }
  21. int calc(int x){
  22. int n=0;
  23. while(x) a[++n]=x%10,x/=10;
  24. return dfs(n,F(A),1);
  25. }
  26. signed main(){
  27. memset(f,-1,sizeof(f));
  28. scanf("%lld",&t);
  29. for(int k=1;k<=t;k++){
  30. scanf("%lld%lld",&A,&B);
  31. printf("Case #%lld: %lld\n",k,calc(B));
  32. }
  33. return 0;
  34. }

5. Codeforces 55D Beautiful Number

题目大意:给定两个正整数 a 和 b,求在 [a,b] 中的所有整数中,有多少个数能够整除它自身的所有非零数位。a≤b≤1018

Solution:

一种直接的想法是维护当前数模 1,2,...,9 的余数。每次乘十再加上新的数位后取个模就是新的状态。另外再维护一个状态表示 1,2,..,9 这些数位有哪些出现过。状态数18*2*9!*29≈69

注意到若是我们维护了模 9 的余数,便自然能够推出模 3 的余数。类似这样的冗余状态有不少。

按照上面的思路,一个数能被其所有的非零数位整除,即它能被所有的非零数位的最小公倍数整除,这个最小公倍数的最大值显然是 1 到 9 的最小公倍数 2520 ,然后就可以对 2520 的模进行状态转移。 我们可以维护这个数本身模 lcm(1,2,...,9)=2520 的余数。从这个值可推出模所有 1,2,..,9 数位的余数。这一步优化将状态从 9!→2520。目前的状态数为 18*2*2520*512≈47

dp[i][p][j][k] 表示当前 dp 到第 i 位,与上界的大小关系为 p,目前数的大小模 2520 的余数为 j,数位出现的状态数为 k 的方案数。每 dp 到新的一位,只需要枚举之前的状态和这一位选用的数,就可做到 O(1) 转移。

类似地,我们观察维护的最后一维。是否有和之前优化过程类似的冗余的地方?

如果数位当中已经出现了 9,那么出现 3 并不会在这个数上增加什么限制。

题目当中的约束可以等价地转换为:出现了的数位的 LCM | 原数%2520。

这些 LCM 只会取到 2520 的因子。2520=23×32×5×7,所以 1 到 9 这九个数的最小公倍数只会出现 4×3×2×2=48 种情况,是可以接受的。状态数降到了18*2*2520*48≈46

  1. #include<bits/stdc++.h>
  2. #define int long long
  3. using namespace std;
  4. const int N=2520;
  5. int t,l,r,cnt,a[20],v[N+5],f[20][N+5][50];
  6. int dfs(int i,int p,int x,bool less){ //i:数位 p:目前数的大小模 2520 的余数 x:i位之前的数的每一位数的最小公倍数
  7. if(!i) return p%x==0;
  8. if(!less&&f[i][p][v[x]]!=-1) return f[i][p][v[x]];
  9. int end=less?a[i]:9,ans=0;
  10. for(int j=0;j<=end;j++) //当前数位
  11. ans+=dfs(i-1,(p*10+j)%N,j?x/__gcd(x,j)*j:x,less&(j==end)); //x/__gcd(x,j)*j 即 x*j/gcd(x,j)=lcm(x,j)。计算的是包含当前位时所有位上的数的最小公倍数(当前位所选数不为 0,如果为 0 就是原数)
  12. if(!less) f[i][p][v[x]]=ans;
  13. return ans;
  14. }
  15. int calc(int x){
  16. int n=0;
  17. while(x) a[++n]=x%10,x/=10;
  18. return dfs(n,0,1,1);
  19. }
  20. signed main(){
  21. memset(f,-1,sizeof(f));
  22. for(int i=1;i<=N;i++)
  23. if(N%i==0) v[i]=++cnt; //标记 2520 的因子
  24. scanf("%lld",&t);
  25. while(t--){
  26. scanf("%lld%lld",&l,&r);
  27. printf("%lld\n",calc(r)-calc(l-1));
  28. }
  29. return 0;
  30. }

6. 2012 Multi-University Training Contest 6 XHXJ's LIS(HDU 4352)

题目大意:给定两个正整数 a 和 b,求在 [a,b] 中的所有整数中,有多少个数(不包含前导零)的数位最长上升子序列长度恰好为 k。T≤104,a≤b≤1018

Solution:

由于 LIS 只能 dp 求,所以我们维护的状态也应是dp状态。

考虑 n2 LIS 的求法:当确定了一个新的数位后,这个位置的 dp 值跟之前所有位置的dp 值都有关系。维护这些信息的复杂度比维护具体这个数是什么还要高,显然不现实。

1.考虑树状数组优化 LIS 的求法:在树状数组 LIS 的求法中,最终的答案和当新加入一个数后 dp 值的更新只跟树状数组有关。

  • 下标:0~9
  • 取值范围:1~10
  • 状态数:1010,难以接受!

如果我们直接维护树状数组所维护的序列,发现一个性质:单调不降。且由于要求的是严格上升序列,所以 0 位置的值必定不超过 1。

我们考虑维护差分序列,下标范围:0~9, 取值范围:0~1。状态数下降到 210

2.考虑二分求 LIS 的做法:其中 f(i) 表示长度为 i 的上升子序列结尾最小可以是多少。下标范围:1~10,取值范围:0~9,状态数:1010

冷静思考,发现 f(i) 实际上是严格单增的。

因此只需要记录一下 0~9 哪些数在 f(i) 中出现过就足够还原出整个 dp 数组的信息。状态数 210

用十位二进制表示 0~9 出现的情况,和二分求 LIS 一样的方法进行更新。

  1. #include<bits/stdc++.h>
  2. #define int long long
  3. using namespace std;
  4. const int N=11;
  5. int t,l,r,k,f[25][1<<N][N][2],a[N]; //f[i][j][k][0/1]:当前的数位为 i,状态为 j,要求的 LIS 长度为 k,与上界之间的关系为 0/1 的方案数。
  6. int solve(int x,int S){ //获取新的状态
  7. for(int i=x;i<=9;i++)
  8. if(S&(1<<i)) return S^(1<<i)^(1<<x);
  9. return S^(1<<x);
  10. }
  11. int dfs(int i,int S,bool have0,bool less){ //i:数位 S:当前的状态 have0:是否含有前导零 less:与上界之间的关系
  12. if(!i) return __builtin_popcount(S)==k; //状态中 1 的数目就是 LIS 的长度
  13. if(!less&&f[i][S][k][less]!=-1) return f[i][S][k][less];
  14. int end=less?a[i]:9,ans=0;
  15. for(int j=0;j<=end;j++) //当前数位
  16. ans+=dfs(i-1,(have0&&j==0)?0:solve(j,S),have0&&j==0,less&&j==end);
  17. if(!less) f[i][S][k][less]=ans;
  18. return ans;
  19. }
  20. int calc(int x){
  21. int n=0;
  22. while(x) a[++n]=x%10,x/=10;
  23. return dfs(n,0,1,1);
  24. }
  25. signed main(){
  26. memset(f,-1,sizeof(f));
  27. scanf("%lld",&t);
  28. for(int i=1;i<=t;i++){
  29. scanf("%lld%lld%lld",&l,&r,&k);
  30. printf("Case #%lld: %lld\n",i,calc(r)-calc(l-1));
  31. }
  32. return 0;
  33. }

7.HDU4507 恨7不成妻

题目大意:如果一个整数符合下面 3 个条件之一,那么我们就说这个整数和 7 有关——

  • 整数中某一位是 7;
  • 整数的每一位加起来的和是 7 的整数倍;
  • 这个整数是 7 的整数倍;

求在区间 [L,R] 内和 7 无关的数字的平方和。L,R≤1018

Solution:

“某一位是 7”只需记录是否出现过 7,“每一位加起来的和是 7 的整数倍”只需记录数位和 mod 7 的值,“是 7 的整数倍”只需记录当前数 mod 7 的值。此题的重点在于如何在 dp 的过程中维护平方和。

我们之前接触的大部分数位 dp 统计的都是满足性质的数的个数。转移时,都是若满足条件,则转移后的 dp 值+=转移前的 dp 值。思考这个过程。

令 f0 表示满足性质的数的个数。

转移前的 f0:Σ合法的 xi 1

假设当前的数位是 c。在每一个数的后面都加上当前的数位 c,并不会改变它们的个数(一个数后面添加一个 c 仍是一个数)。

所以转移后的 f0 依然是:Σ合法的 xi 1

Σ合法的 xi 1⇒Σ合法的 xi 1,转移前和转移后的 f0 都是满足性质的数的个数,这也就是为什么可以直接把转移前的 dp 值加到转移后的 dp 值上。

思考另一个东西。

令 f1 表示满足条件的数之和。

Σ合法的 xi xi⇒Σ合法的 xi (10xi+c)⇒10 Σ合法的 xi xi+c·Σ合法的 xi 1⇒10·转移前的 f1+c·转移前的 f0

如果我们维护了 f0,那么就可以通过这个式子去更新 f1

类似地,令 f2 表示满足条件的数的平方和。

Σ合法的 xi xi2⇒Σ合法的 xi (10xi+c)2⇒100Σ合法的 xi xi2+20·c·Σ合法的 xi xi+c2·Σ合法的 xi 1⇒100·转移前的 f2+20·c·转移前的 f1+c2·转移前的 f0

于是,如果我们已经维护了 f0 和 f1,就可以完成对 f2 的维护。

「算法笔记」数位 DP的更多相关文章

  1. 「算法笔记」树形 DP

    一.树形 DP 基础 又是一篇鸽了好久的文章--以下面这道题为例,介绍一下树形 DP 的一般过程. POJ 2342 Anniversary party 题目大意:有一家公司要举行一个聚会,一共有 \ ...

  2. 「算法笔记」期望 DP 入门

    一.数学期望 1. 由来 在 \(17\) 世纪,有一个赌徒向法国著名数学家帕斯卡挑战,给他出了一道题目:甲乙两个人赌博,他们两人获胜的机率相等,比赛规则是先胜三局者为赢家,一共进行五局,赢家可以获得 ...

  3. 「算法笔记」快速数论变换(NTT)

    一.简介 前置知识:多项式乘法与 FFT. FFT 涉及大量 double 类型数据操作和 \(\sin,\cos\) 运算,会产生误差.快速数论变换(Number Theoretic Transfo ...

  4. 「笔记」数位DP

    目录 写在前面 引入 求解 特判优化 代码 例题 「ZJOI2010」数字计数 「AHOI2009」同类分布 套路题们 「SDOI2014」数数 写在最后 写在前面 19 年前听 zlq 讲课的时候学 ...

  5. 「算法笔记」状压 DP

    一.关于状压 dp 为了规避不确定性,我们将需要枚举的东西放入状态.当不确定性太多的时候,我们就需要将它们压进较少的维数内. 常见的状态: 天生二进制(开关.选与不选.是否出现--) 爆搜出状态,给它 ...

  6. 「算法笔记」2-SAT 问题

    一.定义 k-SAT(Satisfiability)问题的形式如下: 有 \(n\) 个 01 变量 \(x_1,x_2,\cdots,x_n\),另有 \(m\) 个变量取值需要满足的限制. 每个限 ...

  7. 「算法笔记」Polya 定理

    一.前置概念 接下来的这些定义摘自 置换群 - OI Wiki. 1. 群 若集合 \(s\neq \varnothing\) 和 \(S\) 上的运算 \(\cdot\) 构成的代数结构 \((S, ...

  8. 「算法笔记」旋转 Treap

    一.引入 随机数据中,BST 一次操作的期望复杂度为 \(\mathcal{O}(\log n)\). 然而,BST 很容易退化,例如在 BST 中一次插入一个有序序列,将会得到一条链,平均每次操作的 ...

  9. 「算法笔记」FHQ-Treap

    右转→https://www.cnblogs.com/mytqwqq/p/15057231.html 下面放个板子 (禁止莱莱白嫖板子) P3369 [模板]普通平衡树 #include<bit ...

随机推荐

  1. 为 Rainbond Ingress Controller 设置负载均衡

    Rainbond 作为一款云原生应用管理平台,天生带有引导南北向网络流量的分布式网关 rbd-gateway.rbd-gateway 组件,实际上是好雨科技团队开发的一种 Ingress Contro ...

  2. 学习java的第十七天

    一.今日收获 1.java完全学习手册第三章算法的3.1比较值 2.看哔哩哔哩上的教学视频 二.今日问题 1.在第一个最大值程序运行时经常报错. 2.哔哩哔哩教学视频的一些术语不太理解,还需要了解 三 ...

  3. JVM2 类加载子系统

    目录 类加载子系统 类加载器子系统 类加载器ClassLoader角色 类加载的过程 案例 加载Loading 连接Linking 初始化Intialization clinit() 类的加载器 虚拟 ...

  4. 日常Java测试 2021/11/14

    课堂测试三 package word_show; import java.io.*;import java.util.*;import java.util.Map.Entry; public clas ...

  5. 日常Java 2021/10/17

    今天开始Javaweb编译环境调试,从tomcat容器开始,然后mysql的下载,连接工具datagrip,navicat for mysql,然后就是编写自己的sql,安装jdbc,eclipse连 ...

  6. pyqt5 改写函数

    重新改写了keyPressEvent() class TextEdit(QTextEdit): def __init__(self): QtWidgets.QTextEdit.__init__(sel ...

  7. 容器之分类与各种测试(四)——unordered_set和unordered_map

    关于set和map的区别前面已经说过,这里仅是用hashtable将其实现,所以不做过多说明,直接看程序 unordered_set #include<stdexcept> #includ ...

  8. Output of C++ Program | Set 2

    Predict the output of below C++ programs. Question 1 1 #include<iostream> 2 using namespace st ...

  9. shell神器curl命令的用法 curl用法实例笔记

    shell神器curl命令的用法举例,如下: ##基本用法(配合sed/awk/grep) $curl http://www.jquerycn.cn ##下载保存 $curl http://www.j ...

  10. 对于HTML和XML的理解

    1.什么是HTML??? HTML就是 超文本标记语言(超文本含义:超过文本 --图片 .视频.音频. 超链接) 2.HTML作用 把网页的信息格式化的展现,对网页信息进行规范化展示 连接(https ...