数位dp
数位dp
一类套路dp题
数位dp一般与数的大小无关,而与数的组成有关。
从高位枚举到低位,如果这一位与原数的同一位不同,且满足上限的限制,则后面可以随便填,可以用f数组来储存这个值,这也是为什么记忆化搜索时需要判断(! limit)
数位dp一般用记忆化搜索来实现,方便快捷。
f数组中的维度由题目给出的限制数量决定,相当套路
前导0的问题:如果前导0对答案有影响(如windy数和手机号码)则需要判断有无前导0,若无影响则不用记录。
f数组的初始值一般设为-1,因为有些题目f的值本就该为-1,若设为0,可能会导致TLE。
询问[m,n]时,要注意m是否为0,需要特殊判断。
数位dp一般会与枚举等算法结合。
记忆化搜索---->可行性剪枝
例题
洛谷P4317 花神的数论题
设 sum(i) 表示 i 的二进制表示中 1 的个数。给出一个正整数 N ,花神要问
你派(Sum(i)),也就是 sum(1)—sum(N) 的乘积。答案对一个质数取模。对于 100% 的数据,N≤10^15
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define ll long long
#define mol 10000007
ll n;int num[80];
ll sum[80];
ll f[55][55];
ll quickpow(ll a,ll b)
{
ll ret=1;
while(b)
{
if(b&1)ret=ret*a%mol;
a=a*a%mol;
b>>=1;
}
return ret%mol;
}
ll dfs(int x,int cnt,int limit,int tmp)
{
if(x==0&&cnt==tmp)return 1;
if(x==0)return 0;
if(!limit&&f[x][cnt]!=-1)return f[x][cnt];
int up=limit==1?num[x]:1;
ll ans=0;
for(int i=0;i<=up;i++)
{
ans+=dfs(x-1,i==1?cnt+1:cnt,(limit==1&&i==up)?1:0,tmp);
}
if(!limit)f[x][cnt]=ans;
return ans;
}
void solve(ll x)
{
int tot=0;
while(x)
{
num[++tot]=x%2;
x>>=1;
}
ll ans=1;
for(int i=1;i<=tot;i++)
{
memset(f,-1,sizeof(f));
sum[i]=dfs(tot,0,1,i);
if(i!=1)
ans=ans*quickpow(i,sum[i])%mol;
}
printf("%lld",ans%mol);
return;
}
int main()
{
scanf("%lld",&n);
solve(n);
return 0;
}
洛谷P4124 手机号码
◦数字L到R中有多少个数字满足以下两个条件。
◦ 1:要出现至少3个相邻的相同数字
◦ 2:号码中不能同时出现8和4。
◦ 10^10 < = L < = R < 10^11
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define ll long long
ll L,R;
int num[20];
ll f[20][2][2][15][2][2];
ll dfs(int x,int eig,int fou,int pre,int equ,int exist,int pd,int limit)
{
if(x==0&&(eig==0||fou==0)&&exist==1)return 1;
if(x==0)return 0;
if(!pd&&!limit&&f[x][eig][fou][pre][equ][exist]!=-1)return f[x][eig][fou][pre][equ][exist];
int up=limit==1?num[x]:9;
ll ans=0;
for(int i=0;i<=up;i++)
{
ans+=dfs(x-1,i==8?1:eig,i==4?1:fou,(pd==1&&i==0)?-1:i,(i==pre&&pd==0)?1:0,(equ==1&&i==pre&&pd==0)?1:exist,pd==1&&i==0?1:0,limit==1&&i==up?1:0);
}
if(!limit&&!pd)f[x][eig][fou][pre][equ][exist]=ans;
return ans;
}
ll solve(ll x)
{
memset(num,0,sizeof(num));
memset(f,-1,sizeof(f));
int tot=0;
while(x!=0)
{
num[++tot]=x%10;x/=10;
}
return dfs(tot,0,0,-1,0,0,1,1);
}
int main()
{
scanf("%lld%lld",&L,&R);
printf("%lld",solve(R)-solve(L-1));
return 0;
}
洛谷P4127 同类分布
给出两个数a,b,求出[a,b]中各位数字之和能整除原数的数的个数。
对于所有的数据,1 ≤ a ≤ b ≤ 10^18
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define ll long long
ll L,R;
int num[20];
ll f[20][200][200];
ll dfs(int x,int sum,int mod,int mol,int limit)
{
if(x==0&&sum==mol&&mod==0)return 1;
if(x==0)return 0;
if(!limit&&f[x][sum][mod]!=-1)return f[x][sum][mod];
int up=limit==1?num[x]:9;
ll ans=0;
for(int i=0;i<=up;i++)
{
ans+=dfs(x-1,sum+i,(mod*10%mol+i+mol)%mol,mol,limit==1&&i==up?1:0);
}
if(!limit)f[x][sum][mod]=ans;
return ans;
}
ll solve(ll x)
{
memset(num,0,sizeof(num));
int tot=0;
while(x!=0)
{
num[++tot]=x%10;x/=10;
}
ll ans=0;
for(int i=1;i<=tot*9;i++)
{
memset(f,-1,sizeof(f));
ans+=dfs(tot,0,0,i,1);
}
return ans;
}
int main()
{
scanf("%lld%lld",&L,&R);
printf("%lld",solve(R)-solve(L-1));
return 0;
}