数位DP

记录今天在Acwing学习的几道数位Dp题目,整理了思路,方便以后的复习:

1.度的数量

题目描述

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

例如,设 X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:

17 = 2 4 + 2 0 17=2^4+2^0 17=24+20
18 = 2 4 + 2 1 18=2^4+2^1 18=24+21
20 = 2 4 + 2 2 20=2^4+2^2 20=24+22

样例
输入
15 20
2
2

输出
3

数位DP

K个互不相同的B的整数次幂,等价于求数字num在B进制表示下是否是由K个1且其他位全是0组成,可用数位Dp来做。

对于数字num,存放其每一位上的数字(B进制表示下),从高位向低位枚举,假设枚举到第i位,第i位上的数字是x,那么分以下几种情况讨论:

  • 1.x == 0
    那么第i位只能是0,后面数位上现在都不能确定,只能继续向后看.
  • 2.x == 1
    这里第i位可以分成两种情况:
    • 1.第i位放0,那么后面的数位上可以放k-last1res += f[i][k-last];
    • 2.第i位放1,那么后面数位上的情况不能用组合数计算,因为要保证答案中的数字比原数字要小
  • 3.x > 1
    同样第i位分成两种情况
    • 1.第i位放0,那么后面的数位上可以放k-last1res += f[i][k-last];
    • 2.第i位放1,那么后面的数位上可以放k-last-11res += f[i][k-last-1];
C++ 代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>

using namespace std;

const int N = 34;

int f[N][N];  //组合数
int k,b;
int l,r;

void init(){
    for(int i = 0; i<N; i++)
        for(int j = 0; j<=i; j++)
            if(!j) f[i][j] = 1;
            else f[i][j] = f[i-1][j-1] + f[i-1][j];
}

int dp(int n){
    int res = 0, last = 0;
    if(!n) return res;
    
    vector<int> a;
    while(n) a.push_back(n%b),n/=b;
    
    int len = a.size()-1;
    for(int i = len; i>=0; --i){
        int x = a[i];
        if(x){
            res += f[i][k-last];  //第i位放0,后i-1位放k-last个1(0~i-1位,所以第i位后面有i位)
            if(x>1){
                //第i位放1,后i-1位放k-last-1个1
                if(k-last-1>=0) res += f[i][k-last-1];
                break;  //这一位已经不合法了,所以上一步结束后直接break
            }else{
                //如果x为1,考虑到组成的数必须比原数字小,所以这里不能用组合数计算
                last++;
                if(last>k) break;  //1如果放完了,直接break
            }
        }
        if(!i && last == k) res ++;  //树的最右侧的分支
    }
    return res;
}
int main()
{
    init();  //预处理出组合数
    
    cin>>l>>r>>k>>b;
    cout<<dp(r) - dp(l-1);
    return 0;
}

2.windy数

题目描述

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

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

数据范围

1 ≤ A ≤ B ≤ 2 × 1 0 9 1≤A≤B≤2×10^9 1AB2×109

样例
输入
1 10

输入
9

数位Dp

假设我们当前枚举到第i位(设共有n位),且第i位上的数字为x,那么对于答案中第i位数字j来说,有两类:

  • 1.0~x-1 (如果第i位是最高位,这里是1~x-1)
    括号中提到的1~x-1后面会解释 ,我们用last记录上一位的数字,然后枚举j,如果abs(j-last) >= 2 就累加答案,res += f[i+1][j];

  • 2.x
    不需要枚举jlast = x,再枚举之后的数位即可

上述做完之后,由于上面的答案都是n位的,对于数位个数低于n的,再累加到答案中就行了

f数组的处理

f[i][j] 表示一共有i位,且最高位数字为j的满足windy数定义的数的个数

状态转移: 因为第i位是j已经确定,考虑第i-1位,设第i-1位数字为k,那么根据windy数定义只要abs(k-j) >= 2就可以转移过来

$ f[i][j] = \sum_{k=0}^{9}if(abs(k-j)>=2) f[i-1][k] $

关于前导0

上面提到了枚举的第i位是最高位,那么不能填0,这里解释一下,如果我们填0,那么答案就会加上f[i+1][0],,举这样一个例子,
对于数字13,他是满足windy数定义的,那么加上前导0之后的013就不会被f[3][0]加进去,原因就是abs(0-1)<2,这样就导致答案漏掉。

C++ 代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 11;

int f[N][N];  //f[i][j] 表示一共有i位,且最高为是数字j的满足wingy数定义的数的个数

void init(){
    for(int i = 0; i<=9; i++) f[1][i] = 1;
    for(int i = 2; i<=N; i++)
        for(int j = 0; j<=9; j++)
            for(int k = 0; k<=9; k++)
                if(abs(k-j)>=2) f[i][j] += f[i-1][k];
}

int dp(int n){
    int res = 0,last = -10;
    
    vector<int> a;
    while(n)a.push_back(n%10),n/=10;
    
    int len = a.size()-1;
    //答案是a.size()位的
    for(int i =len; i>=0; --i){
        int x = a[i];
        for(int j = (i==len); j<x; ++j)   //最高位从1开始
            if(abs(j-last)>=2) res += f[i+1][j];
            
        if(abs(x-last)<2) break;  //不满足定义,直接break
        last = x;
        
        if(!i) res ++;
    }
    //答案小于a.size()位的
    for(int i = 1; i<=len; i++)
        for(int j = 1; j<=9; j++)
            res += f[i][j];
    
    return res;
}
int main()
{
    init();
    int l,r;
    cin>>l>>r;
    cout<<dp(r)-dp(l-1);
    return 0;
}

3.数字游戏

题目描述

科协里最近很流行数字游戏。

某人命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如 123,446。

现在大家决定玩一个游戏,指定一个整数闭区间 [a,b],问这个区间内有多少个不降数。

样例
输入
1 9
1 19
输出
9
18
数据范围

1 ≤ a ≤ b ≤ 2 31 − 1 1≤a≤b≤2^{31}−1 1ab2311


数位DP

按照数位DP分析步骤: 假设我们当前枚举到第i位,且第i位上的数字是x,那么现在对于答案的第i位数字j来说,可以填两类数字:

  • 1.j取0~x-1
    那么res += f[i+1][j];

  • 2.j取x
    last记录x,再枚举下一位

f数组

f[i][j] 表示一共有i位,且最高位数字为j的不降数的个数
状态转移: 因为最高位已经固定,所以假设第i-1位是k,根据不降数定义k>=j,所以
f [ i ] [ j ] = ∑ k = j 9 f [ i − 1 ] [ k ] f[i][j] = \sum_{k=j}^{9}f[i-1][k] f[i][j]=k=j9f[i1][k]

C++ 代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 12;
int f[N][N];  //f[i][j] 表示最高位是j且一共有i位的不降数的个数

void init(){
    for(int i = 0; i<=9; i++) f[1][i] = 1;
    for(int i = 2; i<=N; i++){
        for(int j = 0; j<=9; j++)
            for(int k = j; k<=9; k++)
                f[i][j] += f[i-1][k]; //f[i][j] = f[i-1][j] + f[i-1][j+1] + f[i-1][j+2] +...+ f[i-1][9];
    }
}

int dp(int n){
    if(!n) return 1;
    int res = 0,last = 0;
    
    vector<int> a;
    while(n) a.push_back(n%10),n/=10;
    int len = a.size()-1;
    
    for(int i = len; i>=0; --i){  //从高到低枚举
        int x = a[i];
        for(int j = last; j<x; j++)//last表示上一位数字大小,当前枚举的数字不能比last小(不降数定义)
            res += f[i+1][j]; //(0~i位,一共有i+1位)
            
        if(x<last) break;  //若小,则break
        last = x;  //更新last
        
        if(!i) res++;
    }
    return res;
}
int main()
{
    init();
    int l,r;
    while(cin>>l>>r) cout<<dp(r) - dp(l-1)<<endl;;
    return 0;
}

4.数字游戏Ⅱ

题目描述

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

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

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

数据范围

1 ≤ a , b ≤ 2 31 − 1 , 1≤a,b≤2^{31}−1, 1a,b2311,
1 ≤ N < 100 1≤N<100 1N<100

样例
输入
1 19 9

输出
2

数位DP

在几道数位Dp题目练习过后,这类题目重点在于找到左边那一类如何直接计算
对于这一题来说,假设我们当前枚举到的第i位,且第i位上的数字是x,那么对于答案中的第i位数字j来说,可以填两类数:

  • 1.0~x-1
    我们用last表示到当前为止,前面数位上的数字之和,对此,当前第i位数字为j,前面数字之和为last,那么
    i位(包括j这一位)数字之和sumlast的关系就是(last+sum)%N == 0,那么sum%N的结果等价于(-last)%N,
    所以res += f[i+1][j][(-last%N)]; (后面会提到f数组的处理)
  • 2.x
    如果jx,那么不用枚举了,last += x,再枚举下一位即可

f数组的处理

f[i][j][k] 表示一共有i位,且最高位数字是j,且所有位数字和模N结果为k的数的个数

状态转移: 因为第i位已经是j,且所有数字之和模Nk,所以我们考虑第i-1位,假设第i-1位数字是x,由于j已经知道,
那么剩下的i-1位数字之和模N的结果就是(k-j)%N,那么状态转移方程就是:

$ f[i][j][k] = \sum_{k=0}{N-1}\sum_{x=0}{9} f[i-1][x][mod(k-j,N)]$

C++ 代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 12, M = 102;

int f[N][10][M];  //f[i][j][k] 表示一共有i位,且最高位数字是j,且所有位数字和模p位k的数字个数
int p;

int mod(int u,int v){
    return (u%v+v)%v;  //c++的%会得到负数,需要做一个偏移
}

void init(){
    memset(f,0,sizeof f);   //多组测试数据,f数组要清空
    for(int i = 0; i<=9; i++) f[1][i][i%p]++;
    
    for(int i = 2; i<N; i++)
        for(int j = 0; j<=9; j++)
            for(int k = 0; k<p; k++)
                for(int x = 0; x<=9; x++)
                    f[i][j][k] += f[i-1][x][mod(k-j,p)];
}

int dp(int n){
    if(!n) return 1;
    int res = 0,last = 0;
    
    vector<int> a;
    while(n) a.push_back(n%10),n/=10;
    
    int len = a.size()-1;
    for(int i = len; i>=0; --i){
        int x =a[i];    
        for(int j = 0; j<x; j++)  //第i位放0~x-1
            res += f[i+1][j][mod(-last,p)]; //0~i位,所以一共有i+1位
            
        last += x;
        if(!i && last % p == 0) res ++; 
    }
    return res;
}
int main()
{
    
    int l,r;
    while(cin>>l>>r>>p){
        init();
        cout<<dp(r)-dp(l-1)<<endl;
    }
    
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值