數位DP學習小結

1、學習心得體會

問題描述:ios

通常體現爲,定義某種性質K,問某區間內具備K性質的數的個數算法

每每給的區間會很大,對區間內的每一個數進行判斷顯然會超時數組

因而數位DP登場學習

數位DP,顧名思義,是對數字的每一位進行DPspa

心得體會:.net

1.數位DP須要較爲熟練的記憶化搜索做爲基礎,雖然有的題能夠直接用循環進行狀態轉移,但記憶化搜索的狀態轉移更經常使用更容易理解code

2.時刻記住:abcd這個四位數 = a*1000+b*100+c*10+dblog

3.關於dp狀態的保存,通常一維都是保存數字在整個數中的位置(數位),而後根據題目給定的數字的性質肯定二維三維要保存什麼狀態遞歸

感受數位DP的關鍵就在於狀態的保存(固然狀態轉移也很重要)通常而言,dp[i][j][k]表達的信息是:具備i性質,j性質,k性質的數的個數字符串

4.在記憶化搜索的過程當中,對於數字abcd,搜索是從左往右搜索,但實際上計算是從右往左計算的(遞歸原理)

5.數位DP須要注意所搜索的範圍不能大於本來被視做字符串的那個數字(這個在記憶化搜索中常表現爲一個變量標記上界)

同時有時候0的狀況也須要特別注意(具體狀況具體分析吧)

6.數位DP剛剛入門的時候以爲很難,作多了以爲萬變不離其宗,代碼長得都差很少,都是套路,惟一的變化也就是定義的數的屬性不同

因此感受數位DP的難點也就是狀態的保存,針對題目給的性質保存相應的狀態

7.剛開始寫數位DP的時候用循環寫狀態轉移,感受理解起來沒有記憶化搜索清晰,因而後面都是用記憶化搜索寫的了

8.理論再強大,理解再深入,不如多作題,作第一道題的時候感受似懂非懂,後面作着作着也就漸漸理解了,書讀百遍其義自見也是這個理

2、從具體題目中體會

玩了一個數位DP的專題:打開專題

終於作完了數位DP的專題,不過也只是初窺門檻,之後還要繼續努力^_^

總體看起來,代碼其實大體都一個套路,都是一個dfs 記憶化搜索,一個cal計算

數位DP通常都是用數組的維度保存數的性質,而後dp的值表示具備這樣性質的數的個數

我的以爲記憶化搜索比迭代好理解代碼看着也舒服,全部除了第一次寫的D題,其餘都是用記憶化寫的

而後說說這個專題,CD兩題算是基礎題,G題定義的數字的性質蠻新穎,可是想到怎麼保存狀態也是基礎題

H題在基礎上加了倍數的判斷

AB兩題在狀態的保存上轉了點彎,特別是B題還結合了狀態壓縮和最長上升子序列

F題比較特別,求的不是數的個數,而是全部知足性質的數的平方和

E題也是數位,不過保存的是二進制數的每一位

(專題總體寫下來收穫蠻大的^_^)

A - Beautiful numbers 

題意:

定義beautiful 數:這個數能被它的每一位整除

例如12 能被12整除,故12beautiful

求區間[l,r]內的beautiful數的個數

分析:

利用記憶化搜索把小於等於num 的數中的全部beautiful數都搜出來

怎麼搜呢?仍是按照數字「位」來搜。

如今問題是:

1.怎麼判斷beautiful數?

2.怎麼保存狀態?

顯然,須要將beautiful數的性質用狀態保存下來。

beautiful數須要整除它全部的非零位

那麼它只需整除它全部位的最小公倍數便可

數字1~9的最小公倍數爲2520(設爲mxlcm

考慮這樣一種保存狀態的方法:

dp[i][j][k]表示長度爲 i,全部數位的lcmjmxlcmk的答案

那麼須要開一個dp[20][2520][2520]的數組,類型是long long

這數組顯然是開不下的,要想辦法壓縮

對這個數組的第二維,「全部數位的lcmj」,其實j的取值雖然可能達到2520

可是j實際的數最多隻有50

因而能夠考慮開一個hs數組,hs[j] = id;表示給全部數位的lcmj的編號爲id

這樣一個dp[20][50][2520]的數組保存狀態,那麼萬事俱備能夠搜索了。

搜索的具體方法在代碼中介紹

代碼:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
/*
    若一個數能整除它的全部的非零數位,
    那麼至關於它能整除個位數的最小公倍數。
    所以記憶化搜索中的參數除了len(當前位)和up(是否達到上界),
    有一個prelcm表示前面的數的最小公倍數,
    判斷這個數是不是Beautiful Numbers,還要有一個參數表示前面數,
    可是這個數太大,須要縮小它的範圍。
    縮小前面組成的數的範圍:
    能夠發現全部個位數的最小公倍數是2520,假設當前的Beautiful Numbers是x,
    那麼 x % lcm{dig[i]} = 0,
    又 2520%lcm{dig[i]} = 0,
    那麼x%2520%lcm{ dig[i] } = 0,x範圍由9*10^18變爲2520。

*/
ll gcd(ll a,ll b)
{
    return b?gcd(b,a%b):a;
}
ll lcm(ll a,ll b)
{
    return a/gcd(a,b)*b;
}
const int mxlcm = 2520;// 0~9全部數字的 lcm
int  dig[25];//保存數字的每一位
ll dp[25][50][mxlcm+5];//dp[i][j][k]表示長度爲 i 全部數字lcm爲j  %mxlcm 爲 k(即餘數爲k)的數的個數
int  hs[mxlcm+5];//hs[i]表示全部數位的lcm 爲 i的數的編號
/*
    關於上界 up 的做用:
    好比數字是 543
    那麼當搜到 5 的時候
    枚舉 0~5 其中枚舉 4 的時候能夠去搜49九、
    可是枚舉到 5 的時候 599 是大於543的
    因此這裏用 up 控制搜索到的數字在 cal計算的數字範圍內
*/
ll dfs(int len,int plcm,int pnum,bool up)//記憶化搜索
{     //當前位      前面數字的lcm  前面的數   是否達到上界
    if (len == 0) return pnum%plcm == 0;//整個數都搜完了,即搜到個位,只需單獨判斷個位便可
    if (!up&&dp[len][hs[plcm]][pnum]!=-1) return dp[len][hs[plcm]][pnum];
    int n = 9;
    if (up)//到界了
    {
        n = dig[len];//達到上界的時候是dig[len],其餘時候是9
    }
    ll ans = 0;
    for (int i = 0;i <= n;++i)//枚舉這一位可能的數字
    {
        int nnum = (pnum*10 + i)%mxlcm;
        int nlcm = plcm;
        if (i) nlcm = lcm(plcm,i);
        ans += dfs(len-1,nlcm,nnum,up&&(i == n));//全部的可能加起來
    }
    if (!up) dp[len][hs[plcm]][pnum] = ans;
    return ans;
}
ll cal(ll x)//計算[1,num]中beautifun的個數
{
    int len = 0;//將數字的每一位保存在數組dig中
    while (x)
    {
        dig[++len] =x%10;
        x/=10;
    }
    return dfs(len,1,0,1);//傳參
}
void init()//hash預處理
{
    int id = 0;mem(dp,-1);//記憶化dp只初始化一次便可
    for (int i = 1;i <= mxlcm;++i)
    {
        if (mxlcm%i == 0) hs[i] = ++id;
    }
}
int main()
{
    int T;init();
    scanf("%d",&T);
    while (T--)
    {
        ll l,r;
        scanf("%I64d %I64d",&l,&r);
        printf("%I64d\n",cal(r) - cal(l-1));
    }
    return 0;
}

B - XHXJ's LIS

題意:

當把數當字符串看的時候,求區間[l,r]最長公共子序列的長度爲K的數的個數

我我的以爲這題出的很好,將數位DP,狀態壓縮和最長公共子序列的nlogn算法結合起來

另寫了題解: HDU 4352 XHXJ's LIS(數位DP+狀壓)


C - 不要62

題意:

區間[l,r]內數字的數位不含62且不含4的數的個數

分析:

這題數據小,能夠水過,用dp[i]表示前i個數中知足的數的個數

if ok(i) dp[i] = dp[i-1] + 1  else dp[i] = dp[i-1]  先求出全部dp,而後直接輸入輸出

用正常的數位DP的方法寫的話:

     狀態保存:
    dp[len][0]表示長度爲len 且不含4和62,最高位不是2的個數
    dp[len][1]表示長度爲len 且不含4和62,最高位是2的個數
    狀態轉移:
    dp[i][0] = 8*dp[i-1][0] + dp[i-1][1] (除去4還有8種可能)
    dp[i][1] = 7*dp[i-1][1] + dp[i-1][1] (除去4還要除去6,不然會構成62)

正常的循環能夠寫,不過感受記憶化搜索更好理解寫着更清晰,故而用記憶化寫的

代碼:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
/*
    狀態保存:
    dp[len][0]表示長度爲len 且不含4和62,最高位不是2的個數
    dp[len][1]表示長度爲len 且不含4和62,最高位是2的個數
    狀態轉移:
    dp[i][0] = 8*dp[i-1][0] + dp[i-1][1] (除去4還有8種可能)
    dp[i][1] = 7*dp[i-1][1] + dp[i-1][1] (除去4還要除去6,不然會構成62)

*/
int dp[10][2];
int dig[10];//保存數字的每一位
int dfs(int len,bool is6,bool up)//當前搜的位,這一位的前一位是否是6,是否爲上界
{
    if (len == 0) return 1; //dp[0][0] = 1;
    if (!up&&dp[len][is6]!=-1) return dp[len][is6];
    int ans = 0;
    int n = 9;if (up) n = dig[len];
    for (int i = 0;i <= n;++i)
    {
        if (i==4) continue;// 4
        if (is6&&i==2) continue;// 62
        ans += dfs(len-1,i==6,up&&(i == n));
    }
    if (!up) dp[len][is6] = ans;
    return ans;
}
int cal(int x)
{
    int len = 0;
    while (x)
    {
        dig[++len] = x%10;
        x/=10;
    }
    return dfs(len,0,1);
}
int main()
{
    mem(dp,-1);
    int l,r;
    while (scanf("%d %d",&l,&r)&&(l||r))
    {
        printf("%d\n",cal(r)-cal(l-1));
    }
    return 0;
}

D - Bomb

本渣的第一道數位DP題,對着別人的題解啃了半天,用循環來進行狀態轉移

(後來深入體會到記憶化搜索寫着更清晰更好理解!)

題意:

區間[1,r]中數位不含49的數的個數(感受數位DP的題意都是一個調調)

分析:

    dp[i][0] 表示長度爲 i 的數中 不含 49 的數的個數
    dp[i][1] 表示長度爲 i 的數中 不含 49 但最高位爲 9 的數的個數
    dp[i][2] 表示長度爲 i 的數中 含49的數的個數

        dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1];
        dp[i][1] = dp[i-1][0];
        dp[i][2] = dp[i-1][2] * 10 + dp[i-1][1];

先把表打好,而後對於具體輸入的r具體處理

代碼1(循環):

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N = 20;
ll dp[N][3];
void init()
{
    dp[0][0] = 1;
    for (int i = 1;i <= N;++i)
    {
        dp[i][0] = dp[i-1][0] * 10 - dp[i-1][1];
        dp[i][1] = dp[i-1][0];
        dp[i][2] = dp[i-1][2] * 10 + dp[i-1][1];
    }
}
int len;
ll s[30];
void cal(ll x)
{
    len = 0;
    while (x)
    {
        s[++len] = x%10;
        x/=10;
    }
}
int main()
{
    int T;scanf("%d",&T);
    init();
    while (T--)
    {

        ll n;
        scanf("%I64d",&n);
        ++n;cal(n);
        int lr = 0;ll sun = 0;bool fd = 0;
        for (int i = len;i >= 1;--i)
        {
            sun += (ll)(s[i])*dp[i-1][2];
            if (fd) sun += (ll)(s[i])*(dp[i-1][0]);
            if (!fd&&s[i] > 4)
            {
                sun += dp[i-1][1];
            }
            if (lr == 4&&s[i] == 9) fd = 1;
            lr = s[i];
        }
        printf("%I64d\n",sun);
    }
    return 0;
}

代碼2(記憶化搜索):

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
#define Sint(n) scanf("%d",&n)
#define Sll(n) scanf("%I64d",&n)
#define Schar(n) scanf("%c",&n)
#define Sint2(x,y) scanf("%d %d",&x,&y)
#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)
#define Pint(x) printf("%d",x)
#define Pllc(x,c) printf("%I64d%c",x,c)
#define Pintc(x,c) printf("%d%c",x,c)
using namespace std;
typedef long long ll;
/*
	記憶化絕對比循環好理解,我發誓~~~^_^ 
	dp[len][four][have]表示正在處理的位長度爲len
	正在處理的前面一位是否是4
	已經處理過的位裏面是否是已經有49了 
*/
ll dp[22][2][2];
int dig[22];
ll dfs(int len,bool four,bool have,bool up)
{
	if (len == 0) return have;
	ll &ot = dp[len][four][have];
	if (!up&&~ot) return ot;
	int n = 9;if (up) n = dig[len];
	ll ans = 0;
	for (int i = 0;i <= n;++i)
	{
		bool newhave = have;
		if (four&&i == 9) newhave = 1;
		ans += dfs(len-1,i==4,newhave,up&&i==n);
	}
	if (!up) ot = ans;
	return ans;
} 
ll cal(ll x)
{
	int len = 0;
	while (x)
	{
		dig[++len] = x%10;
		x/=10;
	}
	return dfs(len,0,0,1);
}
int main()
{
	mem(dp,-1);
    int T;Sint(T);
    while (T--)
    {
    	ll n;Sll(n);
    	Pllc(cal(n),'\n');
	}
    return 0;
}


E - Round Numbers

題意:求區間[l,r]內二進制數中0比1多的數的個數

分析:以前作的都是十進制數的數位DP,這個能夠轉換成二進制數的數位DP(花神的數論題也是二進制上的數位DP)

dp[i][j][k]中i,j,k分別表示長度,0的個數,1的個數,記憶化搜索的時候多傳的兩個參數分別表示是否到達上界,前一位是否爲0

代碼:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
int dig[70];
ll dp[70][70][70];//長度 0的個數  1的個數 
ll dfs(int len,int zero ,int one,bool up,bool z)
{
	if (len == 0)
	{
		return z||(zero>=one);
	}
	ll &ot = dp[len][zero][one];
	if (!up&&!z&&ot!=-1) return ot;
	int n = 1;if (up) n = dig[len];
	ll ans = 0;
	for (int i = 0;i <= n;++i)
	{
		if (z&&i==0) ans += dfs(len-1,0,0,up&&(i==n),z&&(i==0));
		else ans += dfs(len-1,zero + (!i),one+i,up&&(i==n),z&&(i==0)); 
	} 
	if (!up&&!z) ot = ans;
	return ans;
} 
ll cal(ll x)
{
	int len = 0;
	while (x)
	{
		dig[++len] = x%2;
		x/=2;
	}
	return dfs(len,0,0,1,1);
}
int main()
{
    ll l,r;mem(dp,-1);
    while (scanf("%lld %lld",&l,&r) == 2)
    {
    	printf("%lld\n",cal(r)-cal(l-1));
	}
    return 0;
}



F - 吉哥系列故事――恨7不成妻

這個題目不一樣於通常的數位DP求區間內具備某性質的數的個數,而是求區間內具備某性質的全部數的平方和

感受蠻厲害的樣子,另寫了題解: HDU 4507 吉哥系列故事——恨7不成妻(數位DP)


G - Balanced Number

題意:

求區間[l,r]內的平衡數的個數

所謂平衡數是指,把這個數的某一數位設置爲支點。支點左右兩邊按|i - p|*dig計算力矩,若是能找到支點使左右力矩相等就是平衡數

例如4139    取3爲支點,左邊力矩 = 4*2+1*1  = 9,右邊力矩 = 9*1 = 9全部是平衡數

分析:

仍是根據數的性質保存狀態,顯然數的性質涉及到力矩,支點的位置

因此dp[i][j][k] :

                                          i : 正在處理的數位
                                          j : 支點的位置
                                         k : 左右力矩之和(正負算,爲 0 就是平衡的)
                                        dp[i][j][k]就是具備上述性質的數的個數

    dp[i][j][k] = dp[i-1][j][ k + dig*(i-j)]
    其中 dig 爲數位 i 處枚舉的可能的數字

    狀態轉移中j沒有轉移?支點的位置須要另外枚舉

枚舉支點位置?會不會出現算某個位置的時候算了一遍數字x,算另外一個位置的時候又算了一遍x這樣算重複的狀況?

想一下,一個數若是是平衡數,那麼它的支點位置必然是固定的(0除外)

因此不會算重複,只有最後把重複的0減去就好,0算了len遍,故重複的0有(len-1)個

這裏提一個事,這題的dp[i][j][k]保存了3個維度的信息,可是實際上,它在狀態轉移的時候第二維的j並無改變

看代碼裏面的dfs的過程也是,傳入的一個參數p歷來都沒有變過,爲何不省去這一維呢?

首先,dfs的時候確實能夠不傳參數p,直接讓其在全局裏面,而後枚舉位置的時候改變其值,每次遞歸的時候能夠直接用

可是dp仍是應該保存這一維,由於題目是多組數據,秉着算過就記錄下來的原理,能夠爲後面更多組數節約時間

前面B題裏面也有一維沒有參與狀態轉移,可是依舊保存下來了是一樣的原理(B題的dfs沒有傳那個不變的參數K)

代碼:

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
/*
    dp[i][j][k] :
                i : 正在處理的數位
                j : 支點的位置
                k : 左右力矩之和(正負算,爲 0 就是平衡的)
                dp[i][j][k]就是具備上述性質的數的個數

    dp[i][j][k] = dp[i-1][j][ k + dig*(i-j)]
    其中 dig 爲數位 i 處枚舉的可能的數字

    狀態轉移中j沒有轉移?支點的位置須要另外枚舉
*/
ll dp[22][22][1600];//9*(1+2+3......+18) = 1539
int dig[22];//數字數位上的數字
ll dfs(int len,int p,int sum,bool up)//前3個參數對應dp3個維度的意義,up標記上界
{
    if (len == 0) return sum==0;
    if (sum < 0) return 0;
    if (!up&&dp[len][p][sum]!=-1) return dp[len][p][sum];
    ll ans = 0;
    int n = 9;if (up) n = dig[len];
    for (int i = 0;i <= n;++i)
    {
        ans += dfs(len-1,p,sum+i*(len-p),up&&(i==n));
    }
    if (!up) dp[len][p][sum] = ans;
    return ans;
}
ll cal(ll x)
{
    if (x == -1) return 0;//這題的 l 能夠爲 0,l-1就是-1了
    int len = 0;
    while (x)
    {
        dig[++len] = x%10;
        x/=10;
    }
    //須要枚舉支點的位置
    ll ans = 0;
    for (int i = 1;i <= len;++i)
    {
        ans += dfs(len,i,0,1);
    }
    return ans - (len - 1);//減去重複的0
}
int main()
{
    int T;mem(dp,-1);
    scanf("%d",&T);
    while (T--)
    {
        ll l,r;
        scanf("%I64d %I64d",&l,&r);
        printf("%I64d\n",cal(r)-cal(l-1));
    }
    return 0;
}


H - B-number

題意:

求區間[1,r]內數位含13且能夠整除13的數個數

分析:

含13的話類比第D題不要49,整除13類比F題整除7(都是一個調調)

直接類比D題和F題去作,這裏就很少說直接上代碼了:

PS:網上看了一下別人的題解,別人保存的狀態好像和個人有點差異

        不過個人想法也很天然,反正本身看得蠻舒服的。。。。(主要是受前面D題F題的影響,天然而然的思想^_^)

#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
using namespace std;
typedef long long ll;
/*
    dp[i][j][k][l]:
                    i : 正在處理的數位
                    j : %13 = j的數
                    k :這一位的前一位是否爲 1(是--1,否--0)
                    l :前面是否已經含 13(是--1,否--0)
*/
ll dp[22][13][2][2];
ll dig[22];
ll dfs(int len, int r,bool is1,bool has,bool up)
{
    if (len == 0) return r == 0&&has;
    if (!up&&dp[len][r][is1][has]!=-1) return dp[len][r][is1][has];
    ll ans = 0;
    int n = 9;if (up) n = dig[len];
    for (int i = 0;i <= n;++i)
    {
        bool nhas = has;
        if (is1&&i==3) nhas = 1;//已經有了13
        ans += dfs(len-1,(i+r*10)%13,i==1,nhas,up&&(i==n));
    }
    if (!up) dp[len][r][is1][has] = ans;
    return ans;
}
ll cal(ll x)
{
    int len = 0;
    while (x)
    {
        dig[++len] = x%10;
        x/=10;
    }
    return dfs(len,0,0,0,1);
}
int main()
{
    ll r;mem(dp,-1);
    while (~scanf("%I64d",&r))
    {
        printf("%I64d\n",cal(r));
    }
    return 0;
}