数位DP总结

数位DP

感谢大佬的分享
凌乱之风

注意数位DP只与位数有关,当给出的数的范围很大但知道位数的情况下,可以考虑用数位DP

$dfs$写法

int dfs(int pos, int pre, int lead, int limit,其它记录转移状态的参数) {
    if (!pos) {
        边界条件
    }
    if (!limit && !lead && dp[pos][pre] != -1) return dp[pos][pre];
    int res = 0, up = limit ? a[pos] : 无限制位;
    for (int i = 0; i <= up; i ++) {
        if (不合法条件) continue;
        res += dfs(pos - 1, 未定参数, 未定参数, limit && i == up);
    }
    return limit ? res : dp[pos][pre] = res;
}
int cal(int x) {
    memset(dp, -1, sizeof dp);
    len = 0;
    while (x) a[++ len] = x % 进制, x /= 进制;
    return dfs(len, 未定参数, 1, 1);
}
int main() {
    cin >> l >> r;
    cout << cal(r) - cal(l - 1) << endl;
}

例题

1.Windy

Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为 2的正整数被称为 Windy 数。

Windy 想知道,在 A和 B 之间,包括 A和 B,总共有多少个 Windy 数?

#include 
using namespace std;
vectorv;
int dp[15][15];
int dfs(int pos,int pre,int lead,int limit)
{
    if(pos==-1) return 1;
    if(!limit&&!lead&&dp[pos][pre]!=-1) return dp[pos][pre];
    
    int res=0,mx=limit?v[pos]:9;
    for(int i=0;i<=mx;i++)
    {
        if(abs(i-pre)<2)  continue;
        if(lead&&i==0) res+=dfs(pos-1,pre,1,limit&&i==mx);
        else res+=dfs(pos-1,i,0,limit&&i==mx);
    }
    return limit?res:dp[pos][pre]=res;
}
int cal(int x)
{
    //memset(dp,-1,sizeof dp);
    v.clear();
    while(x)
    {
        v.push_back(x%10);
        x/=10;
    }
    int n=v.size();
    return dfs(n-1,11,1,1);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    memset(dp,-1,sizeof dp);
    int a,b;
    cin>>a>>b;
    cout<

2.数字游戏

由于科协里最近真的很流行数字游戏。

某人又命名了一种取模数,这种数字必须满足各位数字之和 mod N 为 0。

现在大家又要玩游戏了,指定一个整数闭区间$[a,b]$问这个区间内有多少个取模数。

#include 
using namespace std;
int p;
vectorv;
int dp[15][1005];
int dfs(int pos,int sum,int limit){
    if(pos==-1) return sum%p==0;
    if(!limit&&dp[pos][sum]!=-1)return dp[pos][sum];
    int res=0,mx=limit?v[pos]:9;
    for(int i=0;i<=mx;i++)
    {
        res+=dfs(pos-1,sum+i,limit&&i==mx);
    }
    return limit?res:dp[pos][sum]=res;
}
int cal(int x){
    memset(dp,-1,sizeof dp);
    //注意初始赋值为-1,因为可能有些状态不符合条件答案为0,就可以忽略,否则可能会T
    v.clear();
    while(x)
    {
        v.push_back(x%10);
        x/=10;
    }
    int n=v.size();
    return dfs(n-1,0,1);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int a,b;
    while(cin>>a>>b>>p){
    cout<

3. 度的数量

求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。

#include 
#define ll long long
using namespace std;
vectorv;
ll dp[35][35];
ll dfs(int pos,int cnt,int limit,int b,int k)
{
    if(pos==-1) return cnt==k;
    if(!limit&&dp[pos][cnt]!=-1) return dp[pos][cnt];
    ll res=0;
    int mx=limit?v[pos]:b-1;
    for(int i=0;i<=mx;i++)
    {
        if((i==1&&cnt==k)||i>1) continue; //i>1保证互不相等
        res+=dfs(pos-1,cnt+(i==1),limit&&i==mx,b,k);
    }
    return limit?res:dp[pos][cnt]=res;
}
ll cal(int x,int b,int k)
{
    memset(dp,-1,sizeof dp);
    v.clear();
    while(x)
    {
        v.push_back(x%b);
        x/=b;
    }
    int n=v.size();
    return dfs(n-1,0,1,b,k);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    int x,y;
    int k,b;
    cin>>x>>y>>k>>b;
    cout<

4. 恨7不成妻

如果一个整数符合下面三个条件之一,那么我们就说这个整数和 7 有关:

  1. 整数中某一位是 7;
  2. 整数的每一位加起来的和是 7 的整数倍;
  3. 这个整数是 7 的整数倍。

现在问题来了:吉哥想知道在一定区间内和 7 无关的整数的平方和。

#include 
using namespace std;
constexpr long long mod = 1e9+7;
using ll=long long;
ll Power[25];
vectorv;
struct dp{
     ll cnt,sum,sum2;
}f[20][7][7];

dp dfs(int pos,int sum,int num,int limit){

    if(pos==-1) return (dp){sum&&num,0,0};
    if(!limit&&f[pos][sum][num].cnt!=-1)  return f[pos][sum][num];
    int mx=limit?v[pos]:9;
    dp ans{0,0,0};
    for(int i=0;i<=mx;i++)
    {
        if(i==7) continue;
        dp tmp=dfs(pos-1,(sum+i)%7,(num*10+i)%7,limit&&(i==mx));
        
        ll k=i*Power[pos]%mod;
        ans.cnt=(ans.cnt+tmp.cnt)%mod;//tmp.cnt存的是第pos-1位到最后一位满足条件的数的值
        ans.sum=((ans.sum+tmp.cnt*k%mod)%mod+tmp.sum)%mod;
        /*
        比如1222 1333 1444
        现在后3位的和已经知道了,那么当前位是1的和就是 1000*3+(222+333+444)
        */
        ans.sum2=((ans.sum2+(k*k%mod*tmp.cnt%mod+tmp.sum2)%mod)%mod+tmp.sum*k%mod*2%mod)%mod;
        /*
        以1222为例,1222^2=1000^2+2*1000*222+222^2
        */

    }
    return limit?ans:f[pos][sum][num]=ans;
}

ll cal(ll x)
{
    v.clear();
    while(x)
    {
        v.push_back(x%10);
        x/=10;
    }
    int n=v.size();
    return dfs(n-1,0,0,1).sum2%mod;
}
int main()
{
    
    ios::sync_with_stdio(false);
    cin.tie(0);
    int T;
    cin>>T;
     Power[0]=1;
    for(int i=1;i<=20;i++)
    Power[i]=Power[i-1]*10%mod;
    memset(f,-1,sizeof f);
    while(T--)
    {      
        ll l,r;
        cin>>l>>r;
       cout<<(cal(r)-cal(l-1)+mod)%mod<<'\n';   
    }
}


6.同类分布

给出两个数a,b,求出[a,b]中各位数字之和能整除原数的数的个数。$1\le a,b\le 10^{18}$

#include 
#define ll long long
using namespace std;

ll dp[20][170][170];
vectorv;
ll dfs(int pos,int sum,int rem,int limit,int mod){
    if(sum+9*(pos+1)>l>>r;
    cout<

7.花神的数论题

设$sum(i)$表示$i$二进制中$1$的个数,给定$N$,求$\prod_{i=1}^Nsum(i)$

转化为$\prod_{i=1}{bit(N)}i{cnt[i]}$

/*
设dp[i][j][k]为在前i位中,已经有j个1,且该数总计有k个1的数的个数。
还有组合数学的做法。
*/

#include 
#define ll long long
using namespace std;
const int mod=10000007;
ll dp[64][64][64];
vectorv;
ll dfs(int pos,int cnt,int num,int limit){
    if(pos==-1) return cnt==num;
    if(!limit&&dp[pos][cnt][num]!=-1) return dp[pos][cnt][num];
   
    ll res=0;
    int mx=limit?v[pos]:1;
    for(int i=0;i<=mx;i++){
        res+=dfs(pos-1,cnt+(i==1),num,limit&&i==mx);
    }
    return limit?res:dp[pos][cnt][num]=res;
}
ll qmi(ll x,ll y)
{
    ll res=1;
    while(y)
    {
        if(y&1) res=res*x%mod;
        x=x*x%mod;
        y>>=1;
    }
    return res;
}
ll cal(ll x){
    v.clear();
    while(x){
        v.push_back(x%2);
        x/=2;
    }
    int n=v.size();
    ll res=1;
    for(int i=1;i<=n;i++)
    {
        memset(dp,-1,sizeof dp);
        res=res*qmi(i,dfs(n-1,0,i,1))%mod;
    }
    return res;
    
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    ll n;
    cin>>n;
    cout<

8.J. Junior Mathematician

定义函数$f(x)=\sum_{i=1}{k-1}\sum_{j=i+1}kd(x,i)\times d(x,j)$,其中$d(x,i)$为$x$在十进制下第$i$位的数字,$k$为$x$在十进制下的位数。

定义一个数是好的数字为$x \equiv f(x) \pmod m$

给定$L$和$R$,求$[L,R]$中好的数的个数

$10\le L\le R\le 10^{5000}$ $2\le m\le 60$

答案对$10^9+7$取模

将$x \equiv f(x) \pmod m$转换为$x-f(x) \equiv 0 \pmod m$

这样记录$x \pmod m$和$f(x) \pmod m$的维数可以合并为1维

作差这个思想很常见

#include 
#define ll long long
using namespace std;
const int mod=1e9+7;
int pw[5005]; //这个不要开ll
ll dp[5005][65][65];
int v[5005];
int P;
inline ll dfs(int pos,int sum,int rem,int limit)
{
    if(pos==-1) return rem==0;
    if(!limit&&dp[pos][sum][rem]!=-1) return dp[pos][sum][rem];
    ll res=0;
    int mx=limit?v[pos]:9;
    for(int i=0;i<=mx;i++)
    res+=dfs(pos-1,(sum+i)%P,((rem+pw[pos+1]*i-sum*i)%P+P)%P,limit&&i==mx);
    /*
    例如:
    1234当枚举到第4位时
    12340-f(12340)=12340-(f(123)+(1+2+3)*4)=12300-f(123)+40-(1+2+3)*4=rem+i*pw[pow+1]-sum(i)
    */
    res%=mod;

    return limit?res:dp[pos][sum][rem]=res;
}
inline int cal(string s,bool flag)
{
    
    int n=s.size();
    //用memset会T
    for(int a=0;a<=n;a++)
        for(int b=0;b<=P;b++)
            for(int c=0;c<=P;c++) dp[a][b][c]=-1;
           
    for(int i=n-1;i>=0;i--) v[n-1-i]=s[i]-'0';
    if(flag)
    {
        v[0]--;
        for(int i=0;i>T;
    while(T--){

        string l,r;
        cin>>l>>r;
        cin>>P;
        int len=r.size();
        pw[1]=1;
        for(int i=2;i<=len;i++) pw[i]=pw[i-1]*10%P;
        cout<<(cal(r,0)-cal(l,1)+mod)%mod<<'\n';
    }
}

你可能感兴趣的:(数位DP总结)