Windy数
题目描述
核心思路
题目说不含前导0,比如15是Windy数,加上前导0,那么015就不是Windy数了。数位DP的解题套路:
- 利用前缀和思想,欲求区间[l,r]中含有Windy数的个数,那么我们可以先求出区间[0,r]中含有Windy数的个数,然后再求出区间[0,l-1]中含有Windy数的个数,相减得到的就是区间[l,r]中含有的Windy数的个数了。
- 分类填数。设整数 x x x一共有n位,设 x x x表示为 a n a n − 1 a n − 2 ⋯ a 2 a 1 a_na_{n-1}a_{n-2}\cdots a_2a_1 anan−1an−2⋯a2a1,从高位到低位依次枚举填数。因为不含前导0,因此最高位就只能填1到 a n a_n an,其他位可以填0到 a i a_i ai。每个位上填数时,有两个分支,分为两类:左分支是0到 a i − 1 a_i-1 ai−1和 a i a_i ai,这样填数就可以保证不超过 x x x。
为什么预处理时,对于i=0还需要处理呢?即为什么是i=0;i<=9;i++而不是i=1;i<=9;i++呢?
比如 f [ 2 ] [ 2 ] f[2][2] f[2][2]表示一共有两位数字,且最高位数字是2(即十位数字是2),设原数为22,那么最低位(即个位)可以是0,1,2。对于最低位是0时,得到数字20,很明显这个数字是满足Windy数的。统计 f [ 2 ] [ 2 ] f[2][2] f[2][2]时会用到 f [ 1 ] [ 0 ] f[1][0] f[1][0]。因为当进入if语句时,会执行 f [ i ] [ j ] = f [ i ] [ j ] + f [ i − 1 ] [ k ] f[i][j]=f[i][j]+f[i-1][k] f[i][j]=f[i][j]+f[i−1][k],即 f [ 2 ] [ 2 ] = f [ 2 ] [ 2 ] + f [ 1 ] [ 0 ] f[2][2]=f[2][2]+f[1][0] f[2][2]=f[2][2]+f[1][0],等式右边的 f [ 2 ] [ 2 ] f[2][2] f[2][2]取的是全局的0,假设我们不对 f [ i ] [ 0 ] f[i][0] f[i][0]做预处理,那么此时 f [ i ] [ 0 ] f[i][0] f[i][0]用的也是全局的0,也就是说 f [ 1 ] [ 0 ] = 0 f[1][0]=0 f[1][0]=0。那么得到的 f [ 2 ] [ 2 ] = 0 + 0 = 0 f[2][2]=0+0=0 f[2][2]=0+0=0,那么就会错过数字20没有被算进去了。
如何理解last初始化为-1呢?
首先这个数不含前导0,那么一个数的最高位就肯定是从 ≥ 1 \geq 1 ≥1开始了,为了使得能够从最高位开始进入循环,我们来看这个条件:if(abs(j-last)>=2),对于当前的最高位,如果来到了这个语句,说明j=1,那么要想使得进入if语句,则要满足abs(j-last)>=2,即abs(1-last)>=2,因此解得 l a s t ≤ − 1 last\leq -1 last≤−1,于是只要last的初始值是 ≤ − 1 \leq-1 ≤−1的就行了。
如何理解j=(i==nums.size()-1)呢?
这句话的意思是说,nums.size()-1是最高位,如果 i = = n u m s . s i z e ( ) − 1 i==nums.size()-1 i==nums.size()−1,说明i指向的是最高位,如果i指向的是最高位,那么 i = = n u m s . s i z e ( ) − 1 i==nums.size()-1 i==nums.size()−1就成立,返回1,因此j=1,那么以后就处理后面的低位了,不会再判断是否有 i = = n u m s . s i z e ( ) − 1 i==nums.size()-1 i==nums.size()−1了,而是j++了。如果不成立则返回0,说明最高位是0.写这句话其实就是为了特殊处理最高位是0的情况。其实也可以这么写:
int flag==num.size()-1;
for(int j=flag;j<x;j++)
如何处理不含前导0的情况呢?
低于n位数的数要特判,首位数不能是0。
处理前导0的代码是什么意思,低于n位数的数要特判是什么意思?
和数字游戏的区别:数字游戏它的左分支是0~x-1,可以取0,也就是说数字游戏它的高位可以是0,比如0013,也就是说数字游戏它是再处理位数是nums.size()的时候,就已经把小于位数小于num.size()的情况算进去了,比如0013,虽然处理位数看起来是4位,但有前导0,真正有效的位数是是后面两位(13),因此,数字游戏那一题就不需要对含有前导0(可以理解为位数小于num.size的数)特殊处理了。但是这一题不一样,题目说了Windy数是不含有前导0的,比如0013,它含有前导0,它的位数是4位,但是13它是满足Windy数的,它的位数是2位,因此,我们需要对小于4位的0013来看看有没有满足Windy数的,因此,最后需要特殊处理位数小于num.size的数。
代码
写法1
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
//因为题目给的整数最大是2e9,那么最多是10位,所以这里开大一些取11位
const int N=11;
int f[N][N];//f[i][j]表示一共有i位且最高位数字是j的Windy数的个数
//预处理出Windy数的个数
void init()
{
for(int j=0;j<=9;j++)
f[1][j]=1;
for(int i=2;i<N;i++)//阶段:枚举位数,从有2位数一直枚举到有N位数
{
for(int j=0;j<=9;j++)//枚举前面一位
{
for(int k=0;k<=9;k++)//枚举后面一位
{
if(abs(k-j)>=2)
f[i][j]+=f[i-1][k];
}
}
}
}
int func(int n)
{
//特判n是0,则返回0
if(!n)
return 0;
vector<int>nums;
int res=0;//答案
int last=-1;//记录上一位的数字
while(n)
{
nums.push_back(n%10);
n/=10;
}
//处理位数是nums.size()的数
for(int i=nums.size()-1;i>=0;i--)
{
int x=nums[i];//取出第i位的数
//处理左分支,可选的数是0<=j<=x-1
for(int j=(i==nums.size()-1);j<x;j++)
{
//比如原数num=7598 下标就是[3,2,1,0] 当前i=3,假设我们写成了f[i][j],即f[3][j]
//那么就表示一共有3位且最高位数字是j的Windy数的个数 但是很明显7598是4位
if(abs(j-last)>=2)
res+=f[i+1][j];
}
//处理右分支,右分支选定了x这个数,则右分支固定了x这个数,如果满足条件,则可以往右分支走
if(abs(x-last)<2)//剪枝
break;
last=x;//更新last
if(!i)//如果成功走到了最右下角,说明n这个数它自身也是一个Windy数
res++;
}
//处理位数是小于nums.size()的数,这里可以看作是特殊处理有前导零的数
//因为本题中特殊规定了,windy数不能包含前导零。如果把位数较低的数看成是包含前导零,那么首位就不能是1
//但windy数的首位可以是1。
for(int i=1;i<nums.size();i++)//i是枚举位数 从有1位枚举到有nums.size()-1位
{
//由于是特殊处理有前导零的数,因此j不是从0开始,而是从1开始,表示最高位是>=1
for(int j=1;j<=9;j++)
{
//为什么不是写成f[i+1][j],那是因为i的下标从1开始取了,如果从0开始取,那么还是要写成f[i+1][j]
//比如原数num=598,那么下标为[3,2,1] 假设此时i=3,如果我们写成了f[i+1][j],即f[4][j],
//那么表示一共有4位且最高位数字是j的Windy数的个数,但是很明显598只有3位
res+=f[i][j];
}
}
return res;
}
int main()
{
init();
int l,r;
scanf("%d%d",&l,&r);
cout <<func(r)-func(l-1)<<endl;
return 0;
}
写法2
#include<iostream>
#include<algorithm>
using namespace std;
const int N=11;
int a[N];
int f[N][N];
void init()
{
for(int j=0;j<=9;j++)
f[1][j]=1;
for(int i=2;i<N;i++)
{
for(int j=0;j<=9;j++)
{
for(int k=0;k<=9;k++)
{
if(abs(k-j)>=2)
f[i][j]+=f[i-1][k];
}
}
}
}
int func(int n)
{
//特判,如果n==0,则返回0
if(!n)
return 0;
int cnt=0;//数组a中的元素个数
while(n)
{
a[++cnt]=n%10;
n/=10;
}
int res=0;//答案
int last=-1;//记录上一位的数字
//从高位枚举到低位
for(int i=cnt;i>=1;i--)//答案是cnt位的
{
int now=a[i];//取出第i位的数字
int flag=i==cnt;//判断是否是最高位
//走左侧分支,可选数的范围是[j,now-1]
for(int j=flag;j<now;j++)
{
if(abs(j-last)>=2)
res+=f[i][j];//累加答案
}
//走右侧分支
if(abs(now-last)<2)//剪枝
break;
last=now;//更新last
//如果走到了最右下角,说明n这个数它自身也是Windy数
if(i==1)
res++;
}
//答案小于cnt位的
for(int i=1;i<cnt;i++)
{
for(int j=1;j<=9;j++)
res+=f[i][j];
}
return res;
}
int main()
{
init();
int l,r;
scanf("%d%d",&l,&r);
cout <<func(r)-func(l-1)<<endl;
return 0;
}