目录
概述
我们来介绍最基本的数位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] = dp[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(
)
空间复杂度: O(
)
数字进制转换
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问题,后续将介绍更复杂的问题。