数位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;
}
posted @ 2019-07-29 13:15  Akaina  阅读(192)  评论(0编辑  收藏  举报