1. 程式人生 > 實用技巧 >數位 DP 學習筆記

數位 DP 學習筆記

概念

Q:什麼是數位 DP?

A:就是一種對數進行的 DP。

Q:常見題型?

A:找出區間 \([L, R]\) 中滿足某種條件 \(f(i)\) 的數的個數,限制 \(f(i)\) 只與 \(i\) 的每一位上的數字有關,而與 \(i\) 的值無關。

Q:如何求解?

A:一般都有套路,具體下面會講。

常見思路

數位 DP 一般使用記憶化搜尋實現。

根據某種套路,我們將詢問區間 \([L, R]\) 拆成 \([0,R]\)\([0,L-1]\),輸出答案的時候直接字首和一下即可。

一般設 \(dp_{cur,lst,\dots,f,g}\) 表示當前填到了第 \(cur\) 位,上一位的狀態是 \(lst\)

(有時候不止要記上一位的,可能還要記前面很多位的,因此打了省略號),\(f\) 表示前 \(cur\) 位是否與上界相同,\(g\) 則表示有無前導零。

由於數位 DP 基本上都是套個模板,因此這裡就直接給出模板。

int l, r; //答案區間
int dp[N][N][...][2][2]; //DP 陣列
int tot, b[N]; //將數按位拆開

int dfs(int cur/*當前填到了哪一位*/, int lst, .../*上一位的狀態*/, bool f/*前 cur 位是否與上界相同*/, bool g/*有無前導零*/) //數位 DP 函式
{
	if (cur == tot + 1) return 1; //邊界
	if (dp[cur][lst][...][f][g] != -1) return dp[cur][lst][...][f][g]; //記憶化搜尋,已經搜過就直接返回
	
	int v = 9;
	if (f) v = b[cur]; //求出當前列舉的位上的數的上界
	
	int ans = 0;
	for (int i = 0; i <= v; i+=1) //列舉這一位填什麼數
	{
		if (g == true) //又前導零
		{
			if (i == 0) ans += dfs(cur + 1, -1, ..., f && (i == v), true); //這一位不填數
			else ans += dfs(cur + 1, i, ..., f && (i == v), false); //這一位填上 i
		}
		else /*這裡根據題目不同可能要加上各種限制條件*/ ans += dfs(cur + 1, i, ..., f && (i == v), false); //累加這一位填上 i 的答案
	}
	
	return dp[cur][lst][...][f][g] = ans; //記憶化
}

inline int solve(int x)
{
	memset(dp, -1, sizeof dp); //DP 陣列初始化
	tot = 0;
	while (x)
	{
		b[++tot] = x % 10;
		x /= 10;
	}
	reverse(b + 1, b + 1 + tot); //將上界按位拆開
	return dfs(1, -1, true, true); //進行一次 DP
}

int main()
{
    l = gi <int> (), r = gi <int> ();
    printf("%d\n", solve(r) - solve(l - 1)); //字首和計算答案
	return 0;
}

具體的實踐還是需要依題目而定,因此我們來看幾道例題。

例題

T1. [SCOI2009] windy 數

可以說是數位 DP 的一道入門題。

根據模板,我們只需要記錄上一個位置填的數是什麼,然後轉移的時候判斷當前填的數是否滿足條件即可。

#include <bits/stdc++.h>
#define DEBUG fprintf(stderr, "Passing [%s] line %d\n", __FUNCTION__, __LINE__)
#define File(x) freopen(x".in","r",stdin); freopen(x".out","w",stdout)

using namespace std;

typedef long long LL;
typedef pair <int, int> PII;
typedef pair <int, PII> PIII;

template <typename T>
inline T gi()
{
	T f = 1, x = 0; char c = getchar();
	while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return f * x;
}

const int INF = 0x3f3f3f3f, N = 13, M = N << 1;

int l, r, tot;
int dp[N][N][2][2];
int b[N];

int dfs(int cur, int lst, bool f, bool g)
//填到第 cur 位,上一位的數是 lst,f 表示是否與上界相等,g 表示是否有前導零
{
	if (cur == tot + 1) return 1;
	if (dp[cur][lst][f][g] != -1) return dp[cur][lst][f][g];
	
	int v = 9;
	if (f) v = b[cur];
	
	int ans = 0;
	for (int i = 0; i <= v; i+=1)
	{
		if (g == true)
		{
			if (i == 0) ans += dfs(cur + 1, -1, f && (i == v), true);
			else ans += dfs(cur + 1, i, f && (i == v), false);
		}
		else if (abs(i - lst) >= 2) /*判斷能否轉移*/ ans += dfs(cur + 1, i, f && (i == v), false);
	}
	
	return dp[cur][lst][f][g] = ans;
}

inline int solve(int x)
{
	memset(dp, -1, sizeof dp);
	tot = 0;
	while (x)
	{
		b[++tot] = x % 10;
		x /= 10;
	}
	reverse(b + 1, b + 1 + tot);
	return dfs(1, -1, true, true);
}

int main()
{
    l = gi <int> (), r = gi <int> ();
    printf("%d\n", solve(r) - solve(l - 1));
	return 0;
}

T2. [ZJOI2010]數字計數

對每一個數碼進行一次數位 DP,需要記錄一下填到第 cur 位當前要統計的數的出現次數,別的都沒什麼太大差別。

#include <bits/stdc++.h>
#define DEBUG fprintf(stderr, "Passing [%s] line %d\n", __FUNCTION__, __LINE__)
#define File(x) freopen(x".in","r",stdin); freopen(x".out","w",stdout)

using namespace std;

typedef long long LL;
typedef pair <int, int> PII;
typedef pair <int, PII> PIII;

template <typename T>
inline T gi()
{
	T f = 1, x = 0; char c = getchar();
	while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return f * x;
}

const int INF = 0x3f3f3f3f, N = 15, M = N << 1;

LL l, r;
int tot, b[N];
LL dp[N][N][2][2];

LL dfs(int now, int cur, int lst, bool f, bool g)
{
	if (cur == tot + 1) return lst;
	if (dp[cur][lst][f][g] != -1) return dp[cur][lst][f][g];
	
	int v = 9;
	if (f) v = b[cur];
	
	LL ans = 0;
	for (int i = 0; i <= v; i+=1)
	{
		if (g)
		{
			if (i == 0) ans += dfs(now, cur + 1, 0, f && (i == v), true);
			else ans += dfs(now, cur + 1, lst + (i == now), f && (i == v), false);
		}
		else ans += dfs(now, cur + 1, lst + (i == now), f && (i == v), false);
	}
	
	return dp[cur][lst][f][g] = ans;
}

inline LL solve(int now, LL x)
{
	tot = 0;
	while (x)
	{
		b[++tot] = x % 10;
		x /= 10;
	}
	reverse(b + 1, b + 1 + tot);
	memset(dp, -1, sizeof dp);
	return dfs(now, 1, 0, true, true);
}

int main()
{
	//File("");
    l = gi <LL> (), r = gi <LL> ();
    for (int i = 0; i <= 9; i+=1)
	    printf("%lld ", solve(i, r) - solve(i, l - 1));
	return 0;
}

T3. [CQOI2016]手機號碼

這題要記錄的狀態可能多一些……

對於當前狀態,我們需要記錄 \(p1\)\(p2\)(前兩個數是什麼)、\(ok\)(是否已經出現至少 \(3\) 個相鄰的相同數字)、\(h8\)\(h4\)(分別為是否已經出現了 \(8\)\(4\)),因此轉移的時候有一些細節需要注意。

#include <bits/stdc++.h>
#define DEBUG fprintf(stderr, "Passing [%s] line %d\n", __FUNCTION__, __LINE__)
#define File(x) freopen(x".in","r",stdin); freopen(x".out","w",stdout)

using namespace std;

typedef long long LL;
typedef pair <int, int> PII;
typedef pair <int, PII> PIII;

template <typename T>
inline T gi()
{
	T f = 1, x = 0; char c = getchar();
	while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return f * x;
}

const int INF = 0x3f3f3f3f, N = 15, M = N << 1;

LL l, r;
int tot, b[N];
LL dp[N][N][N][2][2][2][2][2];

LL dfs(int cur, int p1, int p2, bool ok, bool h8, bool h4, bool f, bool g)
{
	if (cur == tot + 1) return ok == true;
	if (dp[cur][p1][p2][ok][h8][h4][f][g] != -1) return dp[cur][p1][p2][ok][h8][h4][f][g];
	
	int v = 9;
	if (f) v = b[cur];
	
	LL ans = 0;
	for (int i = 0; i <= v; i+=1)
	{
		if (g)
		{
			if (i == 0) ans += dfs(cur + 1, p1, p2, ok, h8, h4, f && (i == v), g);
			else ans += dfs(cur + 1, i, p1, ok, (i == 8), (i == 4), f && (i == v), false);
		}
		else
		{
			if (h8 && (i == 4)) continue; 
			if (h4 && (i == 8)) continue; //同時出現了 8 和 4
			if (p1 == p2 && i == p1) 
				ans += dfs(cur + 1, i, p1, true, h8 || (i == 8), h4 || (i == 4), f && (i == v), false); //已經出現了 3 個相鄰的相同數字
			else
				ans += dfs(cur + 1, i, p1, ok, h8 || (i == 8), h4 || (i == 4), f && (i == v), false);
		}
	}
	
	return dp[cur][p1][p2][ok][h8][h4][f][g] = ans;
}

inline LL solve(LL x)
{
	tot = 0;
	while (x)
	{
		b[++tot] = x % 10;
		x /= 10;
	}
	reverse(b + 1, b + 1 + tot);
	memset(dp, -1, sizeof dp);
	return dfs(1, -1, -1, false, false, false, true, true);
}

int main()
{
    l = gi <LL> (), r = gi <LL> ();
    printf("%lld\n", solve(r) - solve(l - 1));
	return 0;
}

T4. AcWing310 啟示錄

與上一道題目差不多的套路,記錄一下前兩位是否為 6 以及是否已經出現了連續至少 \(3\) 個 6。

注意還需要進行一個二分,找到第一個 \(ans\) 使得 \(ans\) 是滿足 \(\le ans\) 的魔鬼數個數為 \(k\) 的數中最小的一個。

#include <bits/stdc++.h>
#define DEBUG fprintf(stderr, "Passing [%s] line %d\n", __FUNCTION__, __LINE__)
#define File(x) freopen(x".in","r",stdin); freopen(x".out","w",stdout)

using namespace std;

typedef long long LL;
typedef pair <int, int> PII;
typedef pair <int, PII> PIII;

template <typename T>
inline T gi()
{
	T f = 1, x = 0; char c = getchar();
	while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return f * x;
}

const int INF = 0x3f3f3f3f, N = 20, M = N << 1;

int T, x;
int tot, b[N];
LL dp[N][2][2][2][2];

LL dfs(int cur, bool p1, bool p2, bool ok, bool f)
{
    if (cur == tot + 1) return (ok == true);
    if (dp[cur][p1][p2][ok][f] != -1) return dp[cur][p1][p2][ok][f];
    
    int v = 9;
    if (f) v = b[cur];
    
    LL ans = 0;
    for (int i = 0; i <= v; i+=1)
    {
        ans += dfs(cur + 1, i == 6, p1, ok || (p1 && p2 && (i == 6)), f && (i == v));
    }
    
    return dp[cur][p1][p2][ok][f] = ans;
}

inline LL solve(LL mid)
{
    tot = 0;
    while (mid)
    {
        b[++tot] = mid % 10;
        mid /= 10;
    }
    reverse(b + 1, b + 1 + tot);
    memset(dp, -1, sizeof dp);
    return dfs(1, false, false, false, true);
}

int main()
{
    T = gi <LL> ();
    while (T--)
    {
        x = gi <LL> ();
        LL l = 0, r = 1000000000000000ll, ans = 0;
        while (l <= r)
        {
            LL mid = (l + r) >> 1;
            if (solve(mid) >= x) ans = mid, r = mid - 1;
            else l = mid + 1;
        }
        printf("%lld\n", ans);
    }
	return 0;
}

T5. AcWing311 月之謎(思考題)

此題中我們發現“整除”這個操作並不好直接處理,有什麼方式可以完成呢?

提示:考慮到單純的數位 DP 複雜度並不高,於是可以列舉一下這個數的各位數字之和再進行數位 DP。