栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

第一章 动态规划(8):数位DP模型

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第一章 动态规划(8):数位DP模型

目录
  • 1、计数问题
  • 2、度的数量
  • 3、数字游戏
  • 4、Windy数
  • 5、数字游戏 II
  • 6、不要62
  • 7、恨7不成妻

数位DP技巧:

  1. [X, Y] → f(Y) - f(X - 1),f(N)表示 1 ~ N 中满足某种性质的个数。比如第一题计数问题;
  2. 利用树的角度考虑,比如度的数量。
1、计数问题

ACWing 338

算法思路:一定要分情况讨论

首先,题目要求在[a, b]中0 ~ 9这10个数中分别出现的次数,那么我们先实现一个函数count(n, x):求出从 1 ~ n 中 x 出现的次数,则在[a, b]中x出现的次数表示为cout(b, x) - count(a-1, x)(类似于前缀和)。

对count(n, x)函数,例如当x=1时,则count(n, 1)表示为从1 ~ n中找出1出现的次数。先求出1在1 ~ n中每一个位上出现的次数,则其总和即为count(n, 1)。

例如有abcdefg,则1在第四位出现可以表示为1 <= xxx1yyy <= abcdefg,即为表示在1 ~ abcdefg之间有多少个数的形式是xxx1yyy。分情况讨论:

  • ① 若xxx = 000 ~ abc - 1时,xxx1yyy < abc1yyy <= abc1efg,则yyy 可取 000 ~ 999,总共有abc * 1000种选法;
  • ② 若xxx = abc时:
    • 若d < 1即d = 0时,则第四位为1即abc1yyy,但是abc1yyy > abc0efg,超出了范围,总共有0种选法;
    • 若d = 1时,即abc1yyy,则yyy 只能取 000 ~ efg,总共有efg + 1种选法;
    • 若d > 1时,则abcdefg > abc1yyy,故yyy可取000 ~ 999,共1000种选法。

由以上,依次类推,可以求出1在任意一位数上出现的次数,然后将每一个位上的1出现的次数累加起来,就能得到1在1 ~ n位中整个出现的次数。依次类推,可以求出1 ~ 9每一个数在1 ~ n中整个出现的次数。

边界情况:

  • 当枚举数字0的时候,对于②不存在前导零,不用考虑;对于①,只能从001开始枚举。
# include 
# include 
using namespace std;

int dgt(int n) // 计算整数n有多少位
{
    int res = 0;
    while (n) ++ res, n /= 10;
    return res;
}

int cnt(int n, int i) // 计算从1到n的整数中数字i出现多少次 
{
    int res = 0, d = dgt(n); // d是n的位数
    for (int j = 1; j <= d; ++ j) // 计算从右到左第j位上数字i出现多少次
    {
        // l和r是第j位左边和右边的整数 (视频中的abc和efg); dj是第j位的数字
        int p = pow(10, j - 1), l = n / p / 10, r = n % p, dj = n / p % 10;
        // 计算第j位左边的整数小于l (视频中xxx = 000 ~ abc - 1)的情况
        if (i) res += l * p; 
        // 如果i = 0, 左边高位不能全为0(视频中xxx = 001 ~ abc - 1)
        if (!i && l) res += (l - 1) * p; 
        // 计算第j位左边的整数等于l,不能存在前导零 (视频中xxx = abc)的情况
        if ( (dj == i) && (i || l) ) res += r + 1;
        if ( (dj > i) && (i || l) ) res += p;
    }
    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);
        for (int i = 0; i <= 9; ++ i) cout << cnt(b, i) - cnt(a - 1, i) << ' ';
        cout << endl;
    }
    return 0;
}
2、度的数量

ACwing 1081

题中引例的意思实质上是:找到区间[x,y]中的某些数,这些数的B进制(引例是B=2二进制) 表示中仅包含k = 2个1,其余为0。

假设我们需要求 [ 0 , N ] [0, N] [0,N]中满足某种性质的个数即数位DP技巧中的f[N],若数 N N N是 n n n位数,其表示为 a n − 1 、 a n − 2 、 . . . 、 a 0 a_{n-1}、a_{n-2} 、... 、a_0 an−1​、an−2​、...、a0​。

  1. 一定要注意审题:这个数恰好等于 K 个互不相等的 B 的整数次幂之和。这句话的意思就表明了 K 个互不相等的 B 的整数次幂前面的系数只能是0 or 1!!!所以在下面图中,比如结点 0 0 0 ~ a n − 1 − 1 a_{n-1}-1 an−1​−1 仅有两个分支0 or 1,大于1的分支都不合法!
  2. 这里 N N N是已经转换为了 B B B进制下的数,比如: 19 19 19,在 3 3 3进制为 19 = ( 201 ) 3 19 =(201)_3 19=(201)3​,即 19 = 2 × 3 2 + 0 × 3 1 + 1 × 3 0 19 = 2times3^2+0times3^1+1times3^0 19=2×32+0×31+1×30。

组合数的计算公式 C a b = C a − 1 b − 1 + C a − 1 b C_a^b=C_{a-1}^{b-1}+C_{a-1}^b Cab​=Ca−1b−1​+Ca−1b​。代码中使用f[a][b]来表示:从a中选择b个数的方案数。

#include 
#include 
#include 
#include 

using namespace std;

const int N = 35; // 1 ≤ X ≤ Y ≤ 2^31−1,所以最多31位

int K, B;
int f[N][N]; // f[a][b]:从a个数中选择b个数的方案数

void init() // 预处理f[a][b]
{
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) // 如果j == 0
                f[i][j] = 1;
            else 
                f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
}

int dp(int n) // 从0 ~ n里面求出满足要求的个数
{
    if (!n) return 0; // 判边界:因为 K >= 1
    
    vector nums; // 存储n在B进制下的每一位
    while (n) nums.push_back(n % B), n /= B;
    
    int res = 0, last = 0; // res结果,last前缀信息:前面几位上有几个1
    for (int i = nums.size() - 1; i >= 0; i -- ) // 依次处理每一位
    {
        int x = nums[i]; // x = a(n-1)
        
        // 处理左边:0 ~ x-1,由题只能填 0 or 1
        if (x) // 只有 x>0 的时候才会有左边这个分支
        {
            // 当前这一位填0:从 0 ~ i-1 的i个数选择 k-last 位填上1
            res += f[i][K - last]; 
            
            // 只有x > 1 时,当前位置才能填上 1
            if (x > 1) 
            {
                // 当前这一位填1:从 0 ~ i-1 的i个数中选择 k-last-1 位填上1
                if (K - last - 1 >= 0)
                    res += f[i][K - last - 1];
                break; // 因为这一位上已经大于1了,而题目要求只能填 1 or 0,所以对应树的右分支a(n-1)无了
            }
            else // x == 1,即 a(n-1) = 1,开始往下一层走
            {
                last ++ ;
                if (last > K) break;
            }
        }
        
        // 处理右边:只有最后一个点的右分支a0
        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;
}
3、数字游戏

ACwing 1082

算法思路
先求 0 ~ N 中不下降数的个数,记为f(N)。然后使用f(R) - f(L-1)即为所求。

首先,将 N N N拆分成成 n n n位数: N = a n − 1 、 a n − 2 、 . . . 、 a 0 N=a_{n-1}、a_{n-2}、 ...、 a_0 N=an−1​、an−2​、...、a0​

这里的预处理使用DP来解决。上一题是直接可以使用排列组合求出。

集合
f[i,j]:最高位为j,且一共有i位的不降数的集合的个数

集合划分
按照第i-2位上填的值来划分(第i-1位上为j)

若第i-2位上为k,若去掉第i-1位上的j,则剩下就表示为在剩余i-1中,最高位为k,一共i-1位不降数的个数,表示为f[i-1, k]。所以 f ( i , j ) = ∑ k = 0 9 f ( i − 1 , j + k ) f(i, j)= sum_{k=0}^9 f(i-1, j+k) f(i,j)=∑k=09​f(i−1,j+k) 。

#include 
#include 
#include 
#include 

using namespace std;

const int N = 15;

int f[N][N]; // f[i][j]:一共有i位,且最高位为j的数的个数

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];
}

// 0 ~ n 里面满足要求的个数
int dp(int n) 
{
    if (!n) return 1; // n = 0
    
    vector nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0, last = 0; // last前缀信息:存储上一位数
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i]; // a(n-1) = x
        
        // 考虑左边
        for (int j = last; j < x; j ++ ) // 枚举[0, x-1],同时保证后面一位大于前一位,即从last开始枚举
        	res += f[i + 1][j]; // 一共剩余i+1位
        
        // 考虑右边:判断当前这一位能否填x
        if (x < last) // 右边分支不存在
            break;
        last = x;
        
        // 考虑最后的右分支:最后一位数
        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、Windy数

ACwing 1083

算法思路
先求 1 ~ N 中Windy数的个数,记为f(N)。然后使用f(R) - f(L-1)即为所求。(注意是从1开始,不能包含前导零)

首先,将 N N N拆分成成 n n n位数: N = a n − 1 、 a n − 2 、 . . . 、 a 0 N=a_{n-1}、a_{n-2}、 ...、 a_0 N=an−1​、an−2​、...、a0​

预处理

集合
f[i, j]:共有i位,最高位是j的Windy数的个数

集合划分
按次高位第j-1位来划分,次高位为k ∈ (0, 9),所以 f ( i , j ) = ∑ k = 0 9 f ( i − 1 , k ) , 其 中 ∣ j − k ∣ ≥ 2 f(i, j)=sum_{k=0}^9f(i-1,k),其中|j-k| ge2 f(i,j)=∑k=09​f(i−1,k),其中∣j−k∣≥2

#include 
#include 
#include 
#include 

using namespace std;

const int N = 11;

int f[N][10];

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(j - k) >= 2)
                    f[i][j] += f[i - 1][k];
}

int dp(int n)
{
    if (!n) return 0;
    
    vector nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0;
    // 记录上一位数的信息
    // 因为第一位可以任意填,所以last只需要填一个能与0~9的差的绝对值都大于等于2的数即可
    int last = -2; 
    
    // 处理无前导零的方案
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        
        // 处理左边分支
        for (int j = i == nums.size() - 1; j < x; j ++ ) // 判断是处理第一个左分支不能为0
            if (abs(j - last) >= 2)
                res += f[i + 1][j]; // 一共有 i+1 位
        
        // 处理右边分支
        if (abs(x - last) >= 2) last = x;
        else break;
        
        // 处理最后一位a0,它必是合法的
        if (!i)
            res ++ ;
    }
    
    // 特殊处理有前导零的数
    // 对于数字13,他是满足windy数定义的,那么加上前导0之后的013就不会被f[3][0]加进去
    // 原因就是abs(0-1)<2,这样就导致答案 漏掉
    for (int i = 1; i < nums.size(); 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) << endl;
    
    return 0;
}
5、数字游戏 II

ACwing 1084

预处理

集合
f[i, j, k]:一共有i位,最高位是j,且给为数字之和模N的余数是k的方案数个数。

集合划分
按次高位i-1上的值为x来划分,即共有i-1位,最高位为x,且i-1位上的数字之和模N的余数是k-j的方案数。

于是 f ( i , j , k ) = ∑ x = 0 9 f ( i − 1 , x , ( k − j ) m o d    N ) f(i, j, k)=sum_{x=0}^9f(i - 1, x, (k - j)mod N) f(i,j,k)=∑x=09​f(i−1,x,(k−j)modN)

#include 
#include 
#include 
#include 

using namespace std;

const int N = 11, M = 110;

int P;
int f[N][10][M];

int mod(int x, int y)
{
    // 因为 c++ 里面取模负数会得到负数,要得到正数需要处理一下
    return (x % y + y) % y; 
}

void init()
{
    memset(f, 0, sizeof 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;
    
    vector nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0;
    int last = 0; // 前缀信息:存储前面各位数之和
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        
        // 左边分支
        // (前面各位数之和last + 后面各位数之和sum) mod P = 0
        // 所以sum = -last mod P
        for (int j = 0; j < x; j ++ )
            res += f[i + 1][j][mod(-last, P)]; 
            
        // 右边分支
        last += x;
        
        // 最后右边分支a0
        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;
}
6、不要62

ACwing 1085

算法思路
先求 0 ~ N 中满足性质的个数,记为f(N)。然后使用f(R) - f(L-1)即为所求。

首先,将 N N N拆分成成 n n n位数: N = a n − 1 、 a n − 2 、 . . . 、 a 0 N=a_{n-1}、a_{n-2}、 ...、 a_0 N=an−1​、an−2​、...、a0​

预处理

集合
f[i, j]:一共有i位数字,最高位是j的数字,其中不包含4和62的数字一共有多少个

集合划分
根据次高位来划分,因此有 f ( i , j ) = ∑ k = 0 9 f ( i − 1 , k ) , 其 中 j ≠ 4 , k ≠ 4 , j k ≠ 62 f(i,j)=sum_{k=0}^9f(i-1,k),其中jne4,kne4,jkne62 f(i,j)=∑k=09​f(i−1,k),其中j​=4,k​=4,jk​=62。

#include 
#include 
#include 
#include 

using namespace std;

const int N = 35;

int f[N][10];

void init()
{
    for (int i = 0; i <= 9; i ++ )
        if (i != 4) f[1][i] = 1;
        
    for (int i = 2; i < N; i ++ )
        for (int j = 0; j <= 9; j ++ )
        {
            if (j == 4) continue;
            for (int k = 0; k <= 9; k ++ )
            {
                if (k == 4 || j == 6 && k == 2) continue;
                f[i][j] += f[i - 1][k];
            }
        }
}

int dp(int n)
{
    if (!n) return 1;
    
    vector nums;
    while (n) nums.push_back(n % 10), n /= 10;
    
    int res = 0;
    int last = 0; // 前缀信息:上一位的值,只要不是6即可
    for (int i = nums.size() - 1; i >= 0; i -- )
    {
        int x = nums[i];
        
        // 左边
        for (int j = 0; j < x; j ++ )
        {
            if (j == 4 || last == 6 && j == 2)
                continue;
            res += f[i + 1][j];
        }
        
        // 右边
        if (x == 4 || last == 6 && x == 2) 
            break;
        last = x;
        
        // 最后一个右分支
        if (!i) res ++ ;
    }
    
    return res;
}

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

    return 0;
}
7、恨7不成妻

ACwing 1086

算法思路
先求 0 ~ N 中满足性质的个数,记为f(N)。然后使用f(R) - f(L-1)即为所求。

首先,将 N N N拆分成成 n n n位数: N = a n − 1 、 a n − 2 、 . . . 、 a 0 N=a_{n-1}、a_{n-2}、 ...、 a_0 N=an−1​、an−2​、...、a0​

预处理

集合
f[i, j, a, b]:一共有i位,最高位是j,这个数本身模7的余数是a,这个数的各位数字之和模7的余数时b的情况下,所有数的平方和

#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 20, P = 1e9 + 7;

struct F {
    // 分别表示A1、A2、...、At的0次方和,一次方和,平方和
    int s0, s1, s2;
} f[N][10][7][7];

int power7[N], power9[N];

int mod(LL x, int y) // 取得正余数
{
    return (x % y + y) % y;
}

void init() {
    for (int i = 0; i <= 9; i++) // 处理一位数
    {
        if (i == 7) continue;
        auto &v = f[1][i][i % 7][i % 7];
        v.s0++, v.s1 += i, v.s2 += i * i;
    }

    LL power = 10;
    for (int i = 2; i < N; i++, power *= 10)
        for (int j = 0; j <= 9; j++) {
            if (j == 7) continue;
            for (int a = 0; a < 7; a++)
                for (int b = 0; b < 7; b++)
                    for (int k = 0; k <= 9; k++) {
                        if (k == 7) continue;
                        auto &v1 = f[i][j][a][b], &v2 = f[i - 1][k][mod(a - j * power, 7)][mod(b - j, 7)];
                        v1.s0 = mod(v1.s0 + v2.s0, P);
                        v1.s1 = mod(v1.s1 + v2.s1 + j * (power % P) % P * v2.s0, P);
                        v1.s2 = mod(v1.s2 + j * j * (power % P) % P * (power % P) % P * v2.s0 + v2.s2 +
                                    2 * j * power % P * v2.s1, P);
                    }
        }

    power7[0] = 1;
    for (int i = 1; i < N; i++) power7[i] = power7[i - 1] * 10 % 7;

    power9[0] = 1;
    for (int i = 1; i < N; i++) power9[i] = power9[i - 1] * 10ll % P;
}

// 余数不等于a,不等于b的情况
F get(int i, int j, int a, int b) {
    int s0 = 0, s1 = 0, s2 = 0;
    for (int x = 0; x < 7; x++)
        for (int y = 0; y < 7; y++)
            if (x != a && y != b) {
                auto v = f[i][j][x][y];
                s0 = (s0 + v.s0) % P;
                s1 = (s1 + v.s1) % P;
                s2 = (s2 + v.s2) % P;
            }
    return {s0, s1, s2};
}

int dp(LL n) {
    if (!n) return 0;

    LL backup_n = n % P;
    vector nums;
    while (n) nums.push_back(n % 10), n /= 10;

    int res = 0;
    LL last_a = 0, last_b = 0;  // 上一个数本身的值 上一个数各位数之和
    for (int i = nums.size() - 1; i >= 0; i--) {
        int x = nums[i];
        for (int j = 0; j < x; j++) {
            if (j == 7) continue;
            int a = mod(-last_a * power7[i + 1], 7);
            int b = mod(-last_b, 7);
            auto v = get(i + 1, j, a, b);
            res = mod(
                    res +
                    (last_a % P) * (last_a % P) % P * power9[i + 1] % P * power9[i + 1] % P * v.s0 % P +
                    v.s2 +
                    2 * last_a % P * power9[i + 1] % P * v.s1,
                    P);
        }

        if (x == 7) break;
        last_a = last_a * 10 + x;
        last_b += x;

        if (!i && last_a % 7 && last_b % 7) res = (res + backup_n * backup_n) % P;
    }

    return res;
}

int main() {
    int T;
    cin >> T;

    init();

    while (T--) {
        LL l, r;
        cin >> l >> r;
        cout << mod(dp(r) - dp(l - 1), P) << endl;
    }

    return 0;
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/875848.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号