「动态规划::数位DP」相邻数位递推 / Luogu P2657|LeetCode 600(C++)

目录

概述

相邻数位递推

思路

算法过程

Code

复杂度

数字进制转换

思路

算法过程

Code

复杂度

总结


概述

我们来介绍最基本的数位DP:相邻数位递推。


相邻数位递推

Luogu P2657:

题目背景

windy 定义了一种 windy 数。

题目描述

不含前导零且相邻两个数字之差至少为 2 的正整数被称为 windy 数。windy 想知道,在 a 和 b 之间,包括 a 和 b ,总共有多少个 windy 数?

输入格式

输入只有一行两个整数,分别表示 a 和 b。

输出格式

输出一行一个整数表示答案。

思路

我们怎么用DP处理数字问题呢?

 我们发现题目是对相邻的数有要求,那么我们可以从这里入手。

需要的状态包括:数位(即数有多长),和这个数用于递推的那一位(我们如果从低位向高位递推,那么就需要最高位的数)。

定义 dp[i][j],表示 i 位数且最高位为 j 时的 windy 数总数

递推公式是:dp[i][j] = \sumdp[i][k],其中 k 与 j 的绝对值至少为2

初始情况下,dp[1][j] = 1,表示长度为1的数都是 windy 数。

算法过程

我们怎么利用DP数组呢?

可以预见DP数组的计算是这样的: 

for (int j = 0; j < 10; j++)
        dp[1][j] = 1;
for (int i = 2; i < N; i++)
    for (int j = 0; j < 10; j++)
        for (int k = 0; k < 10; k++) if (abs(j - k) >= 2)
            dp[i][j] += dp[i - 1][k];

我们发现考虑了前导0,这似乎不健康。但是别着急,我们并不是用DP数组表示一个完整的数,而是等会的递推会在这些带前导0的数前面再接一些数位,使之成为完整的数。

做一些预处理的工作: 

int num[N]{}, len = 0;
for (; n; n /= 10)
    num[++len] = n % 10;

由于我们计算的数受到上界 n 的限制,我们先将 n 拆解成长度为 logn 的数组(这也是时空复杂度的来源) 

我们怎么使所有数都小于等于上界 n 呢?

从高位开始,试着贴着上界走。每次循环中我们在逻辑上试着将 n 的高位放置在所有可能的数的高位上,然后那些低位由dp数组统计。

形象的,用每个字母表示一个数位上的数,

n = abcdefgh
    i  8 7 6 5 4 3 2 1 (从高位向低位枚举)
num[i] a b c d e f g h
           |
       a b ?..........

我们枚举的 j 枚举当前位,显然 j 应该小于 c,至于 j 等于 c 的可能,应该留给下次循环,即高位为 abc 时枚举 j < d。 

发现 j 在最高位时,枚举应该从1开始,次高位以及更低位从0开始枚举(不含前导0)。

由于windy数受上一位限制,所以需要 last 变量保存上一位,所有满足abs(j - last) >= 2 的 dp[i][j]加入统计。

int res = 0, last = -2;
for (int i = len; i; i--) {
    int now = num[i];
    for (int j = (i == len); j < now; j++)
        if (abs(j - last) >= 2)
            res += dp[i][j];
    if (abs(now - last) < 2) break;
    else last = now;
    if (i == 1) res++;
}

枚举完一次 j 后,我们将 now 在逻辑上加入前缀高位,但是前提是 now 和 last 符合 windy 数要求,否则 break,因为带着这个前缀高位的数都不可能是windy数。 

如果枚举到 i = 1,那么我们在逻辑上达到了 n,证明这个上界本身也是 windy 数,加入答案。

最后, 数位较小,即统计上界不受限制的所有不含前导0的数,

for (int i = len - 1; i; i--)
    for (int j = 1; j < 10; j++)
        res += dp[i][j];
if (!n) return 0;

此外,发现我们的计算结果统计的范围是[1, n],所以在一开始对 n = 0 特判,直接返回0。 

Code

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 11;
int dp[N][N];
auto init = []() {
    for (int j = 0; j < 10; j++)
        dp[1][j] = 1;
    for (int i = 2; i < N; i++)
        for (int j = 0; j < 10; j++)
            for (int k = 0; k < 10; k++) if (abs(j - k) >= 2)
                dp[i][j] += dp[i - 1][k];
    return 0;
    }();
int solve(int n) {
    if (!n) return 0;
    int num[N]{}, len = 0;
    for (; n; n /= 10)
        num[++len] = n % 10;
    int res = 0, last = -2;
    for (int i = len; i; i--) {
        int now = num[i];
        for (int j = (i == len); j < now; j++)
            if (abs(j - last) >= 2)
                res += dp[i][j];
        if (abs(now - last) < 2) break;
        else last = now;
        if (i == 1) res++;
    }
    for (int i = len - 1; i; i--)
        for (int j = 1; j < 10; j++)
            res += dp[i][j];
    return res;
}
int main() {
    int a, b; cin >> a >> b;
    cout << solve(b) - solve(a - 1);
    return 0;
}

复杂度

时间复杂度: O((logn)^{3} + n)

空间复杂度: O((logn)^{2}


数字进制转换

LeetCode 600:

给定一个正整数 n ,请你统计在 [0, n] 范围的非负整数中,有多少个整数的二进制表示中不存在 连续的 1 

本质只是加了个进制转换。

思路

对于 int 的进制转换非常简单,只需要不断对 2 取余即可。

int bits[N]{}, len = 0;
for (; n; n /= 2) bits[++len] = n % 2;

算法过程

显而易见的递推:

dp[1][1] = dp[1][0] = 1;
for (int i = 2; i < N; i++) {
    dp[i][0] = dp[i - 1][1] + dp[i - 1][0];
    dp[i][1] = dp[i - 1][0];
}
return 0;

对于本题,我们不需要额外计算数位较小的数,因为我们的dp带有前导0,在处在最高位时,j  = 0的情况可以直接将之计入答案。 但是上一题不行,因为由于上一题的限制条件,dp只是部分带前导0的数,而不是全部带前导0的数。

Code

static constexpr int N = 32;
int dp[N][2];
auto init = [](){
    dp[1][1] = dp[1][0] = 1;
    for (int i = 2; i < N; i++) {
        dp[i][0] = dp[i - 1][1] + dp[i - 1][0];
        dp[i][1] = dp[i - 1][0];
    }
    return 0;
}();
class Solution {
public:
    int findIntegers(int n) {
        int bits[N]{}, len = 0;
        for (; n; n /= 2) bits[++len] = n % 2;
        int res = 0, last = -1;
        for (int i = len; i; i--) {
            int now = bits[i];
            if (now == 1) res += dp[i][0];
            if (last == now && last == 1) break;
            else last = now;
            if (i == 1) res++;
        }
        return res;
    }
};

复杂度

时间复杂度: O(logn)

空间复杂度: O(logn) 


总结

这是最基本的数位DP问题,后续将介绍更复杂的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值