• 数位DP 详解


    天堂在左,战士向右

    引言

    数位DP在竞赛中的出现几率极低,但是如果不会数位DP,一旦考到就只能暴力骗分。
    以下是数位DP详解,涉及到的例题有:

    • [HDU2089]不要62
    • [HDU3652]B-number

    概述

    首先我们要理清的是,到底数位DP是什么。
    事实上,一般数位DP的题目题面描述都会有以下内容:

    • 求出一段区间([l,r])中,满足某一特殊条件的数有多少个

    例题1 不要62中,特殊条件是数中不能出现"62";在例题2 B-number中,特殊条件是数中出现了13且该数可以被13整除;

    一般题目中的数据范围([l,r])会使得(O(n))超时。因此,直接遍历将无法拿到全分。而数位DP则是在范围内按位递推出最大值的快捷算法。以例题2 B-number中的数位DP为例:
    图1-1
    足以显示出数位DP的优越性。

    主要实现

    由于DP的本质就是记忆化搜索,我们通过记忆化搜索的方式实现动态规划。这种方式相比正面递推,在大多数情况下要简介一些。

    一、搜索过程

    例题1 不要62为例。
    首先,我们需要定义以下基本的变量与函数,它们是在所有数位DP中通用的:

    int digit[maxn];
    int dfs(int len,int fp,int str){
    }
    

    其中,(digit[i])表示一个数在所有数位都取最大值的情况下,第(i)位的最大值。


    而dfs函数中的(len)表示当前层我们还需处理多少数位。当(len)的值为(-1)时,则代表我们已经处理到了最低位。


    fp则代表当前数位是否受到最高位的限制。举个例子,我们规定在一次运算中(r)的值为(530)。此时我们已经计算到了第2位。若前一位为(5),则这一位最大也只能取(3),否则会超出(r)的限制,此时fp的值为1;若前一位小于(5),则当前位不受最高位的限制,我们可以取任意数字,则此时fp的值为0。


    str则代表当前状态,我们稍后再做解释。


    这时,我们分析题面,发现:

    • 限制条件只有一个,即不能出现62

    因此,我们可以将这一限制条件填入已经设出的状态(str)中。当之前的数位中已经出现了62,我们就使其为1,否则我们使其为0。
    这时,根据这一条件,就可以设出DP数组了

    int dp[maxn][100];//表示在处理到第i位、之前的数位中出现/未出现62使的方案数。
    

    不过此时,我们会发现一个问题:如果上一位出现了6,而是否出现62由当前位决定时,怎么办呢?因此,我们要对(str)的定义稍作更改。
    我们令(str)表示:若上一位出现了(6),则(str=1);若已经出现了(62),则(str=2);否则(str=0)
    此时,我们已经可以通过定义写下判断状态的子函数了。

    int check(int str,int i){
        if(str==0){
            if(i==6)return 1;
            return 0;
        }
        else if(str==6){
            if(i==6)return 1;
            if(i==2)return 2;
            return 0;
        }
        return 2;
    }
    

    回过头来,我们再来继续完成dfs函数。
    首先,写下当我们搜索到最后一位时的返回操作与记忆化搜索的返回操作。

    int digit[maxn];
    int dfs(int len,int fp,int str){
        if(len==-1)return str==2;
        if(!fp && dp[len][str])return dp[len][str];
        //条件中的!fp是对[l,r]取开区间。
    }
    

    接下来我们要做的是判断当前状态下我们能取到的最大数位。

    int fpmax=fp?digit[len]:9;
    

    接着我们再遍历搜索下一个数位,并返回答案。

    int ret=0;//返回值
    for(register int i=0;i<=fpmax;i++){
        ret+=dfs(len-1,fp && i==fpmax ,check(str,i));
    }
    return dp[len][str]=ret;
    

    整个子函数的代码如下:

    int dfs(int len,int fp,int str){
        if(len==-1)return str==2;
        if(!fp && ~dp[len][str])return dp[len][str];
        int fpmax=fp?digit[len]:9,ret=0;
        for(register int i=0;i<=fpmax;i++){
            ret+=dfs(len-1,fp && i==fpmax,check(str,i));
        }
        return dp[len][str][rel]=ret;
    }
    

    例题1 不要62的代码如下:

    #include<bits/stdc++.h>
    #define int long long
    //#define local
    using namespace std;
    int dp[100][200][100],digit[100];
    int check(int str,int i){
        if(str==0){
            if(i==6)return 1;
            return 0;
        }
        else if(str==6){
            if(i==6)return 1;
            if(i==2)return 2;
            return 0;
        }
        return 2;
    }
    int dfs(int len,int fp,int str){
        if(len==-1)return str==2;
        if(!fp && ~dp[len][str])return dp[len][str];
        int fpmax=fp?digit[len]:9,ret=0;
        for(register int i=0;i<=fpmax;i++){
            ret+=dfs(len-1,fp && i==fpmax,check(str,i));
        }
        return dp[len][str][rel]=ret;
    }
    int f(int n){
        int len=0;
        while(n){
            digit[len++]=n%10;
            n/=10;
        }
        return dfs(len-1,1,0);
    }
    signed main(){
    	#ifdef local
    	freopen("1.txt","r",stdin);
    	#endif
    	ios::sync_with_stdio(false);
    	cin.tie(0);
        int a,b;
        memset(dp,-1,sizeof(dp));
        while(cin>>b>>a){
        	//cout<<"-->"<<b<<endl;
            printf("%d
    ",f(b)-f(a-1));
        }
        //cout<<f(1000)<<endl;
        return 0;
    }
    

    注意,在f函数中,可以看到我们首次dfs的代码是

    dfs(len-1,1,0);
    

    为什么(len)的值为总长度-1,而不是总长度本身呢?
    因为这样我们处理到最后时(len=-1)而不是(len=0)
    换句话说,只是笔者的习惯而已233333

    细节-关于状态

    事实上,不是所有时候DP数组都只用开二维。
    在很多时候,我们都要在dfs函数中同时记录我们当前处理到的数是多少,例如例题2 B-number
    在这道题中,我们要处理我们记录的数是否能被13整除,因此我们要对DP数组作一点小小的微调。

    int dp[maxn][5][20];
    

    多出来的一维用于记录计算出来的数对13取模后的值。不记录其本身是因为空间限制,且失去了数位DP的优越性。
    对于dfs中所处理的数的记录,不难想到用这样的方法:

    int dfs(int len,int fp,int str,int rel){
        for(register int i=1;i<=9;i++)dfs(len-1,fp && i==digit[i],check(str,i),rel*10+i);
    }
    

    因此,对于例题2 B-number,我们的完整代码变成了这样:

    #include<bits/stdc++.h>
    #define int long long
    //#define local
    using namespace std;
    int dp[100][200][100],digit[100];
    int check(int str,int i){//返回:0-->什么都没有,1-->已出现过1,2-->已出现过13 
        if(str==0){
            if(i==1)return 1;
            return 0;
        }
        else if(str==1){
            if(i==1)return 1;
            if(i==3)return 2;
            return 0;
        }
        return 2;
    }
    int dfs(int len,int fp,int rel,int str){
    	if(len==-1)return rel==0&&str==2;
        if(!fp && ~dp[len][str][rel])return dp[len][str][rel];
        int fpmax=fp?digit[len]:9,ret=0;
        for(register int i=0;i<=fpmax;i++){
            ret+=dfs(len-1,fp&&i==fpmax,(rel*10+i)%13,check(str,i));
        }
        return fp?ret:dp[len][str][rel]=ret;
    }
    int f(int n){
        int len=0;
        while(n){
            digit[len++]=n%10;
            n/=10;
        }
        return dfs(len-1,1,0,0);
    }
    signed main(){
    	#ifdef local
    	freopen("1.txt","r",stdin);
    	#endif
    	ios::sync_with_stdio(false);
    	cin.tie(0);
        int b;
        memset(dp,-1,sizeof(dp));
        while(cin>>b){
        	//cout<<"-->"<<b<<endl;
            printf("%d
    ",f(b));
        }
        //cout<<f(1000)<<endl;
        return 0;
    }
    
    

    后记

    数位dp虽然大多在套模板,但是里面的判断和细节还是很多的,多写几道数位dp之后才能发现其中的规律,完全将其掌握。

  • 相关阅读:
    SQL实战(四)
    SQL实战(三)
    SQL实战(二)
    数据库SQL实战(一)
    算法(二)——背包问题
    华为机试(五)
    算法(一)
    华为机试练习(四)
    华为往年机试题目(三)
    T分布(T-Distribution)
  • 原文地址:https://www.cnblogs.com/Chen574118090/p/11609706.html
Copyright © 2020-2023  润新知