数位dp详解

哈哈哈,本菜鸡经过长时间研究,终于略懂的这看似神奇的数位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 } 
View Code

好了,终于讲完了,累死我了,有错的地方还请指正,能看到这的兄弟也不容易,数位dp中的dp是难点,不过多加训练还是能体会到其中的奥妙的,大家可以点进这里,一起训练和进步吧^-^。

 https://www.cnblogs.com/liuzuolin/p/10348706.html

猜你喜欢

转载自www.cnblogs.com/liuzuolin/p/10333272.html