数位DP学习笔记

从头开始学习数位DP!

例一 Acwing310 启示录

https://www.acwing.com/problem/content/312/

先预处理出 \(f\) 数组。

\(f[i,3]\) 表示 \(i\) 位数中有多少个魔鬼数。

\(f[i,0/1/2]\) 表示 \(i\) 位数中有 \(0/1/2\) 个连续的6的非魔鬼数的个数。

\[f[i,0]=9\times(f[i-1,0]+f[i-1,1]+f[i-1,2])\\ f[i,1]=f[i-1,0]\\ f[i,2]=f[i-1,1]\\ f[i,3]=f[i-1,2]+10\times f[i-1,3] \]

有了 \(f\) 数组,我们该怎样求出第 \(X\) 个魔鬼数呢?

考虑试填法,即从小到大依次确定每一位数字。

具体实现见代码:

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

inline int read(void) {
    int x = 0, f = 1; char ch = getchar();
    while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
    while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
    return f * x;
}

long long f[22][4];

inline void prework(void) {
    f[0][0] = 1;
    for (int i = 1; i <= 20; ++ i) {
        f[i][0] = 9 * (f[i - 1][0] + f[i - 1][1] + f[i - 1][2]);
        f[i][1] = f[i - 1][0];
        f[i][2] = f[i - 1][1];
        f[i][3] = f[i - 1][2] + 10 * f[i - 1][3];
    }
}

int main() {
    prework();
    int T = read();
    while (T --) {
        int n = read();
        int m;
        for (m = 0; f[m][3] < n; ++ m);

        for (int i = m, k = 0; i >= 0; -- i) {
            for (int j = 0; j <= 9; ++ j) {
                long long cnt = f[i - 1][3];
                if (j == 6 || k == 3) {
                    for (int l = max(0, 3 - k - (j == 6)); l < 3; ++ l) 
                        cnt += f[i - 1][l];
                }
                if (cnt < n) {
                    n -= cnt;
                }
                else {
                    if (k < 3) {
                        if (j == 6) ++ k;
                        else k = 0;
                    }
                    printf("%d" ,j);
                    break;
                }
            }
        }
        puts("");
    }
    return 0;
}

例二 Acwing311 月之谜

https://www.acwing.com/problem/content/313/

\(f[i,j,k,l]\) 表示由 \(i\) 位数字构成、各位数字之和是 \(j\)、对 \(k\) 取模余数是 \(l\) 的数有多少个。

\[F[i,j,k,l]=\sum_{p=0}^9F[i-1,j-p,k,(l-p*10^{i-1})\bmod k] \]

采取试填法的思想,只要填了一个比 \(R\) 要小的数字,那么后面的数无论是多少,整个数值都不会超过 \(R\)。如果填了一个正好是 \(R\) 那一位的数字,就继续向后枚举接下来的每一位。

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

inline int read(void) {
    int x = 0, f = 1; char ch = getchar();
    while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
    while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
    return f * x;
}

int f[13][91][91][91];
int pwr[11][83];

void prework(void) {
    for (int k = 1; k <= 82; ++ k) {
        pwr[0][k] = 1 % k;
        for (int i = 1; i <= 10; ++ i) pwr[i][k] = pwr[i - 1][k] * 10 % k;
        f[0][0][k][0] = 1;
        for (int i = 1; i <= 10; ++ i) {
            for (int j = 0; j <= 82; ++ j) {
                for (int l = 0; l < k; ++ l) {
                    for (int p = 0; p <= 9; ++ p) {
                        if (j - p < 0) break;
                        f[i][j][k][l] += f[i - 1][j - p][k][((l - p * pwr[i - 1][k]) % k + k) % k];
                    }
                }
            }
        }
    }
}

inline int count(int R) {
    int a[11], n;
    for (n = 0; R; R /= 10) a[++ n] = R % 10;
    int ans = 0;
    for (int sum = 1; sum <= 82; ++ sum) {
        int t = 0;
        int q = 0;
        for (int i = n; i; -- i) {
            for (int p = 0; p < a[i]; ++ p) {
                if (sum - t - p < 0) break;
                ans += f[i - 1][sum - t - p][sum][((-q - p * pwr[i - 1][sum]) % sum + sum) % sum];
            }
            t += a[i];
            q = (q + a[i] * pwr[i - 1][sum]) % sum;
        }
        if (t == sum && q == 0) ++ ans;
    }
    return ans;
}

int main() {
    prework();
    int L = read(), R = read();
    cout << count(R) - count(L - 1) << endl;
    return 0;
}
posted @ 2021-05-19 20:25  蓝田日暖玉生烟  阅读(52)  评论(0编辑  收藏  举报