数位动态规划:数的个数

数位DP介绍

数位DP一般用来统计一个区间 [ L , R ] [L,R] [L,R]中满足条件 f ( i ) f(i) f(i)的数的个数。 条 件 f ( i ) 条件 f(i) f(i)一般与数的大小无关,而与数的组成有关(即数位,个位、十位、百位…),因此数的大小对复杂度的影响很小。

数位DP本质是对暴力枚举的优化,使得新的枚举方式满足DP性质,从而能进行记忆化。

暴力枚举

对于一个求区间 [ L , R ] [L,R] [L,R]满足条件的数的个数,最简单暴力的做法如下:

for(int i = L; i <= R; i ++)
	if(check(i)) ans ++;

然而这样枚举没有状态可言,不方便记忆。

数位DP

控制上界枚举。从最高位开始向下枚举,例如:n = 213,那么从百位开始枚举,百位可能的情况有0,1,2;然后在每一位上的枚举的数,组合起来都不能超过上界213。这样就计算出了从 1 − n 1-n 1n所有符合条件的方案f[n],然后利用前缀和的思想就可以统计出区间 [ L , R ] [L,R] [L,R]满足条件的方案数f[R] - f[L - 1]

解题技巧

  1. 求区间 f[L, R] = f[R] - f[L - 1]
  2. 分情况讨论,从集合划分的角度分析问题

问题应用

度的数量

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

例如,设 X = 15 , Y = 20 , K = 2 , B = 2 X=15,Y=20,K=2,B=2 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

输入格式

第一行包含两个整数 X X X Y Y Y,接下来两行包含整数 K K K B B B

输出格式

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

数据范围

1 ≤ X ≤ Y ≤ 2 31 − 1 1≤X≤Y≤2^{31}−1 1XY2311,
1 ≤ K ≤ 20 1≤K≤20 1K20,
2 ≤ B ≤ 10 2≤B≤10 2B10

输入样例

15 20
2
2

输出样例

3

算法思想

如果将数 N N N表示为一个 n n n B B B进制数的形式: a [ n − 1 ] a [ n − 2 ] . . . a [ 0 ] a[n-1]a[n-2]...a[0] a[n1]a[n2]...a[0],其中 0 ≤ a [ i ] < B 0 ≤ a[i] < B 0a[i]<B,那么求区间 [ 0 , N ] [0,N] [0,N]所有满足条件的方案,就是求 a [ n − 1 ] a [ n − 2 ] . . . a [ 0 ] a[n-1]a[n-2]...a[0] a[n1]a[n2]...a[0]中有 k k k1、其它位均为0的数的个数。

因为是统计区间 [ 0 , N ] [0,N] [0,N]上的每一个数字,所以枚举的上限为 N N N,所有符合条件的方案可以根据 N N N的第i位数字x的情况分为3类:

  • x > 1,则第i位上可以取01,并且剩下的i位可以随便取,可以直接用组合数进行计算得后面的所有情况:
    • i位取0时,如果前面已经安排了last1,那么再从剩下的i位里选择k - last1即可
    • i位取1时,如果前面已经安排了last1,那么再从剩下的i位里选择k - last - 11即可
  • x = 1,则第i位上可以取01
    • i位取0时,后面i位都可以随意取值,如果前面已经安排了last1,那么再从剩下的i位里选择k - last1即可
    • i位取1时,后面i位要在小于N的前提下取值,只能取k - last - 11,不能用组合数直接计算
  • x = 0,则第i 位上也只能取 0 ,所以直接计算后面数位的取值情况就可以了。

对于最后一位还要进行单独讨论,如果在最后一位 ,所有的k1都已经取好了,也就是k==last了,那么最后一位可以是0,总的方案数增加1

代码实现

#include 
#include 
using namespace std;
const int N = 35;
int f[N][N]; //f[i][j]表示从i个数中选j个数的方案数
int K, B;

//处理f[i][j]
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];
        }
}

//求区间[0...n]中所有满足条件的数的个数
int dp(int n)
{
    // 题目中求B的整数次幂,所以n == 0时,结果为0
    if(!n) return 0; 
    //将数n拆成若干位B进制数
    vector<int> nums;
    while(n) nums.push_back(n % B), n /= B;

    int res = 0;
    //已经处理的数位中包含1的个数,前面已经填了多少个1
    int last = 0; 
    //从高位到低位处理
    for(int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i]; //当前数位    
        if(x) //x == 1 或者 x > 1的情况
        {
            //加上第i位取0时的方案数,也就是对于后面i位取 K - last 个1的数量  
            res += f[i][K - last];
            //x > 1,且i位取1时,后面i位随便取k-last-1个1
            if(x > 1)
            {
                if(K - last - 1 >= 0)
                    //加上当前为取1时的方案数,也就是对于后面i位取 K- last - 1 个1的数量  
                    res += f[i][K - last - 1];  
                //如果x > 1,所有情况可以通过组合数计算出来,不需要对下一位进行分情况讨论了
                break;
            }
            else//x==1,表示第i位取1,则情况数需要对下一个数位分情况讨论
            {
                last ++; //x = 1所以使用了一个1
                if(last > K) break; //如果已经有了K个1
            }
        }

        if(!i && last == K) res ++; //最后一位单独判断
    }
    return res;
}

int main()
{
    init();

    int L, R;
    cin >> L >> R >> K >> B;

    cout << dp(R) - dp(L - 1) <<endl;

    return 0;
}

你可能感兴趣的:(动态规划,C++算法及题解,动态规划,算法,c++)