一、关于数位 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 的第一种写法:

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

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

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

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:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60,mod=1e7+7;
int n,f[N][N][2],ans=1,cnt,a[N];
int mul(int x,int a,int mod){ //快速幂
int ans=mod!=1;
for(x%=mod;a;a>>=1,x=x*x%mod)
if(a&1) ans=ans*x%mod;
return ans;
}
signed main(){
scanf("%lld",&n);
while(n) a[++cnt]=n%2,n/=2;
reverse(a+1,a+1+cnt),f[0][0][0]=1;
for(int i=1;i<=cnt;i++)
for(int j=0;j<i;j++)
for(int k=0;k<=1;k++){
f[i][j][k|a[i]]+=f[i-1][j][k];
f[i][j+1][k]+=f[i-1][j][k]*max(k,a[i]);
}
for(int j=1;j<=cnt;j++)
ans=ans*mul(j,f[cnt][j][0]+f[cnt][j][1],mod)%mod;
printf("%lld\n",ans);
return 0;
}

写法2:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=60,mod=1e7+7;
int n,f[N][N][2],a[N],t;
int mul(int x,int n,int mod){ //快速幂
int ans=mod!=1;
for(x%=mod;n;n>>=1,x=x*x%mod)
if(n&1) ans=ans*x%mod;
return ans;
}
int dfs(int i,int cnt,bool less){ //i:数位 cnt:1的个数 less:与上界之间的关系
if(!i) return t==cnt;
if(!less&&f[i][cnt][less]!=-1) return f[i][cnt][less];
int end=less?a[i]:1,ans=0;
for(int j=0;j<=end;j++) //当前数位
ans+=dfs(i-1,cnt+(j==1),less&&j==end);
if(!less) f[i][cnt][less]=ans;
return ans;
}
int calc(int x){
int n=0;
while(x) a[++n]=x%2,x/=2;
int ans=1;
for(int i=1;i<=n;i++){ //枚举1的个数
memset(f,-1,sizeof(f));
t=i,ans=ans*mul(i,dfs(n,0,1),mod)%mod;
}
return ans;
}
signed main(){
scanf("%lld",&n);
printf("%lld\n",calc(n));
return 0;
}

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 的方案数。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=15,M=2e4+5;
int t,A,B,f[N][M][2],a[N];
int F(int x){ //计算 F(x)
int cnt=0,y=1;
while(x) cnt+=x%10*y,x/=10,y<<=1;
return cnt;
}
int dfs(int i,int sum,bool less){ //i:数位 j:每个数位对 F 值贡献之和 less:与上界之间的关系
if(!i) return sum>=0;
if(sum<0) return 0;
if(!less&&f[i][sum][less]!=-1) return f[i][sum][less];
int end=less?a[i]:9,ans=0;
for(int j=0;j<=end;j++) //当前数位
ans+=dfs(i-1,sum-j*(1<<(i-1)),less&&j==end);
if(!less) f[i][sum][less]=ans;
return ans;
}
int calc(int x){
int n=0;
while(x) a[++n]=x%10,x/=10;
return dfs(n,F(A),1);
}
signed main(){
memset(f,-1,sizeof(f));
scanf("%lld",&t);
for(int k=1;k<=t;k++){
scanf("%lld%lld",&A,&B);
printf("Case #%lld: %lld\n",k,calc(B));
}
return 0;
}

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

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2520;
int t,l,r,cnt,a[20],v[N+5],f[20][N+5][50];
int dfs(int i,int p,int x,bool less){ //i:数位 p:目前数的大小模 2520 的余数 x:i位之前的数的每一位数的最小公倍数
if(!i) return p%x==0;
if(!less&&f[i][p][v[x]]!=-1) return f[i][p][v[x]];
int end=less?a[i]:9,ans=0;
for(int j=0;j<=end;j++) //当前数位
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 就是原数)
if(!less) f[i][p][v[x]]=ans;
return ans;
}
int calc(int x){
int n=0;
while(x) a[++n]=x%10,x/=10;
return dfs(n,0,1,1);
}
signed main(){
memset(f,-1,sizeof(f));
for(int i=1;i<=N;i++)
if(N%i==0) v[i]=++cnt; //标记 2520 的因子
scanf("%lld",&t);
while(t--){
scanf("%lld%lld",&l,&r);
printf("%lld\n",calc(r)-calc(l-1));
}
return 0;
}

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 一样的方法进行更新。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=11;
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 的方案数。
int solve(int x,int S){ //获取新的状态
for(int i=x;i<=9;i++)
if(S&(1<<i)) return S^(1<<i)^(1<<x);
return S^(1<<x);
}
int dfs(int i,int S,bool have0,bool less){ //i:数位 S:当前的状态 have0:是否含有前导零 less:与上界之间的关系
if(!i) return __builtin_popcount(S)==k; //状态中 1 的数目就是 LIS 的长度
if(!less&&f[i][S][k][less]!=-1) return f[i][S][k][less];
int end=less?a[i]:9,ans=0;
for(int j=0;j<=end;j++) //当前数位
ans+=dfs(i-1,(have0&&j==0)?0:solve(j,S),have0&&j==0,less&&j==end);
if(!less) f[i][S][k][less]=ans;
return ans;
}
int calc(int x){
int n=0;
while(x) a[++n]=x%10,x/=10;
return dfs(n,0,1,1);
}
signed main(){
memset(f,-1,sizeof(f));
scanf("%lld",&t);
for(int i=1;i<=t;i++){
scanf("%lld%lld%lld",&l,&r,&k);
printf("Case #%lld: %lld\n",i,calc(r)-calc(l-1));
}
return 0;
}

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. 利用python爬取城市公交站点

    利用python爬取城市公交站点 页面分析 https://guiyang.8684.cn/line1 爬虫 我们利用requests请求,利用BeautifulSoup来解析,获取我们的站点数据.得 ...

  2. int是几位;short是几位;long是几位 负数怎么表示

    其实可以直接通过stm32的仿真看到结果:(这里是我用keil进行的测试,不知道这种方法是否准确) 从上面看, char是8位  short是4*4=16位  int是8*4=32位  long是8* ...

  3. STL学习笔记1

    STL六大部件 容器.分配器.算法.迭代器.适配器.仿函数 他们的关系如下

  4. RAC中常见的高级用法-bind方法

    RAC操作思想:      Hook(钩子)思想 RAC核心方法:bind      bind方法      假设想监听文本框的内容,并且在每次输出结果的时候,都在文本框的内容拼接一段文字" ...

  5. Linux 易错小结

    修改文件夹(递归修改)权限 chmod -R 777 /html Linux查看进程的4种方法 第一种: ps aux ps命令用于报告当前系统的进程状态.可以搭配kill指令随时中断.删除不必要的程 ...

  6. 1.ElasticSearch相关概念

    1.为ElasticSearch设置跨域访问 http.cors.enabled: truehttp.cors.allow-origin: "*" 2.什么是ElasticSear ...

  7. numpy基础教程--clip函数的使用

    在numpy中,clip函数的原型为clip(self, min=None, max=None, out=None),意思是把小于min的数全部置换为min,大于max的数全部置换为max,在[min ...

  8. redis实例cpu占用率过高问题优化

    目录 一.简介 一.简介 前情提要: 最近接了大数据项目的postgresql运维,刚接过来他们的报表系统就出现高峰期访问不了的问题,报表涉及实时数据和离线数据,离线读pg,实时读redis.然后自然 ...

  9. 隐藏和显示div的两种方法

    方式一 style="visibility: none;" visiblity:visible -------->可见 visiblity:hidden -------> ...

  10. 漫谈IRP

    I/O Request Packet(IRP) IRP概述: IRP是由I/O管理器发出的,I/O管理器是用户态与内核态之间的桥梁,当用户态进程发出I/O请求时,I/O管理器就捕获这些请求,将其转换为 ...