简单基础数位 dp 题

终于比较理解了数位 \(dp\) ( qwq

处理大数区间的计数,分成每一位考虑,\(f_{pos}\) 考虑从高到低位在第 \(pos\) 位并且满足某些条件的答案,这个东西我们可以记忆化搜索,但是注意要设计好状态,不然会漏或者重复计算某些情况 qwq 。

需要观察题目有没有前导 \(0\) 限制,不过这个也不是重点了。

关键在于对状态的设计。


[ZJOI2010]数字计数

数位 \(dp\) 模板题。

设当前我们要统计的数为 \(G\)\(dp_{pos,sta}\) 表示从高到低第 \(pos\) 位已经选了 \(sta\)\(G\) 的方案数。直接 \(dp\) 即可。

int DFS(int pos,int sta,int lim,int zero)
{// lim 表示最高位限制,zero 表示是否在前导零
    if(!pos) return sta;
    if(!lim && !zero && (~dp[pos][sta])) return dp[pos][sta];
    int sum=0, up=(lim)?s[pos]:9;
    for(ri int i=0;i<=up;i++)
    {
        int pp=zero&&(!i);
        sum+=DFS(pos-1,sta+((!pp)&&i==G),lim&&(i==up),zero&&(!i));//如果不在前导零才可以贡献,否则当统计 0 的贡献时候会炸掉。
    }
    if(!lim && !zero) dp[pos][sta]=sum;
    return sum;
}

[SCOI2009] windy 数

又一个数位 \(dp\) 模板题。

\(dp_{pos,pre}\) 为第 \(pos\) 位且上一位的数是 \(pre\) 时的答案,直接 \(dp\) 。注意当处于前导 \(0\) 状态却满足条件时可以直接 \(continue\)

int DFS(int pos,int pre,int lim,int zero)
{
    if(!pos) return 1;
    if((!lim) && (!zero) && (~dp[pos][pre+20])) return dp[pos][pre+20];
    int sum=0, up=(lim)?s[pos]:9;
    for(ri int i=0;i<=up;i++)
    {
        if(abs(i-pre)<2&&(!zero)) continue;
        sum+=DFS(pos-1,i,lim&&(i==up),zero&&(!i));
    }
    if(!lim && !zero) dp[pos][pre+20]=sum;
    return sum;
}

萌数

比起前两道要难一些的数位 \(dp\) 题。

考虑一个字符串存在至少一个子串是回文串的情况。显然,这个子串的最短长度为 \(2\)\(3\),所以我们可以考虑记录在第 \(pos\) 位时,前两位分别的值。然后你发现这连样例都过不去...

考虑一种情况是 \(090\),这种情况你发现 \(pos-2\) 位置上是处于前导 \(0\) 状态。所以我们还需要记录处于第 \(pos\) 位时,前两位是否处于前导 \(0\) 状态。然后发现这个东西要开 \(6\) 维数组...

\(dp_{pos,m1,m2,sta,qwq1,qwq2}\) 记录第 \(pos\) 位时,第 \(pos-1\) 位的数是 \(m1\),第 \(pos-2\) 位的数是 \(m2\),当前是否已经产生回文串 \(sta\),第 \(pos-1\) 位是否处于前导 \(0\) 状态 \(qwq1\),第 \(pos-2\) 位是否处于前导 \(0\) 状态 \(qwq2\)

int DFS(int pos,int sta,int m1,int m2,int qwq1,int qwq2,int lim,int zero)
{
    if(!pos)
    {
        /*if(sta)
        {
            for(ri int i=len;i;i--) printf("%lld",cnt[i]);
            puts("");
        }*/
        return sta;
    }
    if((!lim) && (!zero) && (~dp[pos][m1][m2][sta][qwq1][qwq2])) return dp[pos][m1][m2][sta][qwq1][qwq2];
    int sum=0, up=(lim)?(s[pos]-'0'):9;
    for(ri int i=0;i<=up;i++)
    {
        int qq=(((m1==i)&&(!qwq1))||((m2==i)&&(!qwq2)))||sta;
        int pp=(zero&&(!i));
        cnt[pos]=i;
        sum=(sum+DFS(pos-1,qq,i,m1,pp,qwq1,lim&&(i==up),pp))%Mod;
        cnt[pos]=0;
    }
    if((!lim) && (!zero) ) dp[pos][m1][m2][sta][qwq1][qwq2]=sum;
    return sum;
}

[CF55D] Beautiful numbers

带些思考的数位 \(dp\) 题。

考虑 \(1-9\) 区间的数的 \(lcm\) 最大为 \(2520\),所以你对于算出的那个数 \(x\),只需要考虑 \(x\%2520\) 即可。然后发现要开个差不多 \(20\times 2520 \times 2520\) 的数组还要用好多次,会 \(MLE\)...

观察到 \(1-9\) 区间的数的 \(lcm\) 个数仅有 \(48\) 个,可以用类似于离散化一样的思想预处理一下就好了。

inline void Init()
{
    for(ri int a=0;a<=3;a++)
    for(ri int b=0;b<=2;b++)
    for(ri int c=0;c<=1;c++)
    for(ri int d=0;d<=1;d++)
    g[++cnt]=ksc(2,a)*ksc(3,b)*ksc(5,c)*ksc(7,d);
    sort(g+1,g+1+cnt);
    for(ri int i=1;i<=cnt;i++) book[g[i]]=i;
    pw[0]=1;
    for(ri int i=1;i<=18;i++) pw[i]=pw[i-1]*10;
}
int DFS(int pos,int sta,int now,int lim,int zero)
{
    if(!pos)
    {
        if(sta%g[now]) return 0;
        return 1;
    }
    if((!lim) && (!zero) && (~dp[pos][sta][now])) return dp[pos][sta][now];
    int sum=0, up=(lim)?s[pos]:9;
    for(ri int i=0;i<=up;i++)
    {
        int pp=(sta+i*pw[pos-1]%Mod)%Mod;
        int qq=g[now];
        if(i) qq=qq*i/__gcd(i,qq);
        qq=book[qq];
        sum+=DFS(pos-1,pp,qq,lim&&(i==up),zero&&(!i));
    }
    if((!lim) && (!zero)) dp[pos][sta][now]=sum;
    return sum;
}

[AHOI2009]同类分布

原数的值这个状态显然存不下...考虑到上一题我们的做法,将这个原数对某个数进行取模。

发现各位数字之和的值域 \(x\) 非常小,设原数为 \(sta\),则我们要得到 \(sta\%x==0\)。所以我们可以枚举各位数字之和 \(x\),把 \(x\) 作为模数即可。

int DFS(int pos,int sta,int now,int Mod,int lim,int zero)
{
    if(!pos)
    {
        if((!sta) && now==Mod) return 1;
        return 0;
    }
    if((!lim) && (!zero) && (~dp[pos][sta][now])) return dp[pos][sta][now];
    int sum=0, up=(lim)?s[pos]:9;
    for(ri int i=0;i<=up;i++) sum+=DFS(pos-1,(sta*10+i)%Mod,now+i,Mod,lim&&(i==up),zero&&(!i));
    if((!lim) && (!zero)) dp[pos][sta][now]=sum;
    return sum;
}
inline int S(int k)
{
    int cnt=0;
    while(k) s[++cnt]=k%10, k/=10;
    int res=0;
    for(ri int i=1;i<=cnt*9;i++)
    {
        memset(dp,-1,sizeof(dp));
        res+=DFS(cnt,0,0,i,1,1);
    }
    return res;
}

花神的数论题

考虑把原数先变为二进制表示。然后枚举二进制数的 \(1\) 的个数 \(i\),利用快速幂统计贡献。

注意在数位 \(dp\) 求方案数时不能对 \(1e7+7\) 取模,因为它不是质数。

int DFS(int pos,int sta,int now,int lim,int zero)
{
    if(!pos) return (now==sta);
    if((!lim) && (!zero) && (~dp[pos][sta])) return dp[pos][sta];
    int sum=0, up=(lim)?s[pos]:1;
    for(ri int i=0;i<=up;i++) sum=(sum+DFS(pos-1,sta+i,now,lim&&(i==up),zero&&(!i)));
    if((!lim) && (!zero)) dp[pos][sta]=sum;
    return sum;
}
inline int S(int k)
{
    int cnt=0;
    while(k) s[++cnt]=(k&1ll), k>>=1ll;
    int res=1;
    for(ri int i=1;i<=cnt;i++)
    {
        memset(dp,-1,sizeof(dp));
        res=res*ksc(i,DFS(cnt,0,i,1,1))%Mod;
    }
    return res;
}

[CF628D] Magic Numbers

和之前题目一样的套路,直接判断第 \(i\) 位与 \(d\) 的关系即可。

int DFS(int pos,int sta,int lim,int zero)
{
    if(!pos) return (!sta);
    if((!lim) && (!zero) && (~dp[pos][sta])) return dp[pos][sta];
    int sum=0, up=(lim)?(s[pos]-'0'):9;
    for(ri int i=0;i<=up;i++)
    {
        if((len-pos+1)&1ll)
        {
            if(i==d) continue;
            sum=(sum+DFS(pos-1,(sta*10+i)%m,lim&&(i==up),zero&&(!i)))%Mod;
        }
        else
        {
            if(i^d) continue;
            sum=(sum+DFS(pos-1,(sta*10+i)%m,lim&&(i==up),zero&&(!i)))%Mod;
        }
    }
    if((!lim) && (!zero)) dp[pos][sta]=sum;
    return sum;
}

[CF401D] Roman and Numbers

直接状压 \(dp\) 即可。但是我还是用了个数位 \(dp\),一开始我是用 \(dp_{sta,now}\) 表示当前顺序表示出来的数 \(\%\) \(m\) 的值,当前选的数的集合(用二进制状压表示)为 \(now\)。然后直接 \(TLE\)。。。

这种做法最后还要对 \(0-9\) 的每个数出现次数 \(cnt\) 进行去重。所以你发现在数位 \(dp\) 时会非常慢。考虑进行一个优化,对每个 \(0-9\) 的数在某位确定了之后,不再需要考虑相同的数编号互换的情况,我们可以把数位拆分后排序,这样不会影响答案并可以去掉重复操作。

inline int DFS(int pos,int sta,int now)
{
    if(!pos) return (!sta);
    if(~dp[sta][now]) return dp[sta][now];
    int sum=0;
    for(ri int i=1;i<=cnt;i++)
    {
        if(!now && !s[i]) continue;
        if(!((1ll<<(i-1))&now) && (i==1 || s[i]!=s[i-1] || ((1ll<<(i-2))&now)))
            sum+=DFS(pos-1,(sta*10+s[i])%m,now|(1ll<<(i-1)));
    }
    dp[sta][now]=sum;
    return sum;
}
inline int S(int k)
{
    while(k) s[++cnt]=k%10, k/=10;
    sort(s+1,s+1+cnt);
    memset(dp,-1,sizeof(dp));
    return DFS(cnt,0,0);
}

[CQOI2016]手机号码

直接暴力记录 \(dp_{pos,sta,m1,m2,t1,t2}\) 表示在第 \(pos\) 位,是否已经满足条件 \(sta\)\(pos+1\) 位的数 \(m1\)\(pos+2\) 位的数 \(m2\),是否出现过 \(4\)\(t1\) 记录,\(8\)\(t2\) 记录。直接暴力数位 \(dp\) 即可。注意当数字总位数不为 \(11\) 时直接返回 \(0\),因为我们在数位 \(dp\) 时可以直接跳过前导 \(0\)

int DFS(int pos,int sta,int m1,int m2,int t1,int t2,int lim)
{
    if(!pos)
    {
        if(sta) return 1;
        return 0;
    }
    if((!lim) && (~dp[pos][sta][m1][m2][t1][t2])) return dp[pos][sta][m1][m2][t1][t2];
    int sum=0;
    int dn=(pos==cnt)?1:0;
    int up=(lim)?s[pos]:9;
    for(ri int i=dn;i<=up;i++)
    {
        int f1,f2;
        f1=t1|(i==4);
        f2=t2|(i==8);
        if(f1&&f2) continue;
        int qwq=sta;
        if(i==m1&&m1==m2) qwq=1;
        sum+=DFS(pos-1,qwq,i,m1,f1,f2,lim&&(i==up));
    }
    if(!lim) dp[pos][sta][m1][m2][t1][t2]=sum;
    return sum;
}
inline int S(int k)
{
    cnt=0; memset(dp,-1,sizeof(dp));
    while(k) s[++cnt]=k%10, k/=10;
    if(cnt^11ll) return 0;
    else return DFS(cnt,0,0,0,0,0,1);
}
posted @ 2020-08-02 18:19  zkdxl  阅读(156)  评论(3编辑  收藏  举报