跳转至

数位 DP

数位 DP 的本质就是将数字的每位分开考虑,从而降低时间复杂度。数位 DP 的问题分为几种类型:

  • 求出 \([l,r]\) 区间内,数位满足指定条件的数的个数;
  • 求出数位满足指定条件的数中,按大小排名第 \(k\) 的是哪个;
  • ...

常用的方法有

  • 差分法:将区间询问转化为前缀询问;
  • 试填法:求出第 \(k\) 小(大)的数字

P4127 [AHOI2009] 同类分布

给出两个数 \(a,b\),求出 \([a,b]\) 间各位数字之和能整除原数的数的个数。

\(1\le a\le b\le 10^{18}\)

注意到一个 \(\le 10^{18}\) 的数字的数位之和不会超过 \(18\times 9=162\),我们枚举 \([1,162]\) 之间的所有数,然后进行数位 dp,时间复杂度 \(O(162\times 18\times 162^2\times 10)\)

HDU4352 数位LIS

多次询问,第一行给出询问次数 \(T\)

每次询问给定 \(l,r,k\),问区间 \([l,r]\) 内有多少数字满足,各数位的 LIS 长度恰好为 \(k\)

\(T\le 10^4,\ 0<L\le R<2^{63}-1,\ 1\le k\le 10\)

先差分转化为前缀的询问。然后考虑如何处理 LIS 的限制。我们有两种 dp 来求 LIS:\(f_{i}\) 表示前缀中最后一个数字为 \(i\) 的 LIS;\(f_i\) 表示前缀中长度为 \(i\) 的 LIS 中最后一个数字的最小值。其中第一种方法我们常用 BIT 进行优化,第二种方法就是常说的二分法。

第一种 dp 有值的位置不连续,似乎没什么前途。而观察第二种 dp 我们发现一些性质:\(f_i\) 有值的部分是一个前缀,并且值严格单调递增。因此,我们只需记录哪些数字出现在 \(f_i\) 中,然后将它们顺序排列,即可得到 \(f\) 数组。

由于值域很小,\(f_i\) 最多出现 \(0\sim 9\) 十个数字。因此可以直接将 \(f_i\) 状压进一个 \(2^{10}\)int 中即可。写出状压状态的转移函数,剩余部分的实现是容易的,记得处理前导零。

代码
#include<iostream>
#include<bitset>
#include<cassert>
#define int long long
using namespace std;

int k, ans;
int num[20], nn;

int lg[1 << 10], popcnt[1 << 10];

int trans(int s, int c) {
    if(s == 0) return s ^ (1 << c);
    int tmp = lg[s & ((1 << c) - 1)];
    int pre = s & ~((1 << (tmp + 1)) - 1);
    if(!pre) return s ^ (1 << c);
    pre = lg[pre & -pre];
    if(c < pre) return s ^ (1 << pre) ^ (1 << c);
    return s;
}

int f[11][20][1 << 10];

int dfs2(int x, int s, bool zero) {
    if(!zero && ~f[k][x][s]) return f[k][x][s];
    if(x == 0) {
        if(zero) return 0;
        if(popcnt[s] == k) return f[k][x][s] = 1;
        return f[k][x][s] = 0;
    }
    int res = 0;
    for(int i = 0; i <= 9; i++) {
        res += dfs2(x - 1, zero && (i == 0) ? 0 : trans(s, i), zero && (i == 0));
    }
    if(!zero) {
        f[k][x][s] = res;
    }
    return res;
}

void dfs1(int x, int s, bool zero) {
    if(x == 0) {
        if(popcnt[s] == k) ++ans;
        return;
    }
    for(int i = 0; i < num[x]; i++) {
        ans += dfs2(x - 1, zero && (i == 0) ? 0 : trans(s, i), zero && (i == 0));
    }
    dfs1(x - 1, trans(s, num[x]), 0);
}

int calc(int r) {
    ans = 0;
    nn = 0;
    while(r) {
        num[++nn] = r % 10;
        r /= 10;
    }
    dfs1(nn, 0, 1);
    return ans;
}

signed main() {

    lg[0] = -1;
    for(int i = 2; i < (1 << 10); i++) lg[i] = lg[i >> 1] + 1;
    for(int i = 1; i < (1 << 10); i++) popcnt[i] = popcnt[i ^ (i & -i)] + 1;

    for(int k = 0; k <= 10; k++) {
        for(int i = 19; i >= 0; i--) {
            for(int j = 0; j < 1024; j++) f[k][i][j] = -1;
        }
    }

    int T;
    cin >> T;

    while(T--) {
        int l, r;
        cin >> l >> r >> k;
        cout << calc(r) - calc(l - 1) << '\n';
    }

    return 0;
}

AT_abc138_f [ABC138F] Coincidence

给定两个正整数 \(l,r\),询问满足以下条件的数对 \((x,y)\) 的个数:

  • \(l\le x\le y\le r\)
  • \(y\bmod x=y\operatorname{xor} x\)

\(1\le l\le r\le 10^{18}\)

注意到若 \(y\ge 2x\),那么 \(y\) 的最高位不存在于 \(x\) 中,因此 \(y\operatorname{xor}x\) 的结果中该位一定为 \(1\);而 \(y\bmod x\) 中该位一定为 \(0\),矛盾。因此 \(y<2x\),从而推出 \(y\bmod x=y-x\)。又因为 \(y\operatorname{xor}x=y\bmod x=y-x\),因此 \(y\) 的二进制位包含 \(x\) 的二进制位。

因此,\((x,y)\) 合法的充要条件是:\(x,y\) 最高位相同,并且 \(x\subseteq y\)。这里使用数位 dp 即可,记一个前导零,\(x\) 是否顶格,\(y\) 是否顶格即可。

代码
#include<iostream>
#include<cstring>
#define int long long
using namespace std;
const int MOD = 1e9 + 7;

int l, r;

int f[70][2][2][2];

int dfs(int x, bool z, bool mn, bool mx) {
    int &res = f[x + 1][z][mn][mx];
    if(~res) return res;
    if(x < 0) {
        return res = 1;
    }
    res = 0;
    for(int i = 0; i <= 1; i++) {
        for(int j = 0; j <= i; j++) {
            if(z && (i ^ j)) continue;
            if(mn && (j == 0) && (l & (1ll << x))) continue;
            if(mx && (i == 1) && !(r & (1ll << x))) continue;
            res = (res + dfs(x - 1, z && !i, mn && (j == (bool)(l & (1ll << x))), mx && (i == (bool)(r & (1ll << x))))) % MOD;
        }
    }
    return res;
}

signed main() {

    memset(f, -1, sizeof(f));

    cin >> l >> r;

    cout << dfs(63, 1, 1, 1) << '\n';

    return 0;
}