kuangbin专题十五数位dp总结

本文深入探讨数位DP技巧,通过多个实例解析如何利用状态压缩和动态规划解决复杂计数问题,涵盖数字特性分析、最优序列求解等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里是一篇讲数位dp的文章
数位dp最重要的就是状态的建立!
A - Beautiful numbers
一道数位dp的好题。
不过理清思路后也挺好理解的。
题目要求求能被它所有位数整除的数的个数,换句话说就是能被它所有位数的lcm整数的数的个数,所以我们得维护一个lcm。又因为a%p=a%(k*p)%p。所以我们可以先将所有的数模上lcm(1,2,3…9),然后在最后再判断模lcm是否为0.
那么这样开的数组大小为20*2520*2520.
很遗憾的是会mle,实际上他们的lcm不会有2520个,所以我们可以将他们的lcm离散化一下,这样数组就能开下去了。

#include<bits/stdc++.h>
using namespace std;
const int maxn=100;
const int mod=2520;
typedef long long ll;
ll dp[maxn][mod+10][50];
int _a[maxn];

int gcd(int a,int b){return b==0?a:gcd(b,a%b);}
int lcm(int a,int b){return a/gcd(a,b)*b;}
int h[mod+1000];

ll dfs(int d,int val,int _lcm,bool limit,bool lead)
{
    if(d==-1)
        return val%_lcm == 0;
    if(!limit&&!lead&&dp[d][val][h[_lcm]]!=-1)return dp[d][val][h[_lcm]];
    int End=limit?_a[d]:9;
    ll res=0;
    for(int i=0;i<=End;i++)
    {
        if(i==0)
        {
            res+=dfs(d-1,(val*10)%mod,_lcm,limit&&(i==_a[d]),lead&&(!i));
        }
        else
        {
            res+=dfs(d-1,(val*10+i)%mod,lcm(_lcm,i),limit&&(i==_a[d]),lead&&(!i));
        }
    }
    if(!limit&&!lead)
        dp[d][val][h[_lcm]]=res;
    return res;
}

void init()
{
    int cnt=0;
    for(int i=1;i<=mod;i++)
        if(mod%i==0)
        {
            h[i]=cnt;
            cnt++;
        }
}

ll solve(ll num)
{
    int cnt=0;
    while(num)
    {
        _a[cnt]=num%10;
        num/=10;
        cnt++;
    }
    return dfs(cnt-1,0,1,true,true);

}

int main()
{
    init();
    memset(dp,-1,sizeof(dp));
    int T;
    scanf("%d",&T);
    while(T--)
    {
        ll l,r;
        scanf("%lld %lld",&l,&r);
        printf("%lld\n",solve(r)-solve(l-1));
    }
    return 0;
}

B - XHXJ’s LIS
题目挺好的,会做这题必须得会nlogn求最长递增序列,因为k<=10,所以我们可以用10位的2进制来记录已经有的递增序列,该位为1就代表有这个数字。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
using namespace std;

typedef long long ll;
ll L,R;
int K;
ll dp[50][2000][15];
ll a[50];

int getidx(int val,int num)
{
    for(int i=0;i<=9;i++)
    {
        int tmp=(val>>i)&1;
        if(tmp&&i>=num)
        {
           return i;
        }
    }
    return -1;
}

int getnum(int val)
{
    int res=0;
    for(int i=0;i<=9;i++)
        if((val>>i)&1)res++;
    return res;
}

ll dfs(int d,int val,bool limit,bool lead)
{
    if(d==-1)
        return getnum(val)==K;
    if(!limit&&!lead&&dp[d][val][K]!=-1)
        return dp[d][val][K];
    int End=limit?a[d]:9;
    ll res=0;
    for(int i=0;i<=End;i++)
    {

        int idx=getidx(val,i);
        int Del=idx==-1?0:(1<<idx);
        if(i==0)
        {
            if(lead)
                res+=dfs(d-1,val,limit&&i==End,1);
            else
                res+=dfs(d-1,val-Del+(1<<i),limit&&i==End,0);
        }
        else
            res+=dfs(d-1,val-Del+(1<<i),limit&&i==End,0);
    }
    if(!limit&&!lead)
        dp[d][val][K]=res;
    return res;
}

ll solve(ll num)
{
    int cnt=0;
    while(num)
    {
        a[cnt++]=num%10;
        num/=10;
    }
   return dfs(cnt-1,0,true,true);
}

int main()
{
    int T;
    scanf("%d",&T);
    int cas=1;
    memset(dp,-1,sizeof(dp));
    while(T--)
    {
      scanf("%lld %lld %d",&L,&R,&K);
      printf("Case #%d: %lld\n",cas++,solve(R)-solve(L-1));

    }
}

E - Round Numbers
这题一开始想复杂了,实际上把数字转换成2进制就会好做很多。另外注意要剔除前导零的影响。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
using namespace std;

int dp[64][64][64];

int a[64];

int dfs(int d,int x,int y,bool limit,bool lead)
{
    if(d==-1)return x>=y;
    if(!limit&&!lead&&dp[d][x][y]!=-1)return dp[d][x][y];
    int End=limit?a[d]:1;
    int res=0;
    for(int i=0;i<=End;i++)
    {
        if(i)res+=dfs(d-1,x,y+1,limit&&i==End,lead&&!i);
        else
            {
                if(lead)
                    res+=dfs(d-1,x,y,limit&&i==End,1);
                else
                    res+=dfs(d-1,x+1,y,limit&&i==End,0);
            }
    }
    if(!limit&&!lead)dp[d][x][y]=res;
    return res;

}

int solve(int num)
{
    int cnt=0;
    while(num)
    {
        a[cnt++]=num%2;
        num/=2;
    }
    return dfs(cnt-1,0,0,true,true);
}

int main()
{
    memset(dp,-1,sizeof(dp));
    int L,R;
    scanf("%d %d",&L,&R);
    //cout<<solve(R)<<endl;
    printf("%d\n",solve(R)-solve(L-1));
    return 0;


}

F - Balanced Number
这题思路还是对的,结果调了好久没对,突然发现没有初始化。只要另外维护对称轴的位置及值就行了。
有一个小细节需要注意每次枚举轴都会将所有都是0的计算一次,所以最后的答案需要减掉(枚举次数-1).

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
using namespace std;
typedef long long ll;

ll dp[20][20][2000];
int a[20];

ll dfs(int d,int loc,int sum,bool limit,bool lead)
{
    if(d==-1)return sum==0;
    if(sum<0)return 0;
    if(!limit&&!lead&&dp[d][loc][sum]!=-1)return dp[d][loc][sum];
    int End=limit?a[d]:9;
    ll res=0;
    for(int i=0;i<=End;i++)
    {
        int tmp=(d-loc)*i;
        res+=dfs(d-1,loc,sum+tmp,limit&&i==End,lead&&!i);
    }
    if(!limit&&!lead)
        dp[d][loc][sum]=res;
    return res;
}

ll solve(ll num)
{
    //int offset=1000;
    int cnt=0;
    while(num)
    {
        a[cnt++]=num%10;
        num/=10;
    }
    ll res=0;
    for(int i =0;i<cnt;i++)
        res+=dfs(cnt-1,i,0,true,true);
    return res-(cnt-1);
}

int main()
{
    int T;
    scanf("%d",&T);
    memset(dp,-1,sizeof(dp));
    while(T--)
    {
        ll x,y;
        scanf("%lld %lld",&x,&y);
        ll R=solve(y);
        ll L=(x==0?0:solve(x-1));
        //cout<<R<<endl;
        //cout<<L<<endl;
        printf("%lld\n",R-L);
    }
    return 0;
}

H - F(x)
这题虽然简单,但我还是t了。。
原因是因为我每次计算的时候都是从0开始,这意味着dp[pos][val] 维护的值就是从0到B中值不大于A的数。
这样每次都得将dp数组初始化。所以会T,实际上我们换种想法类比一下:上面的操作就是一直以0为起始位置,来计算能到目的地的方法个数,而目的地有很多,所以我每次都得重新计算,那我为什么不计算目的地到0的个数呢,0又不会变。
这样就不用重新计算了。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
using namespace std;

int dp[15][5000];
int a[15];
int tab[15];
int mx;
void init()
{
    tab[0]=1;
    for(int i=1;i<=10;i++)tab[i]=tab[i-1]*2;
}

int dfs(int d,int val,bool limit,bool lead)
{
    if(d==-1)return val>=0;
    if(val<0)return 0;
    if(!limit&&!lead&&dp[d][val]!=-1)return dp[d][val];
    int End=limit?a[d]:9;
    int res=0;
    for(int i=0;i<=End;i++)
    {
        res+=dfs(d-1,val-tab[d]*i,limit&&i==End,lead&&!i);
    }
    if(!limit&&!lead)
        dp[d][val]=res;
    return res;
}

int solve(int num)
{
    int cnt=0;
    while(num)
    {
        a[cnt++]=num%10;
        num/=10;
    }
    return dfs(cnt-1,mx,true,true);
}

void getmx(int A)
{
    int cnt=0;
    while(A)
    {
        a[cnt++]=A%10;
        A/=10;
    }
    mx=0;
    for(int i=cnt-1;i>=0;i--)
        mx+=tab[i]*a[i];
}

int main()
{
    init();
    int T;
    memset(dp,-1,sizeof(dp));
    scanf("%d",&T);
    int A,B;
    int cas=1;
    while(T--)
    {

        scanf("%d %d",&A,&B);
        getmx(A);
        printf("Case #%d: %d\n",cas++,solve(B));
    }
    return 0;

}

J - 吉哥系列故事――恨7不成妻
这题并不会做,看了题解才会的。
首先我们得维护三个值,个数,和,平方和。设当前状态为res,它的下一状态为tmp。用结构体来维护。
求个数不多说了。res.cnt+=tmp.cnt.
求和 res.sum+=tmp.sum+tmp.cnt*i(i为当前枚举的位的值)。
求平方和,设下一状态的数值为x(注意是数值)。那么 sqrsum=(10di+x)2=102di2+x2+210dix s q r s u m = ( 10 d ∗ i + x ) 2 = 10 2 ∗ d ∗ i 2 + x 2 + 2 ∗ 10 d ∗ i ∗ x
对于上面的式子,我们还得乘以一个tmp.cnt(也不能说乘以tmp.cnt,就是对于这个式子我们要求出所有下一状态的平方和)。那么 x2 x 2 这一项就变成了tmp.sqrsum, xtmp.cnt x ∗ t m p . c n t 就变成了tmp.sum。所以我们就可以求出res.sqrsum啦。
注意一些细节,该模的地方要模就行了。
ans算出来的结果可能为负数,所以要模两次。

#include<iostream>
#include<cstring>
#include<stdio.h>
#include<algorithm>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;
ll tab[20];

struct node
{
    ll cnt,sum,sqsum;
    node(ll _c,ll _sum,ll _sqsum):cnt(_c),sum(_sum),sqsum(_sqsum){}
    node(){}
}dp[20][10][10];
int a[20];

node dfs(int d,int sum,int num,bool limit,bool lead)
{
    if(d==-1)
        return sum&&num?node(1,0,0):node(0,0,0);
    if(!limit&&!lead&&dp[d][sum][num].cnt!=-1)return dp[d][sum][num];
    int End=limit?a[d]:9;
    node res=node(0,0,0);
    node tmp;
    for(int i=0;i<=End;i++)
    {
        if(i==7)continue;
        tmp=dfs(d-1,(sum+i)%7,(num*10+i)%7,limit&&i==End,lead&&!i);
        (res.cnt+=tmp.cnt)%=mod;
        (res.sum+=(tmp.sum+(tab[d]*i)*tmp.cnt%mod)%mod)%=mod;
        (res.sqsum+=((tmp.sqsum+(((tmp.sum*2)%mod*tab[d])%mod*i)%mod)%mod+(((tab[d]*i%mod)*(tab[d]*i%mod))%mod*tmp.cnt)%mod)%mod)%=mod;
    }
    if(!lead&&!limit)
        dp[d][sum][num]=res;
    return res;
}

ll solve(ll num)
{
    int cnt=0;
    while(num)
    {
        a[cnt++]=num%10;
        num/=10;
    }
    return dfs(cnt-1,0,0,true,true).sqsum;

}

void init()
{
    tab[0]=1;
    for(int i=1;i<=20;i++)tab[i]=tab[i-1]*10%mod;
    for(int i=0;i<20;i++)
        for(int j=0;j<10;j++)
        for(int k=0;k<10;k++)dp[i][j][k].cnt=-1;
}

int main()
{
    init();
    int T;
    scanf("%d",&T);
    while(T--)
    {
        ll L,R;
        scanf("%lld %lld",&L,&R);
        ll ansr=solve(R);
        ll ansl=solve(L-1);
        ll ans=(ansr-ansl);
        ans=(ans%mod+mod)%mod;
        printf("%lld\n",ans);
    }
    return 0;
}

K - Balanced Numbers
一道简单题,维护数字的奇偶性就行了,还有维护那些数字是否出现过,所以我用3进制来维护。还好内存没炸。。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
using namespace std;
typedef unsigned long long ll;
ll dp[25][60000];
int a[25];
ll tab[15];
void init()
{
    tab[0]=1;
    for(int i=1;i<=10;i++)tab[i]=tab[i-1]*3;
}


bool check(int val)
{
    for(int i=0;i<=9;i++)
    {
        int tmp=(val/tab[i])%3;
        if(tmp==0)continue;
        if((tmp%2&&i%2)||(tmp%2==0&&i%2==0))
            return 0;
    }
    return 1;
}

int change(int val,int i)
{
    int _val=val;
    val/=tab[i];
    int tmp=val%3;
    if(tmp==0)
        _val+=tab[i];
    else if(tmp==1)
        _val+=tab[i];
    else
        _val-=tab[i];
    return _val;
}

ll dfs(int d,int val,bool limit,bool lead)
{
    if(d==-1)
    {
        if(check(val))return 1;
        return 0;
    }
    if(!limit&&!lead&&dp[d][val]!=-1)return dp[d][val];
    int End=limit?a[d]:9;
    ll res=0;
    for(int i=0;i<=End;i++)
    {
        if(i==0)
        {
            if(lead)
                res+=dfs(d-1,val,limit&&i==End,1);
            else
                res+=dfs(d-1,change(val,i),limit&&i==End,0);
        }
        else
            res+=dfs(d-1,change(val,i),limit&&i==End,0);
    }
    if(!limit&&!lead)dp[d][val]=res;
    return res;
}

ll solve(ll num)
{
    int cnt=0;
    while(num)
    {
        a[cnt++]=num%10;
        num/=10;
    }
    return dfs(cnt-1,0,true,true);
}

int main()
{
    init();
    int T;
    memset(dp,-1,sizeof(dp));
    scanf("%d",&T);
    while(T--)
    {
        ll A,B;
        scanf("%lld %lld",&A,&B);
        printf("%lld\n",solve(B)-solve(A-1));
    }
    return 0;


}

数位dp感觉很简单有感觉很巧妙,不算是很入门吧,还得继续训练!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值