数位 DP 的技巧

本文意在探究对于数位 DP 题目的一些技巧。

首先我们应该清楚,数位 DP 一般而言处理答案大于大致 \(10^6\) 的题目。否则我们有技巧枚举每个答案排序后查询。

数位 DP 有两种写法,一种是迭代,一种是记忆化搜索。本文详细介绍记忆化搜索,因为记忆化搜索好写,好调,好想,这些都是在赛时的优势,而相比之下迭代不好写,不好调,不好想,唯一的优势只有方便读者理解数位 DP 的本质。因为接下来的内容做题有关,所以不讨论迭代。

具体来说,我们记忆化搜索要记录当前的位数和是否前面的数取到上界。除了这两个状态之外,我们还需要题目对数限制以及特殊答案贡献的具体变量。

然后就是,因为取到上界的所有状态都只会被计算一次,所以我们不记录。

int f[pos][...];//不含 lim
...
int dfs(int pos,bool lim,...) {
	
}

首先我们从一道例题起手。

P1980 [NOIP 2013 普及组] 计数问题

考虑一下,如果 \(n\le 10^{10^3}\) 怎么做?

首先考虑限制,对于一个数,没有限制。每个数都能够对答案贡献。

其次考虑一个数对答案的贡献具体是多少,它对答案贡献的值为各位上的数字等于 \(x\) 的个数,于是我们需要加入变量 \(sum\) 表示目前 \(1\)\(pos\) 的位置上有多少个数等于 \(x\),另外,对于 \(x=0\),我们对答案的贡献需要去除前导 \(0\),所以记录状态 \(zero\) 表示现在填 \(0\) 是否计入贡献。

好的,现在所有状态为 \(pos,lim,sum,zero\)

\(zero,lim\) 两个变量当然可以不计入记忆化状态,但这题对空间的限制并不大,所以无所谓。

代码:

// Problem: P1980 [NOIP 2013 普及组] 计数问题
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P1980
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define int long long
#define upp(a, x, y) for (int a = x; a <= y; a++)
#define dww(a, x, y) for (int a = x; a >= y; a--)
#define pb(x) push_back(x)
#define endl '\n'
#define x first
#define y second
#define PII pair<int, int>
using namespace std;
const int N = 1e3 + 10;
int f[N][N][2];
int a[N];
int dfs(int pos, bool lim, bool zero, int sum, int p) {
    if (!lim && f[pos][sum][zero] != -1) return f[pos][sum][zero];
    if (!pos) return sum;
    int res = 0;
    int up = lim ? a[pos] : 9;
    upp(i, 0, up) {
        res = res + dfs(pos - 1, (lim && i == up), zero || (i),
                        sum + (zero && i == p), p);
    }
    if (lim == 0) f[pos][sum][zero] = res;
    return res;
}
int solve(int x, int k) {
    memset(f, -1, sizeof f);
    int len = 0;
    while (x) {
        a[++len] = x % 10;
        x /= 10;
    }
    return dfs(len, 1, (k != 0), 0, k);
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int n, k;
    cin >> n >> k;
    cout << solve(n, k) << endl;
    return 0;
}

再来一道。

P4999 烦人的数学作业

  1. 所有数都能对答案贡献。

  2. 每个数贡献为所有数位之和,记录状态 \(sum\) 表示当前数位之和。

  3. 整理状态 \(pos,lim,sum\)

代码:

// Problem: P4999 烦人的数学作业
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P4999
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)

#include <bits/stdc++.h>
#define int long long
#define upp(a, x, y) for (int a = x; a <= y; a++)
#define dww(a, x, y) for (int a = x; a >= y; a--)
#define pb(x) push_back(x)
#define endl '\n'
#define x first
#define y second
#define PII pair<int, int>
using namespace std;
const int N = 200, X = 1e9 + 7;
int f[N][N], a[N];
int dfs(int pos, int lim, int sum) {
    if (f[pos][sum] != -1 && lim == 0) return f[pos][sum];
    if (!pos) return sum;
    int res = 0;
    upp(i, 0, (lim ? a[pos] : 9)) {
        (res += dfs(pos - 1, (lim && i == (lim ? a[pos] : 9)), sum + i) % X) %=
            X;
    }
    if (lim == 0) f[pos][sum] = res;
    return res;
}
int solve(int x) {
    memset(f, -1, sizeof f);
    int len = 0;
    while (x) {
        a[++len] = x % 10;
        x /= 10;
    }
    return dfs(len, 1, 0);
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    int qq;
    cin >> qq;
    while (qq--) {
        int l, r;
        cin >> l >> r;
        cout << ((solve(r) - solve(l - 1)) % X + X) % X << endl;
    }
    return 0;
}

再来一道。

代码源周赛 Round 11 E. [R11E]波浪数

  1. 一个数贡献的条件是为波浪数,波浪数说白了就是一上一下,记录这次应该是上还是下或者还未决定 \(go=1/0/2\)。如果还未决定,我们就可以填上继续 \(0\),或者填上一个正数,然后决定上下是 \(0/1\) 各继续计算,注意如果 \(pos=1\) 的时候,上和下都会只取到一个数,为了保证不重不漏,我们只再计算其中一种情况即可。对于之前就已经决定是上还是下,我们直接枚举数就行了,为了保证上下我们记录状态 \(last\) 表示上一个填的数,没什么好讲的。

  2. 所有数的贡献都为 \(1\)

  3. 所有状态为 \(pos,lim,go,last\)

代码:

#include <bits/stdc++.h>
#define int long long
#define upp(a, x, y) for (int a = x; a <= y; a++)
#define dww(a, x, y) for (int a = x; a >= y; a--)
#define pb(x) push_back(x)
#define endl '\n'
#define x first
#define y second
#define PII pair<int, int>
using namespace std;
const int N = 1e5 + 10, X = 998244353;
int f[N][3][10], a[N];
int dfs(int pos, bool lim, int go, int last) { // 0 代表降
    if (f[pos][go][last] != -1 && lim == 0) return f[pos][go][last];
    if (!pos) return 1;
    int res = 0, up = (lim ? a[pos] : 9);
    if (go == 2) {
        upp(i, 0, up) {
            if (i) {
                if (pos > 1)
                    (res += dfs(pos - 1, (lim && i == a[pos]), 1, i)) %= X;
                (res += dfs(pos - 1, (lim && i == a[pos]), 0, i)) %= X;
            } else
                (res += dfs(pos - 1, (lim && i == a[pos]), go, last)) %= X;
        }
    } else if (go == 1) {
        upp(i, last + 1, up) {
            (res += dfs(pos - 1, (lim && i == a[pos]), 0, i)) %= X;
        }
    } else {
        upp(i, 0, min(last - 1, up)) {
            (res += dfs(pos - 1, (lim && i == a[pos]), 1, i)) %= X;
        }
    }
    if (!lim) f[pos][go][last] = res;
    return res;
}
int solve(string x) {
    int len = 0;
    dww(i, x.size() - 1, 0) { a[++len] = x[i] - '0'; }
    return dfs(len, 1, 2, 0);
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    memset(f, -1, sizeof f);
    int qq;
    cin >> qq;
    while (qq--) {
        string l, r;
        cin >> l >> r;
        int ans = ((solve(r) - solve(l)) % X + X) % X;
        int now = 2, flag = 1;
        upp(i, 1, (int)l.size() - 1) {
            if (l[i] == l[i - 1]) {
                flag = 0;
                break;
            }
            if (now == 2) {
                if (l[i] > l[i - 1])
                    now = 0;
                else
                    now = 1;
            } else {
                if (now && l[i] < l[i - 1]) {
                    flag = 0;
                    break;
                }
                if (!now && l[i] > l[i - 1]) {
                    flag = 0;
                    break;
                }
                now ^= 1;
            }
        }
        if (flag || l.size() == 1) ans++;
        cout << ans % X << endl;
    }
    return 0;
}
posted @ 2025-05-13 21:41  PM_pro  阅读(39)  评论(0)    收藏  举报