【数位dp】| AcWing 算法基础班试题总结

数位dp常用于求某个区间 [L,R] 内符合某个要求的数,
利用前缀和的思想,我们求出[0,N]区间内符合要求的数,然后分别求出[0,R]和[0,L-1]符合区间的数,二者相减即可。

对数 an-1an-2……a0 的划分常采用的形式:

Case1 : 枚举0到an-1-1,然后,后n-1位随便取
Case2:最高位取an-1固定,次高位枚举0到an-2-1,后n-2位随便取,然后第n-2位取an-2,一直枚举到最后一位只能取a0,这个数就是an-1an-2……a0

左分支和最后一个结点a0用于计数,除此之外的右分支用于分类讨论。

1081. 度的数量

题目描述

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

输入格式
第一行包含两个整数 X 和 Y,接下来两行包含整数 K 和 B。

输出格式
只包含一个整数,表示满足条件的数的个数。

数据范围
1≤X≤Y≤231−1,
1≤K≤20,
2≤B≤10

输入样例:

15 20
2
2

输出样例:

3

Hint:

X=15,Y=20,K=2,B=2,则有且仅有下列三个数满足题意:
17=24+20 ;18=24+21 ;20=24+22

分析:

给定一个十进制数n,求0到n之间符合要求的数的个数:
将该十进制数转换成B进制数的形式, an-1an-2……a0 ,ai的取值在0~B-1之间,但是由于Bai 互不相等,所以每个只能出现一次,因此符合要求的B进制数每一位上只能取0或1,最终是K个1,n-K个0。

因此符合要求的B进制数最大值是111……1(n个1),又由于K>=1,所以符合要求的数在000……01到111……11之间。

本来根据数位dp的思想,是要枚举每一个 左分支从0到ai-1的每个数 和 右分支 ai 的情况的,但是就题论题,这个题不需要,因为每一位的值一旦大于1就可以提前结束了,只用枚举0和1就行了。

简单想法: 当前最高位取一个数后,后面的数任意取是否在n的范围之内。

1.如果这个数n的B进制数的最高位an-1大于1,那么说明这个数从符合要求的 0到111……1 之间可以任意取,因此只要在n位中取够K个1就行了,即C(n,k)。计数就已经结束了。
2.如果最高位an-1等于1,那么只有当最高位取0的时候,后n-1位可以任意取,当取1的时候,要受限于第an-2的范围,因此,当最高位取0的时候,计数后n-1位取K个1的情况C(n-1,k),当最高位取1的时候,此时不能计数,固定该位为1,考虑把an-2当作"最高位"处理,开始下一位处理。
3.如果最高位an-1小于1,即为0,说明它只能取0,且后n-1位还不能任意选择,因此固定该位为0,开始处理下一位。

算法实现

核心:

我们把最终的枚举情况分为两部分,最高位是否为0?
如果最高位为0,则该位固定为0,直接走右分支。
如果最高位大于0,那么它取0的情况是必然要有的,最高位取0后,后n-1位就取K个1,即C(n-1,k)
  然后判断该位是否大于1,
   如果大于1的话,就是上面的情况1,再考虑最高位取1,那么后n-1位取K-1个1,即C(n-1,k-1),注意到刚好有C(n,k)=C(n-1,k)+C(n-1,k-1),递推公式,此时就结束了。
   如果该位等于1,就是上面的情况2,固定该位为1,因为后面的不能任意取,然后走右分支,继续把an-2当作最高位再次划分。

即上面所说的枚举,根据最高位固定后,后面的是否可以任意取,把每一位ai,划分成0到ai-1和ai两个集合。

预处理组合数:
递推公式C(a,b)=C(a-1,b-1)+C(a-1,b)
初始化情况C(0,0)=1; C(0,k)=0;(k>0) C(k,0)=1;

#include <iostream>
#include <vector>

using namespace std;

const int N=35; //以二进制拆分,拆分的位数最多,最多拆分成31位,(2^31)-1
int K,B,f[N][N];

void init()  //预处理组合数 dp[i][j],即C(i,j)
{
    
    //c(a,b)=c(a-1,b)+c(a-1,b-1)
    for (int i=0;i<=N;i++) {
    
    //特地从0开始,只为了初始化一下C(0,0)=1,再往下走j从1开始,大于0就不会循环了,所以C(0,j)=0
        f[i][0]=1;
        for (int j=1;j<=i;j++) //C(k,0)=1,在上一行已经初始化过了,
            f[i][j]=f[i-1][j-1]+f[i-1][j];
    }
}

int dp(int n)//求0~n之间满足条件的数
{
    
    
    if (n==0) return 0; //K>=1,所以该数必大于等于1,n为0时,0~0之间不满足这样的数
    //将十进制数拆成B进制数
    vector<int> nums;//存储B进制数
    while (n) {
    
    
        nums.push_back(n%B);
        n=n/B;
    }
    
    int res=0,t=K; //K不能被破坏,要多次调用该函数,所以定义一个t
    for (int i=nums.size()-1;i>=0;i--) {
    
    //n表示成B进制数一共有i+1位,第i+1位的下标是i,i+1位里要有K个1。
        int x=nums[i];
        if (x) {
    
     //当x大于0时,说明该位可以取0,剩下的随便选,才有走左分支的机会,否则x==0时就直接跳过,走右分支,开始下一位
            res+=f[i][t]; //x取0时的情况,剩下i位要取K个1
            if (x>1) {
    
    //如果x>1的话,说明该位取1也可以,剩下的位也可以随便取,因为n>111……1,不再走右分支判断了,直接左分支全部计数完成
                res+=f[i][t-1]; //i+1位取1的话,剩下i位取K-1个1
                break; //已经全部枚举完了,可以退出了。
            } //以上是左分支的情况
            else {
    
     //x==1的话,走右分支,固定第i+1位,开始讨论第i位
                t--; //如果x为1的话,这位必须固定选1,走右分支,后i-1位只剩K-1个1,但是也不能随便选。
                if (!t) {
    
    res++; break;}    //一路下来都是右分支的情况,可能取0,也可能取1,如果取够K个1就停止,可能是中途某个右分支,或者到最后一个右分支,后面位数都取0,这是最后一个符合要求的数,且该数最大。
            }
        }
    }
    
    return res;
}

int main()
{
    
    
    init();
    int l,r;
    cin>>l>>r>>K>>B;
    cout<<dp(r)-dp(l-1);
    
    return 0;    
}

对于枚举0到n之间符合要求数,都是左分支的数小于右分支,一路右分支直到右分支也停下来,此时这个数是符合要求的数里面最大的数了。

1082 数字游戏

题目描述

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

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

输入格式
输入包含多组测试数据。
每组数据占一行,包含两个整数 a 和 b。

输出格式
每行给出一组测试数据的答案,即 [a,b] 之间有多少不降数。

数据范围
1≤a≤b≤231−1

输入样例:

1 9
1 19

输出样例:

9
18

分析:
先求整体,即不受约束条件的时候,求以数字j为最高位,且一共有i位的情况下的非降数的个数。

最高位j可以取0,这样求i位数时,可以描述到1位到i-1位数之间的数,
假设n=233,
1.i=3时,_ _ _
 j=0时,描述000到099之间的非降数;
 j=1时,描述100到199之间的非降数;100到110之间肯定不是非降数。
然后i=2时,后面不能随便取,只能到233,所以固定第一位,
2.i=2时,2 _ _
 j=0时,200到209之间的非降数;
 j=1时,210到219之间的非降数;
……依次类推。

这可以用递推的方式实现。(见后面叙述)

我们求0到n之间的非降数,给定一个n
假设给定一个十进制数字n,每一位表示成 an-1an-2an-3……a1
第一次,最高位是an-1,它前面没有数字限制,可以分成两个分支,0到an-1-1和an-1 取左分支:0到an-1时,后面n-1位可以任意取,分别根据前面的递推得出这些情况下的非降数个数,进行累加。取右分支时:固定该位为an-1,然后走下一位,这是下一位由于an-2,由于非降数的限制,他就不能从0开始枚举了,要从an-1开始枚举,因此现在应该分成两个分支,an-1到an-2-1和an-2,然后依次按上面的情况进行递推。由于是非降数,一定要确保an-2 >= an-1,最次取等号的时候,就是不走左分支,直接走右分支,固定该位,但是如果有an-2 < an-1,那就到头了,因为它违反了非降数的定义,就可以停止了,就不存在非降数了。

枚举开头是从0开始还是从ai开始,可以维护一个变量,用它来记录,由于刚开始时没有限制,从0开始,所以该变量初始化为0。

左分支的数都是要小于右分支的数的,左分支枚举的数从0开始,越往右走,数越大,随时可能中断,一路选择右分支到底就是这个数本身,如果能走到最后一个右分支,说明这个数n就是一个非降数。

左分支负责计数,右分支分类讨论,但是最后一个右分支是存在的最后一种情况。

预处理: 计数类dp
状态表示f[i,j]表示i位数字,最高位是j的不降数的个数。
状态计算:次高位k作为划分,对于满足条件k>=j的,有 f [ i , j ] = ∑ f [ i − 1 , k ] f[i,j]=∑f[i−1,k] f[i,j]=f[i1,k]
初始化: 预先初始化位数为1的不降数,注意到f[1,i]显然是只有一位的时候的做法,全都要初始化为1。

先循环位数,求第i位时需要用到第i-1位,需要先处理好。

算法实现

数据范围x y,最大到231 -1,近似109,因此最低只用求10位数的情况下的非降数个数就可以了。
预处理10位的情况。

#include <iostream>
#include <queue>
#define read(x) scanf("%d",&x)

using namespace std;

const int N=11;
int f[N][N];  //f[i][j]表示一共有i位,且最高位填j的数个数

//预处理,递推公式:当k>=j时,f[i][j]+=f[i-1][k]
void init()
{
    
    
    //初始化,从长度i=1开始。
    for (int j=0;j<=9;j++) f[1][j]=1;
    //递推
    for (int i=2;i<=N;i++)  //先循环位数,求第i位时需要用到第i-1位,需要先处理好
        for (int j=0;j<=9;j++) //i位时,最高位以j结尾,寻找j
            for (int k=j;k<=9;k++)  f[i][j]+=f[i-1][k];
                //寻找符合条件的次高位k
}

int dp(int n)
{
    
    
    if (n==0) return 1; //0也是一个数,1种情况
    
    //存储十进制数的每个位上的数
    vector<int> nums;
    while (n) nums.push_back(n%10),n/=10;
    
    int i,res=0,last=0; //last记录上一位的前驱是多少,初始化为0,因此第一次枚举最高位的时候可以从0枚举
    for (i=nums.size()-1;i>=0;i--) {
    
    //下标是i,实际上有i+1位,因为是从0开始存储的
        int x=nums[i];
        if (x<last) break; //如果当前位的最大值小于上一位的值,后面就不存在非降数了
        //走左分支,最高位的值为a^n=x,从枚举0到x-1,由于小于x,所以后面可以任意取,直接求出结果
        for (int j=last;j<x;j++) res+=f[i+1][j];
        //本来应该是枚举从0到x-1的,但是由于是非降数,所以从该位承接上一位数(last维护),开始枚举
        
        //走右分支,前面枚举到到x-1了,这次该位置取x,维护last的值
        last=x;
    }
    //顺利一路右分支走到底,这是右分支可以统计的最后一种情况,即n这个数本身也是非降数。
    if (i==-1) res++;
    return res;
}

int main()
{
    
    
    init();
    int l,r;
    while(cin>>l>>r) cout<<dp(r)-dp(l-1)<<endl;
    
    return 0;
}

求0到n之间的非降数时:
n==0的话显然直接输出1;
否则
将n每一位拆开,从最高位开始对每一位依次考虑,
1) 若上一位最大值(last)已经高于本位最大值,则说明已经求完所有方案数,直接break
2) 否则:
(1)若不选当前位置最大值(0~an-1),进入左分支,则当前位置取从上一位最大值到这一位最大值之间的数j,以此作为最高位,假设此时算上最高位共有i位,记录所有的f[i][j],将这些方案数全部累计到答案中。
(2)若选当前位最大值an进入右分支,不计数,只更新last。
当右分支走到底时,是最后一种情况,n本身,说明它也是非降数,否则就中途退出了。

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/114493203