引入
数位dp,顾名思义,对数的每一位进行dp,具体我们来看题分析解决。
原理
传送门luoguP2657windy数
此题就是一道非常标准的数位dp的题目,限制相邻位上的数字,要统计范围内的所有满足的数,如果暴力枚举肯定不够优美,那么我们对于每一位进行逐位逐位的确定,那么就能降低大量的复杂度。
查询l~r区间比较麻烦,我们可以求出0~r区间和0~l-1区间,相减即可。
我们用a来表示每一位上的数,然后就进行dfs,其中:
t表示当前所在的位数,pre表示上一位数,st表示是否前面全是0(若前面全是0,那么该位不受2的限制),limit表示是否受最高位限制(比如r=432,若最高位是1,那么下面位数可以任意取,若最高位是4,那么下面位数只能取到3,2)。带入进去判定即可,我们发现,若r=432时,第一次搜到了132,下一次搜到232,那么已经搜过,直接返回,即记忆化搜索。这样我们可以节约大量时间。
代码实现:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll dp[15][15];
int a[15],len;
ll dfs(int t,int pre,bool st,bool limit)
{
if(t==0) return 1;//搜索结束
if(limit==0 && dp[t][pre]!=0) return dp[t][pre];//记忆化搜索,没有限制的情况下
ll sum=0;int res=limit?a[t]:9;//是否首高位限制,否则0~9都可以
for(int i=0;i<=res;i++)
{
if(abs(i-pre)<2) continue;
if(st==1 && i==0) sum+=dfs(t-1,-2,1,limit&&i==res);//如果全是0,下一位没有限制
else sum+=dfs(t-1,i,0,limit&&i==res);
}
if(limit==0 && st==0) dp[t][pre]=sum;
return sum;
}
ll solve(int x)
{
len=0;
while(x)
a[++len]=x%10,x/=10;
memset(dp,0,sizeof(dp));
return dfs(len,-2,1,1);//分别表示当前位置,上一位置,是否之前位置全是0,是否有最高位限制
//由于在最高位,这一位没有受至少差2的限制,但会受最高位限制
}
int main()
{
int l,r;
scanf("%d%d",&l,&r);
cout<<solve(r)-solve(l-1);
return 0;
}
再来一道例题传送门luoguP2602数字计数
首先要开long long
我们还是记忆化搜索,枚举每一种数字0~9,搜索是否是这个数,是就统计加1,我们要特别注意0的情况,如果是前导0,那么就不能统计,由于每个数字其实差不多,那么我们可以记下没有限制的dp值,下次搜到直接return。
代码实现:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=100;
int dp[N][N],a[N],len;
int dfs(int t,bool st,bool limit,int sum,int k)
{
int ans=0;
if(t==0) return sum;//搜完了,返回总数
if(limit==0 && st==0 && dp[t][sum]!=-1) return dp[t][sum];
int res=limit?a[t]:9;
for(int i=0;i<=res;i++)
{
if(st&&!i)
ans+=dfs(t-1,1,i==res&&limit,sum,k);
else
ans+=dfs(t-1,0,i==res&&limit,sum+(i==k),k);
}
if(limit==0 && st==0) dp[t][sum]=ans;
return ans;
}
int solve(int x,int k)
{
len=0;
memset(dp,-1,sizeof(dp));//多次使用,初始化
while(x) a[++len]=x%10,x/=10;
return dfs(len,1,1,0,k);//前导0,最高位限制,统计的数量,统计哪一个数
}
signed main()
{
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=0;i<=9;i++)
cout<<solve(m,i)-solve(n-1,i)<<" ";
return 0;
}
再再来一道例题传送门luoguP4999烦人的作业,
该题和上题差不多,只需统计后的个数乘上这个数,就是该数字总的和,加到一起就ok了。
核心代码:
ans=((ans+i*(solve(m,i)-solve(n-1,i))+mod)%mod+mod)%mod;
注意可能会出现负的,那么就多模几下。
再再再来一道例题传送门luoguP4317花神的数论题。
根据数据,最多有250,即50位,那么就可以枚举有多少个1,然后去搜索填这么多个1,由于是连乘,可以使用快速幂来解决。
代码实现:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=55,mod=1e7+7;
int dp[N][2][N][N],a[N],len;
int fpow(int u,int v)
{
int ans=1;
while(v!=0)
{
if(v&1) ans=(ans*u)%mod;
u=u*u%mod;
v>>=1;
}
return ans;
}
int dfs(int t,int limit,int sum,int k)
{
if(t==0) return sum==k;
if(dp[t][limit][sum][k]!=-1) return dp[t][limit][sum][k];
int res=limit?a[t]:1;
int ans=0;
for(int i=0;i<=res;i++)
ans=ans+dfs(t-1,limit&&i==res,sum+(i==1),k);
return dp[t][limit][sum][k]=ans;
}
int solve(int x)
{
while(x) a[++len]=x&1,x>>=1;
int ans=1;
for(int i=1;i<=len;i++)
{
memset(dp,-1,sizeof(dp));
ans=(ans*fpow(i,dfs(len,1,0,i)))%mod;
}
return ans;
}
signed main()
{
int n;
scanf("%lld",&n);
cout<<solve(n);
return 0;
}
今天的题有点多嘿嘿