数位DP——学习笔记

1 篇文章 0 订阅

题目特征

数位DP题目一般都是 [ l , r ] [l,r] [l,r]间满足某种情况的数有几个, 一般 l , r l,r l,r都比较大
或者直接就是算 [ 1 , N ] [1,N] [1,N]

题目解法

数位DP虽然是DP,但是大部分题都能用一个相同的方法来解决
虽然很多题目能用递推写,但是递推难度较记忆化搜索较高,因此最好选用记忆化搜索来写题

模板

ll dfs(int pos, ... , bool lead, bool isMax) {//当前位pos, ...为省略条件, lead判前导零, isMax判前几位数是否选的都是最大值
    if (!pos) return 1;//此处为越过一个数的最后一位(最小的一位), 如123, 越过3这里说明当前已经是123了,所以只有一个数
    //有时候不一定都是返回1,看条件
    if (!isMax && !lead && dp[pos][...][...] != -1) return dp[pos][...][...];//记忆化, 如果有直接返回
    int up = isMax ? a[pos] : 9; //如果一直是最大, 当前位最多也就是a[pos], 超过了就大于这个数了
    ll res = 0;
    for (int i = 0; i <= up; i++) {
        //按限制条件来
        //如, 判是否有前导0
        if (lead) res += dfs(pos - 1, ... , !i, isMax && i == a[pos]);
        else res += dfs(pos - 1, ... , 0, isMax && i == a[pos]);
    }
    return isMax || lead ? res : dp[pos][...][...] = res;//如果有前导0或者是前几位都是最大, 直接返回
    //否则dp[pos][...][...]记录值再返回
}

ll calc(ll x) {
    int pos = 0;
    while (x) a[++pos] = x % 10, x /= 10;//数字用数组表示
    return dfs(pos, ... , 1, 1);
}

练习题目

最经典的也是最简单的入门题就是

1. HDU2089 不要62

题意简单,求 [ l , r ] [l, r] [l,r]内有多少数满足,不存在4并且不存在62连号

写起来也非常简单:

ll dfs(int pos, int pre, bool lead, bool isMax) {//当前位, 前一个数是什么, 前导0, 前几位都是最大
    if (!pos) return 1;//如果到最后了,因为这里循环里面判了条件了, 所以最后只有合法情况, 那么就是1
    //如果不合法了,显然返回0
    if (!isMax && !lead && dp[pos][pre] != -1) return dp[pos][pre];
    int up = isMax ? a[pos] : 9;
    ll res = 0;
    for (int i = 0; i <= up; i++) {
        if (i == 4) continue;//有4不要
        if (pre == 6 && i == 2) continue;//有62不要
        if (lead) res += dfs(pos - 1, !i ? -1 : i, !i, isMax && i == a[pos]);//有前导0
        else res += dfs(pos - 1, i, 0, isMax && i == a[pos]);//无前导0
    }
    return isMax || lead ? res : dp[pos][pre] = res;
}

ll calc(ll x) {
    int pos = 0;
    while (x) a[++pos] = x % 10, x /= 10;
    return dfs(pos, -1, 1, 1);
}

2. [SCOI2009]windy数

[ l , r ] [l, r] [l,r]间有多少数满足相邻两个数字之差至少为2

比起上一题来说其实也没有难太多,也是简单题:

ll dfs(int pos, int pre, bool lead, bool isMax) {
    if (!pos) return 1;//最后都是符合情况的
    if (!isMax && !lead && dp[pos][pre] != -1) return dp[pos][pre];
    int up = isMax ? a[pos] : 9;
    ll res = 0;
    for (int i = 0; i <= up; i++) {
        //情况分两种
        //一是有前导0, 即前面没有数, 那么可以直接往下搜
        //二是没有前导0, 那么我们只有在前后两个数差大于等于2的时候才会去搜
        if (lead) res += dfs(pos - 1, !i ? -1 : i, !i, isMax && i == a[pos]);
        else if (abs(i - pre) >= 2) res += dfs(pos - 1, i, 0, isMax && i == a[pos]);
    }
    return isMax || lead ? res : dp[pos][pre] = res;
}

ll calc(ll x) {
    int pos = 0;
    while (x) a[++pos] = x % 10, x /= 10;
    memset(dp, -1, sz(dp));//注意此处,为什么每次搜前都初始化呢
    //不要62这题 初始化一次就可以, 那是因为不要4和不要62连号对于每一个数来说都是同样的限制条件
    //但是这里不一样, 相邻两个数差为2这个限制条件对于每个数来说是不一样的
    return dfs(pos, -1, 1, 1);
}

3. [CQOI2016]手机号码

求的条件为 至少3个相邻的数字相同, 不能同时出现8和4

这里稍微要复杂一点点,但是处理起来一样的:

ll dfs(int pos, int pre, int ppre, bool three, bool four, bool eight, bool lead, bool isMax) {
    if (four && eight) return 0;//如果同时出现4和8,直接返回0
    if (!pos) return three;//到达最后,如果有3连返回1,没有返回0
    if (!isMax && pre != -1 && ppre != -1 && dp[pos][pre][ppre][three][four][eight] != -1) return dp[pos][pre][ppre][three][four][eight];
    int up = isMax ? a[pos] : 9;
    ll res = 0;
    for (int i = 0; i <= up; i++) {
    	//一样的处理,分前导0和无前导0
        if (!lead) res += dfs(pos - 1, i, pre, three || (i == pre && i == ppre), four || i == 4, eight || i == 8, 0, isMax && i == a[pos]);
        else res += dfs(pos - 1, !i ? -1 : i, -1, 0, i == 4, i == 8, !i, isMax && i == a[pos]);
    }
    return isMax || pre == -1 || ppre == -1 ? res : dp[pos][pre][ppre][three][four][eight] = res;
}

ll calc(ll x) {
    int pos = 0;
    while (x) a[++pos] = x % 10, x /= 10;
    return dfs(pos, -1, -1, 0, 0, 0, 1, 1);
}

4. 烦人的数学作业

[ l , r ] [l,r] [l,r]内每个数的数字和,如123这个数的数字和为1+2+3=6

这里我们单独统计每一个数字:

ll dfs(int pos, int dig, int cnt, bool lead, bool isMax) {//当前位, 选的数字, 前面出现几次了, 前导0, 最大
    if (!pos) return cnt;//到达最后返回这个数在前面出现的次数
    if (!isMax && dp[pos][cnt] != -1) return dp[pos][cnt];
    int up = isMax ? a[pos] : 9;
    ll res = 0;
	for (int i = 0; i <= up; i++)//这里我当时写的时候简写了,照样分2种,有无前导0
		res = (res + dfs(pos - 1, dig, cnt + (dig == i && (!lead || i)), lead && !i, isMax && i == 			a[pos])) % mod;
    return isMax || lead ? sum : dp[pos][cnt] = sum;
}

ll calc(ll x, int dig) {
    int pos = 0;
    while (x) a[++pos] = x % 10, x /= 10;
    memset(dp, -1, sz(dp));//条件对于每一个数dig来说都是不一样的,所以每次都初始化
    return dfs(pos, dig, 0, 1, 1);
}
//最后main函数里面这样统计即可
		ll ans = 0;
        for (int dig = 1; dig <= 9; dig++) {//0不用算
            ll x = (calc(qr, dig) - calc(ql, dig) + mod) % mod;
            ans = (ans + x * dig % mod) % mod;
        }

5. [ZJOI2010]数字计数

上一题的双倍经验,一样的

6. [HAOI2010]计数

最开始会想不到这是数位DP,思路比较神奇的题,我没用记忆化搜索来做

剩下继续找题写就完事了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值