zoukankan      html  css  js  c++  java
  • 数位DP 学习笔记

    (P.s.:)下文出现的部分词汇可能并不严谨,还请各位谅解(QwQ)

    毕竟我比较菜kk


    数位(DP? !) 机房某大佬:那不是快乐源泉吗?

    记得之前在(qbxt)时听某林姓神仙讲得一脸懵(B),最近再系统性的学习一遍,感觉也没什么东西(……)

    首先,数位(DP)一般用于解决这样的问题:

    给定一个区间([L, R]),求符合某一条件的数的个数。而这一条件往往与大小无关,而是与数位的组成有关。

    前置知识(:)前缀和,记忆化搜索。

    首先我们将这个问题转化一下,思考一下就可以得到(:)这个问题显然满足前缀和的性质,我们用([0,R])内符合条件的数的个数减去([0,L-1])内符合条件的数的个数即为答案。

    然后我们再思考一下:很显然,如果我们枚举数位,如果是十进制,显然只有(0sim9)十个数字可以填,所以很显然可以用(f[pos][val])来记录(pos)位为(val)时后面的答案,这样就大大缩短了程序运行时间。

    当然能够记忆化还有两个前提:首先,它必须满足没有上限。 上限是什么意思呢?举一个例子,当我们要求([0,5648])内符合某条件的数的个数时,当我们搜索到百位时(6)时,显然十位只能枚举到(4),只是我们就称十位是有上限的,这时就不能记忆化。这是个很重要的内容,我们来详细看一下。

    分两种情况考虑(:)

    • 我们当前枚举到的这一位有上限,而我们记忆化的结果是一个一般性的,即我们记忆化记录的是后面所有位都可以取(0sim9)时的答案,而这一位有上限,不能随意取(0sim9),所以我们不能直接取记忆化记录的答案。

    • 我们枚举到的这一位依旧有上限,那么我们在枚举时只会枚举到上限处,所以在这一位不能记忆化记录这一位的答案,因为他不具备一般性,依旧是上面的例子,我们枚举到十位(0sim4),这是求出的是([5600,5640])的答案,如果这时记录,很显然会少情况。

    其次,我们需要特殊考虑一下前导(0)的问题,如果前导(0)对答案无影响,那自然最好,如果有影响,那么我们可能需要数组多开一
    维或(dfs)参数多一维,这需要根据具体题目来定。

    那么,数位(DP)的一般模板就基本成型了:

    int dfs(int pos, bool limit, bool lead_zero, int sum) {
        if (!pos) return 1; // or return sum;
        if (!limit && !lead_zero && f[pos][val]) return f[pos][val]; //记忆化
        int ans = 0;
        int lim = limit ? val[pos] : 9; // 根据有无上限确定,枚举范围
        for (int i = 0; i <= lim; i++) {
            if (right(i))
                ans += dfs(pos - 1, limit && (i == lim), (i == 0),  sum + (i == 0) * calc(i));
        }
        if (!limit && !lead_zero) f[pos][val] = ans; // 记忆化
        return ans;
    }
    
    int solve(int n) {
        memset(f, 0, sizeof(f)); 
        int len = 0;
        len = 0;
        while (n) val[++len] = n % 10, n /= 10; // 拆数位
        return dfs(len, 1, 1,0); // 求值
    }
    
    

    然后你就会发现,基本上所有的数位(DP)都长这个亚子,当然根据题目不同参数或答案的计算会有所不同。

    但大体框架都长这个亚子(……)

    然后我们看几道例题:

    HDU 2089 不要62

    这道题的条件是数字中不包含(4)(62)

    显然我们随便乱搞一下就可以了。

    code:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    
    int val[55], len, f[55][55];
    
    int dfs(int pos, bool limit, bool lead_six) { //lead_six表示上一位是否为6
        if (!pos) return 1;
        if (!limit && f[pos][lead_six]) return f[pos][lead_six];
        int ans = 0;
        int lim = limit ? val[pos] : 9;
        for (int i = 0; i <= lim; i++) {
            if (lead_six && i == 2) continue;
            if (i == 4) continue; // 保证符合条件
            ans += dfs(pos - 1, limit && (i == lim), i == 6);
        }
        if (!limit) f[pos][lead_six] = ans;
        return ans;
    }
    
    int solve(int n) {
        memset(f, 0, sizeof(f));
        int lne = 0;
        len = 0;
        while (n) val[++len] = n % 10, n /= 10;
        return dfs(len, 1, 0);
    }
    
    int main() {
        int L, R;
        while (scanf("%d%d", &L, &R) == 2) {
            if (L == 0 && R == 0) break;
            printf("%d
    ", solve(R) - solve(L - 1));
        }
        return 0;
     }
    
    

    windy数

    条件(:)不含前导(0),且相邻两数之差至少为(2)的数的个数。

    这道题我们换一种方式,不采用记忆化搜索的方式来实现数位(DP),相邻两数之差至少为(2),很显然我们可以开一个数组(f[i][j])表示(i)位且最高位为(j)(windy)数个数,这个数组我们可以通过预处理直接处理出来。

    接下来我们考虑一个例子(:[0,5648])

    我们分这么几步来求(:)

    • (0sim999)

    • (1000sim4999)

    • (5000sim5649)

    然后,为什么第三步要枚举到(5649)呢,为了保证我们数字严格小于(R),也是保证答案的正确性,我们再枚举个位时只枚举到减一 的位置,那么代码就很显然了。

    code:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #define int long long
    #define abs(a) ((a) < 0 ? -(a) : (a))
    
    int f[15][15], num[15], a, b;
    
    void init() {
        for (int i = 0; i <= 9; i++) f[1][i] = 1;
        for (int i = 2; i <= 10; i++) {
            for (int j = 0; j <= 9; j++) {
                for (int k = 0; k <= 9; k++)
                    if (abs(j - k) >= 2) f[i][j] += f[i - 1][k];
            }
        } // 预处理f数组
    }
    
    int calc(int x) {
        int len = 0, ans = 0;
        memset(num, 0, sizeof(num));
        while (x) {
            num[++len] = x % 10;
            x /= 10;
        } // 拆分数位
        for (int i = 1; i <= len - 1; i++) {
            for (int j = 1; j <= 9; j++)
                ans += f[i][j];
        } //处理 0~999
        for (int i = 1; i < num[len]; i++)
            ans += f[len][i]; //处理 1000~4999
        for (int i = len - 1; i >= 1; i--) {
            for (int j = 0; j < num[i]; j++) {
                if (abs(num[i + 1] - j) >= 2) ans += f[i][j];
            }
            if (abs(num[i + 1] - num[i]) < 2) break;
        } // 最难的一部分,处理5000~5648
        // 一个小优化,如果当前位不满足是个windy数,那么就没必要在向后枚举。
        return ans;
    }
    
    signed main() {
        init();
        scanf("%lld%lld", &a, &b);
        printf("%lld
    ", calc(b + 1) - calc(a)); // 因为我们之枚举到减一的位置,所以要整体向后移一位。
        return 0;
    }
    
    

    P4999 烦人的数学作业

    题目大意(:)([L,R])内每个数各个数位上的和。

    这道题我们考虑(0sim9)每个数字,我们只需要把每个数字出现的个数求出来,在乘上这个数字本身最终加和就是最后的答案。

    这样我们设计(f)数组,(f[pos][sum])表示枚举到(pos)位,且当前求的和为(sum)时的答案。

    我们对每一个数字单独考虑,因为一次性记录十个数字的话显然空间会爆炸。

    然后(……)大力套模板!!

    code:

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    #define int long long
    
    const int maxn = 25;
    const int MOD = 1e9 + 7;
    int T, L, R;
    int val[maxn], f[maxn][maxn];
    
    template<class T>
    inline T read(T &x) {
        x = 0; int w = 1, ch = getchar();
        while (ch < '0' || ch > '9') {if (ch == '-') w = -1; ch = getchar();}
        while (ch >= '0' && ch <= '9') {x = x * 10 + ch - 48; ch = getchar();}
        return x *= w;
    }
    
    int dfs(int pos, int limit, int lead_zero, int k, int sum) {
        if (!pos) return sum;
        if (!limit && !lead_zero && f[pos][sum] != -1)
            return f[pos][sum];
        int lim = limit ? val[pos] : 9;
        int ans = 0;
        for (int i = 0; i <= lim; i++) {
            if (lead_zero && !i)
                ans += dfs(pos - 1, limit && (i == lim), 1, k, sum);
            else
                ans += dfs(pos - 1, limit && (i == lim), 0, k, sum + (i == k));
        }
        if (!limit && !lead_zero)
            f[pos][sum] = ans;
        return ans;
    }
    
    int solve(int n, int k) {
        memset(f, -1, sizeof(f));
        int len = 0;
        while (n) val[++len] = n % 10, n /= 10;
        return dfs(len, 1, 1, k, 0);
    }
    
    signed main() {
        read(T);
        while (T--) {
            int ans = 0;
            read(L), read(R);
            for (int i = 1; i <= 9; i++)
                ans += (((solve(R, i) - solve(L - 1, i) + MOD) % MOD) * i % MOD + MOD) % MOD, ans %= MOD;
            printf("%lld
    ", ans);
        }
        return 0;
    }
    
    

    这题的一道双倍经验题目:数字计数

    然后,这些其实都是数位(DP)中一些比较简单的题目,因为数位(DP)确实并不简单,不信的话去题库里找一下数位动规的标签吧,嘿嘿嘿(……)

    蒟蒻再做一些比较好的题目会往这里面补充,不过(:)

    咕咕咕(……)

  • 相关阅读:
    [LeetCode] Merge Interval系列,题:Insert Interval,Merge Intervals
    [LeetCode] Simplify Path,文件路径简化,用栈来做
    [LeetCode] Sort Colors 对于元素取值有限的数组,只遍历一遍的排序方法
    [LeetCode] Largest Rectangle in Histogram O(n) 解法详析, Maximal Rectangle
    实现一个协程版mysql连接池
    Linux搭建kafka
    PHP信号管理
    virtual memory exhausted: Cannot allocate memory
    RSA 非对称加密,私钥转码为pkcs8 错误总结
    Git Flow 工作模型与使用
  • 原文地址:https://www.cnblogs.com/Hydrogen-Helium/p/11737283.html
Copyright © 2011-2022 走看看