今天给大家带来的是数位dp的题目,这类题目相关性比较强,大家可以看我之前的作品进行学习,同时今天会给大家带来几个我在做题中遇到的问题,可以更好的帮助大家理解。
注意点⚠️:
1.首先对于前导0的是否要放到dp数组中:可以看到昨天的二进制问题并没有在dp数组中处理st问题,但是今天的第一道问题处理了st问题,对于是否要加上前导0的判断,关键就是要在题意处理的条件是否与前导0相关,说白了就是前导0是否会影响正常计算。比方说在二进制中,题目只要求求解出1的个数,所以前导0并不会影响结果。而在第一道题中,计算两位数字之间的差值,那么对于数字前最开始的0就要进行判断用bool st。具体的分析可以看第一题的代码。
2.对于i和pos位置的理解:i表示的是当前正在处理数位也就是a[pos]的取值。
第一题
题目描述
小明以他的名字命名了一种数——小明数。
对于一个十进制的数,若任意相邻两个数位的差值的绝对值不超过 KK,则称其为小明数。
现小明给出一个区间 L,R,求区间L,R 一共有多少个小明数。
输入描述
第 1行为一个整数 T,表示测试数据数量。
输入仅一行,包含三个整数 K,L,R。
1≤T≤105, 0≤K≤9, 1≤L≤R≤109。
输出描述
输出共 T 行,每行一个整数,表示每组数据的答案。
#include <bits/stdc++.h>
using namespace std;
using ll= long long;
ll dp[20][2][20][20];
ll a[20],k;
ll dfs(int pos,bool limit,bool st,int is,int p1){
if(pos<0)return is==1;
if(!limit&&dp[pos][st][is][p1]!=-1)return dp[pos][st][is][p1];
ll res=0;
int up=limit?a[pos]:9;
for(int i=0;i<=up;i++){
res+=dfs(pos-1,limit&&(i==up),st||(i>0),!st||is&&(abs(p1-i)<=k),i);
}
if(!limit)dp[pos][st][is][p1]=res;
return res;
}
ll f(ll c){
int pos=0;
while(c){
a[pos++]=c%10;
c/=10;
}
return dfs(pos-1,true,0,1,0);
}
int main()
{
int t;
cin>>t;
while(t--){
memset(dp,-1,sizeof(dp));
int l,r;
cin>>k>>l>>r;
cout<<f(r)-f(l-1)<<'\n';
}
return 0;
}
易错点:
1.memset的位置放置错误,一般的memste放在循环外就可,但是对于这道题来说k的值在变,所以需要放到里面。
2.is的初值为1,是考虑了前置0对于is的影响
第二题
问题描述
在一个神奇的国度里,有一个叫做小熊的数学家。有一天,小熊接到了国王的任务,要求他找出从 1 到 n 的数中,满足每一位上的数字乘积小于等于 m 的数字个数。
小熊感到十分困惑,他意识到,这个问题凭他现在的能力根本无法解决。于是他选择了向你求助。
请你需要帮助小熊,计算出满足条件的数字的个数。
输入格式
输入两个整数 n(1≤n≤1018) 和 m(1≤m≤109),含义如题所述。
输出格式
输出一个整数,表示从 1 到 n 的数中,满足每一位上的数字乘积小于等于 m 的数字个数。
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
map<ll,ll>dp[25][2];
int a[25];
ll m;
ll dfs(int pos,bool limit,bool st,ll sum){
if(pos<0)return st&&sum<=m;
if(!limit&&dp[pos][st].find(sum)!=dp[pos][st].end())return dp[pos][st][sum];
ll res=0;
int up=limit?a[pos]:9;
for(int i=0;i<=up;i++){
res+=dfs(pos-1,limit&&(i==up),st||(i>0),st?sum*i:i);
}
if(!limit)dp[pos][st][sum]=res;
return res;
}
ll f(ll c){
int pos=0;
while(c){
a[pos++]=c%10;
c/=10;
}
return dfs(pos-1,true,0,0);
}
int main()
{
ll n;
cin>>n>>m;
cout<<f(n)<<'\n';
return 0;
}
代码错误处:
1.if(pos<0)return st&&sum<=m;这个地方第一次写成if(st&&pos<0&&sum<=m)这样写是没有正确的返回值的,所以会产生运行错误。
2.map<>dp[][]记忆化要写成dp[][][sum],不要忘掉sum的部分
易错点:
1.
dp[pos][st][sum]
的访问逻辑
1. dp[pos]
- 取出
dp
数组中索引为pos
的元素,类型为map<int,int>[2]
(即长度为 2 的map
数组)。
2. dp[pos][st]
- 从
map<int,int>[2]
数组中取出索引为st
的元素,类型为map<int,int>
。 st
只能是0
或1
,对应前导零状态和有效数位状态。
3. dp[pos][st][sum]
- 对
map<int,int>
类型的元素执行下标操作[sum]
,相当于:find(sum)
:查找键为sum
的元素。- 若存在,返回对应的值;若不存在,插入键
sum
并返回默认值(int
类型默认值为0
2.使用map<int,int>dp[][]存储而不是dp[][][sum]
map
的优势:动态存储稀疏状态
仅存储有效状态
map<int,int> dp[23][2]
中,每个map
仅存储实际出现的sum
值。例如:- 前导零状态(
st=0
)下,sum
始终为0
,只需存储键0
。 - 有效数位状态(
st=1
)下,sum
为各位乘积,仅当sum≤m
时有效(m≤1e9
),键值范围大幅缩小。
- 前导零状态(
说白了用map存储可以规避超过m的部分,同时因为数字很大,对于有些包含前置0的部分可以不存入dp里面,也就是sum不存在的情况,这是普通dp定义无法做到的。
3.dp[][].find(sum)!=dp[][].end()怎么理解
dp[pos][st].find(sum) != dp[pos][st].end()
:dp[pos][st]
是一个map
,存储该状态下的乘积结果。find(sum)
查找键sum
是否存在:- 若存在,返回的迭代器不等于
end()
,条件成立。 - 若不存在,返回
end()
,条件不成立。
- 若存在,返回的迭代器不等于
好了,今天的分享就到这里,对于这部分题目的难度其实还是很大的,希望大家可以关注收藏好好看看,这部分对于我们的提升还是很大的。