数位DP入门

 

数位DP模板:

dp思想,枚举到当前位置$pos$,状态为$state$(这个就是根据题目来的,可能很多,毕竟dp千变万化)的数量(既然是计数,dp值显然是保存满足条件数的个数)

 

int dfs(int pos,bool state,bool lead,bool limit) { //dfs搜到了哪一位,状态是什么,前导0,上限(这一位的限制),即这个位置0~9还是0~shu[pos]
    if(pos == 0) //递归边界,既然是按位枚举,最低位是1,那么pos==0说明这个数我枚举完了
        return 1;
    /*这里一般返回1,表示你枚举的这个数是合法的,
    那么这里就需要你在枚举时必须每一位都要满足题目条件,
    也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。
    不过具体题目不同或者写法不同的话不一定要返回1 */

    //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
    if(!limit&&!lead&&dp[pos][state])
        return dp[pos][state];
    /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/

    int cnt=0,maxx=(limit?shu[pos]:9);//根据limit判断枚举的上界maxx;这个的例子前面用213讲过了
    for(int i=0; i<=maxx; i++) { //枚举,然后把不同情况的个数加到ans就可以了
        if()
            continue;
        else if ...
        
        cnt+=dfs(pos-1,/*状态转移*/,lead&&i==0,,limit&&i==shu[pos]);//最后两个变量传参都是这样写的
        /*这里还算比较灵活,不过做几个题就觉得这里也是套路了
        大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
        去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
        要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
        前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
    }
    //计算完,记录状态
    if(!limit && !lead) dp[pos][state]=ans;
    /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
    return ans;
}

int slove(int x) {
    memset(shu,0,sizeof(shu));
    int k=0;
    while(x) {
        shu[++k]=x%10;//保存a,b每一位的数
        x/=10;
    }
    return dfs(k,/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}

 

有关以上代码的几个疑问:

记忆化为什么是$if(!limit)$才行,大致就是说有无$limit$会出现状态冲突,举例:

约束:数位上不能出现连续的两个1(11、112、211都是不合法的)

假设就是[1,210]这个区间的个数

状态:$dp[pos][pre]$:当前枚举到$pos$位,前面一位枚举的是$pre$(更加前面的位已经合法了)的个数(我的pos从0开始)

先看错误的方法计数,就是不判$limit$就是直接记忆化

那么假设我们第一次枚举了百位是0,显然后面的枚举$limit=false$,也就是数位上0到9的枚举,然后当我十位枚举了1,此时考虑$dp[0][1]$,就是枚举到个位,前一位是1的个数,显然$dp[0][1]=9$;(个位只有是0的时候是不满足的),这个状态记录下来,继续dfs,一直到百位枚举了2,十位枚举了1,显然此时递归到了$pos=0,pre=1$的层,而$dp[0][1]$的状态已经有了即$dp[pos][pre]!=-1$;此时程序直接return$dp[0][1]$了,然而显然是错的,因为此时是有$limit$的个位只能枚举0,根本没有9个数,这就是状态冲突了。有$lead$的时候可能出现冲突,这只是两个最基本的不同的题目可能还要加限制,反正宗旨都是让dp状态唯一。

对于这个错误说两点:一是$limit$为$true$的数并不多,一个个枚举不会很浪费时间,所以我们记录下$! limit$的状态解决了不少子问题重叠。第二,有人可能想到把$dp$状态改一下$dp[pos][state][limit]$就是分别记录不同$limit$下的个数,这种方法一般是对的,关于这个具体会讲,下面有题bzoj3209会用到这个。

数位的部分就是这些,然后就是难点,dp部分,dp大牛的艺术,弱鸡只能看看+...+

既然从高位往低位枚举,那么状态一般都是与前面已经枚举的数位有关并且通常是根据约束条件当前枚举的这一位能使得状态完整(比如一个状态涉及到连续k位,那么就保存前k-1的状态,当前枚举的第k个是个恰好凑成成一个完整的状态,不过像那种状态是数位的和就直接保存前缀和为状态了),不过必然有一位最简单的一个状态dp[pos]当前枚举到了pos位。dp部分就要开始讲例题了,不过会介绍几种常用防$TLE$的优化。

 

$Fighting$(实战)

例一:HDU 2089 不要62

入门题。就是数位上不能有4也不能有连续的62,没有4的话在枚举的时候判断一下,不枚举4就可以保证状态合法了,所以这个约束没有记忆化的必要,而对于62的话,涉及到两位,当前一位是6或者不是6这两种不同情况我计数是不相同的,所以要用状态来记录不同的方案数。
$dp[pos][sta]$表示当前第$pos$位,前一位是否是6的状态,这里sta只需要去0和1两种状态就可以了,不是6的情况可视为同种,不会影响计数。
 
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>

#define N 10101
using namespace std;
typedef long long LL;

int a,b;
int shu[20],dp[20][2];


int dfs(int pos,bool state,bool limit){
    if(!pos) return 1;
    if(!limit&&dp[pos][state]) return dp[pos][state];
    int cnt=0,up=(limit?shu[pos]:9);
    for(int i=0;i<=up;i++){
        if(state&&i==2) continue;
        if(i==4) continue;
        cnt+=dfs(pos-1,i==6,limit&&i==shu[pos]); 
    }
    if(!limit) dp[pos][state]=cnt;
    return cnt;
}

int slove(int x){
    int k=0;
    while(x){
        shu[++k]=x%10;
        x/=10;
    }
    return dfs(k,false,true);
}

int main() {
    while(scanf("%d%d",&a,&b)==2){
        if(a==0&&b==0) break;
        printf("%d\n",slove(b)-slove(a-1));
    }
    return 0;
}

 

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>

#define N 10101
using namespace std;
typedef long long LL;

int a,b;
int shu[20],dp[20][2];


int dfs(int pos,bool sta,bool limit){
    if(!pos) return 1;
    if(!limit&&dp[pos][sta]) return dp[pos][sta];
    int cnt=0,up=limit?shu[pos]:9;
    for(int i=0;i<=up;i++){
        if(sta&&i==2) continue;
        if(i==4) continue;
        cnt+=dfs(pos-1,i==6,limit&&i==shu[pos]);
    }
    return limit ? cnt : dp[pos][sta] =cnt;
}

int slove(int x){
    int k=0;
    while(x){
        shu[++k]=x%10;
        x/=10;
    }
    return dfs(k,false,true);
}

int main() {
    while(scanf("%d%d",&a,&b)==2){
        if(a==0&&b==0) break;
        printf("%d\n",slove(b)-slove(a-1));
    }
    return 0;
}
略微修改

 

 
 
 

第二:相减。

例题:HDU 4734

 
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<cmath>

using namespace std;

int T,a,b,dp[20][5000],shu[20],pw[20],all,k;

int dfs(int pos,int sum,bool limit){
    if(pos==0) return sum<=all;
    if(sum>all) return 0;
    if(!limit&&dp[pos][all-sum]) return dp[pos][all-sum];
    int cnt=0,up= limit ? shu[pos] : 9;
    for(int i=0;i<=up;i++){
        cnt+=dfs(pos-1,sum+i*pw[pos],limit&&i==shu[pos]);//!!
    }
    return limit ? cnt : dp[pos][all-sum]=cnt;
}

int slove(int x){
    k=0;
    while(x){
        shu[++k]=x%10;
        x/=10;
    }
    return dfs(k,0,true);
}

int calc(int x){
    int an=0;
    for(int i=1;x;i++){
        an+=pw[i]*(x%10);
        x/=10;
    }
    return an;
}

int main()
{
    pw[1]=1;
    for(int i=2;i<=15;i++) pw[i]=pw[i-1]*2;
    scanf("%d",&T);
    for(int i=1;i<=T;i++){
        scanf("%d%d",&a,&b);
        all=calc(a);
        printf("Case #%d: %d\n",i,slove(b));
    }
    return 0;
}

 

 
 

借鉴博客1 借鉴博客2 借鉴博客3 借鉴博客4 借鉴博客5

posted @ 2018-09-06 07:48  清风我已逝  阅读(196)  评论(0编辑  收藏  举报