数位dp——奏响数字数位的美妙乐章

数位dp:处理数字数位关系的一种dp方式。

一般的题目特征十分明显:

1.一般和数字本身有很大关系。

2.一般求数字在区间L,R中的一些信息

3.L,R一般很大,通常能达到long long级别。

 

dp方式也比较有套路:

一般有三种方法:

本质上的相似之处,都是集中在处理“填数有无限制”,“填数无限制情况下的固定方案数”,“某些已经搜出来的固定方案数”

1.记忆化搜索(没用过)

是一种倒着记忆的方法。

 

2.递推(我基本都是这个方法)

正着递推出答案。

一般会从高位向低位递推,讨论高位的填数方法,从而递推出低位的限制与否。

从高位到低位可以通过填的位数处理限制问题。

用一个[0/1]表示,填完这一位之后,是否对后面的数有限制。

填一个数,从k<num,k==num,k>num 讨论。

注意处理可能出现的前导零的锅。

 

3.预处理一些固定没有限制的一些位置,然后从高位开始填,

没有限制的话,直接查表。

有限制的话,继续填下一位。

注意处理已经填好的位置的固定影响。

 

经典入门例题:

[ZJOI2010]数字计数

给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。

直接做就好了。

可以处理1~9,1~99,。。的固定答案,或者直接递推下去。

 

处理1~9,1~99,1~999...的方法:

Code:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=20;
ll a,b;
ll cnta[20],cntb[20];
ll f[20],ten[20];
void query(ll x,ll *cnt)
{
    int len=0;
    int num[20];
    ll k=x;
    while(k)
    {
        num[++len]=k%10;
        k/=10;    
    }
    for(int i=len;i>=1;i--)
    {
        for(int j=0;j<=9;j++)
         cnt[j]=cnt[j]+f[i-1]*num[i];
        for(int j=0;j<num[i];j++)
         cnt[j]+=ten[i-1];
        ll num2=0;
        for(int j=i-1;j>=1;j--)
        {
            num2=num2*10+num[j];    
        }
        num2++;
        cnt[num[i]]+=num2;
        cnt[0]-=ten[i-1];
    }
}
int main()
{
    scanf("%lld%lld",&a,&b);
    ten[0]=1;
    for(int i=1;i<=15;i++)
    {
        f[i]=f[i-1]*10+ten[i-1];
        ten[i]=ten[i-1]*10;    
    }
    query(a-1,cnta);
    query(b,cntb);
    for(int i=0;i<=9;i++)
     printf("%lld ",cntb[i]-cnta[i]);
    return 0;
}
数字计数1

 

递推处理:

Code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=15;
struct node{
    ll a[11];
    void init(){
        memset(a,0,sizeof a);
    }
    node operator +(const node &b){
        node c;c.init();
        for(int i=0;i<=9;i++)
           c.a[i]=a[i]+b.a[i];
        return c;
    }
    node operator *(const int &x){
        node c;c.init();
        for(int i=0;i<=9;i++)
         c.a[i]=a[i]*x;
        return c;
    }
    node operator -(const node &b){
        node c;c.init();
        for(int i=0;i<=9;i++)
           c.a[i]=a[i]-b.a[i];
        return c;
    }
    void op(){
        cout<<endl;
        for(int i=0;i<=9;i++)
         cout<<" "<<a[i];cout<<endl;cout<<endl;
    }
}f[N][2],ans,kk;
int num[N],cnt;
ll ten[N];
ll pre[N];
ll A,B;
void sol(){
    for(int i=0;i<=cnt+1;i++)
     for(int j=0;j<=1;j++)
      f[i][j].init();
    for(int i=cnt;i>=1;i--){
        //cout<<num[i]<<" dig"<<endl;
        f[i][1]=f[i+1][1];f[i][1].a[num[i]]++;
        f[i][0]=(f[i+1][1]*num[i])+(f[i+1][0]*10);
        for(int j=0;j<=9;j++)
        {
            if(j<num[i]) f[i][0].a[j]+=(pre[i+1]+1);
            else f[i][0].a[j]+=pre[i+1];
        }
        //f[i][1].op();
        //f[i][0].op();
    }
    for(int i=cnt;i>=1;i--){
        f[1][0].a[0]-=ten[i-1];
    }
}
int main()
{
    scanf("%lld%lld",&A,&B);ten[0]=1;
    for(int i=1;i<=13;i++)ten[i]=ten[i-1]*(ll)10;
    while(B){
        num[++cnt]=B%10;
        pre[cnt]=B;B/=10;
    }
    //pre[cnt+1]=1;
    sol();
    ans=ans+f[1][0]+f[1][1];

    cnt=0;memset(num,0,sizeof num);
    memset(pre,0,sizeof pre);
    A--;
    while(A){
        num[++cnt]=A%10;
        pre[cnt]=A;
        A/=10;
    }
    //pre[cnt+1]=1;
    
    sol();
    kk=kk+f[1][0]+f[1][1];
    ans=ans-kk;
    
    
    for(int i=0;i<=9;i++){
        printf("%lld ",ans.a[i]);
    }
    return 0;
}
数字计数2

 

[SCOI2009]windy数

windy定义了一种windy数。不含前导零且相邻两个数字之差至少为2的正整数被称为windy数。 windy想知道,

在A和B之间,包括A和B,总共有多少个windy数?

Solution:

直接记录最后一个数是什么,要记录是否已经出现了非零数字,即前导零。处理前导零bug

因为前导零不处理,可能会认为最后一位填的是0,从而不能填1、0了。

f[i][last][0/1限制][0/1有无出现非零数字]

Code:

#include<bits/stdc++.h>
using namespace std;
const int N=12;
int f[N][N][2][2];
int num[N],cnt;
int A,B;
int wrk(){
    memset(f,0,sizeof f);
    for(int i=1;i<num[cnt];i++) f[cnt][i][0][1]=1;
    f[cnt][num[cnt]][1][1]=1;
    f[cnt][0][0][0]=1;
     
    for(int i=cnt;i>=2;i--){
        for(int j=0;j<=9;j++){
            for(int k=0;k<=9;k++){
                if(abs(k-j)>=2){
                    if(j==num[i]&&k==num[i-1]) f[i-1][k][1][1]+=f[i][j][1][1];
                    if(j==num[i]&&k<num[i-1]) f[i-1][k][0][1]+=f[i][j][1][1];
                    f[i-1][k][0][1]+=f[i][j][0][1];
                    if(j==0) f[i-1][k][0][1]+=f[i][j][0][0];
                }
            }
        }
        f[i-1][0][0][0]+=f[i][0][0][0];
        f[i-1][1][0][1]+=f[i][0][0][0];
    }
    int ret=0;
    for(int j=0;j<=9;j++){
        ret+=f[1][j][0][1]+f[1][j][1][1];
    }
    //ret+=f[1][0][0][0];
    return ret;
}
int main()
{
    scanf("%d%d",&A,&B);
    while(B){
        num[++cnt]=B%10;B/=10;
    }
    int ansB=wrk();
     
    A--;cnt=0;
    while(A){
        num[++cnt]=A%10;A/=10;
    }
    int ansA=wrk();
     
    //cout<<ansB<<" "<<ansA<<endl;
     
    printf("%d",ansB-ansA);
    return 0;
}
windy数

 

SAC#1 - 萌数

辣鸡蒟蒻SOL是一个傻逼,他居然觉得数很萌!

好在在他眼里,并不是所有数都是萌的。只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。

现在SOL想知道从l到r的所有整数中有多少个萌数。

由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。

 

Solution:

抓住问题本质,长度至少为2的回文子串,只要有2或者3就一定满足。

所以,只要记录上一位,上上位填的数即可。

还要记录是否已经出现过长度为2或者3的回文串。

同样,前导零的锅也要处理的。

Code:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1000+10;
const int mod=1e9+7;
int n,m;
char a[N],b[N];
int la,lb;
ll ans;
ll r,l;
ll f[N][2][2][12][12][2];// lim ; has huiwen ; has >=1
ll wrk(char *s,int len){
    memset(f,0,sizeof f);
    for(int j=0;j<=s[1]-'0';j++){
        if(j!=0){
            if(j<s[1]-'0')f[1][0][0][10][j][1]=1;
            else f[1][1][0][10][j][1]=1;
        }    
        else f[1][0][0][10][10][0]=1;
    }
    if(len==1) return 0;
    for(int i=2;i<=lb;i++){
        for(int j=0;j<=10;j++){
            for(int k=0;k<=10;k++){
                for(int p=0;p<=9;p++){
                    if(p<s[i]-'0'){
                        
                        if(p==k||p==j) (f[i][0][1][k][p][1]+=f[i-1][0][0][j][k][1]+f[i-1][1][0][j][k][1])%=mod;
                        else (f[i][0][0][k][p][1]+=f[i-1][0][0][j][k][1]+f[i-1][1][0][j][k][1])%=mod;
                        (f[i][0][1][k][p][1]+=f[i-1][0][1][j][k][1]+f[i-1][1][1][j][k][1])%=mod;
                    }
                    else if(p==s[i]-'0'){
                        if(p==k||p==j) (f[i][0][1][k][p][1]+=f[i-1][0][0][j][k][1])%=mod;
                        else (f[i][0][0][k][p][1]+=f[i-1][0][0][j][k][1])%=mod;
                        (f[i][0][1][k][p][1]+=f[i-1][0][1][j][k][1])%=mod;
                        
                        if(p==k||p==j) (f[i][1][1][k][p][1]+=f[i-1][1][0][j][k][1])%=mod;
                        else (f[i][1][0][k][p][1]+=f[i-1][1][0][j][k][1])%=mod;
                        (f[i][1][1][k][p][1]+=f[i-1][1][1][j][k][1])%=mod;
                    }
                    else{
                        if(p==k||p==j) (f[i][0][1][k][p][1]+=f[i-1][0][0][j][k][1])%=mod;
                        else (f[i][0][0][k][p][1]+=f[i-1][0][0][j][k][1])%=mod;
                        (f[i][0][1][k][p][1]+=f[i-1][0][1][j][k][1])%=mod;
                    }
                }
            }
        }
        for(int p=1;p<=10;p++){
            if(p==10) (f[i][0][0][10][p][0]+=f[i-1][0][0][10][10][0])%=mod;
            else (f[i][0][0][10][p][1]+=f[i-1][0][0][10][10][0])%=mod;
        }
    }
    ll ret=0;
    for(int j=0;j<=9;j++)
     for(int k=0;k<=9;k++){
         (ret+=f[len][1][1][j][k][1]+f[len][0][1][j][k][1])%=mod;
     }
    return ret;
}
int main()
{
    scanf("%s",a+1);scanf("%s",b+1);
    la=strlen(a+1),lb=strlen(b+1);
   
    l=wrk(a,la);
    r=wrk(b,lb);
 
    bool fl=false;
    for(int i=2;i<=la;i++){
        if(a[i]==a[i-1]||a[i]==a[i-2]) fl=true;
    }
    ans=(r+fl-l+mod)%mod;
    printf("%lld",ans);
    return 0;
}
萌数

 

bzoj 3329 Xorequ

Description

Input

第一行一个正整数,表示数据组数据 ,接下来T行
每行一个正整数N

Output

2*T行
第2*i-1行表示第i个数据中问题一的解,

第2*i行表示第i个数据中问题二的解,

HINT

x=1与x=2都是原方程的根,注意第一个问题的解

不要mod 10^9+7


1<=N<=10^18

1<=T<=1000

 

Solution:

首先一定要化简x^3x=2x

即:3x=x^2x ; x+2x=x^2x 说明,2倍的x和x没有公共1位置,否则就会变小了。

2x就是x二进制下左移一位,所以,x的条件其实是,x的二进制表示下没有连续的两个1.

对于第一问,记录上一位填的是0/1即可转移。

对于第二问,发现,是2的整次幂。2^n显然满足条件,

这里假设可以是0,不需要正整数条件,而0显然也满足条件。

最后也不用减,因为2^n本来就要加1个。

 

即处理有n位可以填0/1。

设f[i]表示,i位随便填0/1,符合的数的个数。

第i位填1的话,i-1位只能填零,方案数就是f[i-2]

第i位填0的话,对i-1位没有影响,就是f[i-1]

所以f[i]=f[i-1]+f[i-2],f[1]=2,f[2]=3

一个斐波那契数列。

矩阵乘法优化一下即可。

 

Code:(多组数据记得memset)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=66;
const int mod=1e9+7;
ll f[N][2][2];
int num[N],cnt;
ll wrk1(){
    memset(f,0,sizeof f);
    f[cnt+1][1][0]=1;
    for(int i=cnt;i>=1;i--){
        for(int k=0;k<=1;k++){
            if(k<num[i]){
                if(k==1) f[i][0][k]+=f[i+1][0][0]+f[i+1][1][0];
                else f[i][0][k]+=f[i+1][0][0]+f[i+1][0][1]+f[i+1][1][0]+f[i+1][1][1];
            }
            else if(k==num[i]){
                if(k==1) f[i][1][k]+=f[i+1][1][0],f[i][0][k]+=f[i+1][0][0];
                else f[i][1][k]+=f[i+1][1][1]+f[i+1][1][0],f[i][0][k]+=f[i+1][0][0]+f[i+1][0][1];
            }
            else {
                if(k==1) f[i][0][k]+=f[i+1][0][0];
                else f[i][0][k]+=f[i+1][0][0]+f[i+1][0][1];
            }
        }
    }
    ll ret=0;
    ret=f[1][0][0]+f[1][0][1]+f[1][1][0]+f[1][1][1];
    return ret-1;
}
struct tr{
    ll a[3][3];
    void pre(){
        memset(a,0,sizeof a);
    }
    void init(){
        for(int i=1;i<=2;i++) a[i][i]=1;
    }
    tr operator *(const tr &b){
        tr c;c.pre();
        for(int i=1;i<=2;i++)
          for(int k=1;k<=2;k++)
            for(int j=1;j<=2;j++){
                (c.a[i][j]+=a[i][k]*b.a[k][j]%mod)%=mod;
            }
        return c;
    }
}A,S,B;
tr qm(tr x,ll y){
    tr ret;ret.pre();ret.init();
    while(y){
        if(y&1) ret=ret*x;
        x=x*x;
        y>>=1;
    }
    return ret;
}
int t;
ll n;
int main()
{
    scanf("%d",&t);
    A.a[1][1]=3,A.a[1][2]=2;
    B.a[1][1]=1,B.a[1][2]=1;
    B.a[2][1]=1,B.a[2][2]=0;
    
    while(t--){
        scanf("%lld",&n);
        ll nn=n;
        cnt=0;
        while(nn){
            num[++cnt]=nn%2;nn/=2;
        }
        ll ans1=wrk1();
        printf("%lld\n",ans1);
        
        ll ans2;
        if(n==1){
            ans2=2;
        }
        else if(n==2){
            ans2=3;
        }
        else{
            S=A*qm(B,n-2);
            ans2=S.a[1][1]%mod;    
        }
        printf("%lld\n",ans2);
    }
    return 0;
}
Xorequ

 

 

CF55D Beautiful numbers

Volodya是一个很皮的男孩。他认为一个能被它自己的每一位数上的数整除(非0)的数是很妙的。我们先忽略他的想法的正确性,只回答在l到r之间有多少个很妙的数字。

输入输出格式

输入:总共有t个询问:

第一行:t;

接下来t行:每行两个数l和r。

注意:请勿使用%lld读写长整型(虽然我也不知道为什么),请优先使用cin(或者是%I64d)。

输出:t行,每行为一个询问的答案。

Solution:

比较巧妙的状态设计,还有优化 的题目。

直接记录被1,2,3..除是肯定不行的,。

发现,lcm(1,2.。。9)=2520

所以,如果最后能被1,2.。。9整除,那么把这个数对2520取模,不会影响整除与否的。

所以,f[i][2520][S]表示,填到i位,对2520取模结果,出现数字的状压集合。

 

但是,这个题有10组数据,就T飞了。

考虑优化没用的状态。

S大小是256的,不用记0,不用记1,要记8个。

但是,最后要找的还是S出现数字的lcm能否整除x

所以,就直接记录lcm就行了。

2250=2^3*3^2*5*7,一共只有48种lcm,离散化记录,256->48

 

但是还是过不去(可能我的代码常数太大?)

发现,记录mod 2250的余数时,假设之前是j,

那么,新的=j*10+k mod 2250(k是当前位的数)

设x=a2250+j;

每次j要乘10,是不是记录mod 225即可?

x=a'225 + j'

x'=a'2250+10j'+k

发现,x mod 2250 的只和j‘有关,

所以,记录j'即可,每次mod 225记录新的j

最后一次,mod 2250 就是mod 2250 的余数了。

 

Code:(luogu最优解)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=25;
const int up=252;
ll L,R;
int a[N],cnt;
ll f[N][252][2][50];
ll g[2525][2][50];
int lcm[1026],tot;
int id[2530];
int to[50][12];
int gcd(int p,int q){
    return q?gcd(q,p%q):p;
}
void dfs(int x,int lc){
    //cout<<" x lc "<<x<<" "<<lc<<endl;
    if(x==10){
        lcm[++tot]=lc;
        return;
    }
    dfs(x+1,lc);
    dfs(x+1,lc*x/gcd(x,lc));
}
ll wrk(){
    ll ret=0;
    memset(f,0,sizeof f);
    memset(g,0,sizeof g);
    f[cnt+1][0][1][1]=1;
    for(int i=cnt;i>=1;i--){
        for(int j=0;j<252;j++){
            for(int k=1;k<=tot;k++){
                for(int p=0;p<=9;p++){
                    if(i!=1){
                        if(p<a[i]){
                            f[i][(j*10+p)%up][0][to[k][p]]+=f[i+1][j][0][k]+f[i+1][j][1][k];
                            
                        }    
                        else if(p==a[i]){
                            f[i][(j*10+p)%up][0][to[k][p]]+=f[i+1][j][0][k];
                            f[i][(j*10+p)%up][1][to[k][p]]+=f[i+1][j][1][k];
                        }
                        else {
                            f[i][(j*10+p)%up][0][to[k][p]]+=f[i+1][j][0][k];
                        }
                    }
                    else{
                        if(p<a[i]){
                            g[(j*10+p)%2520][0][to[k][p]]+=f[i+1][j][0][k]+f[i+1][j][1][k];
                            
                        }    
                        else if(p==a[i]){
                            g[(j*10+p)%2520][0][to[k][p]]+=f[i+1][j][0][k];
                            g[(j*10+p)%2520][1][to[k][p]]+=f[i+1][j][1][k];
                        }
                        else {
                            g[(j*10+p)%2520][0][to[k][p]]+=f[i+1][j][0][k];
                        }
                    }
                }
            }
        }
    }
    for(int j=0;j<2520;j++){
        for(int k=1;k<=tot;k++){
            if(j%lcm[k]==0){
                ret+=g[j][1][k]+g[j][0][k];
            }
        }
    }
    
    return ret;
}
int T;
ll ansl,ansr;
int main()
{
    dfs(1,1);
    sort(lcm+1,lcm+tot+1);
    //cout<<tot<<endl;
    tot=unique(lcm+1,lcm+tot+1)-lcm-1;
    for(int i=1;i<=tot;i++) id[lcm[i]]=i;
    for(int i=1;i<=tot;i++){
        to[i][0]=i;
        for(int j=1;j<=9;j++){
            to[i][j]=id[lcm[i]*j/gcd(lcm[i],j)];
        }
    }
    
    /*cout<<" tot "<<tot<<endl;
    for(int i=1;i<=tot;i++){
        cout<<lcm[i]<<" "<<id[lcm[i]]<<" "<<endl;
        for(int j=0;j<=9;j++){
            cout<<" with "<<j<<" : "<<to[i][j]<<endl;
        }
    }*/
    
    scanf("%d",&T);
    while(T--){
        scanf("%I64d%I64d",&L,&R);
        ansl=ansr=0;
        L--;
        
        if(L==0) ansl=1;
        else{cnt=0;
            while(L){
                a[++cnt]=L%10;L/=10;
            }ansl=wrk();
        }
        
        cnt=0;
        while(R){
            a[++cnt]=R%10;R/=10;
        }ansr=wrk();
        
        printf("%I64d\n",ansr-ansl);
    }
    return 0;
}
Beautiful numbers

 

 [CQOI2016]手机号码

比较麻烦的数位dp

见另一片博客:

[CQOI2016]手机号码

 

[SCOI2014]方伯伯的商场之旅

非常麻烦的数位dp了。

见另外一篇博客:

[SCOI2014]方伯伯的商场之旅

 

posted @ 2018-08-25 17:47  *Miracle*  阅读(341)  评论(2编辑  收藏  举报