前段时间滚去搞了一波半期 然后学校又组织的五天西安游学 于是就一个星期都没有碰oi.....
终于回来了 学习数位DP 看了一下午 做了两道题 才感觉摸到一点火门
直接上两道题解吧
1.不要62
传送门:http://acm.hdu.edu.cn/showproblem.php?pid=2089
题意不多说 是中文都看得懂
为了方便阐述 我们把不含4,62的数称为吉利数 反之为非吉利数
这个题是在一个区间里找个数 所以我们考虑用前缀和的思想 只需找出1~m,1~n各自的个数 再相减就行了 于是这样只用关心上界了(命为work函数 work(x)可以求出【0,x】区间里的吉利数个数)
定义一下dp数组 这道题有什么明显的状态转移吗?注意到长度<=i位的吉利数个数 其实就是10个<=i-1位数的吉利数个数之和
(比如三位数即999 999就等于0~99,100~199,...900~999) 但是在这道题里要丢掉4 以及避免62的出现 这种细节后面再说
因此我们可以预处理出dp数组 由于后面有用 因此我们定义
dp[i][0]:<=i位的吉利数个数 dp[i][1]:长度为i 最高位是2的个数 dp[i][2]:<=i位的非吉利数个数
work函数直接看注释吧 很详细的 不想多讲了
不要灰心 用纸对着一个数字推演下 会看懂的 我们都看了很久才懂
#include<bits/stdc++.h>
using namespace std;
int n,m,dp[15][3],a[12];
//dp[i][0]:<=i位的吉利数个数
//dp[i][1]:长度为i 最高位是2的个数
//dp[i][2]:<=i位的非吉利数个数
void init()
{
dp[0][0]=1;
for(int i=1;i<=8;i++)
{
dp[i][0]=dp[i-1][0]*9-dp[i-1][1]; //dp[i-1][0]*9即0,1,2,3,5,6,7,8,9 dp[i-1][1]是把62筛除
dp[i][1]=dp[i-1][0]; //相当于就是在i-1位时的最高位添一个2
dp[i][2]=dp[i-1][2]*10+dp[i-1][0]+dp[i-1][1];
// dp[i-1][2]*10:先加上之前的非吉利数
// dp[i-1][0]:相当于加上i-1位最高位是2的数
// dp[i-1][1]:相当于在i-1位最高位是2的基础上添加6
}
}
int work(int x) //求0< <=x区间里的吉利数
{
int rec=x,tot=0,ans=0;//tot是数的长度 ans存储非吉利数的个数
bool flag=false;
while(x)
{
a[++tot]=x%10;
x/=10;
}
for(int i=tot;i>=1;i--)
{
ans+=dp[i-1][2]*a[i]; //先加上a[i]个i-1位数的非吉利数 比如231 分为2*100+31 先加上2个100里的不吉利数
if(flag) //如果首位是4 或 62
{
ans+=dp[i-1][0]*a[i]; //flag==true是在上一位判断的 所以这里是加上上一位 注意这个地方是 吉利数 因为考虑到之前加过不吉利数了
}
else
{
if(a[i]>4) ans+=dp[i-1][0]; //包含了4 因此要加上后面的i-1位 他们都属于不吉利数了
if(a[i]>6) ans+=dp[i-1][1]; //即62 注意这个地方dp[i-1][1]刚好是i-1位数首位是2开头的 和dp[i-1][0]不同 因此很巧妙
if(a[i+1]==6&&a[i]>2) ans+=dp[i][1]; //后一位可能是2
if(a[i]==4||(a[i+1]==6&&a[i]==2)) flag=true;
}
}
if(flag) ans++;
return rec-ans;
}
int main()
{
init();
cin>>n>>m;
cout<<work(m)-work(n-1);
return 0;
}
2.windy数
https://www.luogu.org/problemnew/show/P2657#sub
同样的 这也是在一个区间内找 采用与上题一样的策略
设dp[i][j]为长度为i中最高位是j的windy数的个数
方程 dp[i][j]=sum(dp[i-1][k]) 其中 abs(j-k)>=2(abs ->绝对值 在cmath里)
这样的转移与上题相似 都是通过前一位的和转移过来的
work函数还是不讲了 看注释吧
#include<bits/stdc++.h>
using namespace std;
//设dp[i][j]为长度为i中最高位是j的windy数的个数
//方程 dp[i][j]=sum(dp[i-1][k]) 其中 abs(j-k)>=2
int p,q,dp[15][15],a[15];
void init()
{
for(int i=0;i<=9;i++) dp[1][i]=1; //0,1,2,3,4...9都属于windy数
for(int i=2;i<=10;i++)
{
for(int j=0;j<=9;j++)
{
for(int k=0;k<=9;k++)
{
if(abs(j-k)>=2) dp[i][j]+=dp[i-1][k];
}
}
}//从第二位开始 每次枚举最高位j 并找到k 使得j-k>=2
}
int work(int x) //计算<=x的windy数
{
memset(a,0,sizeof(a));
int len=0,ans=0;
while(x)
{
a[++len]=x%10;
x/=10;
}
//分为几个板块 先求len-1位的windy数 必定包含在区间里的
for(int i=1;i<=len-1;i++)
{
for(int j=1;j<=9;j++)
{
ans+=dp[i][j];
}
}
//然后是len位 但最高位<a[len]的windy数 也包含在区间里
for(int i=1;i<a[len];i++)
{
ans+=dp[len][i];
}
//接着是len位 最高位与原数相同的 最难搞的一部分
for(int i=len-1;i>=1;i--)
{
//i从最高位后开始枚举
for(int j=0;j<=a[i]-1;j++)
{
//j是i位上的数
if(abs(j-a[i+1])>=2) ans+=dp[i][j]; //判断和上一位(i+1)相差2以上
//如果是 ans就累加
}
if(abs(a[i+1]-a[i])<2) break;
if(i==1) ans+=1;
}
return ans;
}
int main()
{
init();
cin>>p>>q;
cout<<work(q)-work(p-1)<<endl;
return 0;
}