zoukankan      html  css  js  c++  java
  • 【ybtoj】【数位dp专题】

    前言

    假期最后两天不想做什么太难的,就把数位DP开了吧!正好填之前挖的坑

    数位DP看起来貌似都比较裸...而且题目简短,注意一下代码的细节就好

    本篇记录里全部使用记忆化搜索

    记忆化搜索复杂度=状态数*枚举数

    目录

    • A. 【例题1】B数计数

    • B. 【例题2】区间圆数

    • C. 【例题3】数字计数

    • D. 【例题4】数字整除

    • F. 1.幸运数字

    • G. 2.幸运666

    • H. 3.奶牛编号

    题解

    A. 【例题1】B数计数

    分析:

    此题是第一道ybt的数位dp题,引入一些模板的写法

    对于所有求1~n,L~R的 xx数 个数的,一般都是从高位到低位搜索,而且记录一个 ok 变量来表示当前数位可不可以随便填数字(每一位数字填完最后不能比 n 大)

    由于ok 变量不需要在 dp 数组里记录,可以直接传参到记忆化搜索里,但是ok==0和ok==1时候 dp 数组的记忆化值是不一样的,所以规定只记忆化ok==1(即可以随便填)时的 dp 值,这是因为ok==0的情况很少,所以不用记忆化问题也不大(ps:测试时发现枚举 i 的时候倒序枚举也可以使记忆化不重复,有点玄学,本质是因为先算完了所有ok==0的情况再算ok==1的情况,所以不会重复。但不推荐这么写)

    实际上,应该也可以在 dp 数组中记录 ok ,理论上空间会变大一倍(ok取值0,1),但是搜索部分会快一点点

    那么回归本题,设计dp状态为 dp[pos][res][op]

    pos表示第几位,res表示余数,op表示13数出现的状态(op==0没出现过,op==1上一位是1而且之前没出现过完整的13,op==2表示出现过13),maxn表示当前最大能填的数字,ok表示当前这位是否可以随便填 
    (一开始我想用check表示是否出现过13,其实不需要,可以用op代替)  

    代码:

    A. 【例题1】B数计数
    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int INF = 0x3f3f3f3f;
    int n,dp[11][14][3],dig[11],m,mi[11];
    void init()
    {
    	//memset(dp,-1,sizeof(dp));
    	//memset(dig,0,sizeof(dig));
    	m=0;
    	while(n)
    	{
    		int x=n%10;
    		dig[++m]=x;
    		n/=10;
    	}
    }
    //可以有前导零,不用记录zero 
    //pos表示第几位,res表示余数,op表示13数出现的状态,maxn表示当前最大能填的数字
    //(一开始我想用check表示是否出现过13,其实不需要,可以用op代替)
    //ok表示当前这位是否可以随便填 
    int solve(int pos,int res,int op,bool ok)
    {
    	if(pos==0) return dp[pos][res][op]=(op==2&&res==0);
    	if(dp[pos][res][op]!=-1&&ok) return dp[pos][res][op];
    	int ret=0,maxn=9;
    	if(!ok) maxn=dig[pos];
    	for(int i=maxn;i>=0;i--)
    	{
    		int tmp=(res+mi[pos]*i)%13;
    		//分类讨论当前填的数字大小 
    		if(i<maxn)
    		{
    			//if(check) ret+=solve(pos-1,tmp,op,1,1);
    			if(op==2) {ret+=solve(pos-1,tmp,2,1);continue;}
    			if(i==1) ret+=solve(pos-1,tmp,1,1); 
    			else if(i==3&&op==1) ret+=solve(pos-1,tmp,2,1);
    			else ret+=solve(pos-1,tmp,0,1);
    		}
    		else 
    		{
    			if(op==2) {ret+=solve(pos-1,tmp,2,ok);continue;}
    			if(i==1) ret+=solve(pos-1,tmp,1,ok); 
    			else if(i==3&&op==1) ret+=solve(pos-1,tmp,2,ok);
    			else ret+=solve(pos-1,tmp,0,ok);
    		}
    	}
    //	printf("dp[%d][%d][%d]=%d
    ",pos,res,op,ret);
    	if(ok) dp[pos][res][op]=ret;
    	return ret;
    

    }
    int main()
    {
    mi[1]=1;
    for(int i=2;i<=10;i++) mi[i]=mi[i-1]10;
    while(scanf("%d",&n)!=EOF)
    {
    init();
    printf("%d ",solve(m,0,0,0));
    }
    return 0;
    }
    /

    input:
    131312
    131313
    13333
    13332
    13338
    output:
    550
    551
    60
    60
    61
    */

    B. 【例题2】区间圆数

    分析:

    题目里面要求二进制,那就改成二进制的数位DP就可以啦

    这道题目引入了前导零的处理方法:okz 判断前导零是否结束,prezero记录前导零数量

    设计 dp[pos][cntzero][prezero] 回去看题解,发现可以压成两维 dp[pos][cntzero]

    pos填到第几位,cntzero填的0数量,prezero前导0数量,maxn当前填的最大数,okz前导0是否结束,okm表示当前这位是否可以随便填 
    op表示操作的这个数字是 L 还是 R


    记忆化的时候不用分别记忆okz==0和okz==1的情况(当然记录也没问题),因为对于一个okz==0的情况,在这个dp状态一定是形如 dp[m-k][k][k]这样的样子,这样的状态不会被okz==1的情况覆盖

    (其实如果想不明白就把okz也记忆化,也是没有问题的)

    update:最好还是记忆化 okz , 否则可能因为位数不同而出错(本代码里因为算L,R时都清空了dp数组所以没错),详见下一道题 例题3

    代码:

    B. 【例题2】区间圆数
    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int INF = 0x3f3f3f3f;
    int L,R;
    int dig[3][35],m[3];
    int dp[35][35][35];
    inline void init()
    {
    	//mi[1]=1;
    	//for(int i=2;i<=33;i++) mi[i]=mi[i-1]<<1;
    	memset(dp,-1,sizeof(dp));
    	L--;//忘了这个... 
    	while(L)
    	{
    		int x=L%2;
    		dig[1][++m[1]]=x;
    		L>>=1;
    	}
    	while(R)
    	{
    		int x=R%2;
    		dig[2][++m[2]]=x;
    		R>>=1;
    	}
    

    }
    //pos填到第几位,cntzero填的0数量,prezero前导0数量,maxn当前填的最大数,okz前导0是否结束,okm表示当前这位是否可以随便填
    //op表示dig[op]...
    int solve(int pos,int cntzero,int prezero,int okz,int okm,int op)
    {
    if(pos0) return cntzero-prezero>=m[op]-cntzero;
    if(dp[pos][cntzero][prezero]!=-1&&okm&&okz) return dp[pos][cntzero][prezero];
    int ret=0,maxn=1;
    if(!okm) maxn=dig[op][pos];
    for(int i=0;i<=maxn;i++)
    {
    if(i<maxn)
    {
    if(okz) ret+=solve(pos-1,cntzero+(i
    0),prezero,1,1,op);
    else ret+=solve(pos-1,cntzero+(i0),prezero+(i0),i,1,op);
    }
    else
    {
    if(okz) ret+=solve(pos-1,cntzero+(i0),prezero,1,okm,op);
    else ret+=solve(pos-1,cntzero+(i
    0),prezero+(i==0),i,okm,op);
    }
    }
    if(okm&&okz) dp[pos][cntzero][prezero]=ret;
    return ret;
    }
    int main()
    {
    scanf("%d%d",&L,&R);
    init();
    int ans1=solve(m[1],0,0,0,0,1);
    memset(dp,-1,sizeof(dp));
    int ans2=solve(m[2],0,0,0,0,2);
    printf("%d ",ans2-ans1);
    return 0;
    }

    C. 【例题3】数字计数

    分析:

    做了两道题就会发现,数位DP的套路还是很清晰的

    这道题无非就是填数的过程中记录一下某一个数字出现的次数,在边界的时候返回这个次数作为答案

    不过需要特殊记录 0 出现的次数,因为要除去前导零的个数,类似例题2

    设计dp[pos][num][cnt][zero]  (回去看题解,发现实际上可以压成两维 dp[pos][cnt]

    pos表示当前填到第几位,num表示当前计算的数字,cnt表示num的数量,ok判断是否可以随便填,op判断是l还是r
    okz判断前导零是否结束,zero记录前导零数量 

    再考虑一下记忆化的问题,这道题和上一道题不一样,必须只记忆化 okz==1&&ok==1 的情况,如果不记录 okz 会 90pts WA

    至于为什么...对于L,R位数不一样的情况,比如:

    L:  000

    R:0001

    此时如果不记忆化 okz ,两个dp值的记忆化就会冲突(因为L:okz==0,R:okz==1)

    代码:

    C. 【例题3】数字计数
    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int INF = 0x3f3f3f3f,N = 14;
    ll l,r;
    int dig[2][N],m[2];
    ll dp[N][10][N][N];
    //dp[pos][num][cnt]
    void init()
    {
    	memset(dp,-1,sizeof(dp));
    	l--;
    	while(l)
    	{
    		int x=l%10;
    		dig[0][++m[0]]=x;
    		l/=10;
    	}
    	while(r)
    	{
    		int x=r%10;
    		dig[1][++m[1]]=x;
    		r/=10;
    	}
    }
    //pos表示当前填到第几位,num表示当前计算的数字,cnt表示num的数量,ok判断是否可以随便填,op判断是l还是r
    //okz判断前导零是否结束,zero记录前导零数量 
    ll solve(int pos,int num,int cnt,int ok,int op,int okz,int zero)
    {
    	//printf("%d
    ",(!num)*zero);
    	if(!pos) return dp[pos][num][cnt][zero]=cnt-(!num)*zero;//如果num是0,那计数要减去前导零 
    	if(dp[pos][num][cnt][zero]!=-1&&ok&&okz) return dp[pos][num][cnt][zero];
    	ll ret=0;int maxn=9;
    	if(!ok) maxn=dig[op][pos];
    	for(int i=0;i<=maxn;i++)
    		ret+=solve(pos-1,num,cnt+(i==num),ok||i<maxn,op,okz||i,zero+(!okz&&!i));
    	if(ok&&okz) dp[pos][num][cnt][zero]=ret;
    	return ret;
    }
    int main()
    {
    	scanf("%lld%lld",&l,&r);
    	init();
    	ll ans1=0,ans2=0;
    	for(int i=0;i<=9;i++) 
    	{
    		ans1=0,ans2=0;
    		//memset(dp,-1,sizeof(dp));
    		ans1=solve(m[0],i,0,0,0,0,0);
    		//memset(dp,-1,sizeof(dp));
    		ans2=solve(m[1],i,0,0,1,0,0);
    
    	printf("%lld ",ans2-ans1);
    }
    
    return 0;
    

    }

    D. 【例题4】数字整除

    分析:

    这道题还是有点小技巧的

    首先我们可以很简单地设计出 dp[pos][res][sum],pos表示第几位,res表示余数,sum表示数位之和。但是由于数位之和一直在变(也就是模数一直在变),所以这样记录出来的 res 是无效的

    那么我们多枚举一维 mod 表示模数,边界的时候判断 sum 是否==mod就可以啦

    再考虑 mod 这一维加在哪里好

    本题3000组输入,肯定每次记忆化搜索不清空,才不会TLE,所以本题中 mod 肯定要记录在dp数组里面,空间可以承受

    但是洛谷有一道题P4127 [AHOI2009]同类分布,只有一组输入,但是数据范围到了1018,那 mod 记录在dp数组里就会MLE,不过由于只有一组输入,所以洛谷上只在记忆化搜索和主函数里枚举 mod 之后 每次清空就可以了

    以上是典型的时间换空间

    代码:

    D. 【例题4】数字整除
    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int INF = 0x3f3f3f3f;
    int L,R;
    int dig[3][12],m[3],mi[11];
    int dp[12][100][100][100];
    //复杂度:1e6...... 
    //dp[pos][res][sum][mod] 
    inline void init()
    {
    	m[1]=m[2]=0;
    	L--;//忘了这个... 
    	while(L)
    	{
    		int x=L%10;
    		dig[1][++m[1]]=x;
    		L/=10;
    	}
    	while(R)
    	{
    		int x=R%10;
    		dig[2][++m[2]]=x;
    		R/=10;
    	}
    

    }
    //op判断是L还是R
    int solve(int pos,int res,int sum,int mod,bool ok,int op)
    {
    if(sum>mod) return dp[pos][res][sum][mod]=0;
    if(pos0) return dp[pos][res][sum][mod]=(!res&&modsum);
    if(ok&&dp[pos][res][sum][mod]!=-1) return dp[pos][res][sum][mod];
    int ret=0,maxn=9;
    if(!ok) maxn=dig[op][pos];
    for(int i=0;i<=maxn;i++)
    {
    int tmp=(res+imi[pos])%mod;
    ret+=solve(pos-1,tmp,sum+i,mod,ok||(i<maxn),op);
    }
    if(ok) dp[pos][res][sum][mod]=ret;
    return ret;
    }
    int main()
    {
    mi[1]=1;
    for(int i=2;i<=10;i++) mi[i]=mi[i-1]
    10;
    memset(dp,-1,sizeof(dp));
    while(scanf("%d%d",&L,&R)!=EOF)
    {
    init();
    int ans1=0,ans2=0;
    for(int mod=1;mod<=95;mod++)
    ans1+=solve(m[1],0,0,mod,0,1);
    //memset(dp,-1,sizeof(dp));
    for(int mod=1;mod<=95;mod++)
    ans2+=solve(m[2],0,0,mod,0,2);
    //printf("ans1=%d,ans2=%d ",ans1,ans2);
    printf("%d ",ans2-ans1);
    }
    return 0;
    }
    /*
    11 819
    11 459
    20 743
    18 725
    9 920
    13 877
    15 932
    6 454
    10 533
    16 547
    */

    P4127 [AHOI2009]同类分布
    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int INF = 0x3f3f3f3f;
    ll L,R;
    int dig[3][20],m[3];
    ll mi[20];
    ll dp[20][200][200];
    //复杂度:1e6...... 
    //dp[pos][res][sum][mod] 
    inline void init()
    {
    	m[1]=m[2]=0;
    	L--;//忘了这个... 
    	while(L)
    	{
    		int x=L%10;
    		dig[1][++m[1]]=x;
    		L/=10;
    	}
    	while(R)
    	{
    		int x=R%10;
    		dig[2][++m[2]]=x;
    		R/=10;
    	}
    

    }
    //op判断是L还是R
    int solve(int pos,int res,int sum,int mod,bool ok,int op)
    {
    if(sum>mod) return dp[pos][res][sum]=0;
    if(pos0) return dp[pos][res][sum]=(!res&&modsum);
    if(ok&&dp[pos][res][sum]!=-1) return dp[pos][res][sum];
    int ret=0,maxn=9;
    if(!ok) maxn=dig[op][pos];
    for(int i=0;i<=maxn;i++)
    {
    int tmp=(res+mi[pos]%modi)%mod;
    ret+=solve(pos-1,tmp,sum+i,mod,ok||(i<maxn),op);
    }
    if(ok) dp[pos][res][sum]=ret;
    return ret;
    }
    int main()
    {
    mi[1]=1;
    for(int i=2;i<=19;i++) mi[i]=mi[i-1]
    10;
    memset(dp,-1,sizeof(dp));
    scanf("%lld%lld",&L,&R);
    {
    init();
    ll ans1=0,ans2=0;
    for(int mod=1;mod<=200;mod++)
    {
    ans1+=solve(m[1],0,0,mod,0,1);
    memset(dp,-1,sizeof(dp));
    }
    for(int mod=1;mod<=200;mod++)
    {
    ans2+=solve(m[2],0,0,mod,0,2);
    memset(dp,-1,sizeof(dp));
    }
    //printf("ans1=%d,ans2=%d ",ans1,ans2);
    printf("%lld ",ans2-ans1);
    }
    return 0;
    }
    /*
    11 819
    11 459
    20 743
    18 725
    9 920
    13 877
    15 932
    6 454
    10 533
    16 547
    */

    F. 1.幸运数字

    分析:

    妥妥的例题1的弱化版,信心题

    不过刚看到这个数据范围还吓了一跳,N这么大都输入不进去,难道要高精?

    想到高精之后发现除了字符串输入,没有任何别的操作了,这也告诉我们N的大小不重要,因为预处理的时候都要拆成一位一位的

    dp状态把例题1削弱一下就出来了

    代码:

    F. 1.幸运数字
    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long
    const int INF = 0x3f3f3f3f;
    ll L,R;
    int dig[3][20],m[3];
    ll mi[20];
    ll dp[20][200][200];
    //复杂度:1e6...... 
    //dp[pos][res][sum][mod] 
    inline void init()
    {
    	m[1]=m[2]=0;
    	L--;//忘了这个... 
    	while(L)
    	{
    		int x=L%10;
    		dig[1][++m[1]]=x;
    		L/=10;
    	}
    	while(R)
    	{
    		int x=R%10;
    		dig[2][++m[2]]=x;
    		R/=10;
    	}
    

    }
    //op判断是L还是R
    int solve(int pos,int res,int sum,int mod,bool ok,int op)
    {
    if(sum>mod) return dp[pos][res][sum]=0;
    if(pos0) return dp[pos][res][sum]=(!res&&modsum);
    if(ok&&dp[pos][res][sum]!=-1) return dp[pos][res][sum];
    int ret=0,maxn=9;
    if(!ok) maxn=dig[op][pos];
    for(int i=0;i<=maxn;i++)
    {
    int tmp=(res+mi[pos]%modi)%mod;
    ret+=solve(pos-1,tmp,sum+i,mod,ok||(i<maxn),op);
    }
    if(ok) dp[pos][res][sum]=ret;
    return ret;
    }
    int main()
    {
    mi[1]=1;
    for(int i=2;i<=19;i++) mi[i]=mi[i-1]
    10;
    memset(dp,-1,sizeof(dp));
    scanf("%lld%lld",&L,&R);
    {
    init();
    ll ans1=0,ans2=0;
    for(int mod=1;mod<=200;mod++)
    {
    ans1+=solve(m[1],0,0,mod,0,1);
    memset(dp,-1,sizeof(dp));
    }
    for(int mod=1;mod<=200;mod++)
    {
    ans2+=solve(m[2],0,0,mod,0,2);
    memset(dp,-1,sizeof(dp));
    }
    //printf("ans1=%d,ans2=%d ",ans1,ans2);
    printf("%lld ",ans2-ans1);
    }
    return 0;
    }
    /*
    11 819
    11 459
    20 743
    18 725
    9 920
    13 877
    15 932
    6 454
    10 533
    16 547
    */

    G. 2.幸运666

    image

    分析:

    此题引入数位 DP 解决的一种新类型题:求数位符合某种条件第 (k) 小的数。
    (f(i,0/1/2)) 表示由 (i) 位数字组成的,当前有 (0/1/2) 个数字 (6) 连续的非幸运数的个数。
    (f(i,3)) 表示由 (i) 位数字组成的幸运数的个数。
    这个式子就不必记忆化搜索了,直接递推就很简单。
    然后可以根据 (f(i,3)) 推出答案的位数,为最小的 (i) ,使得 (nle f(i,3))
    然后从高位到低位尝试填数,根据预处理出的 (f) 数组计算剩下的位置有多少种填法,记为 (cnt).
    (cnt<n) ,则说明当前位置填 (j) 时最大的幸运数一定小于答案,所以n-=cnt.
    (cntge n),则说明答案的当前位置必定(j) ,输出 (j) 并且接着考虑下一位 (i+1)
    对于最高位数,预计 (10) 位的时候大概就够 (5 imes 10^7),但是毕竟是估算还是开大一点范围(代码中开了 (20)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long 
    const int INF = 0x3f3f3f3f,N = 5e7+10;
    int T,n,m;
    int f[21][4];
    void init()
    {
        f[0][0]=1;
        for(int i=1;i<=20;i++)
        {
            f[i][0]=9*(f[i-1][0]+f[i-1][1]+f[i-1][2]);
            f[i][1]=f[i-1][0];
            f[i][2]=f[i-1][1];
            f[i][3]=10*f[i-1][3]+f[i-1][2];
        }
    
    }
    int main()
    {
        init();
        scanf("%d",&T);
        while(T--)
        {
            scanf("%d",&n);
            //init();
            for(int i=1;i<=20;i++)
                if(f[i][3]>=n) {m=i;break;}
            ll cnt=0,k=0;
            for(int i=m;i>=1;i--)
            {
                for(int j=0;j<=9;j++)
                {
                    cnt=f[i-1][3];
                    if(k==3) cnt+=f[i-1][0]+f[i-1][1]+f[i-1][2];
                    else if(j==6) 
                        for(int x=3-k-1;x<3;x++) cnt+=f[i-1][x];
                    if(cnt<n) n-=cnt;
                    else 
                    {
                        if(k<3)
                        {
                            if(j==6) k++;
                            else k=0;
                        }
                        printf("%d",j);
                        break;
                    }
                }
            }
            printf("
    ");
        }
        return 0;
    }
    

    H. 3.奶牛编号

    image

    分析:

    (f(i,j)) 表示在前 (i) 位上放 (j)(1) 的方案数,转移很显然:(f(i,j)=f(i-1,j)+f(i-1,j-1)).
    和上一道题一样,从最高位开始填数,对于当前的第 (i) 个位置,可以填 (0,1)
    (sumk) 为能填 (1) 的个数,(i) 为当前位。
    (f(i-1,sumk)<n) ,则说明当前位如果填 (0)最大的编号小于 (n) 。因此当前位必定(1) ,同时 sumk--,n-=f[i-1][sumk].
    否则说明当前位填 (0) 的情况的最大排名 (ge n) ,因此当前位必定(0) ,如果当前他填的这个 (0) 不是前导零就输出。
    注意:特判k==1的情况直接输出 (n) 位,其余情况最大的位数不超过 (5000).
    证明:显然能填的数字 (1) 越多,最大的位数就越小。考虑k==2的情况,最大排名 (N=C^2_n),即在 (n) 位数里选两个,根据组合数的公式可以知道 (5000 imes (5000-1) div 2 > 10^7) ,所以上界约为 (5000) ,证毕

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define ll long long 
    const int INF = 0x3f3f3f3f,N = 1e7+10;
    int n,k,m;
    ll f[6005][11];
    bool fir;
    void init()
    {
        f[0][0]=1;
        //for(int i=0;i<=6000;i++) f[i][0]=1;
        for(int i=1;i<=6000;i++)
            for(int j=0;j<=k;j++)
            {
                if(j) f[i][j]=min((ll)1e7,f[i-1][j-1]+f[i-1][j]);
                else f[i][j]=min((ll)1e7,f[i-1][j]);
                //printf("dp[%d][%d]=%d
    ",i,j,f[i][j]);
            }
    }
    //书上写dfs输出答案,但没必要,直接按照下面i从m->1的循环输出即可
    void dfs(int x,int y,int stp)
    {
        if(!stp) return;
        if(x>f[stp-1][y])
        {
            printf("1");
            fir=1;
            dfs(x-f[stp-1][y],y-1,stp-1);
        }
        else 
        {
            if(fir) printf("0");
            dfs(x,y,stp-1);
        }
    }
    int main()
    {
        scanf("%d%d",&n,&k);
        init();
        for(int i=1;i<=6000;i++)    
            if(f[i][k]>=n) {m=i;break;}
        //printf("m=%d
    ",m);
        if(k==1)
        {
            for(int i=1;i<=n;i++)   
                if(i==1) printf("1");
                else printf("0");
            return 0;
        }
        //dfs(n,k,6000);
        int sumk=k;
        for(int i=m;i>=1;i--)
        {
            if(f[i-1][sumk]<n) 
            {
                printf("1");
                n-=f[i-1][sumk];
                sumk--,fir=1;
            }
            else if(fir) printf("0");
        }
        
        return 0;
    }
    
  • 相关阅读:
    react脚手架和JSX
    promise
    防抖和节流
    call/apply/bind 用法
    js this指向
    vue单页面应用刷新网页后vuex的state数据丢失的解决方案
    Echarts基础
    继承
    原型链
    vue项目中使用生成动态二维码
  • 原文地址:https://www.cnblogs.com/conprour/p/15209520.html
Copyright © 2011-2022 走看看