LeetCode 每日一题笔记
0. 前言
- 日期:2026.06.04
- 题目:3751. 范围内总波动值 I
- 难度:中等
- 标签:数位DP、枚举、数学
1. 题目理解
问题描述
给定闭区间 num1,num2\\mathit{num1},\\mathit{num2}num1,num2,数字波动值规则:
- 位数<3:波动值=0;
- 首尾数位不计入峰谷;
- 中间数位:严格大于左右邻居为峰 ,严格小于左右邻居为谷 ,峰+谷总数为本数波动值;
求区间全部数字波动值累加和。
示例
输入:num1=121, num2=121
输出:1
解释:2>1且2>1,是峰,波动值=1。
2. 解题思路
核心观察
- 朴素思路:逐个遍历区间数字,拆位统计单个数字峰谷数量,累加;适合数据范围小的场景;
- 优化思路(数位DP+预处理):用
f(x)代表0,x0,x0,x总波动和,答案=f(num2)−f(num1−1)f(\mathit{num2})-f(\mathit{num1}-1)f(num2)−f(num1−1),预计算数位统计数组,按数位分段快速求和,大数场景高效。
算法步骤
- 暴力枚举:遍历num1,num2\\mathit{num1},\\mathit{num2}num1,num2每一个数字,拆分字符/逐位取数,遍历中间位统计峰谷;
- 数位优化:预处理各长度数位前缀和数组,实现
count(x)计算0~x总波动,差分得到区间答案。
3. 代码实现
java
package lc3751;
class Solution {
public int totalWaviness(int num1, int num2) {
int res = 0;
for (; num1 <= num2; num1++) {
res += method1(num1);
}
return res;
}
public int method1(int num1) {
if (num1 < 100) {
return 0;
}
int res = 0;
String s = String.valueOf(num1);
int n = s.length();
for (int i = 1; i <= n - 2; i++) {
if (s.charAt(i) > s.charAt(i - 1) && s.charAt(i) > s.charAt(i + 1)) {
res++;
}
if (s.charAt(i) < s.charAt(i - 1) && s.charAt(i) < s.charAt(i + 1)) {
res++;
}
}
return res;
}
}
4. 代码优化说明
(原优化代码不变,添加逐行注释)
java
class Solution {
// 预处理数组:PEAK_VALLY_SUMS[位数][首位数字] 存对应前缀波动总和
private static final long[][] PEAK_VALLY_SUMS = new long[16][11];
// 预处理10的幂次,POW_OF_10S[i]=10^i
private static final long[] POW_OF_10S = new long[16];
static {
long powOf10 = POW_OF_10S[0] = 1;
// 预生成10的各次幂
for(int i = 1; i < POW_OF_10S.length; i++) {
POW_OF_10S[i] = powOf10 *= 10;
}
long prevSum = 0;
// 预填充不同位数、不同首数字对应的波动前缀和
for(int i = 2; i < PEAK_VALLY_SUMS.length; i++) {
long[] row = PEAK_VALLY_SUMS[i];
long sum = 0;
for(int j = 0; j < 10; j++) {
row[j + 1] = sum += prevSum + (45 + 9 * j - j * j) * POW_OF_10S[i - 2];
}
prevSum = sum;
}
}
// 答案 = f(num2)-f(num1-1),差分求区间和
public int totalWaviness(int num1, int num2) {
return totalWaviness0(num2) - totalWaviness0(num1 - 1);
}
// 计算 [0,num] 所有数字波动总和
int totalWaviness0(int num) {
String str = Integer.toString(num);
int len = str.length();
// 小于3位全部无波动
if(len <= 2) {
return 0;
}
int digit = str.charAt(0) - '0';
// 先累加同位数、首数字小于当前首位的全部数字波动和
long sum = PEAK_VALLY_SUMS[len - 1][digit] - 5 * (POW_OF_10S[len - 2] - 1);
int digit1 = -1, digit2;
int prefixCount = 0;
// 从次高位向后逐位遍历,统计固定前缀下合法后缀贡献
for(int i = len - 2; i >= 0; i--) {
digit2 = digit1;
digit1 = digit;
digit = str.charAt(len - 1 - i) - '0';
if(i > 0) {
// 累加剩余低位任意取值的预计算波动和
sum += PEAK_VALLY_SUMS[i][digit];
// 统计合法前后数位配对数量
int pairCount;
if(digit <= digit1) {
pairCount = (19 - digit) * digit >> 1;
} else {
pairCount = (19 - digit1) * digit1 + (digit1 + digit) * (digit - digit1 - 1) >> 1;
}
sum += POW_OF_10S[i - 1] * pairCount;
}
// 存在前前位,判断当前三元组是否构成峰谷,累加对应低位全排列贡献
if(digit2 >= 0) {
if (digit2 > digit1) {
if (digit > digit1 + 1) {
sum += (digit - digit1 - 1) * POW_OF_10S[i];
}
} else if (digit2 < digit1) {
sum += Math.min(digit, digit1) * POW_OF_10S[i];
}
sum += POW_OF_10S[i] * prefixCount * digit;
// 当前中间位形成峰/谷,后续所有后缀数字都要多记1点波动
if(digit > digit1 && digit1 < digit2 || digit < digit1 && digit1 > digit2) {
prefixCount++;
}
}
}
// 补全当前完整数字自身的峰谷计数
sum += prefixCount;
return (int)sum;
}
}
5. 复杂度分析
- 暴力枚举版
时间:O((R−L+1)⋅D)O((R-L+1)\cdot D)O((R−L+1)⋅D),DDD 为数字平均位数;区间极大时会超时;
空间:O(1)O(1)O(1),仅临时变量。 - 数位预处理优化版
时间:O(logN)O(\log N)O(logN),预处理仅执行一次,单次查询按数字位数遍历;
空间:O(1)O(1)O(1),预处理数组长度固定为常数。
6. 总结
- 暴力思路:逻辑直观,适合小数范围,直接逐位判定峰谷;
- 优化思路:前缀差分+数位预处理 ,f(r)−f(l−1)f(r)-f(l-1)f(r)−f(l−1) 求区间和,预处理预存各数位组合贡献,将复杂度从线性枚举压缩至数位对数级别;
- 核心:单个峰谷只由连续三位决定,预处理批量统计同前缀下所有后缀的波动总和。