哈哈哈,本菜鸡经过长时间研究,终于略懂的这看似神奇的数位dp。。。
数位dp,顾名思义就是数位+dp,数位即是一个数的每一位上的数字,dp也就是动态规划了。
首先来讲在何时应该想到要用数位dp吧。(相信大部分人都是为了做题而学的)
数位dp的题目一般都是给定一个区间,如[l , r],然后叫你求在这区间里的数有多少个符合题目给的限制条件(解题时一般都是运用前缀和求解,即[l , r]=[0 , r]-[0 , l-1]。)
其次就是讲解数位dp的原理和模板代码了。(菜鸡的我不知道讲的好不好,尽量从一个初学者的角度来讲解吧。)
1.数位dp的原理。
数位dp是先将你要计算的最大上限按数位(即个、十、百、千,,,)拆分。然后按最高位去遍历每一个数(!!!注意,这里是用数位去遍历0~上限的每一位,其实也就是暴力枚举,不过它加上dp,就可以将时间复杂度降的十分低)。是不是觉得云里雾里,没看懂,没关系,看下面的一个样例你就明白了。
样例:找[35,156]中不包含6的数的个数。
首先将123拆成1、7、6;
这有个位,十位,百位三位。
百位:0 1 2 3 4 5 6 7 8 9
十位:0 1 2 3 4 5 6 7 8 9
个位:0 1 2 3 4 5 6 7 8 9
开始遍历:
百位是:
0(0<1);
十位是:0(0<7),那就能遍历到000,001,002,003,004,005,006,007,008,009。去掉前导0也就是0~9(在有些题目中是要处理掉前导0的,一般是与0有关的),因为6不符合题目条件,所以0~9有9个符合题目的数(将其记录下来);
十位是:1(1<7),那就能遍历到010,011,012,013,014,015,016,017,018,019。去掉前导0也就是10~19,因为16不符合题目条件,所以0~9有9。(!!!注意:如果我们按这样一直遍历下去,那不就相当于暴力了,我们这是数位dp,数位已经在上面体现出来了,接下来就是dp上场了,我们在上一步是不是将0~9中符合题目条件的个数找出来了并且记录了它,那用你的聪明的大脑想想,我们在遍历十位上的数时是不是只要将结果搬过来就行?就如现在的十位是1,那结果不就是出去个位为6的数,因此就可以省去很多没必要的遍历);
十位是:2(2<7),符合的个数是9(020,021,022,023,024,025,026,027,028,029);
十位是:3(3<7),符合的个数是9(030,031,032,033,034,035,036,037,038,039);
...........4,5;
十位是:6 (6<7), 这里因为十位是6,所以个位是什么都不符合。
...........7,8,9;
1(1==1)
十位是:0,1,2,3,4,5,6;个数是(9,9,9,9,9,9,0)
十位是:7(7==7),!!!注意,因为上限是176,现在遍历到的是17X,这里这个X不能是(0~9)了,所以不能直接将9当作符合的个数,因此要一个一个数的去判断是不是我们要的数(即判断170,171,172,173,174,175,176是不是我们要的数)因此这只有6个;
结束遍历;
答案是9+9+9+9+9+9+0+9+9+9+9+9+9+9+9+9+0+6=ans1;
其次就是将35-1拆成3,4;
因为在遍历0~176是已经遍历了0~29,所以在进行遍历是0~29的符合个数会直接得到是9+9+9,而十位是3时个位只能是0~4,所以总个数是9+9+9+6=ans2;
因此我们的到了答案=ans1-ans2;
这里的数据比较小,但是如果数据是long long的范围,那是不是觉得会比平常的枚举快好多;
好了,样例我们已经讲完了,是不是觉得茅舍顿开,豁然开朗,那我们要怎么去用代码实现呢?接着往下看吧。
2,模板代码讲解
数位dp的实现方法很多,我一般都是喜欢用记忆化搜索实现(好吧,我其实只会用记忆化搜索实现);
1 #include<iostream> 2 #include<cstdio> 3 #include<string.h> 4 using namespace std; 5 int dp[15][sta];//一维是当前第几位,sta是按题目所设的一种状态(不是必须有,比如上面例题没有也行),这也就是记忆化的数组; 6 int disit[15];//储存拆分的数 7 //***这是模板代码,所以sta不好写,但是没关系,你就把它先忽略,先理清它具体的写法就好了,等看完下面一个模板题你就明白了******** 8 int dfs(int len,......,bool limit) 9 { 10 if(len==0) 11 return 1; 12 if(!limit&&dp[len][sta]!=-1)// 如上例中的176,如果不是百位是1并且十位是7的情况并且dp[len][sta]计算过时,就可直接返回符合个数 13 return dp[len][sta]; 14 int Maxn=(limit?disit[len]:9);//找到可以遍历的最大数 15 int ans=0; 16 for(int i=0;i<=Maxn;i++) 17 { 18 if(.........)//这里可以根据题目加一些限制条件,可进行剪枝,根据题目而定; 19 .......... 20 ............. 21 ans+=dfs(len-1,.....,limit&&i==disit[len]);//limit&&i==disit[len]是判断是否到了不能记忆化搜索的数; 22 //如上例中的176,如果百位是1并且十位是7,那么就只能枚举剩下的数了; 23 } 24 if(!limit) dp[len][sta]=ans;//记忆化,当没有在上样例中的17X时,就可将计算的结果存入dp数组中进行记忆化; 25 return ans; 26 } 27 //****************拆数************ 28 int solve(int x) 29 { 30 int len=0; 31 while(x) 32 { 33 disit[++len]=x%10; 34 x/=10; 35 } 36 return dfs(len,.....,true); 37 } 38 //*******************主函数一般都是这样,是不是超级简单***************** 39 int main() 40 { 41 //memset(dp,-1,sizeof(dp)); 42 int l,r; 43 while(cin>>l>>r) 44 { 45 memset(dp,-1,sizeof(dp));//题目的限制条件在你计算答案时是固定的话,可以把它放外面,这样可以减少时间复杂度。 46 cout<<solve(r)-solve(l-1)<<endl; 47 } 48 return 0; 49 }
模板代码
1 #include<iostream> 2 #include<string.h> 3 #define LL long long 4 using namespace std; 5 int disit[20]; 6 LL dp[20]; 7 LL dfs(int len,bool limit) 8 { 9 if(!len) return 1; 10 if(!limit&&dp[len]!=-1) return dp[len]; 11 LL ans=0,Maxn=(limit?disit[len]:9); 12 for(int i=0;i<=Maxn;i++) 13 { 14 if(i==6)//当前位是6,则跳出,不搜索 15 continue; 16 ans+=dfs(len-1,limit&&i==Maxn); 17 } 18 if(!limit) dp[len]=ans; 19 return ans; 20 } 21 LL solve(LL x) 22 { 23 memset(disit,0,sizeof(disit)); 24 int len=0; 25 while(x) 26 { 27 disit[++len]=x%10; 28 x/=10; 29 } 30 return dfs(len,true); 31 } 32 int main() 33 { 34 memset(dp,-1,sizeof(dp)); 35 LL l,r; 36 while(cin>>l>>r) 37 { 38 cout<<(solve(r)-solve(l-1))<<endl; 39 } 40 return 0; 41 }
样例代码
模板题HDU2089 http://acm.hdu.edu.cn/showproblem.php?pid=2089
不要62
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 62447 Accepted Submission(s): 24776
Problem Description
杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有4或62的号码。例如:
62315 73418 88914
都属于不吉利号码。但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。
Input
输入的都是整数对n、m(0<n≤m<1000000),如果遇到都是0的整数对,则输入结束。
Output
对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置。
Sample Input
1 100
0 0
Sample Output
80
Author
qianneng
Source
Recommend
lcy
这题数据小,打表也行,不过万一是0<n≤m<1000000000000000呢,所以还是学会用数位dp做吧。
这里就可以用到dp的二维记录上一位数是否是6;
看代码吧
1 #include<iostream> 2 #include<cstdio> 3 #include<string.h> 4 using namespace std; 5 int dp[15][2]; 6 int disit[15]; 7 int dfs(int len,bool sta_6,bool limit)//sta_6是记录上一位是否是6 8 { 9 if(!len) return 1; 10 if(!limit&&dp[len][sta_6]!=-1) return dp[len][sta_6]; 11 int Maxn=(limit?disit[len]:9); 12 int ans=0; 13 for(int i=0;i<=Maxn;i++) 14 { 15 if(i==4) 16 continue; 17 if(sta_6&&i==2) 18 continue; 19 ans+=dfs(len-1,i==6,limit&&i==disit[len]); 20 } 21 if(!limit) dp[len][sta_6]=ans; 22 return ans; 23 } 24 int solve(int x) 25 { 26 int len=0; 27 while(x) 28 { 29 disit[++len]=x%10; 30 x/=10; 31 } 32 return dfs(len,false,true); 33 } 34 int main() 35 { 36 int l,r; 37 memset(dp,-1,sizeof(dp)); 38 while(cin>>l>>r&&l+r) 39 { 40 cout<<solve(r)-solve(l-1)<<endl; 41 } 42 return 0; 43 }
好了,终于讲完了,累死我了,有错的地方还请指正,能看到这的兄弟也不容易,数位dp中的dp是难点,不过多加训练还是能体会到其中的奥妙的,大家可以点进这里,一起训练和进步吧^-^。
https://www.cnblogs.com/liuzuolin/p/10348706.html
原文地址:https://www.cnblogs.com/liuzuolin/p/10333272.html