算法提高-动态规划- 数位DP


数位dp是有模版和套路的,

AcWing 1081. 度的数量

这题我觉得最好结合辰风的题解去看,而且有个bug这题 预处理组合数数组和input交换位置 代码计算的结果不同。 很玄学

#include <iostream>
#include <vector>

const int N = 31 + 5;
int f[N][N];    //从a个数里面选b个1有多少种方法

int l, r, k, b;

int dp(int n)
{
    
    
    if (n == 0) return 0;
    std::vector<int> nums;
    while (n) nums.push_back(n % b), n /= b;//转化为b进制并存储下来每一位
    int last = 0, res = 0;
    for (int i = nums.size() - 1; i >= 0; -- i)//存的时候是逆序的
    {
    
    
        int x = nums[i];
        if (x > 0)
        {
    
    
            //当前位取0
            res += f[i][k - last];//先把当前位取0的情况加上(这里i的原因是i是下标,正好是真实的位置-1,正好可以用来表示当前还剩多少位)
            //当前位取1
            if (x > 1)//当前位取1的时候要看取得是这个位的上限还是 0 ~ 上限 - 1,如果x > 1显然是后者
            {
    
    
                if (k - last - 1 >= 0) res += f[i][k - last - 1];
                break;//取1的时候如果不是上限,后面几位直接求一下组合数就行了,肯定不会超过n的b进制表示的数的大小
            }
            else if (x == 1)//说明当前位取1的时候取的是当前位的上限
            {
    
    
                //取的是上限,还需要继续讨论
                last ++ ;
                if (last > k) break;
            }
        }
        if (i == 0 && last == k) res ++;//如果到了最后一位的时候恰好1选够k个了,那么最后一位选择0也是合法的,最后一位不取0的情况在上面x>0中讨论过了
    }
    return res;
}
int main() 
{
    
    
    //这题 预处理组合数数组和input交换位置 代码计算的结果不同。  很玄学
    
    //预处理组合数数组
    for (int i = 0; i <= N; ++ i)
        for (int j = 0; j <= i; ++ j)
        {
    
    
            if (j == 0) f[i][j] = 1;
            else f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
        }
        
    //input
    std::cin >> l >> r >> k >> b;
    

    
    //dp
    std::cout << dp(r) - dp(l - 1);
    return 0;
}

AcWing 1082. 数字游戏

#include <iostream>
#include <vector>
using namespace std;

const int N = 15;//2^31转换成10进制最多10 ^ 10
int f[N][10];//计算一共i位数且最高位是j的   非降序数方案数有多少

int l, r;


void init()
{
    
    
    for (int i = 0; i <= 9; ++ i) f[1][i] = 1;//当前只有1位的时候,方案数是1
    
    for (int i = 2; i <= N - 1; ++ i)//数组最大就开了N位
        for (int j = 0; j <= 9; ++ j)
            for (int k = j; k <= 9; ++ k)
                f[i][j] += f[i - 1][k];
}

int dp(int n)
{
    
    
    if (n == 0) return 1;//如果n=0,nums.size() - 1返回值就是无穷大,因为nums.size()返回值是无符号形的
    
    int last = 0, res = 0;
    
    vector<int> nums;
    while(n) nums.push_back(n % 10), n /= 10;
    
    for (int i = nums.size() - 1; i >= 0; -- i)
    {
    
    
        int x = nums[i];
        if (x < last) break;
        
        for (int j = last; j < x; ++ j)//枚举0~当前位的上限值 - 1,即左分支
            res += f[i + 1][j];//序号下标从0开始的,i + 1是加上当前正在枚举的这一位一共有i + 1个位,度的数量那题是因为要计算去掉当前位剩下多少位中含j个1的组合数有多少
        
        last = x;
        
        if (i == 0) res ++ ;//枚举完所有位后也算一种方案,因为f[1][i] = 1,当前只有一个位的时候无论这个位上是什么,非降序方案数都是1
    }
    
    return res;
}

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

AcWing 1083. Windy数

这题挺难的,和之前的思路大致一样,但是细节上有很多不同点

// //枚举nums.size位的数字的时候记得最高位从1开始,因为有前导0的情况我们会在 小于nums.size位的数字中加上,不然会加多了,

// //关于前导零,13 符合 windy 数,但 013 却不符合,因为 abs(0 - 1) < 2

#include <iostream>
#include <cmath>
#include <vector>
using namespace std;

const int N = 15;

int f[N][10];//考虑当前一共有i位且最高位为j的时候满足windy数定义的数一共有多少

int l, r;


void init()
{
    
    
    for (int i = 0; i <= 9; ++ i) f[1][i] = 1;
    
    for (int i = 2; i <= N - 1; ++ 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)
{
    
    
    if (n == 0) return 0;
    
    vector<int> nums;
    
    while (n) nums.push_back(n % 10), n /= 10;
    
    
     int last = -2, res = 0;
    //枚举位数位nums.size()的windy数
    for (int i = nums.size() - 1; i >= 0; -- i)
    {
    
    
        int x = nums[i];
        
        
        
        for (int j = (i == nums.size() - 1); j < x;  ++ j)//当前位不能超过当前位的上限,且如果当前位是最高位的话必须从1开始枚举,这点题目中说了
            if (abs(j - last) >= 2) 
                res += f[i + 1][j];
                
            
        if (abs(x - last) < 2) break;//本题中这个判断不能放在最前面了,因为我们j是从0/1开始枚举的,而不是从x - 1开始
            
        last = x;//从这里就可以看出树形dp实际上是将整个树枚举一遍,
                //上面那个for是将左分支的方案收集起来,
                //这里直接定义我们当前位选的就是当前位的上限,再通过最外层的for进入下一位的左分支讨论方案数
                
        if (i == 0) res ++; //说实话,到这我还是不知道为什么所有位讨论完后还要res++
    }
    
    //枚举位数位nums.size()的windy数,位数比n少一位,那么肯定当前数不会比n大,直接枚举我们之前预处理过的f[i][j]即可
    for (int i = 1; i <= nums.size() - 1; ++ i)
        for (int j = 1; j <= 9; ++ j)//因为不含前导0,为了保证第i位有效,我们j不能从0开始
            res += f[i][j];
    
    return res;
}
int main()
{
    
    
    init();
    cin >> l >> r;
    cout << dp(r) - dp(l - 1) ;
    return 0;
}

AcWing 1084. 数字游戏 II

我只能说模运算博大精深。。。

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

const int N = 10 + 5;
int f[N][10][102];//当前一共有i位,且最高位为j的数所有位上的和模p余数为k 的 数一共有多少
int l, r, p;


int mod(int n, int p)
{
    
    
    return (n % p + p) % p;//c++的mod结果可以是负数,因此要加一个偏移量p,再%p的原因是如果mod的结果是正数,这个%就有用了
}
void init()
{
    
    
    memset(f, 0, sizeof f);
    for (int j = 0; j <= 9; ++ j) f[1][j][j % p] ++;
    
    for (int i = 2; i <= N - 1; ++ 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)];//这遍历很有讲究,首先便利ijk三维我们定义好的状态,
                                                             //i位肯定是从i-1位状态转移过来的,因此我们还需要枚举i-1位数的最大位是哪些数,即x
                                                             //具体余数是多少呢,我们在i-1位的数上少了i位数的最大位的j,
                                                             //因此我们i-1位的数sum应该是k-j,为了保证我们k-j是一个正数且合法(第三维的状态应该在p以内)
                                                             //我们用mod(k - j, p)保证
}

int dp(int n)
{
    
    
    if (n == 0) return 1;
    
    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int last = 0, res = 0;
    for (int i = nums.size() - 1; i >= 0; -- i)
    {
    
    
        int x = nums[i];
        //处于左分支 位上取的数在0~当前上限-1
        for (int j = 0; j < x; ++ j)//处于左分支,当前数一定不大于整个数的上限,直接用我们预处理好的f即可
        {
    
    
            res += f[i + 1][j][mod(-last, p)];//从0~i一共有i+1位,我们要保证这i+1位的数的和sum且(sum+last)%p == 0;
                                              //前面几位的和是last,因此我们要使得我们的sum % p = -last,
                                              //因为-last越界了不合法,第三维得在0~p - 1之间,所以我们需要mod(-last, p),
                                              //使f[i+1][j][-last]变成合法的f[i+1][j][0~p-1]也就是f[i + 1][j][mod(-last, p)]
            
        }
        //处于右分枝,即当前位确定为当前位的上限,我们直接通过for遍历下一位即可(右分枝可以分解为下一位的左分支和右分枝)
        last += x;
        
        
        //如果所有位都确定了(取的都是每一位上限,即一直通过右分枝走到底,我们判断这个数的每一位之和符不符合题意)
        if (i == 0 && last % p == 0) res ++;
    }
    return res ;
}

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

AcWing 1085. 不要62

这题不知道为啥我用vector实现就不行

#include <iostream>
#include <vector>
using namespace std;

const int N = 10;
int f[N][10];//一个共i位的数,最高位是j的时候合法的车牌号有多少
int l, r;
void init()
{
    
    
    for (int j = 0; j <= 9; ++ j) f[1][j] = 1;
    f[1][4] = 0;
    
    for (int i = 2; i <= N - 1; ++ i)
        for (int j = 0; j <= 9; ++ j)
            for (int k = 0; k <= 9; ++ k)
            {
    
    
                if (j == 4 || k == 4) continue;
                if (j == 6 && k == 2) continue;
                f[i][j] += f[i - 1][k];
            }
}


int dp(int x)
{
    
    
    if(!x) return 1;                        //日常边界判断,有的数位DP题里必须有,这道题需不需要你交一下试试就知道了

    int a[12] = {
    
    0};                        //我要用a数组把这个数的每一位都取出来
    int l = 0;
    int ans = 0;
    int last = 0;                           //last用来看上一位是不是6,如果是6 那么这一位我就不能填2了
    while(x)
    {
    
    
        a[++ l] = x % 10;
        x /= 10;
    }

    for(int i = l;i >= 1;i --)
    {
    
    
        for(int j = 0;j < a[i];j ++)    //因为是车牌,所以第一位可以是0,故从0循环
        {
    
    
            if(j == 4) continue;
            if(last == 6 && j == 2) continue;
            ans += f[i][j];
        }
        if(a[i] == 4) break;
        if(a[i] == 2 && last == 6) break;

        last = a[i];
        if(i == 1) ans ++;              //本来没加这句话,但是写完发现答案少一,于是加上,表示所有的都填完了,传进来的这个x也能填
    }

    return ans;
}



// int dp(int n)
// {
    
    
//     if (n == 0) return 1;
    
//     vector<int> nums;
//     while (n) nums.push_back(n % 10), n /= 10;
    
    
    
//     int last = 0, res = 0;//last表示上一位是什么数
    
//     for (int i = nums.size() - 1; i >= 0; -- i)
//     {
    
    
//         int x = nums[i];
        
//         //枚举左分支,选择的数字j 在 0 ~ 当前位的上限 - 1
//         for (int j = 0; j < x; ++ j)
//         {
    
    
//             if (j == 4) continue;
//             if (last == 6 && j == 2) continue;
//             res += f[i][j];
//         }
        
//         //枚举右分枝,右分枝就是选择当前位的上限
//         if (x == 4) break;
//         if (last == 6 && x == 2) break;
        
//         last = x;
        
//         if (i == 0) res ++;//枚举到最后一位,没有下一位的左分支可以进入了,已经到头了,即每一位都选择的上限,并且经过前3句的判断合法,那么也算一种方案
//     }
//     return res;
// }

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

猜你喜欢

转载自blog.csdn.net/chirou_/article/details/131857985