学习博客:
“在信息学竞赛中,有这样一类问题:求给定区间中,满足给定条件的某个D 进制数或
此类数的数量。所求的限定条件往往与数位有关,例如数位之和、指定数码个数、数的大小
顺序分组等等。题目给定的区间往往很大,无法采用朴素的方法求解。此时,我们就需要利
用数位的性质,设计log(n)级别复杂度的算法。解决这类问题最基本的思想就是“逐位确定”
的方法。下面就让我们通过几道例题来具体了解一下这类问题及其思考方法。”——刘聪
事实上,为什么会想到用数位DP来做,就是因为限定条件往往和数位有关,而仔细地朴素的暴力方法中,所做的重复的工作太多。这样的条件会使得DP(记忆化搜索)有用武之地。
比如如果我们要统计[0,54321]中满足某个条件的个数,需要将其拆分为
[00000,09999][10000,19999],[20000,29999],[30000,39999],[40000,49999],
[50000,50999],[51000,51999],[52000,52999],[53000,53999],
[54000,54099],[54100,54199],[54200,54299],
[54300,54309],[54310,54319],
[54320,54321]
为什么要这么分呢?随便举个例子,如果我们统计过了[0000,9999]中的满足条件(或者其他各种不满足条件的状态)的个数,那么分别在加上前缀,就可以判断出有多少个满足条件的个数。目的是为了将大的区间划分为小的区间进行求解。
因此,总结一句话,数位DP减少的运算量为:前面几位固定,后面几位可以任意取的个数统计。
模板:
1 typedef long long ll; 2 int a[20]; 3 ll dp[20][state];//不同题目状态不同 4 ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零 5 { 6 //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了 7 if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */ 8 //第二个就是记忆化(在此前可能不同题目还能有一些剪枝) 9 if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state]; 10 /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/ 11 int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了 12 ll ans=0; 13 //开始计数 14 for(int i=0;i<=up;i++)//枚举,然后把不同情况的个数加到ans就可以了 15 { 16 if() ... 17 else if()... 18 ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos]) //最后两个变量传参都是这样写的 19 /*这里还算比较灵活,不过做几个题就觉得这里也是套路了 20 大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论 21 去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目 22 要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类, 23 前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/ 24 } 25 //计算完,记录状态 26 if(!limit && !lead) dp[pos][state]=ans; 27 /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/ 28 return ans; 29 } 30 ll solve(ll x) 31 { 32 int pos=0; 33 while(x)//把数位都分解出来 34 { 35 a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行 36 x/=10; 37 } 38 return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛 39 } 40 int main() 41 { 42 ll le,ri; 43 while(~scanf("%lld%lld",&le,&ri)) 44 { 45 //初始化dp数组为-1,这里还有更加优美的优化,后面讲 46 printf("%lld ",solve(ri)-solve(le-1)); 47 } 48 }
练手题目1:戳这里
题意:求1-n内有多少个数满足各位之和整除该数。
解题思路:数位dp,枚举各位之和。
附ac代码:
1 #include<iostream> 2 #include<algorithm> 3 #include<stdio.h> 4 #include<string.h> 5 #include<string> 6 using namespace std; 7 typedef long long ll; 8 int a[22]; 9 ll dp[20][220][220];//不同题目状态不同 10 int mod; 11 ll dfs(int pos, int state/*state变量*/, int r/*其他记录点,在这里是余数*/, bool limit/*数位上界变量*/) 12 { 13 //递归边界,既然是按位枚举,最低位是0,那么pos==0说明这个数我枚举完了 14 if(pos == 0) return (state == mod && !r); 15 /*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件, 16 也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。 17 不过具体题目不同或者写法不同的话不一定要返回1 */ 18 //第二个就是记忆化(在此前可能不同题目还能有一些剪枝) 19 if(dp[pos][state][r] != -1 && !limit) return dp[pos][state][r]; 20 /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应*/ 21 int up = limit?a[pos]:9;//根据limit判断枚举的上界up; 22 ll ans = 0; 23 //开始计数 24 for(int i = 0; i <= up; ++i) 25 { 26 if(i + state > mod) break;//剪枝 27 ans += dfs(pos - 1, state + i, (r * 10 + i) % mod, limit && i == a[pos]);//最后两个变量传参都是这样写的 28 /*这里还算比较灵活,不过做几个题就觉得这里也是套路了 29 大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论 30 去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目 31 要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类, 32 前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/ 33 } 34 //计算完,记录状态 35 if(!limit) dp[pos][state][r] = ans; 36 /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/ 37 return ans; 38 } 39 ll solve(ll n) 40 { 41 int pos = 0; 42 ll x = n; 43 while(x)//把数位都分解出来 44 { 45 a[++pos] = x % 10;//个人老是喜欢编号为[1,pos],看不惯的就按自己习惯来,反正注意数位边界就行 46 x /= 10; 47 } 48 ll ans = 0; 49 for(int i = 1; i <= 9 * pos; ++i)//枚举模 50 { 51 mod = i; 52 //初始化dp数组为-1 53 memset(dp, -1, sizeof(dp)); 54 ans += dfs(pos/*从最高位开始枚举*/, 0, 0/*一系列状态 */, true);//刚开始最高位都是有限制的,显然比最高位还要高的一位视为0嘛 55 } 56 return ans; 57 } 58 int main() 59 { 60 int t; 61 scanf("%d", &t); 62 ll n; 63 for(int cas = 1; cas <= t; ++cas) 64 { 65 scanf("%lld", &n); 66 printf("Case %d: %lld ", cas, solve(n)); 67 } 68 69 70 }
练手题目2:戳这里
题意:求l-r中有多少个数满足各位>0的数不大于三个。
解题思路:模板题略作修改。
附ac代码:
1 #include <bits/stdc++.h> 2 using namespace std; 3 const int maxn = 22; 4 typedef long long ll; 5 int a[maxn]; 6 ll dp[maxn][maxn]; 7 ll dfs(int pos, int stat, bool lim) 8 { 9 if(!pos) return 1; 10 if(!lim && stat <= 3 && dp[pos][stat] != -1) return dp[pos][stat]; 11 int up = lim?a[pos]:9; 12 ll ans = 0; 13 14 for(int i = 0; i <= up; ++i) 15 { 16 if(stat + (i > 0) <= 3) 17 { 18 ans += dfs(pos - 1, stat + (i > 0), lim && i == a[pos]); 19 } 20 } 21 if(!lim && stat <= 3) dp[pos][stat] = ans; 22 return ans; 23 } 24 ll solv(ll x) 25 { 26 int pos = 0; 27 while(x) 28 { 29 a[++pos] = x % 10; 30 x /= 10; 31 } 32 33 return dfs(pos, 0, true); 34 } 35 int main() 36 { 37 int t; 38 scanf("%d", &t); 39 memset(dp, -1, sizeof(dp)); 40 ll l, r; 41 while(t--) 42 { 43 scanf("%lld %lld", &l, &r); 44 printf("%lld ", solv(r) - solv(l - 1)); 45 } 46 return 0; 47 }