數位 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\)
由於數位 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; }
具體的實踐還是需要依題目而定,因此我們來看幾道例題。
例題
可以說是數位 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。