数位DP算法学习总结

一、数位dp简述+模板

数位dp是一种计数时使用的动态规划算法,一般是要统计一个区间 [left, right] 内符合给定条件数字的个数,例如HDU 2089 不要62中的统计给定区间内不包含4以及62数字的个数,数位dp其实是暴力枚举算法的优化,通过过滤一些数字,以及记忆化(以下会讲)一些数据,达到优化时间复杂度。

首先 dfs(int step, int state, bool lead, bool limit) 函数有四个参数

  • step:表示当前枚举到第几位数,例如1-100,先枚举百位数的数字,再枚举十位数的数字,最后枚举个位数的数字

  • state:状态条件,用于记忆化,比如我们要统计[1,9299]之间不含4数字的个数,按理说我们先应该先从1开始枚举,当枚举到1000的时候,此时我们已经知道到了当只有三位数时,不含4数字的个数,那么当我们在枚举千位的数字时,当千位数字为1时,我们已经知道了其后三位数不含4数字的个数,当千位数字来到2时,我们可以直接返回结果,因为千位数1和2均不含4,那么千位数1和千位数2后面三位数中不论怎么搭配,不含数字4的个数一定是一样的,如此可以记忆化数组,避免重复性的计算。

  • lead:前导零,某些问题中前导零如00xxx的情况也会影响结果,所以要设置一个bool变量lead,当然,在某些问题下,前导零并不影响结果,所以可以不使用它

  • limit:限制标记,我们要枚举所有数字,这些数字一定是有上界的,例如上述我们要枚举[1,9299]之间的数据,当千位数为9时,显然百位数最高只能枚举到2,否则就超出了枚举的上界,但当千位数为8时,此时没有上界可言,因为千位数为8,不论其后三位数是什么都不会超出9299的范围

其次,solve(int x) 的作用是取出一个数的各个位数,用于dfs的遍历

以下是数位dp的模板:

#include<iostream>
using namespace std;
int a[20]; //存储数字位数
int dp[20][2]; //记忆化数组

int dfs(int step, int state, bool lead, bool limit) {
    if (step == -1) return 1; //当遍历到超出数字位数时,说明此情况可行,返回1
    if (!limit && !lead && dp[step][state] != -1) //记忆化,节省重复计算
        return dp[step][state];
    int up = limit ? v[step] : 9;  //取得上界
    int ans = 0; //开始计数
    for (int i = 0; i <= up; i++) {
        if (条件1) ...
        if (条件2) ...
        ans += dfs(step - 1, /*状态转移*/, lead && i == 0, limit && i == v[step]);
    }
    if (!limit && !lead) //在一定条件下记录结果,对应上面记忆化
        dp[step][state] = ans;
    return ans;
}
int solve(int x) {
    int pos = 0;
    while (x) { //取出各个位的数字,存在a中
        a[pos++] = x % 10;
        x /= 10;
    }
    memset(dp, -1, sizeof(dp)); //初始化dp数组值为-1
    return dfs(pos - 1, /*状态*/, true, true); //刚开始比最高位还高的数字为0,所以有前导零,并且有限制
}
int main() {
    int le, ri;
    while (cin >> le >> ri) {
        int ans = solve(ri) - solve(le - 1);
        cout << ans << endl;
    }
    return 0;
}

二、HDU 2089 不要62 数位dp题解

state变量应用限制条件最强的来记忆化,例如此题不存在62限制为两位,不存在4限制只有一位,所以state标记为此位是否为6,假设求解[1,100]中符合条件的个数,如果state表示为is_four,那么十位为5和十位为6其后个位数符合条件的数字是一样的,因为5和6都不是4,但当十位为6时,个位就不能为2(这是题目要求的),所以就造成了数据不一致的情况,以此得出state变量应用限制条件最强的来进行记忆化。

#include<iostream>
#include<vector>
using namespace std;
int a[20];
int dp[20][2];

int dfs(int step, int pre, bool is_six, bool limit) { 
    //pre为前一位数字,用于判断是否存在62,此处is_six就是模板中的state,表示当此位为6和不为6时,后续的结果不同
    if (step == -1) return 1;
    if (!limit && dp[step][is_six] != -1) 
        return dp[step][is_six];
    int up = limit ? a[step] : 9;
    int ans = 0;
    for (int i = 0; i <= up; i++){
        if (i == 4) continue; //存在4则跳过
        if (i == 2 && pre == 6) continue; //存在62则跳过
        ans += dfs(step - 1, i, i == 6, limit && i == a[step]);
    }
    if (!limit) 
        dp[step][is_six] = ans;
    return ans;
}
int solve(int x) {
    int pos = 0;
    while (x != 0) {
        a[pos++] = x % 10;
        x /= 10;
    }
    memset(dp, -1, sizeof(dp)); //初始化dp数组值为-1
    return dfs(pos - 1, -1, 0, true);
}
int main() {
    int n, m;
    while (cin >> n >> m && n != 0 && m != 0) {
        int ans = solve(m) - solve(n - 1);
        cout << ans << endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sumzeek丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值