算法提高课 -- 8

数位DP

本节比较难理解,解析会比较繁琐一些,需要大家耐心的看一看,抱歉啦~

这类题的解题思路比较统一,有一个大致的思路,就是设置一个函数,传入参数x,返回的是[0, x]符合题意所有情况的结果个数;函数内的操作需要根据题意进行分析,但是也有一定的模板可以套。

函数内的操作有一套标准的体系,下面是详细的图例解析:

模板代码:

int dp(int x) {
    if (!x) return 0; // 有的题目0也符合题意
    vector<int> nums;
    while (x) nums.push_back(x % 10), x /= 10; // 并不一定所有题目都是求十进制每位上的数字
    int res = 0; // 存储最终结果
    int last = 0; // 一般情况是存储上一位数字
    for (int i = nums.size(); i >= 0; i--) {
        int x = nums[i]; // 存储从最高到最低位遍历到的数字
        ... // 有关题意得操作
        if (!i) res++; // 如果到最后一位特判,当然if中也可能会有其他条件
    }
    return res;
}

注:我没有写求组合数的初始化函数模板,是因为这个在不同题目中设定都不一样,不太好统一。

接下来我们通过几道例题来更清楚地理解这些模板。

1081.度的数量

题目解析:这一题中和我们刚刚所说的每一位这个概念有些细微的差别,刚刚是十进制的,这里是b进制的每一位;经过我们刚刚模板的分析,此时我们只需要将注意力放在“有关题意操作”这方面就可以了,也就是对每一位上的b进制数x做文章即可

开始分析之前先要明白题目要求我们计算的是什么?分析可得,是x在b进制下允许有多少个1?

每一位上的数字遍历的时候都设为x,x在b进制中分三种情况:

1.x > 1 则表示这个位置既可以放1又可以放0;因此看看k - last > 0 ? 放1 : 不放。

注:这里很重要,很多题解没有讲清楚为什么0和1都行?因为我们函数结果返回的是[0, x]中有多少个符合题意的结果,而并非只考虑x一个数,比它小的结果也要加上!

2.x = 1 则last++即可。

3.x = 0 上面解析图中我们左分支里0的情况,使用组合数即可。

代码 + 注释:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 35;
int n, m, k, b;
int f[N][N]; // f[i][j] 表示在[0, i]中放j个1的方案数
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 x) {
    if (!x) return 0; // 本题中0不符合情况
    vector<int> nums;
    while (x) nums.push_back(x % b), x /= b;
    int res = 0, last = 0; // last表示已经放了1的个数
    for (int i = nums.size() - 1; i >= 0; i--) {
        int x = nums[i];
        if (x) { // x > 0 才能有分直的情况,不然只能是0
            res += f[i][k - last]; // x = 0的情况:第i位是0,在[0, i-1]中选择k - last个1
            if (x > 1) { // x > 1的情况:0 和 1 都行
                if (k - last > 0) res += f[i][k - last - 1]; // 如果有剩余量供我们填1,就填上
                break; // 填完就break,因为 x > 1 是肯定不符合题意的,因此循环无法进行下去了,到这里结束
            }
            else { // x = 1的情况
                last++;
                if (k - last < 0) break; // 如果 1 的数量超过题目所给的范围,则不合法,直接break
            }
        }
        if (!i && k == last) res++;
    }
    return res;
}
int main() {
    scanf("%d%d%d%d", &n, &m, &k, &b);
    init();
    printf("%d\n", dp(m) - dp(n - 1));
    return 0;
}

1082.数字游戏

题目解析:按照给的模板写就行,注意遍历顺序是从最高位到最低位遍历(倒序遍历);上一题代码的last存储的是b进制中1的个数,而本题则存储的是上一位的数字,便于判断不降数;内部操作还是对每一位上的数字进行判断,一种是0~a-1、另一种是a,然后再分支......

代码 + 注释:

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 15;
int n, m;
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];
            }
        }
    }
}
int dp(int n) {
    if (!n) return 1;
    vector<int> 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];
        for (int j = last; j < x; j++) {
            res += f[i + 1][j];
        }
        if (x < last) break; // 不符合不降数的题意
        last = x;
        if (!i) res++;
    }
    return res;
}
int main() {
    init();
    while (scanf("%d%d", &n, &m) != -1) {
        printf("%d\n", dp(m) - dp(n - 1));
    }
    return 0;
}

1083.Windy数

题目解析:本题与之前不同的是有前导零的特殊情况需要特判。

代码 + 注释:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 20;
int n, m;
int f[N][N]; // f[i][j] 表示的是有i位数、最高位是j的数字的个数
void init() {
    for (int i = 0; i < 10; i++) f[1][i] = 1;
    for (int i = 2; i < N; i++) {
        for (int j = 0; j < 10; j++) {
            for (int k = 0; k < 10; k++) {
                if (abs(k - j) >= 2) {
                    f[i][j] += f[i - 1][k];
                }
            }
        }
    }
}
int dp(int x) {
    if (!x) return 0;
    vector<int> nums;
    while (x) nums.push_back(x % 10), x /= 10;
    int res = 0, last = -2;
    // 处理数字长度为nums.size()的
    for (int i = nums.size() - 1; i >= 0; i--) {
        int x = nums[i];
        // 处理左分支: [0, x)
        for (int j = i == nums.size() - 1; j < x; j++) { // 首位不能是0
            if (abs(j - last) >= 2) res += f[i + 1][j];
        }
        // 处理右分支: x
        if (abs(last - x) >= 2) last = x;
        else break;
        if (!i) res++; // 最右叶子节点的特判
    }
    // 处理长度小于nums.size()的
    for (int i = 1; i < nums.size(); i++) { // 处理前导零问题,因为类似于000025这种也算是不降数
        for (int j = 1; j < 10; j++) {
            res += f[i][j];
        }
    }
    return res;
}
int main() {
    init();
    scanf("%d%d", &n, &m);
    printf("%d\n", dp(m) - dp(n - 1));
    return 0;
}

1084.数字游戏II

题目解析:和数字游戏差不太多,但是有一点是需要说明的,就是下面代码中打上“***”的语句,详细说一下为什么要这样转换:

int sum; // 表示[nums.size() - 1, i + 1]所有位置上的数字之和
int j; // 就是代码中的j:nums[i]这个位置上的所有可能取值,遍历小于x的所有数字,对应模板中的左分支
int last; // 和代码中的一样:[nums.size() - 1, i]所有位置上的数字之和
根据题意我们能够得出下面的等式:
sum = last + j --- 1;
sum % mod == 0 --- 2;
由1, 2可推出(last + j) % mod == 0
又因为此时余数为0;因此前一层状态表示为最高位是j,余数为0 - last

代码 + 注释:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 35, M = 110;
int a, b, mod;
int f[N][10][M]; // f[i][j][k] 表示的是一共有i位、第i位是j且对mod取余结果为k的数字个数
int get(int x) { // 对负数取余的技巧
    return ((x % mod) + mod) % mod;
}
void init() {
    memset(f, 0, sizeof f);
    for (int i = 0; i < 10; i++) f[1][i][i % mod]++;
    for (int i = 2; i < N; i++) {
        for (int j = 0; j < 10; j++) {
            for (int k = 0; k < mod; k++) {
                for (int q = 0; q < 10; q++) {
                    f[i][j][k] += f[i - 1][q][get(k - j)]; // ***
                }
            }
        }
    }
}
int dp(int n) {
    if (!n) return 1;
    vector<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    int res = 0, last = 0; // last 表示[0, i]中所有数字的总和
    for (int i = nums.size() - 1; i >= 0; i--) {
        int x = nums[i];
        // 左分支
        for (int j = 0; j < x; j++) {
            res += f[i + 1][j][get(-last)];
        }
        last += x;
        // 右分支
        if (!i && last % mod == 0) res++;
    }
    return res;
}
int main() {
    while (scanf("%d%d%d", &a, &b, &mod) != -1) {
        init();
        printf("%d\n", dp(b) - dp(a - 1));
    }
    return 0;
}

1085.不要62

经过前面的训练,这题看起来就非常简单了,因此直接上代码吧 ^ ^

代码 + 注释:

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
const int N = 10;
int n, m;
int f[N][10]; // f[i][j] 表示一共有i位、最高位是j且符合题意的数字
void init() {
    for (int i = 0; i < 10; i++) {
        if (i == 4) continue;
        f[1][i] = 1;
    }
    for (int i = 2; i < N; i++) {
        for (int j = 0; j < 10; j++) {
            if (j != 4) {
                for (int k = 0; k < 10; 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<int> nums;
    while (n) nums.push_back(n % 10), n /= 10;
    int res = 0, last = 0;
    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();
    while (scanf("%d%d", &n, &m)) {
        if (!n || !m) break;
        printf("%d\n", dp(m) - dp(n - 1));
    }
    return 0;
}

1086.恨7不成妻

这个题贼难,思路绕的地方很多,后面我专门写一篇来讲吧,详细一些。

代码:

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 55, P = 1e9 + 7;
int T;
LL n, m;
int power7[N], power9[N];
struct F {
    // s0 = t   
    // s1 = A1 + A2 + A3 + ... + At
    // s2 = A1² + A2² + ... + At²
    int s0, s1, s2;
} f[N][10][7][7]; // f[i][j][a][b] 表示数字有i位、最高位是j、这个数字 % 7 == a、每一位数字和 % 7 == b的所有数字平方和
int mod(LL x, int y) { // 使得余数必大于0
    return (x % y + y) % y;
}
void init() {
    // 只有一位的数字
    for (int i = 0; i < 10; 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 < 10; j++) {
            if (j == 7) continue;
            for (int a = 0; a < 7; a++) {
                for (int b = 0; b < 7; b++) {
                    for (int k = 0; k < 10; k++) {
                        if (k == 7) continue;
                        auto &v0 = f[i][j][a][b], &v1 = f[i - 1][k][mod(a - j * power, 7)][mod(b - j, 7)];
                        v0.s0 = mod(v0.s0 + v1.s0, P);
                        v0.s1 = mod(v0.s1 + v1.s1 + j * (power % P) % P * v1.s0, P);
                        v0.s2 = mod(v0.s2 + j * j * (power % P) % P * (power % P) % P * v1.s0 + 2 * j * (power % P) % P * v1.s1 + v1.s2, P);
                    }
                }
            }
        }
    }
    power7[0] = power9[0] = 1;
    for (int i = 1; i < N; i++) {
        power7[i] = 10 * power7[i - 1] % 7;
        power9[i] = 10ll * power9[i - 1] % P;
    }
}
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<int> 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() {
    scanf("%d", &T);
    init();
    while (T--) {
        scanf("%lld%lld", &n, &m);
        printf("%d\n", mod(dp(m) - dp(n - 1), P));
    }
    return 0;
}

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值