ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

数位DP 学习笔记

2019-10-25 12:04:59  阅读:198  来源: 互联网

标签:int pos 笔记 枚举 include 我们 DP 数位


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

毕竟我比较菜\kk


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

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

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

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

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

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

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

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

分两种情况考虑\(:\)

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

  • 我们枚举到的这一位依旧有上限,那么我们在枚举时只会枚举到上限处,所以在这一位不能记忆化记录这一位的答案,因为他不具备一般性,依旧是上面的例子,我们枚举到十位\(0\sim4\),这是求出的是\([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\n", solve(R) - solve(L - 1));
    }
    return 0;
 }

windy数

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

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

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

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

  • \(0\sim999\)

  • \(1000\sim4999\)

  • \(5000\sim5649\)

然后,为什么第三步要枚举到\(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\n", calc(b + 1) - calc(a)); // 因为我们之枚举到减一的位置,所以要整体向后移一位。
    return 0;
}

P4999 烦人的数学作业

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

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

这样我们设计\(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\n", ans);
    }
    return 0;
}

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

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

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

咕咕咕\(……\)

标签:int,pos,笔记,枚举,include,我们,DP,数位
来源: https://www.cnblogs.com/Hydrogen-Helium/p/11737283.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有