学个数位DP,差点怀疑人生!
首先是题目类型:
对于限制条件check(x),求[l,r]之间满足check(i)的个数
朴素算法自然是
for (int i = l;i <= r; i++) {
res += check(i);
}
cout << res << endl;
自然,面对1e12这类离谱数据暴力是解决不了问题的(但时间不够时首选)
这是,数位DP便站了出来
既然[l,r]这个区间直接递推不好处理,那[1,x]呢?
如果这个结果满足ans = [1,r] - [1,l - 1],那直接用前缀和思想+递推是不是就能直接搞定?
但问题就是,递推什么?
首先,1和r唯一的区别就是长度了 ,可以以数字长度,也就是数位作为一个元素pos。
每一位能取到0~9十位数字,依次循环搜索的话会出现bug(比如[1,123],会循环999)问题
出在上一位,如果本位的上一位是r的对应位数字,那么这里只能选择[0,r的对应位]。
如 r = 3574
在35??时,第三位只能选择0~7
但34??时,第三位能选择0~9
在347?时,虽然第四位的上一位是r对应位的数,但第二位不是,所以能取0~9。
所以我们需要另外的一个布尔变量limit表示高位限制
如果限制前导零,则前几位是0的数需要特殊判断,如果直接枚举太浪费时间,可以引入一个新布尔变量st专门处理前导零问题
自此,数位dp三大基本元素已经处理完毕,剩余的就是专门处理check的变量
我们可以构造出模板了
long long dfs (int pos,bool st,bool limit) {
if (!pos) return 1;
int rex = limit ? num[pos] : 9;
long long ans = 0;
for (int i = 0; i <= rex; i++) {
ans += dfs(pos - 1,st && (i == 0),limit && (i == rex));
}
return ans;
}
long long part (long long x) {
len = 0;
while (x) {
num[++len] = x % 10;
x /= 10;
}
return dfs(len,1,1);
}
接下来就是例题了
我们可以分别对0~9十位数字求出现次数,并用part(r) - part(l - 1),
很明显,这个数字需要用专门的变量记录,暂定为x好了。
不过对于222来说,我们需要他返还3,表示这里有三个3,单纯的到了就返回1显然不行,所以需要另一个变量sum记录发现的所需数字个数。
#include <iostream>
#include <bits/stdc++.h>
using namespace std;
long long l,r;
int len;
int num[15];
long long dp[15][15];//dp标配,记忆化搜索
long long dfs (int pos,int sum,int x,bool st,bool limit) {//pos第几位,sum有几个,x表示找谁,st标记前导零,limit标记高位限制
if (!pos) return sum;//边界
if (!limit && !st && dp[pos][sum] != -1) return dp[pos][sum];//limit和st是限制,-1在之前memset时初始化过
int rex = limit ? num[pos] : 9;//本位可取最大值
long long ans = 0;//结果
for (int i = 0; i <= rex; i++) {
ans += dfs(pos - 1,sum + ((!st || i) && (i == x)),x,st && (i == 0),limit && (i == rex));//自行理解吧,实在不行用if也行
}
if (!limit && !st) dp[pos][sum] = ans;
return ans;
}
long long part (long long x/*[1,x]*/,int i/*找谁*/) {
len = 0;
while (x) {
num[++len] = x % 10;
x /= 10;
}//预处理分解原数,好直接锁定第x位的数字
memset(dp,-1,sizeof(dp));//初始化
return dfs(len,0,i,1,1);
}
int main () {
cin >> l >> r;
for (int i = 0; i <= 9; i++) {
cout << part(r,i) - part(l - 1,i) << " ";
}
cout << endl;
return 0;
}