【数位DP】CF55D BZOJ3329 HDU4352 SGU390 HDU5519

来源:互联网 发布:淘宝信用贷款还款 编辑:程序博客网 时间:2024/06/05 07:38

前言

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

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

SPOJ10606

SPOJ10606

BZOJ3629

BZOJ3629

CodeForces55D

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

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

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

#include <iostream>#include <cstdio>#include <cstring>#define LL long long int#define mod 2520using 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]表示第ij,那么有:
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 1000000007using 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 intusing 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。然后他会按照相同的规则给下一位乘客售票。初始时,售票员持有的编号是从LR的连续整数。请你求出,售票员可以售票给多少位乘客。

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

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

#include <iostream>#include <cstdio>#define LL long long intusing 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,问有多少不含前导05进制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,将它全部插入。

#include <iostream>#include <cstdio>#include <cstring>#define mod 1000000007#define MAXN 15005#define LL long long intusing 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;}
1 0
原创粉丝点击