数位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\n",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\n",f(b));
    }
    //cout<<f(1000)<<endl;
    return 0;
}

后记

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容