數(shù)位DP 詳解

天堂在左虱而,戰(zhàn)士向右

引言

數(shù)位DP在競(jìng)賽中的出現(xiàn)幾率極低痹筛,但是如果不會(huì)數(shù)位DP氨肌,一旦考到就只能暴力騙分鸿秆。
以下是數(shù)位DP詳解,涉及到的例題有:

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

概述

首先我們要理清的是怎囚,到底數(shù)位DP是什么卿叽。
事實(shí)上,一般數(shù)位DP的題目題面描述都會(huì)有以下內(nèi)容:

  • 求出一段區(qū)間[l,r]中恳守,滿足某一特殊條件的數(shù)有多少個(gè)

例題1 不要62中考婴,特殊條件是數(shù)中不能出現(xiàn)"62";在例題2 B-number中催烘,特殊條件是數(shù)中出現(xiàn)了13且該數(shù)可以被13整除沥阱;

一般題目中的數(shù)據(jù)范圍[l,r]會(huì)使得O(n)超時(shí)。因此伊群,直接遍歷將無(wú)法拿到全分考杉。而數(shù)位DP則是在范圍內(nèi)按位遞推出最大值的快捷算法。以例題2 B-number中的數(shù)位DP為例:

圖1-1

足以顯示出數(shù)位DP的優(yōu)越性舰始。

主要實(shí)現(xiàn)

由于DP的本質(zhì)就是記憶化搜索崇棠,我們通過(guò)記憶化搜索的方式實(shí)現(xiàn)動(dòng)態(tài)規(guī)劃。這種方式相比正面遞推丸卷,在大多數(shù)情況下要簡(jiǎn)介一些枕稀。

一、搜索過(guò)程

例題1 不要62為例谜嫉。
首先萎坷,我們需要定義以下基本的變量與函數(shù),它們是在所有數(shù)位DP中通用的:

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

其中沐兰,digit[i]表示一個(gè)數(shù)在所有數(shù)位都取最大值的情況下哆档,第i位的最大值。


而dfs函數(shù)中的len表示當(dāng)前層我們還需處理多少數(shù)位僧鲁。當(dāng)len的值為-1時(shí)虐呻,則代表我們已經(jīng)處理到了最低位。


fp則代表當(dāng)前數(shù)位是否受到最高位的限制寞秃。舉個(gè)例子斟叼,我們規(guī)定在一次運(yùn)算中r的值為530。此時(shí)我們已經(jīng)計(jì)算到了第2位春寿。若前一位為5朗涩,則這一位最大也只能取3,否則會(huì)超出r的限制绑改,此時(shí)fp的值為1谢床;若前一位小于5兄一,則當(dāng)前位不受最高位的限制,我們可以取任意數(shù)字识腿,則此時(shí)fp的值為0出革。


str則代表當(dāng)前狀態(tài),我們稍后再做解釋渡讼。


這時(shí)骂束,我們分析題面,發(fā)現(xiàn):

  • 限制條件只有一個(gè)成箫,即不能出現(xiàn)62

因此展箱,我們可以將這一限制條件填入已經(jīng)設(shè)出的狀態(tài)str中。當(dāng)之前的數(shù)位中已經(jīng)出現(xiàn)了62蹬昌,我們就使其為1混驰,否則我們使其為0。
這時(shí)皂贩,根據(jù)這一條件栖榨,就可以設(shè)出DP數(shù)組了

int dp[maxn][100];//表示在處理到第i位、之前的數(shù)位中出現(xiàn)/未出現(xiàn)62使的方案數(shù)先紫。

不過(guò)此時(shí)治泥,我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題:如果上一位出現(xiàn)了6,而是否出現(xiàn)62由當(dāng)前位決定時(shí)遮精,怎么辦呢居夹?因此,我們要對(duì)str的定義稍作更改本冲。
我們令str表示:若上一位出現(xiàn)了6准脂,則str=1;若已經(jīng)出現(xiàn)了62,則str=2檬洞;否則str=0狸膏。
此時(shí),我們已經(jīng)可以通過(guò)定義寫下判斷狀態(tài)的子函數(shù)了添怔。

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;
}

回過(guò)頭來(lái)湾戳,我們?cè)賮?lái)繼續(xù)完成dfs函數(shù)。
首先广料,寫下當(dāng)我們搜索到最后一位時(shí)的返回操作與記憶化搜索的返回操作砾脑。

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是對(duì)[l,r]取開區(qū)間。
}

接下來(lái)我們要做的是判斷當(dāng)前狀態(tài)下我們能取到的最大數(shù)位艾杏。

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

接著我們?cè)俦闅v搜索下一個(gè)數(shù)位韧衣,并返回答案。

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;

整個(gè)子函數(shù)的代碼如下:

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函數(shù)中畅铭,可以看到我們首次dfs的代碼是

dfs(len-1,1,0);

為什么len的值為總長(zhǎng)度-1氏淑,而不是總長(zhǎng)度本身呢?
因?yàn)檫@樣我們處理到最后時(shí)len=-1而不是len=0
換句話說(shuō)硕噩,只是筆者的習(xí)慣而已233333

細(xì)節(jié)-關(guān)于狀態(tài)

事實(shí)上假残,不是所有時(shí)候DP數(shù)組都只用開二維。
在很多時(shí)候榴徐,我們都要在dfs函數(shù)中同時(shí)記錄我們當(dāng)前處理到的數(shù)是多少守问,例如例題2 B-number匀归。
在這道題中坑资,我們要處理我們記錄的數(shù)是否能被13整除,因此我們要對(duì)DP數(shù)組作一點(diǎn)小小的微調(diào)穆端。

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

多出來(lái)的一維用于記錄計(jì)算出來(lái)的數(shù)對(duì)13取模后的值袱贮。不記錄其本身是因?yàn)榭臻g限制,且失去了數(shù)位DP的優(yōu)越性体啰。
對(duì)于dfs中所處理的數(shù)的記錄攒巍,不難想到用這樣的方法:

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);
}

因此,對(duì)于例題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-->什么都沒(méi)有柒莉,1-->已出現(xiàn)過(guò)1,2-->已出現(xiàn)過(guò)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;
}

后記

數(shù)位dp雖然大多在套模板沽翔,但是里面的判斷和細(xì)節(jié)還是很多的兢孝,多寫幾道數(shù)位dp之后才能發(fā)現(xiàn)其中的規(guī)律,完全將其掌握仅偎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末跨蟹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子橘沥,更是在濱河造成了極大的恐慌窗轩,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件座咆,死亡現(xiàn)場(chǎng)離奇詭異痢艺,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)介陶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門堤舒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人斤蔓,你說(shuō)我怎么就攤上這事植酥。” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵友驮,是天一觀的道長(zhǎng)漂羊。 經(jīng)常有香客問(wèn)我,道長(zhǎng)卸留,這世上最難降的妖魔是什么走越? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮耻瑟,結(jié)果婚禮上旨指,老公的妹妹穿的比我還像新娘。我一直安慰自己喳整,他們只是感情好谆构,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著框都,像睡著了一般搬素。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上魏保,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天熬尺,我揣著相機(jī)與錄音,去河邊找鬼谓罗。 笑死粱哼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的檩咱。 我是一名探鬼主播揭措,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼税手!你這毒婦竟也來(lái)了蜂筹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤芦倒,失蹤者是張志新(化名)和其女友劉穎艺挪,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兵扬,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡麻裳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了器钟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片津坑。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖傲霸,靈堂內(nèi)的尸體忽然破棺而出疆瑰,到底是詐尸還是另有隱情眉反,我是刑警寧澤,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布穆役,位于F島的核電站寸五,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耿币。R本人自食惡果不足惜梳杏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望淹接。 院中可真熱鬧十性,春花似錦、人聲如沸塑悼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)拢肆。三九已至减响,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間郭怪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工刊橘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鄙才,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓促绵,卻偏偏與公主長(zhǎng)得像攒庵,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子败晴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容