数位DP
我们先由一个例子引入:
给定一个区间 1 n 1\text{~}n 1 n ,求在这个区间内,满足任意相邻位数之差大于2且不含前导零的数的个数
最暴力的办法就是枚举区间内每个数,然后判断每一个数是否满足条件即可。
好像在 n ≤ 1000000 n \le 1000000 n≤1000000 的时候都可以?
但是 n ≤ 2000000000 n \le 2000000000 n≤2000000000 的时候呢?
你可能会说:出题人有病吧,求这么大的区间没什么用。
我也这么认为
但是题目就在那里,暴力就是过不了。
于是就得请出今天的主角:数位DP!!数位DP,顾名思义,就是在数位上做的DP。
数位DP也是DP,那么DP如何转移呢?我们要记住,DP就是优化过的暴力。所以我们肯定是以数位作为状态。但是,我们发现一个问题,每一位有可能无法枚举到9。例如: 231 231 231 当百位是 2 2 2 时,十位无法枚举 4 4 4 和比 4 4 4 更大的数。所以在求解的时候,就应该判断一下最高位是否达到边界。所以我们要更新一下dp数组的定义:
定义 d p [ i ] [ j ] dp [i] [j] dp[i][j] 表示第 i i i 位,上一个状态为 j j j 的没有边界限制的满足条件的数的个数
需要解释几点
- j j j 既可以是前面有无满足条件,也可以是上一个数。反正痕灵活。
- 没有边界限制,也就是说最高位小于给定区间右端点,这样后面的位置就0 ~ 9十种可能,dp才具有普适性。
个人感觉这DP就是个记忆化。
当然,数位DP也有很多变形,但都八九不离十。相对于其他DP而言,数位有模板:
inline ll dfs(int pos/*当前位数*/,int sta/*上一个状态*/,bool limit/*是否有限制*/){
if(pos==0) return sta==1;//不一定等于1,要看最后求的状态是什么
if(!limit && f[pos][sta]!=-1) return f[pos][sta];//没有限制的情况下,用已经记忆化的数组优化时间
int up= limit?a[pos]:9;//确定上界
ll sum=0;
for(int i = 0;i <= up;i++){//枚举该位数字
if().../*满足条件*/
else...
}
if(!limit) f[pos][sta]=sum;/*更新 f 数组*/
return sum;
}
ll solve(int x){
cnt=0;
while(x) a[++cnt]=x%10,x/=10;
return dfs(cnt,-2,0,1);//最高位,limit为 1
throw;
}
int main(){
memset(f,-1,sizeof(f));//初始化,标记非法状态
scanf("%d%d",&L,&R);
printf("%lld",solve(R)-solve(L-1));//右端点减左端点
return 0;
}
可能还是不理解??没关系,有例题
「SCOI2009」windy 数
windy定义了一种windy数。不含前导零且相邻两个数字之差至少为2的正整数被称为windy数。 windy想知道,在A和B之间,包括A和B,总共有多少个windy数?
输入
包含两个整数,A B。
输出
一个整数。
样例输入
1 10
样例输出
9
输入样例二
25 50
输出样例二
20
数据规模和约定
20%的数据,满足 1 <= A <= B <= 1000000 。
100%的数据,满足 1 <= A <= B <= 2000000000 。
这不就是例题吗?废话
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
/*
sta==0,正常
sta==1,windy
f[i][sta]第 i 位, 条件为sta的,枚举的数字没有上界的方案数
*/
int f[50][50],a[50],L,R,cnt;
inline ll dfs(int pos,int pre,int sta,bool limit){
if(pos==0) return sta==1;
if(!limit && f[pos][sta]!=-1) return f[pos][sta];
int up= limit?a[pos]:9;
ll sum=0;
for(int i = 0;i <= up;i++){//枚举该位数字
if(abs(pre-i) < 2) continue;
if(sta==0&&i==0)
sum+=dfs(pos-1,-2,0,limit&&i==a[pos]);
else
sum+=dfs(pos-1,i,1,limit&&i==a[pos]);
}
if(!limit) f[pos][sta]=sum;
return sum;
}
ll solve(int x){
cnt=0;
while(x) a[++cnt]=x%10,x/=10;
return dfs(cnt,-2,0,1);//最高位,limit为 1
throw;
}
int main(){
memset(f,-1,sizeof(f));
scanf("%d%d",&L,&R);
printf("%lld",solve(R)-solve(L-1));
return 0;
}
「HUD2089」不要 62
题目描述
杭州人称那些傻乎乎粘嗒嗒的人为62(音:laoer)。
杭州交通管理局经常会扩充一些的士车牌照,新近出来一个好消息,以后上牌照,不再含有不吉利的数字了,这样一来,就可以消除个别的士司机和乘客的心理障碍,更安全地服务大众。
不吉利的数字为所有含有 4 4 4 或 62 62 62 的号码。例如:
62315 73418 88914 62315\quad 73418\quad 88914 623157341888914
都属于不吉利号码。但是, 61152 61152 61152 虽然含有 6 6 6 和 2 2 2 ,但不是 62 62 62 连号,所以不属于不吉利数字之列。
你的任务是,对于每次给出的一个牌照区间号,推断出交管局今次又要实际上给多少辆新的士车上牌照了。
输入
输入的都是整数对n、m(0<n≤m<1000000),如果遇到都是0的整数对,则输入结束。
输出
对于每个整数对,输出一个不含有不吉利数字的统计个数,该数值占一行位置
样例输入
1 100
0 0
样例输出
80
这也是模板之一。但是要引出一个优化:memset可以放在多次询问的外面。
为什么呢??
因为 f [ i ] [ j ] f[i][j] f[i][j] 表示的是数字的客观的属性。就算没有询问,该是什么数字是不会变的。所以可以放到外面
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
//f[i][0~2]第 i 位,前一个数是否满足条件的没有限制条件的不吉利方案数
ll f[50][50],a[50],cnt;
ll dfs(int pos,int sta,bool limit/*当前位上是否有限制*/){
if(pos==0) return sta==2;
if(!limit && f[pos][sta] != -1) return f[pos][sta];
int up= limit?a[pos]:9;
ll sum=0;
for(int i = 0;i <= up;i++){
if(sta==2||(i==4)||(i==2&&sta==1))
sum+=dfs(pos-1,2,limit && i==a[pos]);
else if(i==6)
sum+=dfs(pos-1,1,limit && i==a[pos]);
else
sum+=dfs(pos-1,0,limit && i==a[pos]);
}
if(!limit) f[pos][sta]=sum;
return sum;
}
ll solve(int x){
cnt=0;
while(x>0){
a[++cnt]=x%10;
x/=10;
}
return dfs(cnt,0,1);
}
int main(){
// freopen("1.in","r",stdin);
memset(f,-1,sizeof(f));
int L,R;
while(~scanf("%d%d",&L,&R) && (L+R)){
printf("%lld\n",R-L+1-solve(R)+solve(L-1));
}
return 0;
}