• 初探数位DP-hdu2089


    2020年04月06日17:12:29 重新看了一遍数位dp, 有了更深的理解:

    假设我们先考虑一个简单的问题, 小于87, 且不包含1的的数字有多少,

    我们这里先定义dp的含义, dp[i]表示长度为i+1的数字能组成多少种情况, 例如i=0时, 长度为1, 那总共有2,3,4,5,6,7,8,9中情况, 所以dp[0]=8, 那dp[1]则是dp[0]*8, 64种情况.

    接下来我们考虑对于87, 如果十位为8, 个位的数字必须限制在小于等于7, 如果十位小于8, 则个位的数字可以任意取 (当然这些都不能取1, 1是不能包含的数字), 因此我们使用记忆化搜索

    dfs(int p, bool limit) 表示从第p位开始能得到的结果, limit即表示该位是否能任意取, 当limit=0时, 返回的dfs值才是dp的值, 这也是为什么在返回或者赋值给dp时, 必须加上!limit的原因

    前导0的情况

    我们可以看到, 在最高位的时候, 我们是可以取到0的, 0即代表这一位没有, 但这里被默认填充上了0, 假设题目中要求不能填充0, 则我们必须允许前导0, 禁止后置的0. 加一个状态即可

    一开始刷dp就遇到了数位dp,以前程序设计艺术上看过一点,基本没懂,于是趁今天遇到题目,想把它搞会,但就目前状态来看仍然是似懂非懂啊,以后还要反复搞

    统计区间[l,r]的满足题意的数的个数,可以转换成求[0,r]-[0,l),这也是数位dp题的一个明显的提示
    F[i,st] 代表 位数为i(可能允许前导0。如00058也是个5位数),状态为st的方案数。这里st根据题目需要确定。
    如i=4,f[i,st]也就是0000~9999的符合条件的数的个数(十进制)
    决策第i位是多少(such as 0~9)
     

    这里采用的是记忆化搜索的处理方式,有模板

    1 int dfs(int i, int s, bool e) {                 //i表示当前的位数,s表示状态,e表示后面位数能否任意填
    2     if (i==-1) return s==target_s;              //最后一位取完,找到一个符合条件的值
    3     if (!e && ~f[i][s]) return f[i][s];         //之前位数对应要求的值已经确定,在这里就直接返回
    4     int res = 0;                                //记录符合条件的值
    5     int u = e?num[i]:9;                         //是否能任意填,能任意填则必须小于原来位数上对应的值,否则则可以去到0-9
    6     for (int d = first?1:0; d <= u; ++d)        //逐个填充值,通常会在下面继续加上一些条件,排除不需要的值
    7         res += dfs(i-1, new_s(s, d), e&&d==u);  //下个位数
    8     return e?res:f[i][s]=res;                   //可以任意填的话,说明到i位还未确定res没有包含所有情况,不可以任意填说明后面已经确定,即f也可以确定
    9 }

     2015-04-20 模板

     1 int dfs(int p,int s,bool e) {
     2     if(p==-1) return 1;
     3     if(!e &&dp[p][s]!=-1) return dp[p][s];
     4     int res= 0;
     5     int u=e?digit[p]:9;
     6     for (int i=0;i<=u;++i)
     7     {
     8         if(i==4||(s&&i==2))
     9             continue ;
    10         res+=dfs(p-1,i==6,e&&i==u);
    11     }
    12     return e?res:dp[p][s]=res;
    13 }
    14 int solve(int n)
    15 {
    16     int len=0;
    17     while(n)
    18     {
    19         digit[len++]=n%10;
    20         n/=10;
    21     }
    22     return dfs(len-1,0,1);
    23 }
    View Code

    正确与否有待进一步确认,第一遍看就暂且这么理解吧

    f为记忆化数组;

    i为当前处理串的第i位(权重表示法,也即后面剩下i+1位待填数);

    s为之前数字的状态(如果要求后面的数满足什么状态,也可以再记一个目标状态t之类,for的时候枚举下t);

    e表示之前的数是否是上界的前缀(即后面的数能否任意填)。

    for循环枚举数字时,要注意是否能枚举0,以及0对于状态的影响,有的题目前导0和中间的0是等价的,但有的不是,对于后者可以在dfs时再加一个状态变量z,表示前面是否全部是前导0,也可以看是否是首位,然后外面统计时候枚举一下位数。

    今天做了一道基础数位dp题,来自hdu2089
    题目大意:给定区间[n,m],求在n到m中没有“62“或“4“的数的个数。
    如62315包含62,88914包含4,这两个数都是不合法的。0<n<=m<1000000
     
    那么就用这道题分析一下,首先放个源码
     1 #include <iostream>
     2 using namespace std ;
     3 int f[8][2] ;//f[i][0]:前i位符合要求 f[i][1]:前i位符合要求且i+1位是6
     4 int digit[9] ;//digit[i]表示n从右到左第i位是多少 
     5 int dfs(int i,int s,bool e)//i表示当前位,s表示i位之前的状态,e表示当前位是否可以随意填写 
     6 { 
     7     if(i==0)
     8         return 1 ;
     9     if(!e && f[i][s]!=-1)
    10         return f[i][s] ;
    11     int res=0 ;
    12     int u=e?digit[i]:9 ;
    13     for(int d=0 ;d<=u ;d++)
    14     {
    15         if(d==4 || (s && d==2))
    16             continue ;
    17         res+=dfs(i-1,d==6,e&&d==u) ;
    18     }
    19     return e?res:f[i][s]=res ;
    20 }
    21 int callen(int n)//计算n的长度 
    22 {
    23     int cnt=0 ;
    24     while(n)
    25     {
    26         cnt++ ;
    27         n/=10 ;
    28     }
    29     return cnt ;
    30 }
    31 void caldigit(int n,int len)//计算n的digit数组 
    32 {
    33     memset(digit,0,sizeof(digit)) ;
    34     for(int i=1 ;i<=len ;i++)
    35     {
    36         digit[i]=n%10 ;
    37         n/=10 ;
    38     }
    39 }
    40 int solve(int n)//计算[0,n]区间满足条件的数字个数 
    41 {
    42     int len=callen(n) ;
    43     caldigit(n,len) ;
    44     dfs(len,0,1) ;
    45 }
    46 int main()
    47 {
    48     int n,m ;
    49     memset(f,-1,sizeof(f)) ;
    50     while(~scanf("%d%d",&n,&m))
    51     {
    52         if(n==0 && m==0)
    53             break ;
    54         printf("%d
    ",solve(m)-solve(n-1)) ;//用[0,m]-[0,n)即可得到区间[n,m]
    55     }
    56     return 0 ;
    57 }

    2015-04-20 二次代码,风格变好了很多

     1 #include<cstdio>
     2 #include<iostream>
     3 #include<algorithm>
     4 #include<cstring>
     5 #include<cmath>
     6 #include<queue>
     7 #include<map>
     8 using namespace std;
     9 #define MOD 1000000007
    10 const int INF=0x3f3f3f3f;
    11 const double eps=1e-5;
    12 #define cl(a) memset(a,0,sizeof(a))
    13 #define ts printf("*****
    ");
    14 const int MAXN=1005;
    15 int n,m,tt;
    16 int digit[9],dp[8][2];
    17 int dfs(int p,int s,bool e) {
    18     if(p==-1) return 1;
    19     if(!e &&dp[p][s]!=-1) return dp[p][s];
    20     int res= 0;
    21     int u=e?digit[p]:9;
    22     for (int i=0;i<=u;++i)
    23     {
    24         if(i==4||(s&&i==2))
    25             continue ;
    26         res+=dfs(p-1,i==6,e&&i==u);
    27     }
    28     return e?res:dp[p][s]=res;
    29 }
    30 int solve(int n)
    31 {
    32     int len=0;
    33     while(n)
    34     {
    35         digit[len++]=n%10;
    36         n/=10;
    37     }
    38     return dfs(len-1,0,1);
    39 }
    40 int main()
    41 {
    42     int i,j,k;
    43     #ifndef ONLINE_JUDGE
    44     freopen("1.in","r",stdin);
    45     #endif
    46     memset(dp,-1,sizeof(dp));
    47     while(scanf("%d%d",&n,&m)!=EOF)
    48     {
    49         if(n==0&&m==0)
    50             break;
    51         printf("%d
    ",solve(m)-solve(n-1));
    52     }
    53 }
    View Code

    这里通过输出中间变量来辅助理解

     1 int dfs(int i,int s,bool e)//i表示当前位,s表示i位之前的状态(这里表示是否为6),e表示当前位是否可以随意填写
     2 {
     3     //printf("*****
    ");
     4     //printf("--%d %d %d
    ",i,s,e);
     5     if(i==0)
     6         return 1 ;      //说明前面的位数已经确定,该方案成立
     7     if(!e && f[i][s]!=-1)
     8     {
     9         //printf("--%d %d %d
    ",i,s,e);
    10         return f[i][s] ;
    11     }
    12 
    13     int res=0 ;
    14     int u=e?digit[i]:9 ;
    15     //printf("--%d %d %d %d
    ",i,s,e,u);
    16     for(int d=0 ;d<=u ;d++)
    17     {
    18         if(d==4 || (s && d==2))
    19             continue ;
    20         printf("--%d %d %d %d
    ",i,s,e,d);
    21         res+=dfs(i-1,d==6,e&&d==u) ;
    22     }
    23     printf("***************  %d %d %d %d
    ",i,s,e,res);
    24     return e?res:f[i][s]=res ;
    25 }

    这里输出了0-200的情况

    1 200
    --3 0 1 0  //从百位开始计算,之前没有6,所以中间为0,后面可以任意填充所以为1,首先在第一位上填0
    --2 0 0 0  //第二位上填0
    --1 0 0 0  //第三位上填0,一种情况,res+1,下面一样
    --1 0 0 1  //第三位上填1
    --1 0 0 2
    --1 0 0 3
    --1 0 0 5
    --1 0 0 6
    --1 0 0 7
    --1 0 0 8
    --1 0 0 9  //第三位上填9
    ***************  1 0 0 9  //f[1][0]=9,个位上的情况且十位不含6全部确定共9种,下一步之前res重新清零
    --2 0 0 1  //十位填2,之后再确定个位,发现个位上的情况已经确定,于是直接返回f[1][0],res+f[1][0]
    --2 0 0 2
    --2 0 0 3
    --2 0 0 5
    --2 0 0 6  //十位填6,之后s变为1,个位需要重新确定
    --1 1 0 0
    --1 1 0 1
    --1 1 0 3
    --1 1 0 5
    --1 1 0 6
    --1 1 0 7
    --1 1 0 8
    --1 1 0 9
    ***************  1 1 0 8  f[1][1]=8  //个位上十位含6共9种情况
    --2 0 0 7  //继续枚举十位
    --2 0 0 8
    --2 0 0 9
    ***************  2 0 0 80   //f[2][0]=80
    --3 0 1 1  //百位填1  ret+f[2][0]
    --3 0 1 2  //百位填2  ret+f[2][0]
    --2 0 1 0  //十位填0
    --1 0 1 0  //个位填0  ret+1
    ***************  1 0 1 1  //个位上只能有0
    ***************  2 0 1 1  //十位上只能有00
    ***************  3 0 1 161  //返回的是ret
    161

    正确性有待商榷,待我再多做几道题看看

    更多数位dp题可以参见我博客,代码风格基本是统一的


  • 相关阅读:
    MFC的序列化的一点研究.
    一次LoadRunner的CPC考试经历
    LAMP架构上(一)
    文件和目录管理
    如何在Linux上清理内存缓存、缓冲与交换空间
    Linux Shell基础(下)
    防火墙(上)
    LAMP架构(三)
    LNMP(二)
    LNMP(一)
  • 原文地址:https://www.cnblogs.com/cnblogs321114287/p/4260821.html
Copyright © 2020-2023  润新知