Page Top

动态规划——数位DP 学习笔记

动态规划——数位DP 学习笔记

定义

引入

数位 DP 往往都是这样的题型:给定一个区间 \([l, r]\),求这个区间中满足某种条件的数的总数。

简单的暴力代码如下:

int ans = 0;
for(int i = l; i <= r; ++i)
    if(check(i)) ++ans;

而当数据规模过大,暴力枚举就 \(\mathbb T\) 飞了,因此引入数位 DP:

概念

数位(digit):对于十进制,即把一个数字按照个位、十位、百位等,一位一位地拆开,它每一位上的数字,也就是 \(0 \sim 9\);其他进制可类比十进制。

数位 DP:一种按照数位暴力枚举的方式,用来解决一类特定问题;这种问题比较好辨认,一般具有这几个特征:

  1. 提供一个数字区间(有时也只提供上界)来作为统计的限制;
  2. 统计满足某种条件的数的数量,有时也有统计总和、平方和等的;
  3. 上界很大,甚至会有 \(10^{18}\) 这么大,暴力枚举验证会超时;
  4. 这些条件经过转化后可以使用「数位」的思想去理解和判断。

原理

例如,当我们在数数的过程中,\(100 \sim 199\)\(200 \sim 299\) 这两部分,后两位是完全相同的,这种重复计算可以通过 DP 的方式进行优化。

实现

计数原理

数位 DP 中通常会利用常规计数问题技巧,比如把一个区间内的答案拆成两部分相减,即查分的思路:

$ans_{[l, r]} = s_r - s_{l - 1}$.

一般根据是否计入 \(0\) 的贡献,将 \(s_k\) 定义为:\(\sum[0, k]\)\(\sum[1, k]\).

数的存储

一般将数字中较低位存在数组的低位之中,即:

typedef long long ll;
ll solve(ll x) {
    int len = 0;
    while (x) a[++len] = x % 10, x /= 10;
    return dfs(...); //记忆化搜索
}

常用形参

统计答案可以选择记忆化搜索,也可以选择循环迭代递推;因为数位 DP 的预处理一般比较变态,所有我一般使用记忆化搜索。

常用的形式参数如下:

  1. pos(int):表示当前枚举的位置,一般从 \(\mathrm{len}\) 开始,到 \(0\) 为止。
  2. limit(bool):表示当前枚举到的位置,可以填的数是否收到限制;若为 true,则该位最大填 \(a_{pos}\);否则最大填 \(R-1\),其中 \(R\) 表示枚举的进制数。
  3. sum(int):表示从 \(\mathrm{len}\)\(pos + 1\) 位的贡献,常用的有求和等。
  4. last(int):表示上一位填的数,当题目限制连续的两个(或多个)数位有条件限制的话常用。
  5. lead0(bool):表示从 \(\mathrm{len}\)\(pos + 1\) 是否都为 \(0\)(前导零)。
  6. r(int):表示从 \(\mathrm{len}\)\(pos + 1\) 这个前缀模一个数 \(\bmod\) 的结果,也可以表示数位和取模的结果。
  7. st(bool):常用与状态压缩,其二进制表示某一位是否满足某一条件等。

如何复用结果

简单分析可知,一定是已经求解过中,状态与当前状态相同的,可以复用,如 possumlast 相同等;特殊的,当 limit == 1lead0 == 1 时,即当前位受到限制时,无需记录状态,因为这一状态不会频繁的复用,这种空间换时间价值不大。

即:

typedef long long ll;
ll f[N][M]; // DP 数组,第一维表示枚举到的数位,第二维表示当前的状态;默认为 -1
ll dfs(int pos, bool limit, int sum) {
    if (!pos) return sum;
    if (!limit && f[pos][sum] != -1) return f[pos][sum];
    int up = limit ? a[pos] : 9;
    ll res = 0; for (int i = 0; i <= up; ++i)
        res = (res + dfs(pos - 1, limit && i == up, sum + i)) % MOD;
    if (!limit) f[pos][sum] = res;
    return res;
}

习题

见:https://www.luogu.com.cn/training/384691

题目讲解

UPD 20240727。

SP1433 KPSUM - The Sum

比较简单(bushi)。

注意仔细讨论即可,例如我们钦定状态表示考虑 / 不考虑正负的。

int a[20], tot;

ll mem[20][300][2][2];

ll dfs(int pos, int sum, bool ov_len, bool pm_last, bool limit, bool lead0) {
    if (pos == 0) {
        if (lead0) return 0;
        if (ov_len == 0) return 64 - sum;
        return pm_last ? sum - 64 : 64 - sum;
    }
    if (!limit && !lead0 && mem[pos][sum][ov_len][pm_last] != -1) return mem[pos][sum][ov_len][pm_last];
    ll res = 0;
    int up = limit ? a[pos] : 9;
    for (int i = 0; i <= up; ++i) {
        if (lead0 && i == 0) res += dfs(pos - 1, sum, ov_len, pm_last, limit & (i == up), 1);
        else res += dfs(pos - 1, sum + i * (ov_len ? -1 : 1), ov_len ^ 1, i & 1, limit & (i == up), 0);
    }
    if (!limit && !lead0) mem[pos][sum][ov_len][pm_last] = res;
    return res;
}

ll calc(ll x) {
    tot = 0;
    while (x) {
        a[++tot] = x % 10;
        x /= 10;
    }
    return dfs(tot, 64, 0, 0, 1, 1);
}

P4127 [AHOI2009] 同类分布

我们枚举数位和,数位 DP 搜数位和等于这个值的,同时数位和整除她的数的个数。

int a[20], tota;
int b[20], totb;

int tar;

ll mem[20][180][180];

ll dfs(int *arr, int pos, int sum, int mod, bool lead0, bool limit) {
    if (pos == 0) {
        if (lead0) return 0;
        if (sum != tar) return 0;
        return mod == 0;
    }
    if (!lead0 && !limit && mem[pos][sum][mod] != -1) return mem[pos][sum][mod];
    ll res = 0;
    int up = limit ? arr[pos] : 9;
    for (int i = 0; i <= up; ++i) res += dfs(arr, pos - 1, sum + i, (mod * 10 + i) % tar, lead0 & (i == 0), limit & (i == up));
    if (!lead0 && !limit) mem[pos][sum][mod] = res;
    return res;
}

void Main() {
    ll l, r;
    cin >> l >> r;
    --l;
    while (l) {
        a[++tota] = l % 10;
        l /= 10;
    }
    while (r) {
        b[++totb] = r % 10;
        r /= 10;
    }
    ll ans = 0;
    int up = max(tota, totb) * 9;
    for (tar = 1; tar <= up; ++tar) {
        memset(mem, -1, sizeof mem);
        ans += dfs(b, totb, 0, 0, 1, 1);
        ans -= dfs(a, tota, 0, 0, 1, 1);
    }
    cout << ans << endl;
}

P9092 [PA2020] Liczba Potyczkowa

多倍经验:CF55D Beautiful numbersSP8177 JZPEXT - Beautiful numbers EXTREME

题目描述:

\([l,r]\) 中数位不含零且数字本身可以被其出现过的所有数码整除的数的个数。

我们朴素思路肯定是状态压缩记录出现了那些数码,然后用一堆变量记录模若干数码的结果。

但是显然 MLE + TLE。

但是我们发现,可以只记录模 \(5,7,8,9\) 的模数,

  • \([x\bmod2=0]=[(x\bmod8)\bmod2=0]\).

  • \([x\bmod3=0]=[(x\bmod9)\bmod3=0]\).

  • \([x\bmod4=0]=[(x\bmod8)\bmod4=0]\).

  • \([x\bmod5=0]\).

  • \([x\bmod6=0]=[x\bmod2=0][x\bmod3=0]\).

  • \([x\bmod7=0]\).

  • \([x\bmod8=0]\).

  • \([x\bmod9=0]\).

也可以将这几个结果状态压缩起来,但是没必要。

int a[20];

ll mem[19][1 << 8][5][7][8][9];

ll dfs(int pos, int app, int limit, int lead0, int m5, int m7, int m8, int m9, ll ppp = 0) {
	if (pos == 0) {
		if (lead0) return 0;
		bool flag = true;
		if (app & (1 << 2 - 2)) flag &= m8 % 2 == 0;
		if (app & (1 << 3 - 2)) flag &= m9 % 3 == 0;
		if (app & (1 << 4 - 2)) flag &= m8 % 4 == 0;
		if (app & (1 << 5 - 2)) flag &= m5 == 0;
		if (app & (1 << 6 - 2)) flag &= m8 % 2 == 0 && m9 % 3 == 0;
		if (app & (1 << 7 - 2)) flag &= m7 == 0;
		if (app & (1 << 8 - 2)) flag &= m8 == 0;
		if (app & (1 << 9 - 2)) flag &= m9 == 0;
		return flag;
	}
	if (!limit && !lead0 && mem[pos][app][m5][m7][m8][m9] != -1) return mem[pos][app][m5][m7][m8][m9];
	int up = limit ? a[pos] : 9;
	ll res = 0;
	for (int i = !lead0; i <= up; ++i) res += dfs(pos - 1, i <= 1 ? app : (app | (1 << i - 2)), limit & (i == up), lead0 & !i, (m5 * 10 + i) % 5, (m7 * 10 + i) % 7, (m8 * 10 + i) % 8, (m9 * 10 + i) % 9, ppp * 10 + i);
	if (!limit && !lead0) mem[pos][app][m5][m7][m8][m9] = res;
	return res;
}

ll calc(ll x) {
	int i = 0;
	while (x) a[++i] = x % 10, x /= 10;
	return dfs(i, 0, 1, 1, 0, 0, 0, 0);
}

P4317 花神的数论题

人话,求,

\[\prod_{i=1}^n\operatorname{popcnt}(i) \]

Solution 1:

考虑化简,

\[\prod_{i=1}^n\sum_ki(k) \]

我们以样例为例,

\[1=(01)_2\\ 2=(10)_2\\ 3=(11)_2 \]

因此,答案,

\[\operatorname{popcnt}(1)\operatorname{popcnt}(2)\operatorname{popcnt}(3)=(0+1)(1+0)(1+1) \]

也就是说,某个数 \(i\le n\) 的某一位为 \(1\),对答案的贡献就是他后面算的答案的乘积。

我们只需要对于每一位,将答案乘上后面的答案即可。

古早实现,

int a[N];
int f[N][N];

int dfs(int pos, bool limit, int cnt)
{
    if (!pos)
        return max(cnt, 1ll);
    if (!limit && ~f[pos][cnt])
        return f[pos][cnt];

    int up = limit ? a[pos] : 1;

    int res = 1;
    for (int i = 0; i <= up; ++i)
        res = res * dfs(pos - 1, limit && i == up, cnt + i) % MOD;

    if (!limit)
        f[pos][cnt] = res;
    return res;
}

int solve(int x)
{
    int len = 0;
    while (x)
    {
        a[++len] = x & 1;
        x >>= 1;
    }
    return dfs(len, true, 0);
}

Solution 2:

注意到 \(\operatorname{popcnt}\) 的取值有限,

我们多跑几次数位 DP 算出来每一个 \(\operatorname{popcnt}\) 的个数即可。

然后随便快速幂一下,这就是板子题了。

P3413 SAC#1 - 萌数

\([l,r]\) 中存在长度大于等于 \(2\) 的回文字串的数的个数。

注意到,回文串可以根据其长度的奇偶性分为奇回文串和偶回文串。

奇回文串中间三个字符一定是奇回文串,偶回文串中间两个字符一定是偶回文串。

因此,只要存在长度为 \(2,3\) 的回文字串,那么一定符合题意。

记录 last,last_last 表示前一个,前一个的前一个,直接转移即可。

有一个技巧,可以使用 last=-1last=10 来表示 lead0

using ll = long long;

constexpr ll MOD = 1e9 + 7;

string a;

void m_prev(string &a) {
    for (int i = a.size(); i; --i) {
        if (a[i - 1] != '0') {
            --a[i - 1];
            if (i == 1) a = a.substr(1);
            return;
        }
        a[i - 1] = '9';
    }
    __builtin_unreachable();
}

ll mem[1010][11][11][2];

ll dfs(int pos, int last, int last_last, bool st, bool limit) {
    if (pos == int(a.size())) return st;
    if (!limit && mem[pos][last][last_last][st] != -1) return mem[pos][last][last_last][st];
    int up = limit ? a[pos] - '0' : 9;
    ll res = 0;
    for (int i = 0; i <= up; ++i) {
        if (last == 10) res += dfs(pos + 1, i == 0 ? 10 : i, last, st, limit & (i == up));
        else if (last_last == 10) res += dfs(pos + 1, i, last, st | (last == i), limit & (i == up));
        else res += dfs(pos + 1, i, last, st | (last == i) | (last_last == i), limit & (i == up));
    }
    res %= MOD;
    if (!limit) mem[pos][last][last_last][st] = res;
    return res;
}

ll calc(string _) {
    a = _;
    return dfs(0, 10, 10, 0, 1);
}

void Main() {
    memset(mem, -1, sizeof mem);
    string l, r;
    cin >> l >> r;
    m_prev(l);
    while (l.size() < r.size()) l = "0" + l;
    while (r.size() < l.size()) r = "0" + r;
    int ans = calc(r);
    if (l.size() > 1)
        ans = (ans - calc(l) + MOD) % MOD;
    cout << ans << endl;
}

Reference

[1] https://oi-wiki.org/dp/number/
[2] https://blog.csdn.net/hzf0701/article/details/116717851
[3] https://blog.csdn.net/m0_63726942/article/details/127060217

posted @ 2023-09-26 17:47  RainPPR  阅读(61)  评论(0编辑  收藏  举报