數字dp 簡單入門

2022-05-01 11:12:09 字數 3528 閱讀 1078

推薦部落格

推薦部落格

數字dp是一種計數用的dp,一般就是要統計乙個區間[le,ri]內滿足一些條件數的個數。所謂數字dp,字面意思就是在數字上進行dp咯。數字還算是比較好聽的名字,數字的含義:乙個數有個位、十位、百位、千位......數的每一位就是數字啦!

之所以要引入數字的概念完全就是為了dp。數字dp的實質就是換一種暴力列舉的方式,使得新的列舉方式滿足dp的性質,然後記憶化就可以了。

兩種不同的列舉:對於乙個求區間[le,ri]滿足條件數的個數,最簡單的暴力如下:

for(int i=le;i<=ri;i++)

if(right(i)) ans++;

然而這樣列舉不方便記憶化,或者說根本無狀態可言。

新的列舉:控制上界列舉,從最高位開始往下列舉,例如:ri=213,那麼我們從百位開始列舉:百位可能的情況有0,1,2(覺得這裡列舉0有問題的繼續看)

然後每一位列舉都不能讓列舉的這個數超過上界213(下界就是0或者1,這個次要),當百位列舉了1,那麼十位列舉就是從0到9,因為百位1已經比上界2小了,後面數字列舉什麼都不可能超過上界。所以問題就在於:當高位列舉剛好達到上界是,那麼緊接著的一位列舉就有上界限制了。具體的這裡如果百位列舉了2,那麼十位的列舉情況就是0到1,如果前兩位列舉了21,最後一位之是0到3(這一點正好對於**模板裡的乙個變數limit 專門用來判斷列舉範圍)。最後乙個問題:最高位列舉0:百位列舉0,相當於此時我列舉的這個數最多是兩位數,如果十位繼續列舉0,那麼我列舉的就是以為數咯,因為我們要列舉的是小於等於ri的所以數,當然不能少了位數比ri小的咯!(這樣列舉是為了無遺漏的列舉,不過可能會帶來乙個問題,就是前導零的問題,模板裡用lead變數表示,不過這個不是每個題目都是會有影響的,可能前導零不會影響我們計數,具體要看題目)

由於這種新的列舉只控制了上界所以我們的main函式總是這樣:

int

main()

w_w 是吧!統計[1,ri]數量和[1,le-1],然後相減就是區間[le,ri]的數量了,這裡我寫的下界是1,其實0也行,反正相減後就沒了,注意題目中le的範圍都是大於等於1的(不然le=0,再減一就g_g了)

在講例題之前先講個基本的動態模板(先看後面的例題也行):dp思想,列舉到當前位置pos,狀態為state(這個就是根據題目來的,可能很多,畢竟dp千變萬化)的數量(既然是計數,dp值顯然是儲存滿足條件數的個數)

typedef long

long

ll;int a[20

];ll dp[

20][state];//

不同題目狀態不同

ll dfs(int pos,/*

state變數

*/,bool lead/*

前導零*/,bool limit/*

數字上界變數

*/)//

不是每個題都要判斷前導零

//計算完,記錄狀態

if(!limit && !lead) dp[pos][state]=ans;

/*這裡對應上面的記憶化,在一定條件下時記錄,保證一致性,當然如果約束條件不需要考慮lead,這裡就是lead就完全不用考慮了

*/return

ans;

}ll solve(ll x)

return dfs(pos-1

/*從最高位開始列舉

*/,/*

一系列狀態

*/,true,true);//

剛開始最高位都是有限制並且有前導零的,顯然比最高位還要高的一位視為0嘛

}int

main()

}

相信讀者還對這個有不少疑問,筆者認為有必要講一下記憶化為什麼是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;(個位只有是1的時候是不滿足的),這個狀態記錄下來,繼續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的優化。

前導零的判斷

int dfs(bool

zero)

......

ans+=dfs(zero&&i==0)//

不區分前導零與零

ans+=dfs(zero&&i==0&&l>1)//

區分前導零與零

hdu 2089 不要62

dp[pos][0]:前一位不是6的,所有長度為pos的數(沒有上界限制)中,不含4不含62的數,共有幾個。

dp[pos][1]:前一位是6的,所有長度為pos的數(沒有上界限制)中,不含4不含62的數,共有幾個。

#include#include

#include

#include

using

namespace

std;

typedef

long

long

ll;int a[20

];int dp[20][2

];int dfs(int pos,int pre,int sta,bool

limit)

if(!limit) dp[pos][sta]=tmp;

return

tmp;

}int solve(int

x)

return dfs(pos-1,-1,0,true);}

intmain()

return0;

}

數字DP入門 數字DP模板

數字dp是一種計數用的dp,一般就是要統計乙個區間 le,ri 內滿足一些條件數的個數。所謂數字dp,字面意思就是在數字上進行dp咯。數字還算是比較好聽的名字,數字的含義 乙個數有個位 十位 百位 千位.數的每一位就是數字啦!之所以要引入數字的概念完全就是為了dp。數字dp的實質就是換一種暴力列舉的...

數字DP入門

數字dp顧名思義就是對數字運用dp思想,將每位數看成下一位的子狀態,即個位看成十位的子狀態,十位看成百位的子狀態。下面給出狀態方程 dp i j dp i 1 k k 0,1,2,3.9 dp i j 表示i位數,首位是j的數字有多少符合要求的,我們從低位逐步遞推求到高位。下面拿一道入門題來具體說明...

數字dp入門

數字dp,顧名思義 對數的位數進行操作。一般 題目會與數的位數相關。數字dp最重要的是其dp陣列的確定,根據需要 可以確定多維dp陣列,一般dp陣列的每一對下標 都只能唯一確定乙個狀態,或者 對結果並不影響。dp陣列一定會有一維表示 當前列舉到的 位數。模版 如 此處設立的dp陣列為二維 int d...