【数位DP-递归法 入门 与 一些数位dp题目】一看就会,一学不废

前置知识

什么是dfs

 dfs,深度优先搜索(Depth First Search),是一种搜索的策略。

如果dfs不懂的话,可以看看一些dfs的题目,这里直接现学可能比较难。

什么是dp

DP,动态规划(Dynamic Programing),根据状态与状态转移方程进行状态转移。
性质:最优化原理(最优子结构性质),无后效性,子问题的重叠性

dp是什么不大懂的话,可以从一些基础dp学起,数位dp可能稍难些。

数位DP介绍

数位DP是解决以下类似的题目的:
求一个区间[L,R] 之间所有符合一些特定性质的数的个数。

题目的抽象描述:
∑ x = L R f x f x = { 1 , 如 果 x    满 足 题 目 要 求 0 , 如 果 x    不 满 足 题 目 要 求 \sum_{x=L}^{R} f_x \\ f_x = \begin{cases} 1, & 如果 &x \;满足题目要求 \\ 0, & 如果 & x \;不满足题目要求 \end{cases} x=LRfxfx={1,0,xx
题目的人文描述:(?)
数位DP解决的是一个区间范围内(区间断点可能大至101000),有多少个数满足 题目给定的要求 的问题。

例题1:不要62

不要62 - ZJNU1160

杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有4或62的号码。例如:
62315 , 73418 ,88914都属于不吉利号码。
但是,61152虽然含有6和2,但不是62连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。

人话翻译:求给定区间[n,m]有多少个数字是里面没有4并且没有连续的62的
数据范围 n、m(0<n≤m<1000000)
在这里插入图片描述

* 解题框架 *

暴力搜索肯定不行,我们来搞搞数位DP的递归做法

首先,我们从搜索范围下手。
如果我们搜索了

[1,m] 区间的答案为s[1,m],
[1,n-1] 区间的答案为s[1,n-1],

那么答案易得:(与差分数组原理类似,想想为什么)
s[n,m] = s[1,m] - s[1,n-1]

那我们问题简化到求区间[1,n]有多少个数字是里面没有4并且没有连续的62

搜索上界

接下来考虑搜索的数字范围。
首先一个数的位数为sz
我们定义一个数的最左边为最高位sz,从左往右依次递减到1,1为最后一位
【区分字符串最左位下标为0,数字最左位下标为sz】

每位数字的下界为0易得。我们分析上界:

如果我们搜索数字上界为 123
我们目前处理的位数pos为sz(即还没有开始处理选择)

我们有两种选择:最高位为0 或者 1
【0表示前导0 ,依据题目看前导0是否影响答案,若影响需额外标记】
【1是因为数字第pos位上界为1,并且现在是限制阶段
【若选择2 ~ 9 ,则表示sz位(百位)大于1,明显超过上界,不行】

接下来,选择第二位。
如果第一位选择了1,则接下来仍然是限制阶段,上界最大为2
如果第一位选择了0,则接下来不是限制阶段,上界最大为9

。。。

分析可得:
【1】
到pos位,如果pos-1位是限制阶段并且pos-1位选择了该位的上界,则这一位仍然是限制阶段上界为上界数字的pos位
【2】
到pos位,如果pos-1位是限制阶段并且pos-1位没有选择了该位的上界,则这一位不再是限制阶段上界为9
【3】
到pos位,如果pos-1位不是限制阶段则这一位也不是限制阶段上界为9

满足条件的判定

如果枚举第pos位的数字,是4则直接跳过,就可以轻易处理掉4对答案的影响。
但是62需要判断连续的两位数字,怎么办?我们用一种很简单的方法:

【简单的方法】
如果该位选择了6 ,则 下一次dfs(pos-1)时 ,pre 为 true
如果该位没选择6 ,则 下一次dfs(pos-1)时 ,pre 为 false
如果pre为true ,则表示上一位选择了6 ,这位不能选择2

记忆化递归

由于是dp,我们需要记忆化递归,否则时间复杂度仍然特别高。
我们需要记录所有的特殊的状态,作为dp数组的不同维度。

这题不同的状态为:
pos:位数
pre:pos-1位有没有选择6

【注意】
如果该位为限制阶段,则不能记录状态!因为上限不定,不能简单记录。

递归终止条件

因为我们每次都不对错误数据进行搜索
因此搜索到尽头即表示一种合法答案!

代码处理

所有的思维全都在上面啦!我们只需要(?)敲成代码即可!

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <bits/stdc++.h>
#define show(x) std::cerr << #x << "=" << x << std::endl
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
#define fin freopen("in.txt", "r", stdin)
#define fout freopen("out.txt", "w", stdout)
#define ll long long
using namespace std;

const int MAX = 20;
const ll MOD = 998244353;
const int INF = 0x3f3f3f3f;
const double EPS = 1e-5;

int dig[MAX];
ll dp[MAX][5];

ll dfs(int pos ,bool pre ,bool limit){
    if(pos==0)return 1;					///搜索尽头
    if(!limit && dp[pos][pre])return dp[pos][pre];	///记忆化递归
    int up = limit ? dig[pos] : 9 ;			///搜索上界
    ll res = 0;
    for(int i=0;i<=up;++i){
        if(pre && i==2)continue;			///不枚举62
        if(i==4)continue; 				///不枚举4
        res += dfs(pos-1 ,i==6 ,limit && i==up);	///下一位dfs
    }
    if(!limit)dp[pos][pre] = res;			///记忆化递归
    return res;
}

ll solve(ll n){
    ///memset(dp,0,sizeof(dp));
    int k =0;
    while(n){						///记录各个位的上界
        dig[++k] = n%10;
        n/=10;
    }
    return dfs(k,0,1);
}

int main()
{
    IOS;
    ll n,m;
    while(cin >> n >> m && n && m){
        cout << solve(m) - solve(n-1) << endl;
    }
    return 0;
}

例题2:萌数

萌数 - 洛谷

只有满足“存在长度至少为2的回文子串”的数是萌的
也就是说,101是萌的,因为101本身就是一个回文数;
110是萌的,因为包含回文子串11;
但是102不是萌的,1201也不是萌的。
现在SOL想知道从l到r的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。

数据范围 l,r < 101000
在这里插入图片描述

搜索上界

由于数字很大,我们需要使用字符串进行存储。
s[n,m] = s[1,m] - s[1,n-1]
注意此时字符串 n - 1 需要特殊处理即可

满足条件的判定

一个串是萌的,只需要其有以下两个连续子串之一即可:
【1】
aa ,即连续两个数字都相同,从00 到99
【2】
aba ,即一个数字和其上上一个数字相同,如010到919 ,中间b任意

因此我们枚举时需要一个bool类型pre
记录枚举到pos位的时候已经合法了吗

【注意】
由于数位DP含前导0,故可能0012也算作符合答案(但是实际却并不是)
我们需要判断该位前面是否有前导0

【1】
如果有前导0并且当前位枚举的也是0
那么我们继续的dfs(合法性不变
【2】
如果没有前导0
当前位枚举与上一位相同或者与上上一位相同
继续dfs(该串一定合法
【3】
如果之前串合法,该位枚举任意数字都合法(合法性不变)

记忆化递归

首先如果该位之前有前导0或者是限制阶段则不记录dp
否则记录所有状态:
pos 位置
q1 上一位的数字
q2 上上一位的数字
pre 是否已经合法

记录成dp[pos][q1][q2][pre]即可

代码处理

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <bits/stdc++.h>
#define show(x) std::cerr << #x << "=" << x << std::endl
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
#define fin freopen("in.txt", "r", stdin)
#define fout freopen("out.txt", "w", stdout)
#define ll long long
using namespace std;

const int MAX = 1050;
const ll MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const double EPS = 1e-5;

int dig[MAX];
ll dp[MAX][12][12][2];
int k;
ll dfs(int pos ,bool pre ,int q1 , int q2 ,bool limit,bool lead){
    if(pos==0)return pre;
    if(!lead && !limit && dp[pos][q1][q2][pre])return dp[pos][q1][q2][pre];		///dp记录
    int up = limit ? dig[pos] : 9 ;		///上界
    ll res = 0;
    for(int i=0;i<=up;++i){
        if(lead && i==0)			///前导0判断
            res += dfs(pos-1 ,pre ,-1 ,-1 ,limit && i==up , lead);
        else
            res += dfs(pos-1 ,pre || i==q1 || i==q2 ,i ,q1 ,limit && i==up , lead && !i);
        res%=MOD;
    }
    if(!lead && !limit)dp[pos][q1][q2][pre] = res;
    return res;
}

ll solve(string &n){
    memset(dp,0,sizeof(dp));
    k = n.size();
    for(int i=0;i<k;++i){
        dig[k-i] = n[i] - '0';
    }
    return dfs(k,0,-1,-1,1,1);
}

int main()
{
    IOS;
    string n,m;
    while(cin >> n >> m){
        if(n=="0" && m=="0")break;
        int wei = 1;
        int L = n.size();
        while(n[L-wei]=='0' && L>wei)n[L-wei]='9',wei++;	///n-1处理
        n[L-wei]-=1;
        if(n.size()!=1 && n[0]=='0')n = n.substr(1,L-1);
        cout << (solve(m) - solve(n)+MOD)%MOD << endl;
    }
    return 0;
}

例题3:烦人的数学作业

烦人的数学作业 - 洛谷

给出一个区间L~R,求L 到R 区间内每个数的数字和
如123这个数的数字和为1+2+3=6。
答案mod 1e9+7

数据范围1≤L≤R≤1018

在这里插入图片描述

搜索上界

由于上界可以使用long long 类型存储,我们不需要字符串处理。

【注意】
答案有取模,因此最终答案应为
(s[1,m] - s[1,n-1] + MOD)%MOD

(如果保险起见应该最好写成以下的形式:)
((s[1,m] - s[1,n-1])%MOD + MOD)%MOD

满足条件的判定

在范围内的所有数字都是符合要求的!
但是我们需要记录从第sz位处理到pos位的时候我们到底做出了多少贡献
比如已经枚举了00512 <- 此时贡献即为5+1+2 = 8

【提醒】
此时前导0对答案不作出贡献,因此前导0的bool类型可以省略

如果此时00512枚举完成了,返回答案即为 8 ,即贡献值

记忆化递归

我们需要的状态为:
pos 当前位置
shu 即到当前位置之前已经所做的贡献值

仍然仍然,在限制阶段不进行dp存储!

代码处理

#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <bits/stdc++.h>
#define show(x) std::cerr << #x << "=" << x << std::endl
#define IOS ios::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
#define fin freopen("in.txt", "r", stdin)
#define fout freopen("out.txt", "w", stdout)
#define ll long long
using namespace std;

const int MAX = 1050;
const ll MOD = 1e9+7;
const int INF = 0x3f3f3f3f;
const double EPS = 1e-5;

int dig[MAX];
ll dp[MAX][MAX];		///注意不要RE!
int k;
ll dfs(int pos ,ll shu ,bool limit){
    if(pos==0)return shu;				///返回贡献
    int up = limit ? dig[pos] : 9 ;
    ll res = 0;
    if(!limit && dp[pos][shu])return dp[pos][shu];
    for(int i=0;i<=up;++i){
        res+=dfs(pos-1,(shu+i)%MOD,limit && i==up);	///下一位贡献增加i
        res%=MOD;
    }
    if(!limit)dp[pos][shu] = res;
    return res;
}

ll solve(ll n){
    memset(dp,0,sizeof(dp));
    k = 0;
    while(n){
        dig[++k] = n%10;
        n/=10;
    }
    return dfs(k,0,1);
}

int main()
{
    IOS;
    ll n,m;
    int T;cin >> T;
    while(T--){
        cin >> n >> m;
        cout << (solve(m) - solve(n-1) + MOD)%MOD << endl;
    }
    return 0;
}

希望大家能更快、更好地AC!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值