zoukankan      html  css  js  c++  java
  • 数位dp详解+hdu2089

    哈哈哈,本菜鸡经过长时间研究,终于略懂的这看似神奇的数位dp。。。

    数位dp,顾名思义就是数位+dp,数位即是一个数的每一位上的数字,dp也就是动态规划了。

    首先来讲在何时应该想到要用数位dp吧。(相信大部分人都是为了做题而学的)

      数位dp的题目一般都是给定一个区间,如[l , r],然后叫你求在这区间里的数有多少个符合题目给的限制条件(解题时一般都是运用前缀和求解,即[l , r]=[0 , r]-[0 , l-1]。)

    其次就是讲解数位dp的原理和模板代码了。(菜鸡的我不知道讲的好不好,尽量从一个初学者的角度来讲解吧。)

      1.数位dp的原理。

      数位dp是先将你要计算的最大上限按数位(即个、十、百、千,,,)拆分。然后按最高位去遍历每一个数(!!!注意,这里是用数位去遍历0~上限的每一位,其实也就是暴力枚举,不过它加上dp,就可以将时间复杂度降的十分低)。是不是觉得云里雾里,没看懂,没关系,看下面的一个样例你就明白了。

    样例:找[35,176]中不包含6的数的个数。

    首先将176拆成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

  • 相关阅读:
    转char varchar nvarchar区别
    NHibernate和Spring.Net框架介绍(一)
    ASP.NET面试题(一)
    存储过程编写经验和优化措施
    软件工程师不可不知的10个概念
    优化数据库前问自己的10个问题
    ZOJ 1610 Count the Colors (线段树)
    POJ 3667 Hotel (线段树)
    HDU Best Reward (扩展KMP)
    POJ 3277 City Horizon (线段树)
  • 原文地址:https://www.cnblogs.com/liuzuolin/p/10333272.html
Copyright © 2011-2022 走看看