zoukankan      html  css  js  c++  java
  • 数位DP入门

    切换至❤新页面,体验更加哦

    数位dp,是一种用来计数的dp

    如果现在给你一道题,需要你求在区间[l,r]内满足条件的解的个数,我们很容易想到去暴力枚举,但要是数据范围太大这种办法就行不通了,这时候数位dp就派上了用场,所谓数位就是把一个数拆成一个一个进制位,然后逐一比较看是否满足题目要求,这其实也是一种暴力方法,只不过时间复杂度小了很多

    那么到底要如何做呢?下面我们来看一道例题

    HDU2089

    概括一下题目意思

    就是给你一个区间[n,m],要你求区间内不含"62"或"4"的数字的个数,如8134(含4),21262455(含62)均不满足题意,而61342这种"6"和"2"并不连在一起的数字则满足题意

    直接统计对于暴力枚举很好求,但是对于数位dp并不容易,所以我们还需要用到差分的思想,即统计0到b+1(注意不是b,至于为什么后面会讲)和0到a的满足条件的个数,再两者相减

    进一步化简,求0到i位数不含4和62的个数

    i=1,求0~9的满足条件的个数

    i=2,求0~99的满足条件的个数

    i=3,求0~999的满足条件的个数

    i=4,求0~9999的满足条件的个数

    ...

    用dp[i][0]表示i位数中幸运数的个数

    用dp[i][1]表示i位数中以2开头的幸运数的个数

    用dp[i][2]表示i位数中非幸运数的个数

    那么,就有以下的递推公式

    dp[i][0]=dp[i-1][0]*9-dp[i-1][1]//表示前i-1位数字中的幸运数前面加上除4以外的0~9的其他数字,共9个,还要减去前i-1位数字中的以2开头的幸运数加上这一位以6开头的数字的个数

    dp[i][1]=dp[i-1][0]//表示前i-1位数字中的幸运数加上这一位的2

    dp[i][2]=dp[i-1][2]*10+dp[i-1][1]+dp[i-1][0]//表示前面已经不合法的数字这一位无论放什么都不合法,所以0~9随便放,加上前i-1位数字中的以2开头的幸运数加上这一位的6,再加上前i-1位数字中的幸运数加上这一位的4的个数

    初始值 dp[0][0]=1,其他均为0

    根据初始值和递推公式,我们就能得到从0到任意i位数字的吉利数字的个数。

    找到0 ~ n 的吉利数字的个数

    我们先求出0 ~ n 之间非吉利数字的个数,用总数减去即可。那,非吉利数字的个数怎么求呢?

    用具体的数字举例来说吧:设 n = 583626

    用digit[10]记录n+1每一位对应的数字,此例中有6位数字(令cnt = 6 表示数字位数),分别是

    digit[6] = 5

    digit[5] = 8

    digit[4] = 3

    digit[3] = 6

    digit[2] = 2

    digit[1] = 7

    digit[0] = 任意数字,占位用的

    用sum记录非吉利数字的个数,初始化为0

    需要一个bool量 flag,记录是否出现了非吉利数字。初始化为false, 未出现。

    我们从数字的最高位起进行判断:digit[6] = 5, 我们求 0 ~ 499999 之间非吉利数的个数。

      首先:加上0 ~ 99999中所有非吉利数字前面添加0~4的任意一个数字的情况 sum += dp[5][2] * digit[6]

      其次:5大于4,故我们要加上 0~99999中所有吉利数字前面添加4的情况 sum += dp[5][0]

    接着,判断第5位digit[5] = 8,即判断500000 ~ 579999 之间的非吉利数字的个数,其实就是判断0 ~ 79999之间的,前面的数字不是6就没有什么用

      首先:加上0 ~ 9999中所有非吉利数字前面添加0~7的任意一个数字的情况 sum += dp[4][2] * digit[5]

      其次:8大于4,故我们要加上 0~9999中所有吉利数字前面添加4的情况 sum += dp[4][0]

      此外:8大于6,故我们要加上0~9999中所有以2开头的吉利数字前添加6的情况 sum += dp[4][1]

    接着,判断第4位digit[4] = 3,即判断580000 ~ 582999 之间的非吉利数字的个数,其实就是判断0 ~ 2999之间的

      首先:加上0 ~ 999中所有非吉利数字前面添加0~2的任意一个数字的情况 sum += dp[3][2] * digit[4]

      其次:2小于4,没有需要特别考虑的

      此外:2小于6,没有需要特别考虑的

    接着,判断第3位digit[3] = 6,即判断583000 ~ 583599 之间的非吉利数字的个数,其实就是判断0 ~ 599之间的

      首先:加上0 ~ 99中所有非吉利数字前面添加0~5的任意一个数字的情况 sum += dp[2][2] * digit[3]

      其次:6大于4,故我们要加上 0~99中所有吉利数字前面添加4的情况 sum += dp[2][0]

    接着,判断第2位digit[2] = 2,即判断583600 ~ 583619 之间的非吉利数字的个数,其实就是判断0 ~ 19之间的,

      首先:加上0 ~ 9中所有非吉利数字前面添加0~1的任意一个数字的情况 sum += dp[1][2] * digit[2]

      其次:2小于4,没有需要特别考虑的

      此外:2小于6,没有需要特别考虑的

      但是,需要注意的是,这里判断的数字出现了62,我们要把flag标识为true。

    最后,判断第1位digit[1] = 7, 判断583620 ~ 583626但是这里flag为true了,表示前面的数字里面已经包含了非吉利数字,所以后面需要把所有的数字情况都加入到非吉利里面。(正是因为每次判断的数字末尾都比该位的数字少1,所以最开始要记录n + 1 的值)

    sum += digit[1] * dp[0][2] + digit[1] * dp[0][0]

     1 #include<bits/stdc++.h>
     2 #define in(i) (i=read())
     3 using namespace std;
     4 int read() {
     5     int ans=0,f=1; char i=getchar();
     6     while(i<'0'||i>'9') {if(i=='-') f=-1; i=getchar();}
     7     while(i>='0'&&i<='9') {ans=(ans<<3)+(ans<<1)+i-'0'; i=getchar();}
     8     return ans*f;
     9 }
    10 int dp[10][3],digit[15];
    11 void init() {
    12     memset(dp,0,sizeof(dp));
    13     dp[0][0]=1;
    14     for(int i=1;i<=8;i++) {
    15         dp[i][0]=dp[i-1][0]*9-dp[i-1][1];
    16         dp[i][1]=dp[i-1][0];
    17         dp[i][2]=dp[i-1][2]*10+dp[i-1][1]+dp[i-1][0];
    18     }
    19 }
    20 int solve(int x)
    21 {
    22     memset(digit,0,sizeof(digit));
    23     int cnt=0,tmp=x;
    24     while(tmp) {
    25         digit[++cnt]=tmp%10;
    26         tmp/=10;
    27     }
    28     digit[cnt+1]=0; int flag=0,ans=0;
    29     for(int i=cnt;i>=1;i--) {
    30         ans+=digit[i]*dp[i-1][2];
    31         if(flag) ans+=digit[i]*dp[i-1][0];
    32         else {
    33             if(digit[i]>4) ans+=dp[i-1][0];
    34             if(digit[i]>6) ans+=dp[i-1][1];
    35             if(digit[i+1]==6 && digit[i]>2) ans+=dp[i][1];
    36         }
    37         if(digit[i]==4 || (digit[i+1]==6 && digit[i]==2)) flag=1;
    38     }
    39     return x-ans;
    40 }
    41 int main()
    42 {
    43     int a,b; init();
    44     while(1) {
    45         in(a);in(b);
    46         if(!a && !b)  break;
    47         cout<<solve(b+1)-solve(a)<<endl;
    48     }
    49     return 0;
    50 }


    最后说那个b+1的情况,我们看到代码中有判断digit[i]>4和digit[i]>6等类似的语句,我们处理第i位时,实际上是处理0~digit[i]-1,即[ 0,digit[i] ),而把digit[i]放到下一次去判断,但我们处理个位时,最后一个是不会去统计的,所以我们把统计的范围+1,即为[ 0,digit[i]+1 )-->[ 0,digit[i] ],所以就有了solve(b+1)-solve(a)这样的语句.(还是高二dalaoNavi-Awson告诉我的,%%%)

    数位dp记忆化搜索写法

     1 #include<bits/stdc++.h>
     2 #define in(i) (i=read())
     3 using namespace std;
     4 typedef long long lol;
     5 lol read() {
     6     lol ans=0,f=1;    
     7     char i=getchar();
     8     while(i<'0'|| i>'9') {if(i=='-') f=-1; i=getchar();}
     9     while(i>='0' && i<='9') {ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();}
    10     return ans*f;
    11 }
    12 lol a,b;
    13 lol dp[19][11];
    14 lol bit[19];
    15 lol dfs(lol len,lol pre,lol limit) {
    16     if(!len) return 1;//如果搜到最后一位了,返回下界值1
    17     if(!limit && dp[len][pre]) return dp[len][pre];//记忆化部分
    18     lol maxn=limit?bit[len]:9;//求出最高可以枚举到哪个数字
    19     lol ans=0;
    20     for(lol i=0;i<=maxn;i++) {
    21         if(i!=4 && !(pre && i==2))//如果这一位不为4并且上一位不为6且这一位不为2
    22             ans+=dfs(len-1,i==6,limit && i==maxn);//满足条件
    23     }
    24     if(!limit) dp[len][pre]=ans;//如果没有限制,代表搜满了,可以记忆化,否则就不能
    25     return ans;
    26 }
    27 lol solve(lol a) {
    28     memset(bit,0,sizeof(bit));
    29     lol k=0;
    30     while(a) {//取出数字的每一位
    31         bit[++k]=a%10;
    32         a/=10;
    33     }
    34     return dfs(k,0,1);
    35 }
    36 int main()
    37 {
    38     //freopen("number.in","r",stdin);
    39     //freopen("number.out","w",stdout);
    40     lol t; in(t);
    41     for(int i=1;i<=t;i++) {
    42         lol a,b; in(a);in(b);
    43         cout<<solve(b)-solve(a-1)<<endl;//差分思想
    44     }
    45     return 0;
    46 }

    应用

    放几道题目上来

    [SCOI2009]windy数 代码

    [HNOI2010]计数 

    [ZJOI2010]数字计数 题解

  • 相关阅读:
    1442. Count Triplets That Can Form Two Arrays of Equal XOR
    1441. Build an Array With Stack Operations
    312. Burst Balloons
    367. Valid Perfect Square
    307. Range Sum Query
    1232. Check If It Is a Straight Line
    993. Cousins in Binary Tree
    1436. Destination City
    476. Number Complement
    383. Ransom Note
  • 原文地址:https://www.cnblogs.com/real-l/p/8540124.html
Copyright © 2011-2022 走看看