【数位DP】CF55D BZOJ3329 HDU4352 SGU390 HDU5519

本文介绍了数位动态规划(DP)的概念和应用场景,通过讲解SPOJ10606、BZOJ3629、CodeForces55D等题目,阐述了如何利用数位DP解决计算区间内满足特定条件的数的个数问题。涉及二进制表示、最长上升子序列和售票员售票等场景,文章提供了相应的解题思路和优化技巧。
摘要由CSDN通过智能技术生成

前言

有一些题之前已经写了题解了,就只留一个链接吧…

一般的数位DP都是计算一段区间满足某条件的数有多少个。
顾名思义数位DP就是按照数一位一位滴进行DP。通常至少有二维,其中一位表示当前在第 i 位上,另一维表示与n的大小关系。
具体实现方法通常有递推版和记忆化搜索版。

SPOJ10606

SPOJ10606

BZOJ3629

BZOJ3629

CodeForces55D

CodeForces-55D
题目大意:题目大意:给定 LR ,求 [LR] 中所有可以被它所有非零数位整除的数的个数。
比如说 52 就是一个满足题意的数。

由于被非零数位整除这一个条件很讨厌,尝试将所有可能的数位转化成同一个数整除。这个数就是 1 9的数的最小公倍数 2520 。只要最后再判断一下就行了。

然后我们发现 f[20][2525][2525] 好像有一点点存不下…
然后 1 9中任意一个集合的 lcm 只有 49 种。所有把他们离散化一下。

#include <iostream>
#include <cstdio>
#include <cstring>
#define LL long long int
#define mod 2520
using namespace std;

LL f[20][mod+5][50];
int h[mod+5], w[20], len, cnt;

int gcd(int a,int b)
{
    if(!b)return a;
    return gcd(b,a%b);
}

int lcm(int a,int b)
{
    if(!b)return a;
    return a*b/gcd(a,b);
}

LL dfs(int i,int l,int rest,bool flag)
{
    if(i==0)return rest%l==0;
    if(!flag&&~f[i][rest][h[l]])return f[i][rest][h[l]];
    int j;
    LL ans=0;
    if(flag)j=w[i];
    else j=9;
    for(;j>=0;--j)
        ans+=dfs(i-1,lcm(l,j),(rest*10+j)%mod,flag&&j==w[i]);
    if(!flag)f[i][rest][h[l]]=ans;
    return ans;
}
//可能有人会问:为什么在计算时f数组不清零?
//其实木有必要清零啊,每一次都是从1到9,每一次算的答案都会是一样的
LL cal(LL n)
{
    if(n==0)return 1;
    len=0;
    while(n){w[++len]=n%10;n/=10;}
    return dfs(len,1,0,1);
}

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

int main()
{
    init();
    int cas;
    LL l, r;
    scanf("%d",&cas);
    while(cas--)
    {
        scanf("%I64d%I64d",&l,&r);
        printf("%I64d\n",cal(r)-cal(l-1));
    }
    return 0;
}

BZOJ3329

BZOJ3329
这样的 x 满足什么性质?
x写成二进制不存在两个相邻的 1
然后令f[i][j][k]表示当前算到第 i 位(二进制),第i位上填的数是 j ,与n的大小关系为 k
于是乎第一问解决。

第二问:
g[i][j]表示第 i j,那么有:
f[i][0]=f[i1][0]+f[i1][1]f[i][1]=f[i1][0]
然后把两个式子合并一下就有: f[i]=f[i1]+f[i2]
这不是Fibonacci sequence
矩阵加速上吧…

#include <iostream>
#include <cstdio>
#include <cstring>
#define LL long long int
#define mod 1000000007
using namespace std;

int len, w[65];

struct mat
{
    LL num[5][5];
    void init(){memset(num,0,sizeof num);}
    mat operator * (const mat &a)const
    {
        mat ans;
        ans.init();
        for(int i=1;i<=2;++i)
            for(int j=1;j<=2;++j)
                for(int k=1;k<=2;++k)
                    ans.num[i][j]=(ans.num[i][j]+num[i][k]*a.num[k][j])%mod;
        return ans;
    }
}a, b;

mat power(mat a,LL pos)
{
    mat ans=a;
    while(pos)
    {
        if(pos&1)ans=ans*a;
        a=a*a;
        pos>>=1;
    }
    return ans;
}

LL f[65][2][2], n;
LL solve1(LL n)
{
    if(n==0)return 0;
    len=0;
    while(n){w[++len]=n&1;n>>=1;}
    memset(f,0,sizeof f);
    f[len][0][0]=1, f[len][1][1]=1;

    for(int i=len-1;i;--i)
    {
        for(int j=0;j<2;++j)
        {
            for(int k=0;k<2;++k)
                if(!k||!j)
                {
                    if(w[i]>k)f[i][k][0]+=f[i+1][j][1];
                    else if(w[i]==k)f[i][k][1]+=f[i+1][j][1];
                    f[i][k][0]+=f[i+1][j][0];
                }
        }
    }
    return f[1][0][1]+f[1][0][0]+f[1][1][0]+f[1][1][1]-1;
}

LL solve2(LL n)
{
    b.num[1][1]=b.num[1][2]=1;
    b=b*power(a,n-1);
    return b.num[1][2];
}

int main()
{
    a.num[1][2]=a.num[2][1]=a.num[2][2]=1;
    int cas;
    scanf("%d",&cas);
    while(cas--)
    {
        scanf("%lld",&n);
        printf("%lld\n%lld\n",solve1(n),solve2(n));
    }
    return 0;
}

HDU 4352

HDU4352
题目大意:定义一个数字的 val 为将其转化为字符串,最长上升子序列的长度即是 val 。求 [LR] val=k 的数的个数。

考虑最长上升子序列的 O(nlogn) 的做法。
维护一个类似于单调栈的东西,插入元素 i 时,用元素i替换最小的且大于 i 的那个数(若元素i是最大的,直接加在最后)。栈的长度就是最长上升子序列长度。

f[i][s][k] 表示计算到第 i 位,在单调栈中元素出现情况为s,与 n 的大小关系为k
然后用记忆化搜索吧,还不能理解的就看代码和注释吧…

#include <iostream>
#include <cstdio>
#include <cstring>
#define LL long long int
using namespace std;

LL l, r, k, f[25][2048][11];
int len, w[25];
//单调栈中插入元素的操作,删除某个数的标记,添加元素j的标记
int get(int s,int j)
{
    for(int l=j;l<10;++l)
        if(s&(1<<l)){s^=1<<l;break;}
    s|=1<<j;
    return s;
}
//is_zero用来统计前导零的
LL dfs(int pos,int s,bool flag,bool is_zero)
{
    if(pos==0)
    {
        int cnt=0;
        //统计有多少个数在单调栈中出现了
        while(s&&cnt<=k)
        {
            if(s&1)++cnt;
            s>>=1;
        }
        return cnt==k;
    }
    if(!flag&&~f[pos][s][k])return f[pos][s][k];
    LL ans=0;
    int j;
    if(flag)j=w[pos];
    else j=9;
    for(l;j>=0;--j)
        if(is_zero&&j==0)
            ans+=dfs(pos-1,0,flag&&j==w[pos],1);
        else ans+=dfs(pos-1,get(s,j),flag&&j==w[pos],0);
    if(!flag)f[pos][s][k]=ans;
    return ans;
}

LL cal(LL n)
{
    if(n==0)return 0;
    len=0;
    while(n){w[++len]=n%10;n/=10;}
    return dfs(len,0,1,1);
}

int main()
{
    memset(f,-1,sizeof f);
    int cas=0, t;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%I64d%I64d%I64d",&l,&r,&k);
        printf("Case #%d: %I64d\n",++cas,cal(r)-cal(l-1));
    }
    return 0;
}

SGU390

SGU390
题目大意:有一位售票员给乘客售票,对于每位乘客,他会卖出多张连续的票,直到已卖出的编号的所有位置上的数的和不小于给定的正数 k 。然后他会按照相同的规则给下一位乘客售票。初始时,售票员持有的编号是从L R 的连续整数。请你求出,售票员可以售票给多少位乘客。

首先此题和上面的题不一样的地方在于不能直接计算cal(n)然后输出 cal(r)cal(l1) 。要同时计算 cal(l,r)

f 数组弄成一个pair类型吧。
first表示票数, second 表示最终剩余容量。
然后枚举第 i 位上的数,更新一下票数和剩余容量。

#include <iostream>
#include <cstdio>
#define LL long long int
using namespace std;

LL l, r;
int k, w[20], len, w2[20], len2;

struct node
{
    LL cnt, rem;
    node(){cnt=-1, rem=0;}
    node(const LL &a,const LL &b){cnt=a, rem=b;}
    void operator += (const node &a){
        cnt+=a.cnt;
        rem=a.rem;
    }
}dp[19][205][1005];
//sum表示目前的数位之和,rem表示剩余容量
node dfs(int pos,LL sum,LL rem,bool f,bool f2)
{
    if(!pos)
    {
        if(sum+rem>=k)return node(1,0);
        return node(0,sum+rem);
    }
    if(!f&&!f2&&~dp[pos][sum][rem].cnt)return dp[pos][sum][rem];
    node ans(0,rem);
    int e, j;
    if(f)j=w[pos];
    else j=0;
    if(f2)e=w2[pos];
    else e=9;
    for(;j<=e;++j)
        ans+=dfs(pos-1,sum+j,ans.rem,f&&j==w[pos],f2&&j==w2[pos]);
    if(!f&&!f2)dp[pos][sum][rem]=ans;
    return ans;
}

LL cal(LL l,LL r)
{
    len=0;
    while(l){w[++len]=l%10;l/=10;}
    len2=0;
    while(r){w2[++len2]=r%10;r/=10;}
    for(int i=len+1;i<=len2;++i)w[i]=0;
    return dfs(len2,0,0,1,1).cnt;
}

int main()
{
    scanf("%I64d%I64d%d",&l,&r,&k);
    printf("%I64d\n",cal(l,r));
    return 0;
}

ZOJ2599

ZOJ2599

HDU5519

HDU5519
题目大意:给定a0,a1,a2,a3,a4以及 n ,问有多少不含前导0 5 进制n位数满足数字 i 的个数不超过ai

不含前导零太可恨辣,于是乎令 w(n,a0,a1,a2,a3,a4) 表示个数不超过 ai 位数为 n 的数的个数。(其实这个函数对后面的编码木有作用= =)
那么ans=w(n,a0,a1,a2,a3,a4)w(n1,a01,a1,a2,a3,a4)

为了计算 w ,令dp[i][s]表示计算到第 i 位,哪些数字使用过的用s来压位表示。
有:
f[i][s]+=f[i1][s]cnt[s];
解释:对于出现过的数字都可以再出现一次。
f[i][s]+=f[i1aj][s]C(i1,aj)
解释:对于一个没出现的数 j <script type="math/tex" id="MathJax-Element-69">j</script>,将它全部插入。

#include <iostream>
#include <cstdio>
#include <cstring>
#define mod 1000000007
#define MAXN 15005
#define LL long long int
using namespace std;
const int END=1<<5;

int n, dp[MAXN][END+5], a[10];
LL fac[MAXN], ni[MAXN], cnt[END+5];

void init()
{
    fac[0]=fac[1]=ni[0]=ni[1]=1;
    for(int i=2;i<=15000;++i)
    {
        fac[i]=fac[i-1]*i%mod;
        ni[i]=-mod/i*ni[mod%i]%mod;
        if(ni[i]<0)ni[i]+=mod;
    }
    for(int i=2;i<=15000;++i)ni[i]=ni[i]*ni[i-1]%mod;
    //cnt[i]表示i中有几个1
    for(int i=1;i<END;++i)cnt[i]=cnt[i^(i&(-i))]+1;
}

LL c(int n,int m){return fac[n]*ni[m]%mod*ni[n-m]%mod;}

int solve()
{
    memset(dp,0,sizeof dp);
    for(int s=0;s<END;++s)
        if(cnt[s^(END-1)]&1)dp[0][s]=mod-1;
        else dp[0][s]=1;
    for(int i=1;i<=n;++i)
    {
        for(int s=0;s<END;++s)
        {
            dp[i][s]=(dp[i][s]+dp[i-1][s]*cnt[s]%mod)%mod;
            for(int k=0;k<5;++k)
                if(!((s>>k)&1)&&a[k]<i)
                dp[i][s|(1<<k)]=(dp[i][s|(1<<k)]+dp[i-1-a[k]][s]*c(i-1,a[k]))%mod;
        }
    }
    return dp[n][END-1];
}

int work()
{
    int ans=solve();
    if(a[0]>0)
    {
        --n, --a[0];
        ans=(ans-solve()+mod)%mod;
    }
    return ans;
}

int main()
{
    init();
    int t, cas=0;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d",&n);
        for(int i=0;i<5;++i)scanf("%d",&a[i]);
        printf("Case #%d: %d\n",++cas,work());
    }
    return 0;
}
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值