bzoj1799(洛谷4127)同类分布(月之谜)

题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1799

   https://www.luogu.org/problemnew/show/P4127

经典dp!

一个数能被它的各位和整除,在L-R内有多少个。

1.数位dp的套路,先预处理出第i位、后面任意的所有情况。

  因为涉及整除,所以状态有“模当前数余几”;

  因为涉及各位和,所以状态有“i位和为j”和“模k”;

好了我们有了一份会MLE的四维代码,而且有会超时的18位预处理,答案好歹是正确的。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int sx=18*9;
ll tmp,f[19][165][165][165],cnt1,cnt2;
int lmm,a[19];
ll pw(int k)
{
    ll mul=1;
    for(int i=1;i<=k;i++)
        mul*=10;
    return mul;
}
void pre()
{
    for(int k=1;k<=sx;k++)
            f[0][0][k][0]=1;
    int lm=0;
    for(int i=1;i<=18;i++)
    {
        lm=i*9;
        for(int j=0;j<=lm;j++)
            for(int k=1;k<=sx;k++)
                for(int l=0;l<k;l++)
                    for(int p=0;p<=9&&j-p>=0;p++)
                        f[i][j][k][l]+=f[i-1][j-p][k][((l-p*pw(i-1))%k+k)%k]; 
//                        if(i==1&&j==p&&k==p)printf("j=%d k=%d l=%d p=%d ff=%lld\n",j,k,l,p,f[i][j][k][l]);
                        
    }
}
void chl()
{
    lmm=0;
    while(tmp)
    {
        a[++lmm]=tmp%10;
        tmp/=10;
    }
}
ll calc()
{
    int t=0;
    ll cnt=0,lj=0;
    for(int i=lmm;i;i--)
    {
        int ll=a[i];if(i==1)ll++;
        for(int p=0;p<ll;p++)
            for(int s=max(1,t+p);s<=sx;s++)
                cnt+=f[i-1][s-t-p][s][(s-(lj+p*pw(i-1))%s)%s]; 
        t+=a[i];lj+=a[i]*pw(i-1);
    }
    return cnt;
}
int main()
{
    scanf("%lld",&tmp);tmp--;
    pre();
    chl();
    cnt1=calc();
//    printf("(%lld)\n",cnt1);
    scanf("%lld",&tmp);
    chl();
    cnt2=calc();
//    printf("(%lld)\n",cnt2);
    printf("%lld",cnt2-cnt1);
    return 0;
}
MLE+TLE

在bzoj 50s 下应该还不会超时,考虑解决MLE。

2.发现pre转移的时候,模数s是固定的,即每个状态由模数相同的状态转移过来。

  所以只要把模数s的枚举放在最外面,就可以省掉这一维,代价是每一个s都要预处理一遍。

  尽可能优化一点。

    1)发现L和R的预处理有一部分是重复的,所以把R - L这一部分也在循环里实现而不是最后再减;

    2)(微弱)其实预处理到lm[1]-1位就行了。

bzoj上能过了(30s+),洛谷上超时一个点,而且其他点好慢……

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
ll f[22][180][180],l,r,mul[22],ans;
int lm[3],a[3][22],sx;
void pw()
{
    mul[0]=1;
    for(int i=1;i<=lm[1];i++)
        mul[i]=mul[i-1]*10;
}
void pre(int s)
{
    memset(f,0,sizeof f);
    f[0][0][0]=1;//
    int lmm=0;
    for(int i=1;i<=lm[1];i++)
    {
        lmm=min(s,i*9);
        for(int j=0;j<=lmm;j++)
            for(int l=0;l<s;l++)//l<j是错的(为何) 
                for(int p=0;p<=9&&j-p>=0;p++)
                    f[i][j][l]+=f[i-1][j-p][((l-p*mul[i-1])%s+s)%s];//考虑l-p*pw(i-1)<0 
    }
}
void chl(int k,ll tmp)
{
    while(tmp)
    {
        a[k][++lm[k]]=tmp%10;
        tmp/=10;
    }
}
ll calc(int k,int s)
{
    ll cnt=0;
//    printf("s=%d\n",s);
    int t=0;ll lj=0;
    for(int i=lm[k];i;i--)
    {
        int llm=a[k][i];if(i==1)llm++;
        for(int p=0;p<llm&&s-t-p>=0;p++)
                cnt+=f[i-1][s-t-p][(s-(lj+p*mul[i-1])%s)%s];//考虑lj+p*pw(i-1)==0 
        t+=a[k][i];lj+=a[k][i]*mul[i-1];
//        printf("i=%d cnt=%d\n",i,cnt);
    }
    return cnt;
}
int main()
{
    scanf("%lld%lld",&l,&r);
    chl(0,l-1);chl(1,r);
    sx=(lm[1]-1)*9+a[1][lm[1]];pw();
    for(int s=1;s<=sx;s++)
    {
        pre(s);
        ans+=calc(1,s)-calc(0,s);
    }
    printf("%lld",ans);
    return 0;
}
TLE

3.发现大家写的都是记忆化深搜。

  为什么比较快呢?因为它不需要把一些用不到的状态也预处理出来。

  但具体是什么我也不清楚。详见代码?

  然后发现大牛的代码:写得好好!于是近乎抄了下来。

  这里只要有最后那个d=0时的限制就不用记录各位和为j了,只要最后整个各位和等于s就行。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
ll input()
{
    int f=1;ll s=0;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=s*10+ch-'0';ch=getchar();}
    return s*f;
}
const int N=165,M=20;
ll x,y,dp[2][M][N][N];
int cnt,a[M],dfn[2][M][N][N],tot,mod;
ll calc(bool p,int d,int s,int m)//p为有无限制 ,d是第几位,s是模数也是各位和 ,m是前面填好的东西%s余数 
{
    if(!d)return (!s&&!m);//初值,第0位时仅当s为0(个位填好后整个的各位和为s)、m为0(整除)时为1
    if(dfn[p][d][s][m]==tot)return dp[p][d][s][m];
    dfn[p][d][s][m]=tot;ll tmp=0;
    int l=max(0,s-9*(d-1)),r=min(((p)?9:a[d]),s);//r<=s
    for(int i=l;i<=r;i++)tmp+=calc(p|(i<a[d]),d-1,s-i,(m*10+i)%mod);//i是当前位填几 
    return dp[p][d][s][m]=tmp;
}
ll solve(ll ret)
{
    for(cnt=0;ret;ret/=10)a[++cnt]=ret%10;
    ll ans=0;int sx=cnt*9;
    for(mod=1;mod<=sx;mod++)tot++,ans+=calc(0,cnt,mod,0);
    return ans;
}
int main()
{
    x=input();y=input();
    return printf("%lld",solve(y)-solve(x-1)),0;
}

有点奇怪的是下面这份代码数组范围应该都够,但仅改数组范围就能修复一个RE(变成上边那个TLE),也就是说数组越界?

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
ll f[19][165][165],l,r,mul[19],ans;
int lm[2],a[2][19],sx;
void pw()
{
    mul[0]=1;
    for(int i=1;i<=lm[1];i++)
        mul[i]=mul[i-1]*10;
}
void pre(int s)
{
    memset(f,0,sizeof f);
    f[0][0][0]=1;//
    int lmm=0;
    for(int i=1;i<=lm[1];i++)
    {
        lmm=min(s,i*9);
        for(int j=0;j<=lmm;j++)
            for(int l=0;l<s;l++)//l<j是错的(为何) 
                for(int p=0;p<=9&&j-p>=0;p++)
                    f[i][j][l]+=f[i-1][j-p][((l-p*mul[i-1])%s+s)%s];//考虑l-p*pw(i-1)<0 
    }
}
void chl(int k,ll tmp)
{
    while(tmp)
    {
        a[k][++lm[k]]=tmp%10;
        tmp/=10;
    }
}
ll calc(int k,int s)
{
    ll cnt=0;
//    printf("s=%d\n",s);
    int t=0;ll lj=0;
    for(int i=lm[k];i;i--)
    {
        int llm=a[k][i];if(i==1)llm++;
        for(int p=0;p<llm&&s-t-p>=0;p++)
                cnt+=f[i-1][s-t-p][(s-(lj+p*mul[i-1])%s)%s];//考虑lj+p*pw(i-1)==0 
        t+=a[k][i];lj+=a[k][i]*mul[i-1];
//        printf("i=%d cnt=%d\n",i,cnt);
    }
    return cnt;
}
int main()
{
    scanf("%lld%lld",&l,&r);
    chl(0,l-1);chl(1,r);
    sx=(lm[1]-1)*9+a[1][lm[1]];pw();
    for(int s=1;s<=sx;s++)
    {
        pre(s);
        ans+=calc(1,s)-calc(0,s);
    }
    printf("%lld",ans);
    return 0;
}
一个RE

真是经典。以后也许要回顾回顾了呢。

人家的代码好好……

posted on 2018-04-09 13:59  Narh  阅读(438)  评论(0编辑  收藏  举报

导航