普通视图

发现新文章,点击刷新页面。
今天 — 2026年5月13日LeetCode 每日一题题解

每日一题-使数组互补的最少操作次数🟡

2026年5月13日 00:00

给你一个长度为 偶数 n 的整数数组 nums 和一个整数 limit 。每一次操作,你可以将 nums 中的任何整数替换为 1 到 limit 之间的另一个整数。

如果对于所有下标 i下标从 0 开始),nums[i] + nums[n - 1 - i] 都等于同一个数,则数组 nums互补的 。例如,数组 [1,2,3,4] 是互补的,因为对于所有下标 inums[i] + nums[n - 1 - i] = 5

返回使数组 互补最少 操作次数。

 

示例 1:

输入:nums = [1,2,4,3], limit = 4
输出:1
解释:经过 1 次操作,你可以将数组 nums 变成 [1,2,2,3](加粗元素是变更的数字):
nums[0] + nums[3] = 1 + 3 = 4.
nums[1] + nums[2] = 2 + 2 = 4.
nums[2] + nums[1] = 2 + 2 = 4.
nums[3] + nums[0] = 3 + 1 = 4.
对于每个 i ,nums[i] + nums[n-1-i] = 4 ,所以 nums 是互补的。

示例 2:

输入:nums = [1,2,2,1], limit = 2
输出:2
解释:经过 2 次操作,你可以将数组 nums 变成 [2,2,2,2] 。你不能将任何数字变更为 3 ,因为 3 > limit 。

示例 3:

输入:nums = [1,2,1,2], limit = 2
输出:0
解释:nums 已经是互补的。

 

提示:

  • n == nums.length
  • 2 <= n <= 105
  • 1 <= nums[i] <= limit <= 105
  • n 是偶数。

差分数组(Python/Java/C++/Go)

作者 endlesscheng
2026年5月9日 13:12

题目说,可以把 $\textit{nums}$ 中的任意元素变成 $[1,\textit{limit}]$ 中的整数,所以 $\textit{nums}[i]+\textit{nums}[n-1-i]$ 可以变成 $[2,\textit{limit}\cdot 2]$ 中的整数。

如果写个二重循环,先枚举 $\textit{nums}[i]+\textit{nums}[n-1-i]$ 都变成 $2,3,4,\ldots, \textit{limit}\cdot 2$,再遍历 $\textit{nums}$,计算操作次数,那么时间复杂度为 $\mathcal{O}(n\cdot \textit{limit})$,太慢了。

定义 $a[k]$ 表示 $\textit{nums}[i]+\textit{nums}[n-1-i]$ 都变成 $k$ 所需的操作次数。

横看成岭侧成峰,单看 $(\textit{nums}[i],\textit{nums}[n-1-i])$,这对元素会如何影响数组 $a$?哪些 $a[k]$ 需要增加 $1$,哪些 $a[k]$ 需要增加 $2$?

设 $x = \textit{nums}[i]$,$y = \textit{nums}[n-1-i]$。

  • 如果 $x$ 和 $y$ 都不修改,那么 $a[x+y]$ 不变。
  • 如果只修改 $y$,那么 $x+y$ 可以是 $[x+1,x+\textit{limit}]$ 中的整数;如果只修改 $x$,那么 $x+y$ 可以是 $[y+1,y+\textit{limit}]$ 中的整数。这两个区间的并集为 $[\min(x,y)+1, \max(x,y)+\textit{limit}]$。这个区间内的整数 $k$,除了 $k=x+y$ 时 $a[x+y]$ 不变,其余 $a[k]$ 都增加 $1$,表示当 $k$ 在这个区间内时,把 $x+y$ 变成 $k$ 只需操作 $1$ 次。
  • 其余在 $[2,\min(x,y)]\cup[\max(x,y)+\textit{limit}+1,\textit{limit}\cdot 2]$ 中的 $k$,把 $a[k]$ 都增加 $2$,表示当 $k$ 在这个区间内时,把 $x+y$ 变成 $k$ 需要操作 $2$ 次。

这里用到了区间增加同一个数的操作,这可以用差分数组实现,原理讲解

计算差分数组的前缀和,就得到了数组 $a$。

答案为 $\min\limits_{k=2}^{\textit{limit}\cdot 2}a[k]$。

注:题目保证 $n$ 是偶数。

优化前

###py

class Solution:
    def minMoves(self, nums: List[int], limit: int) -> int:
        n = len(nums)
        diff = [0] * (limit * 2 + 2)

        for i in range(n // 2):
            x = nums[i]
            y = nums[-1 - i]
            l = min(x, y) + 1
            r = max(x, y) + limit

            # [2, l-1] += 2
            diff[2] += 2
            diff[l] -= 2

            # [l, r] += 1
            diff[l] += 1
            diff[r + 1] -= 1

            # x+y 实际操作 0 次,从 [l, r] 中去掉
            # [x+y, x+y] -= 1
            diff[x + y] -= 1
            diff[x + y + 1] += 1

            # [r+1, limit*2] += 2
            diff[r + 1] += 2
            # limit*2+1 不在 [2, limit*2] 中,可以省略

        ans = inf
        sum_d = 0
        for d in diff[2 : limit * 2 + 1]:
            sum_d += d
            ans = min(ans, sum_d)
        return ans

###java

class Solution {
    public int minMoves(int[] nums, int limit) {
        int n = nums.length;
        int[] diff = new int[limit * 2 + 2];

        for (int i = 0; i < n / 2; i++) {
            int x = nums[i];
            int y = nums[n - 1 - i];
            int l = Math.min(x, y) + 1;
            int r = Math.max(x, y) + limit;

            // [2, l-1] += 2
            diff[2] += 2;
            diff[l] -= 2;

            // [l, r] += 1
            diff[l]++;
            diff[r + 1]--;

            // x+y 实际操作 0 次,从 [l, r] 中去掉
            // [x+y, x+y] -= 1
            diff[x + y]--;
            diff[x + y + 1]++;

            // [r+1, limit*2] += 2
            diff[r + 1] += 2;
            // limit*2+1 不在 [2, limit*2] 中,可以省略
        }

        int ans = Integer.MAX_VALUE;
        int sum = 0;
        for (int i = 2; i <= limit * 2; i++) {
            sum += diff[i];
            ans = Math.min(ans, sum);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int minMoves(vector<int>& nums, int limit) {
        int n = nums.size();
        vector<int> diff(limit * 2 + 2);

        for (int i = 0; i < n / 2; i++) {
            int x = nums[i];
            int y = nums[n - 1 - i];
            int l = min(x, y) + 1;
            int r = max(x, y) + limit;

            // [2, l-1] += 2
            diff[2] += 2;
            diff[l] -= 2;

            // [l, r] += 1
            diff[l]++;
            diff[r + 1]--;

            // x+y 实际操作 0 次,从 [l, r] 中去掉
            // [x+y, x+y] -= 1
            diff[x + y]--;
            diff[x + y + 1]++;

            // [r+1, limit*2] += 2
            diff[r + 1] += 2;
            // limit*2+1 不在 [2, limit*2] 中,可以省略
        }

        int ans = INT_MAX;
        int sum = 0;
        for (int i = 2; i <= limit * 2; i++) {
            sum += diff[i];
            ans = min(ans, sum);
        }
        return ans;
    }
};

###go

func minMoves(nums []int, limit int) int {
n := len(nums)
diff := make([]int, limit*2+2)
for i, x := range nums[:n/2] {
y := nums[n-1-i]
l := min(x, y) + 1
r := max(x, y) + limit

// [2, l-1] += 2
diff[2] += 2
diff[l] -= 2

// [l, r] += 1
diff[l]++
diff[r+1]--

// x+y 实际操作 0 次,从 [l, r] 中去掉
// [x+y, x+y] -= 1
diff[x+y]--
diff[x+y+1]++

// [r+1, limit*2] += 2
diff[r+1] += 2
// limit*2+1 不在 [2, limit*2] 中,可以省略
}

ans := math.MaxInt
sum := 0
for _, d := range diff[2 : limit*2+1] {
sum += d
ans = min(ans, sum)
}
return ans
}

优化

  • diff[2] += 2 执行了 $\dfrac{n}{2}$ 次,相当于 $\textit{diff}[2]$ 整体增加了 $n$。也可以初始化 $\textit{sum}=n$。
  • diff[l] -= 2diff[l] += 1 合并为 diff[l] -= 1
  • diff[r+1] -= 1diff[r+1] += 2 合并为 diff[r+1] += 1

###py

class Solution:
    def minMoves(self, nums: List[int], limit: int) -> int:
        n = len(nums)
        diff = [0] * (limit * 2 + 2)

        for i in range(n // 2):
            x = nums[i]
            y = nums[n - 1 - i]
            l = min(x, y) + 1
            r = max(x, y) + limit
            diff[l] -= 1
            diff[x + y] -= 1
            diff[x + y + 1] += 1
            diff[r + 1] += 1

        ans = inf
        sum_d = n
        for d in diff[2 : limit * 2 + 1]:
            sum_d += d
            ans = min(ans, sum_d)
        return ans

###java

class Solution {
    public int minMoves(int[] nums, int limit) {
        int n = nums.length;
        int[] diff = new int[limit * 2 + 2];

        for (int i = 0; i < n / 2; i++) {
            int x = nums[i];
            int y = nums[n - 1 - i];
            int l = Math.min(x, y) + 1;
            int r = Math.max(x, y) + limit;
            diff[l]--;
            diff[x + y]--;
            diff[x + y + 1]++;
            diff[r + 1]++;
        }

        int ans = Integer.MAX_VALUE;
        int sum = n;
        for (int i = 2; i <= limit * 2; i++) {
            sum += diff[i];
            ans = Math.min(ans, sum);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int minMoves(vector<int>& nums, int limit) {
        int n = nums.size();
        vector<int> diff(limit * 2 + 2);

        for (int i = 0; i < n / 2; i++) {
            int x = nums[i];
            int y = nums[n - 1 - i];
            int l = min(x, y) + 1;
            int r = max(x, y) + limit;
            diff[l]--;
            diff[x + y]--;
            diff[x + y + 1]++;
            diff[r + 1]++;
        }

        int ans = INT_MAX;
        int sum = n;
        for (int i = 2; i <= limit * 2; i++) {
            sum += diff[i];
            ans = min(ans, sum);
        }
        return ans;
    }
};

###go

func minMoves(nums []int, limit int) int {
n := len(nums)
diff := make([]int, limit*2+2)
for i, x := range nums[:n/2] {
y := nums[n-1-i]
l := min(x, y) + 1
r := max(x, y) + limit
diff[l]--
diff[x+y]--
diff[x+y+1]++
diff[r+1]++
}

ans := math.MaxInt
sum := n
for _, d := range diff[2 : limit*2+1] {
sum += d
ans = min(ans, sum)
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n+ \textit{limit})$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(\textit{limit})$。

专题训练

见下面差分题单的「§2.1 一维差分」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

借这个问题学习一下差分数组,O(n) 算法

作者 liuyubobobo
2020年11月29日 12:35

假设 res[x] 表示的是,nums[i] + nums[n - 1 - i]x 的时候,需要多少次操作。

我们只需要计算出所有的 x 对应的 res[x], 取最小值就好了。

根据题意,nums[i] + nums[n - 1 - i] 最小是 2,即将两个数都修改为 1;最大是 2 * limit,即将两个数都修改成 limit

所以,res[x]x 的取值范围是 [2, 2 * limit]。我们用一个 res[2 * limit + 1] 的数组就好。


关键是,如何求出每一个 res[x] 位置的值,即修改后互补的数字和为 x,需要多少操作?

为了叙述方便,假设 nums[i]Anums[n - 1 - i]B

显然有:

  • 如果修改后两个数字的和是 A + B,我们使用的操作数是 0 (没有修改));

  • 否则的话,如果修改后两个数字和在 [1 + min(A, B), limit + max(A, B)] 的范围,我们使用的操作数是 1 (只需要修改 A 或者 B 就好);

  • 否则的话,如果修改后两个数字和在 [2, 2 * limit] 的范围,我们使用的操作数是 ``2`(两个数字都要修改));


所以,我们的算法是遍历每一组 nums[i]nums[n - 1 - i],然后:

  • 先将 [2, 2 * limit] 的范围需要的操作数 + 2

  • 之后,将 [1 + min(A, B), limit + max(A, B)] 的范围需要的操作数 - 1(即 2 - 1 = 1,操作 1 次);

  • 之后,将 [A + B] 位置的值再 -1(即 1 - 1 = 0,操作 0 次)。

可以看出,整个过程都是在做区间更新。最后,我们查询每一个位置的值,取最小值就好。


对于这个需求,我们当然可以使用线段树或者树状数组解决,但代码量稍大,且复杂度也是 O(nlogn) 的。

但是仔细观察,我们发现,我们只需要作区间更新,和单点查询。

对于这个需求,有一种非常常规的”数据结构“,叫差分数组,完全满足需求,并且编程极其简单,整体可以在 O(n) 的时间解决。

打上引号,是因为差分数组就是一个数组而已。


简单来说,差分数组 diff[i],存储的是 res[i] - res[i - 1];而差分数组 diff[0...i] 的和,就是 res[i] 的值。

大家可以用一个小数据试验一下,很好理解。


如果我们想给 [l, r] 的区间加上一个数字 a, 只需要 diff[l] += a,diff[r + 1] -= a

这样做,diff[0...i] 的和,就是更新后 res[i] 的值。

依然是,大家可以用一个小数据试验一下,其实很好理解。


有了差分数组这个武器,这个问题就很简单了。

我的参考代码如下(C++):

class Solution {
public:
    int minMoves(vector<int>& nums, int limit) {
        
        // 差分数组, diff[0...x] 的和表示最终互补的数字和为 x,需要的操作数
        // 因为差分数组的计算需要更新 r + 1,所以数组的总大小在 limit * 2 + 1 的基础上再 + 1
        vector<int> diff(limit * 2 + 2, 0);

        int n = nums.size();
        for(int i = 0; i < n / 2; i ++){
            int A = nums[i], B = nums[n - 1 - i];

            // [2, 2 * limit] 范围 + 2
            int l = 2, r = 2 * limit;
            diff[l] += 2, diff[r + 1] -= 2;

            // [1 + min(A, B), limit + max(A, B)] 范围 -1
            l = 1 + min(A, B), r = limit + max(A, B);
            diff[l] += -1, diff[r + 1] -= -1;

            // [A + B] 再 -1    
            l = A + B, r = A + B;
            diff[l] += -1, diff[r + 1] -= -1;
        }

        // 依次求和,得到 最终互补的数字和 i 的时候,需要的操作数 sum
        // 取最小值
        int res = n, sum = 0;
        for(int i = 2; i <= 2 * limit; i ++){
            sum += diff[i];
            if(sum < res) res = sum;
        }
        return res;
    }
};

整体时间复杂度是 O(n) 的;空间复杂度也是 O(n) 的。


觉得有帮助请点赞哇!

差分+扫描

作者 lucifer1004
2020年11月29日 12:10

本场周赛题解 | 我的LeetCode比赛题解

我们考虑任意一个数对$(a,b)$,不妨假设$a\leq b$。假设最终选定的和值为$target$,则我们可以发现,对于$(a,b)$这个数对:

  • 当$target<1+a$时,需要操作两次;
  • 当$1+a\leq target<a+b$时,需要操作一次;
  • 当$target=a+b$时,不需要操作;
  • 当$a+b<target\leq b+limit$时,需要操作一次;
  • 当$target>b+limit$时,需要操作两次。

我们设初始操作次数为两次。令$target$从数轴最左端开始向右移动,我们会发现:

  • 在$1+a$处,操作次数减少一次;
  • 在$a+b$处,操作次数减少一次;
  • 在$a+b+1$处,操作次数增加一次;
  • 在$b+limit+1$处,操作次数增加一次。

因此,我们可以遍历数组中的所有数对,求出每个数对的这四个关键位置,然后更新差分数组。最后,我们遍历(扫描)差分数组,就可以找到令总操作次数最小的$target$,同时可以得到最少的操作次数。

  • 时间复杂度$O(N+L)$。
  • 空间复杂度$O(L)$。

###cpp

class Solution {
public:
    int minMoves(vector<int>& nums, int limit) {
        int n = nums.size();
        vector<int> delta(limit * 2 + 2);
        for (int i = 0; i < n / 2; ++i) {
            int lo = 1 + min(nums[i], nums[n - i - 1]);
            int hi = limit + max(nums[i], nums[n - i - 1]);
            int sum = nums[i] + nums[n - i - 1];
            delta[lo]--;
            delta[sum]--;
            delta[sum + 1]++;
            delta[hi + 1]++;
        }
        int now = n;
        int ans = n;
        for (int i = 2; i <= limit * 2; ++i) {
            now += delta[i];
            ans = min(ans, now);
        }
        return ans;
    }
};
昨天 — 2026年5月12日LeetCode 每日一题题解

每日一题-完成所有任务的最少初始能量🔴

2026年5月12日 00:00

给你一个任务数组 tasks ,其中 tasks[i] = [actuali, minimumi] :

  • actuali 是完成第 i 个任务 需要耗费 的实际能量。
  • minimumi 是开始第 i 个任务前需要达到的最低能量。

比方说,如果任务为 [10, 12] 且你当前的能量为 11 ,那么你不能开始这个任务。如果你当前的能量为 13 ,你可以完成这个任务,且完成它后剩余能量为 3 。

你可以按照 任意顺序 完成任务。

请你返回完成所有任务的 最少 初始能量。

 

示例 1:

输入:tasks = [[1,2],[2,4],[4,8]]
输出:8
解释:
一开始有 8 能量,我们按照如下顺序完成任务:
    - 完成第 3 个任务,剩余能量为 8 - 4 = 4 。
    - 完成第 2 个任务,剩余能量为 4 - 2 = 2 。
    - 完成第 1 个任务,剩余能量为 2 - 1 = 1 。
注意到尽管我们有能量剩余,但是如果一开始只有 7 能量是不能完成所有任务的,因为我们无法开始第 3 个任务。

示例 2:

输入:tasks = [[1,3],[2,4],[10,11],[10,12],[8,9]]
输出:32
解释:
一开始有 32 能量,我们按照如下顺序完成任务:
    - 完成第 1 个任务,剩余能量为 32 - 1 = 31 。
    - 完成第 2 个任务,剩余能量为 31 - 2 = 29 。
    - 完成第 3 个任务,剩余能量为 29 - 10 = 19 。
    - 完成第 4 个任务,剩余能量为 19 - 10 = 9 。
    - 完成第 5 个任务,剩余能量为 9 - 8 = 1 。

示例 3:

输入:tasks = [[1,7],[2,8],[3,9],[4,10],[5,11],[6,12]]
输出:27
解释:
一开始有 27 能量,我们按照如下顺序完成任务:
    - 完成第 5 个任务,剩余能量为 27 - 5 = 22 。
    - 完成第 2 个任务,剩余能量为 22 - 2 = 20 。
    - 完成第 3 个任务,剩余能量为 20 - 3 = 17 。
    - 完成第 1 个任务,剩余能量为 17 - 1 = 16 。
    - 完成第 4 个任务,剩余能量为 16 - 4 = 12 。
    - 完成第 6 个任务,剩余能量为 12 - 6 = 6 。

 

提示:

  • 1 <= tasks.length <= 105
  • 1 <= actuali <= minimumi <= 104

交换论证法(Python/Java/C++/C/Go/JS/Rust)

作者 endlesscheng
2026年5月9日 11:34

按照什么规则排序?

本文把 $\textit{actual}$ 简称为 $a$,把 $\textit{minimum}$ 简称为 $m$。

如果 $\textit{tasks}$ 的某个排列 $T$ 是最优的(完成所有任务所需的初始能量最少),那么 $T$ 中的相邻任务满足什么性质?

设 $T$ 中的一对相邻任务为 $t_1 = (a_1,m_1)$ 和 $t_2 = (a_2,m_2)$。设初始能量为 $E_0$,完成这两个任务之前的能量为 $E$。由于从一开始到完成这两个任务之前,所消耗的能量之和是固定的 $S$,所以有 $E = E_0-S$。根据该式,$E$ 越小,初始能量 $E_0$ 也越小。

先完成哪个任务更好?

  • 如果先完成 $t_1$ 再完成 $t_2$,那么完成 $t_1$ 之前,必须满足 $E\ge m_1$;完成 $t_2$ 之前,必须满足 $E-a_1\ge m_2$。联立得 $E\ge \max(m_1,m_2+a_1)$。
  • 如果先完成 $t_2$ 再完成 $t_1$,那么完成 $t_2$ 之前,必须满足 $E\ge m_2$;完成 $t_1$ 之前,必须满足 $E-a_2\ge m_1$。联立得 $E\ge \max(m_2,m_1+a_2)$。

如果先完成 $t_1$ 再完成 $t_2$ 更优(或者相同),则有

$$
\max(m_1,m_2+a_1)\le \max(m_2,m_1+a_2)
$$

两边同时减去 $a_1+a_2$,得

$$
\max(m_1-a_1-a_2,m_2-a_2)\le \max(m_2-a_1-a_2,m_1-a_1)
$$

为方便阅读,令 $X=m_1-a_1$,$Y = m_2-a_2$,上式化简为

$$
\max(X-a_2,Y)\le \max(Y-a_1,X)
$$

  • 如果 $X<Y$,那么上式左侧等于 $Y$,右侧严格小于 $Y$(题目保证 $a_1 > 0$),此时 $\max(X-a_2,Y) > \max(Y-a_1,X)$。
  • 如果 $X\ge Y$,那么上式左侧两个值都 $\le X$,右侧等于 $X$,此时 $\max(X-a_2,Y) \le \max(Y-a_1,X)$。

所以当且仅当 $X\ge Y$ 成立时,$\max(X-a_2,Y)\le \max(Y-a_1,X)$ 成立。

所以当且仅当 $m_1-a_1 \ge m_2-a_2$ 成立时,先完成 $t_1$ 再完成 $t_2$ 更优(或者相同)。

这意味着,对于 $\textit{tasks}$ 中的相邻任务,设左边的任务为 $(a_1,m_1)$,右边的任务为 $(a_2,m_2)$,如果 $m_1-a_1 < m_2-a_2$,那么交换这两个任务可以让初始能量变得更小(或者不变)。于是通过交换 $\textit{tasks}$ 的相邻任务(类似冒泡排序的过程),把 $\textit{tasks}$ 按照 $m_i-a_i$ 从大到小排序,可以得到 $\textit{tasks}$ 的最优排列。

计算初始能量(正序)

设初始能量为 $E_0$,设执行 $\textit{tasks}[i] = (a_i,m_i)$ 之前,累计耗费的能量为 $S$,那么当前能量为 $E_0 - S$,必须满足

$$
E_0 - S \ge m_i
$$

$$
E_0 \ge S + m_i
$$

由此可以得到 $n$ 个关于 $E_0$ 的下界,所有下界取最大值,即为答案。

###py

class Solution:
    def minimumEffort(self, tasks: List[List[int]]) -> int:
        tasks.sort(key=lambda t: t[0] - t[1])  # 按照 minimum - actual 从大到小排序

        ans = 0
        s = 0  # 累计耗费的能量
        for actual, minimum in tasks:
            # 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            # 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = max(ans, s + minimum)
            s += actual
        return ans

###java

class Solution {
    public int minimumEffort(int[][] tasks) {
        // 按照 minimum - actual 从大到小排序
        Arrays.sort(tasks, (a, b) -> (b[1] - b[0]) - (a[1] - a[0]));

        int ans = 0;
        int s = 0; // 累计耗费的能量
        for (int[] t : tasks) {
            int actual = t[0];
            int minimum = t[1];
            // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = Math.max(ans, s + minimum);
            s += actual;
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int minimumEffort(vector<vector<int>>& tasks) {
        ranges::sort(tasks, {}, [](auto& t) { return t[0] - t[1]; }); // 按照 minimum - actual 从大到小排序

        int ans = 0;
        int s = 0; // 累计耗费的能量
        for (auto& t : tasks) {
            int actual = t[0], minimum = t[1];
            // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = max(ans, s + minimum);
            s += actual;
        }
        return ans;
    }
};

###c

int cmp(const void* p, const void* q) {
    int* a = *(int**)p;
    int* b = *(int**)q;
    return (b[1] - b[0]) - (a[1] - a[0]); // 按照 minimum - actual 从大到小排序
}

int minimumEffort(int** tasks, int tasksSize, int* tasksColSize) {
    qsort(tasks, tasksSize, sizeof(int*), cmp);

    int ans = 0;
    int s = 0; // 累计耗费的能量
    for (int i = 0; i < tasksSize; i++) {
        int actual = tasks[i][0];
        int minimum = tasks[i][1];
        // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
        // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
        ans = MAX(ans, s + minimum);
        s += actual;
    }
    return ans;
}

###go

func minimumEffort(tasks [][]int) (ans int) {
slices.SortFunc(tasks, func(a, b []int) int {
return (b[1] - b[0]) - (a[1] - a[0]) // 按照 minimum - actual 从大到小排序
})

s := 0 // 累计耗费的能量
for _, t := range tasks {
actual, minimum := t[0], t[1]
// 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
// 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
ans = max(ans, s+minimum)
s += actual
}
return
}

###js

var minimumEffort = function(tasks) {
    tasks.sort((a, b) => (b[1] - b[0]) - (a[1] - a[0])); // 按照 minimum - actual 从大到小排序

    let ans = 0;
    let s = 0; // 累计耗费的能量
    for (const [actual, minimum] of tasks) {
        // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
        // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
        ans = Math.max(ans, s + minimum);
        s += actual;
    }
    return ans;
};

###rust

impl Solution {
    pub fn minimum_effort(mut tasks: Vec<Vec<i32>>) -> i32 {
        tasks.sort_unstable_by_key(|t| t[0] - t[1]); // 按照 minimum - actual 从大到小排序

        let mut ans = 0;
        let mut s = 0; // 累计耗费的能量
        for t in tasks {
            let actual = t[0];
            let minimum = t[1];
            // 题目要求 E0 - s >= minimum,即 E0 >= s + minimum
            // 由此可以得到 n 个关于 E0 的下界,所有下界的最大值即为答案
            ans = ans.max(s + minimum);
            s += actual;
        }
        ans
    }
}

计算初始能量(倒序)

也可以从右到左计算。

设完成任务 $t_i = (a_i,m_i)$ 之后的能量为 $E$,那么完成 $t_i$ 之前的能量为 $E+a_i$,但这个能量又必须 $\ge m_i$,所以完成 $t_i$ 之前的能量至少为

$$
\max(E+a_i, m_i)
$$

为了最小化初始能量,完成最后一个任务后的能量应当为 $0$,作为 $E$ 的初始值。

代码实现时,可以改成按照 $m_i-a_i$ 从小到大排序,这样可以正序遍历数组,更方便。

###py

class Solution:
    def minimumEffort(self, tasks: list[list[int]]) -> int:
        tasks.sort(key=lambda t: t[1] - t[0])  # 按照 minimum - actual 从小到大排序

        e = 0
        for actual, minimum in tasks:
            # 完成该任务之后的能量为 e,那么完成该任务之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = max(e + actual, minimum)
        return e

###java

class Solution {
    public int minimumEffort(int[][] tasks) {
        // 按照 minimum - actual 从小到大排序
        Arrays.sort(tasks, (a, b) -> (a[1] - a[0]) - (b[1] - b[0]));

        int e = 0;
        for (int[] t : tasks) {
            int actual = t[0];
            int minimum = t[1];
            // 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = Math.max(e + actual, minimum);
        }
        return e;
    }
}

###cpp

class Solution {
public:
    int minimumEffort(vector<vector<int>>& tasks) {
        ranges::sort(tasks, {}, [](auto& t) { return t[1] - t[0]; }); // 按照 minimum - actual 从小到大排序

        int e = 0;
        for (auto& t : tasks) {
            int actual = t[0], minimum = t[1];
            // 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = max(e + actual, minimum);
        }
        return e;
    }
};

###c

int cmp(const void* p, const void* q) {
    int* a = *(int**)p;
    int* b = *(int**)q;
    return (a[1] - a[0]) - (b[1] - b[0]); // 按照 minimum - actual 从小到大排序
}

int minimumEffort(int** tasks, int tasksSize, int* tasksColSize) {
    qsort(tasks, tasksSize, sizeof(int*), cmp);

    int e = 0;
    for (int i = 0; i < tasksSize; i++) {
        int actual = tasks[i][0];
        int minimum = tasks[i][1];
        // 完成 tasks[i] 之后的能量为 e,那么完成 tasks[i] 之前的能量为 e+actual,同时该能量必须至少为 minimum
        e = MAX(e + actual, minimum);
    }
    return e;
}

###go

func minimumEffort(tasks [][]int) (e int) {
slices.SortFunc(tasks, func(a, b []int) int {
return (a[1] - a[0]) - (b[1] - b[0]) // 按照 minimum - actual 从小到大排序
})

for _, t := range tasks {
actual, minimum := t[0], t[1]
// 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
e = max(e+actual, minimum)
}
return
}

###js

var minimumEffort = function(tasks) {
    tasks.sort((a, b) => (a[1] - a[0]) - (b[1] - b[0])); // 按照 minimum - actual 从小到大排序

    let e = 0;
    for (const [actual, minimum] of tasks) {
        // 完成该任务之后的能量为 e,那么完成该任务之前的能量为 e+actual,同时该能量必须至少为 minimum
        e = Math.max(e + actual, minimum);
    }
    return e;
};

###rust

impl Solution {
    pub fn minimum_effort(mut tasks: Vec<Vec<i32>>) -> i32 {
        tasks.sort_unstable_by_key(|t| t[1] - t[0]); // 按照 minimum - actual 从小到大排序

        let mut e = 0;
        for t in tasks {
            let actual = t[0];
            let minimum = t[1];
            // 完成 t 之后的能量为 e,那么完成 t 之前的能量为 e+actual,同时该能量必须至少为 minimum
            e = minimum.max(e + actual);
        }
        e
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{tasks}$ 的长度。瓶颈在排序上。
  • 空间复杂度:$\mathcal{O}(1)$。不计入排序的栈开销。

专题训练

见下面贪心题单的「§1.7 交换论证法」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

【儿须成名酒须醉】Python3+排序+贪心+数学+模拟

作者 liupengsay
2022年7月11日 19:58

【儿须成名酒须醉】Python3+排序+贪心+数学证明+模拟


提交结果

  • 执行用时: 164 ms , 在所有 Python3 提交中击败了 94.62% 的用户
  • 内存消耗: 42.6 MB , 在所有 Python3 提交中击败了 58.06% 的用户
  • 通过测试用例: 42 / 42

解题思路

  1. 考虑两个任务的能量组合任务1为$[a_1,m_1]$与任务2为$[a_2,m_2]$
  2. 则先做任务1再做任务2的最小初始能量为$s_{12}=max(m_1,m_2+a_1)$,反之则是$s_{21}=max(m_2,m_1+a_2)$
  3. 不失一般地设$m_1-a_1>=m_2-a_2$,则有$m_1+a_2>=m_2+a_1$
  • 当$m_1<=m_2$时,显然有$s_{12}=m_2+a_1$,而$s_{21}=max(m_2,m_1+a_2)$,由上可得$m_2+a_1<=m_1+a_2$,因此有$s_{12}<=s_{21}$
  • 当$m_1>m_2$时,显然有$s_{12}=max(m_1,m_2+a_1)$,而$s_{21}=m_1+a_2$,由上可得$m_2+a_1<=m_1+a_2$,因此有$s_{12}<=s_{21}$
  1. 由此使用归纳法可知,贪心地将任务$[a,m]$当中$m-a$较大即$a-m$较小的排在前面进行即可

性能优化

复杂度分析

  • 设任务个数为$n$,则有
    • 时间复杂度:$O(nlogn)$
    • 空间复杂度:$O(1)$

代码

###python3

class Solution:
    def minimumEffort(self, tasks: List[List[int]]) -> int:
        tasks.sort(key=lambda x: x[0] - x[1])
        ans = 0
        cur = 0
        for a, m in tasks:
            if cur < m:
                ans += m - cur
                cur = m
            cur -= a
        return ans


逆向思维

从结果状态反推:$初始能量=总消耗能量+剩余能量$ 在剩余能量为$0$时的初始能量最优

  • 执行用时: 180 ms , 在所有 Python3 提交中击败了 80.65% 的用户
  • 内存消耗: 42.5 MB , 在所有 Python3 提交中击败了 81.72% 的用户
  • 通过测试用例: 42 / 42

代码

###python3

class Solution:
    def minimumEffort(self, tasks: List[List[int]]) -> int:
        tasks.sort(key=lambda x: x[1] - x[0])
        ans = 0
        for a, m in tasks:
            ans = ans + a if ans + a > m else m
        return ans

[纯例子讲解] 帮助理解 (老实人,装逼人举例)

作者 lchaok
2021年5月4日 12:13

纯例子讲解帮助不太明白的人感受到其中的道理;

例1:【5,5】【5,7】

定义:minimum-actual越小的人称为越老实的人;反之,称为装逼人。
讲解: 显然【5,5】是个老实人,【5,7】是个装逼人。老实人很实在,最后剩下5给他,他就会欣然接受,不需要额外的需求,但是装逼人不一样,他明明只要5,确装逼说自己要7,这个时候怎么办呢,拿什么东西来装逼呢,只能借一下老实人的来装一下逼

我们明白,这个答案显然是10,装逼人【5,7】可以拿老实人的5给自己来装逼,所以在不改变结果的情况下他可以乱叫6,7,8,9,10,这些情况下答案都不会改变,一旦装逼人叫了【5,11】,就会发现老实人借给他的也不够他装逼了,无可奈何,我们只能给他分配额外资源让他装逼。

一旦相通这个之后,我们解题思路变成了:倒着安排,先把大量“老实人”放在前面把需求数字垫起来,这样后面的装逼人能装更大的逼,例2有两个老实人【5,5】【5,5】,装逼人就能叫道【5,15】,只要老实人还能借,他就还能装

下面的例子是举例让大家理解装逼程度一致,起始先后顺序是一样的
例3:【1,3】【3,5】 他们的老实程度一致 结果: 3(给第一个装逼) + 3(因为3+3>5) = 6 = 5(给第二个先装逼) + 1(5+ 1 > 3,够装逼了)

例4: 【1,4】【3,5】
大家自己思考一下这个就明白结果了,结果放在评论区,希望大家都能理解,喜欢的或者觉得有趣的不妨点个赞!!!!谢谢你们!

class Solution {
public:
    int minimumEffort(vector<vector<int>>& tasks) {//把握额外报的这部分如何让其无损
        sort(tasks.begin(), tasks.end(), [&](vector<int> &a, vector<int> &b){
            return a[1] - a[0] < b[1] - b[0];
        });

        int result = 0;

        for (const auto &task: tasks){
            result = max(result + task[0], task[1]);
        }

        return result;
    }
};
昨天以前LeetCode 每日一题题解

每日一题-分割数组中数字的数位🟢

2026年5月11日 00:00

给你一个正整数数组 nums ,请你返回一个数组 answer ,你需要将 nums 中每个整数进行数位分割后,按照 nums 中出现的 相同顺序 放入答案数组中。

对一个整数进行数位分割,指的是将整数各个数位按原本出现的顺序排列成数组。

  • 比方说,整数 10921 ,分割它的各个数位得到 [1,0,9,2,1] 。

 

示例 1:

输入:nums = [13,25,83,77]
输出:[1,3,2,5,8,3,7,7]
解释:
- 分割 13 得到 [1,3] 。
- 分割 25 得到 [2,5] 。
- 分割 83 得到 [8,3] 。
- 分割 77 得到 [7,7] 。
answer = [1,3,2,5,8,3,7,7] 。answer 中的数字分割结果按照原数字在数组中的相同顺序排列。

示例 2:

输入:nums = [7,1,3,9]
输出:[7,1,3,9]
解释:nums 中每个整数的分割是它自己。
answer = [7,1,3,9] 。

 

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 105

2553. 分割数组中数字的数位

作者 stormsunshine
2024年1月28日 13:37

解法

思路和算法

这道题要求将给定的正整数数组 $\textit{nums}$ 中的所有正整数按数位分割,保持数位顺序存入答案数组。需要首先遍历数组 $\textit{nums}$ 得到数位总数,然后将每个正整数按数位分割并存入答案数组。

首先遍历数组 $\textit{nums}$ 得到所有正整数的数位总数 $\textit{totalLength}$,然后创建长度为 $\textit{totalLength}$ 的答案数组 $\textit{answer}$,再次遍历数组 $\textit{nums}$,遍历过程中维护答案数组的当前下标 $\textit{index}$,对于每个正整数,执行如下操作。

  1. 用 $\textit{start}$ 表示当前正整数的数位填入答案数组的起始下标,$\textit{start} = \textit{index}$。

  2. 每次将 $\textit{num}$ 的最低位填入 $\textit{answer}[\textit{index}]$,然后将 $\textit{index}$ 的值增加 $1$,重复该操作直到 $\textit{num}$ 的所有位都填入答案数组。

  3. 当前正整数 $\textit{num}$ 填入答案数组的下标范围是 $[\textit{start}, \textit{index} - 1]$,为按照数位从低到高的顺序填入。为了和数组 $\textit{nums}$ 中的数位顺序保持一致,需要将答案数组的下标范围 $[\textit{start}, \textit{index} - 1]$ 的子数组翻转。

遍历结束之后,即可得到答案数组。

代码

###Java

class Solution {
    public int[] separateDigits(int[] nums) {
        int totalLength = 0;
        for (int num : nums) {
            totalLength += getLength(num);
        }
        int[] answer = new int[totalLength];
        int index = 0;
        for (int num : nums) {
            int start = index;
            int temp = num;
            while (temp != 0) {
                answer[index] = temp % 10;
                index++;
                temp /= 10;
            }
            reverse(answer, start, index - 1);
        }
        return answer;
    }

    public int getLength(int num) {
        int length = 0;
        while (num != 0) {
            length++;
            num /= 10;
        }
        return length;
    }

    public void reverse(int[] answer, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            int temp = answer[i];
            answer[i] = answer[j];
            answer[j] = temp;
        }
    }
}

###C#

public class Solution {
    public int[] SeparateDigits(int[] nums) {
        int totalLength = 0;
        foreach (int num in nums) {
            totalLength += GetLength(num);
        }
        int[] answer = new int[totalLength];
        int index = 0;
        foreach (int num in nums) {
            int start = index;
            int temp = num;
            while (temp != 0) {
                answer[index] = temp % 10;
                index++;
                temp /= 10;
            }
            Reverse(answer, start, index - 1);
        }
        return answer;
    }

    public int GetLength(int num) {
        int length = 0;
        while (num != 0) {
            length++;
            num /= 10;
        }
        return length;
    }

    public void Reverse(int[] answer, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            int temp = answer[i];
            answer[i] = answer[j];
            answer[j] = temp;
        }
    }
}

###C++

class Solution {
public:
    vector<int> separateDigits(vector<int>& nums) {
        int totalLength = 0;
        for (int num : nums) {
            totalLength += getLength(num);
        }
        vector<int> answer(totalLength);
        int index = 0;
        for (int num : nums) {
            int start = index;
            int temp = num;
            while (temp != 0) {
                answer[index] = temp % 10;
                index++;
                temp /= 10;
            }
            reverse(answer, start, index - 1);
        }
        return answer;
    }

    int getLength(int num) {
        int length = 0;
        while (num != 0) {
            length++;
            num /= 10;
        }
        return length;
    }

    void reverse(vector<int>& answer, int start, int end) {
        for (int i = start, j = end; i < j; i++, j--) {
            swap(answer[i], answer[j]);
        }
    }
};

###Python

class Solution:
    def separateDigits(self, nums: List[int]) -> List[int]:
        answer = []
        index = 0
        for num in nums:
            start = index
            temp = num
            while temp != 0:
                answer.append(temp % 10)
                index += 1
                temp //= 10
            self.reverse(answer, start, index - 1)
        return answer

    def getLength(self, num: int) -> int:
        length = 0
        while num != 0:
            length += 1
            num //= 10
        return length

    def reverse(self, answer: List[int], start: int, end: int) -> None:
        i, j = start, end
        while i < j:
            answer[i], answer[j] = answer[j], answer[i]
            i += 1
            j -= 1

###C

int getLength(int num) {
    int length = 0;
    while (num != 0) {
        length++;
        num /= 10;
    }
    return length;
}

void reverse(int* answer, int start, int end) {
    for (int i = start, j = end; i < j; i++, j--) {
        int temp = answer[i];
        answer[i] = answer[j];
        answer[j] = temp;
    }
}

int* separateDigits(int* nums, int numsSize, int* returnSize) {
    int totalLength = 0;
    for (int i = 0; i < numsSize; i++) {
        totalLength += getLength(nums[i]);
    }
    int* answer = (int*) malloc(sizeof(int) * totalLength);
    *returnSize = totalLength;
    int index = 0;
    for (int i = 0; i < numsSize; i++) {
        int start = index;
        int temp = nums[i];
        while (temp != 0) {
            answer[index] = temp % 10;
            index++;
            temp /= 10;
        }
        reverse(answer, start, index - 1);
    }
    return answer;
}

###Go

func separateDigits(nums []int) []int {
    totalLength := 0
    for _, num := range nums {
        totalLength += getLength(num)
    }
    answer := make([]int, totalLength)
    index := 0
    for _, num := range nums {
        start := index
        temp := num
        for temp != 0 {
            answer[index] = temp % 10
            index++
            temp /= 10
        }
        reverse(answer, start, index - 1)
    }
    return answer
}

func getLength(num int) int {
    length := 0
    for num != 0 {
        length++
        num /= 10
    }
    return length
}

func reverse(answer []int, start int, end int) {
    for i, j := start, end; i < j; i, j = i + 1, j - 1 {
        answer[i], answer[j] = answer[j], answer[i]
    }
}

###JavaScript

var separateDigits = function(nums) {
    let totalLength = 0;
    for (let num of nums) {
        totalLength += getLength(num);
    }
    let answer = new Array(totalLength);
    let index = 0;
    for (let num of nums) {
        let start = index;
        let temp = num;
        while (temp !== 0) {
            answer[index] = temp % 10;
            index++;
            temp = Math.floor(temp / 10);
        }
        reverse(answer, start, index - 1);
    }
    return answer;
};

var getLength = function(num) {
    let length = 0;
    while (num !== 0) {
        length++;
        num = Math.floor(num / 10);
    }
    return length;
};

var reverse = function(answer, start, end) {
    for (let i = start, j = end; i < j; i++, j--) {
        let temp = answer[i];
        answer[i] = answer[j];
        answer[j] = temp;
    }
};

###TypeScript

function separateDigits(nums: number[]): number[] {
    let totalLength = 0;
    for (let num of nums) {
        totalLength += getLength(num);
    }
    let answer = new Array(totalLength);
    let index = 0;
    for (let num of nums) {
        let start = index;
        let temp = num;
        while (temp !== 0) {
            answer[index] = temp % 10;
            index++;
            temp = Math.floor(temp / 10);
        }
        reverse(answer, start, index - 1);
    }
    return answer;
};

function getLength(num: number): number {
    let length = 0;
    while (num !== 0) {
        length++;
        num = Math.floor(num / 10);
    }
    return length;
};

function reverse(answer: number[], start: number, end: number): void {
    for (let i = start, j = end; i < j; i++, j--) {
        let temp = answer[i];
        answer[i] = answer[j];
        answer[j] = temp;
    }
};

复杂度分析

  • 时间复杂度:$O(n \log_{10} m)$,其中 $n$ 是数组 $\textit{nums}$ 的长度,$m$ 是数组 $\textit{nums}$ 的最大元素。计算答案数组的长度与将数位填入答案数组的时间是 $O(n \log_{10} m)$。

  • 空间复杂度:$O(1)$。注意返回值不计入空间复杂度。

倒序插入

2023年3月9日 13:24

这个刚开始写的时候担心数组的长度和时间复杂度的原因,担心通过不了,结果没想过通过了。
我是用一个result数组来装将返回的分割数位值,考虑他们的长度,每一个元素的大小在10的五次方以内,分割之后100000共有六位,也就是最大是6,加上数组的长度最大是1000,于是我就把数组的大小取为6*10000=60000.
虽然我也知道浪费空间,但是我目前没有更好的方法,希望有大佬能够指点。

确定了返回数组了,然后就是然后把原始的整数拆分插入到结果数组中,后面我想到只要逐个不断求余就可以得到每一位数,后面我就在第一层遍历中添加一个循环得到整数的各个数位,但是提交的结果不符合要求,题目要求将数位按原本出现的顺序排列成数组,于是我就用一个数组作为辅助空间,将它反向添加,于是就得到了和题目意思相同的结果,辅助空间的大小就是一个整数拥有的各个位数的数量,由前面可以知道最大是6。

之后返回即可。

int* separateDigits(int* nums, int numsSize, int* returnSize){
    int *result=(int*)malloc(sizeof(int)*60000);
    int count=0;
    //遍历整个数组
    for(int i=0;i<numsSize;i++){
        int tmp=nums[i];
        int help[6];
        int tmpCount=0;
        //得到整数的各个数位,将他们储存在一个辅助数组中
        while(tmp){
            help[tmpCount++]=tmp%10;
            tmp=tmp/10;
        }
        //把数组中的元素倒序添加到结果数组中
        for(int j=tmpCount-1;j>=0;j--){
            result[count++]=help[j];
        }
    }
    *returnSize=count;
    return result;
}

两种方法:用字符串 / 不用字符串(Python/Java/C++/Go)

作者 endlesscheng
2023年2月5日 00:16

写法一:用字符串

把 $\textit{nums}[i]$ 转成字符串,即可从高到低遍历数位。

class Solution:
    def separateDigits(self, nums: List[int]) -> List[int]:
        return [int(ch) for x in nums for ch in str(x)]
class Solution {
    public int[] separateDigits(int[] nums) {
        List<Integer> digits = new ArrayList<>();
        for (int x : nums) {
            for (char ch : String.valueOf(x).toCharArray()) {
                digits.add(ch - '0');
            }
        }

        int m = digits.size();
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            ans[i] = digits.get(i);
        }
        return ans;
    }
}
class Solution {
public:
    vector<int> separateDigits(vector<int>& nums) {
        vector<int> ans;
        for (int x : nums) {
            for (char ch : to_string(x)) {
                ans.push_back(ch - '0');
            }
        }
        return ans;
    }
};
func separateDigits(nums []int) (ans []int) {
for _, x := range nums {
for _, ch := range strconv.Itoa(x) {
ans = append(ans, int(ch-'0'))
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log U)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U=\max(\textit{nums})$。
  • 空间复杂度:$\mathcal{O}(\log U)$。返回值不计入。

方法二:不用字符串

不断地把 $n$ 除以 $10$(下取整)直到 $0$,例如 $123\to 12\to 1\to 0$。在这个过程中的 $n\bmod 10$,即为每个数位。

这样做是从低到高遍历数位,和题目要求的顺序相反。

我们可以从右到左遍历 $\textit{nums}$,从低到高遍历 $\textit{nums}[i]$ 的数位。最后把遍历过的数位反转,即为答案。

class Solution:
    def separateDigits(self, nums: list[int]) -> list[int]:
        ans = []
        for x in reversed(nums):
            while x > 0:
                ans.append(x % 10)
                x //= 10
        ans.reverse()
        return ans
class Solution {
    public int[] separateDigits(int[] nums) {
        List<Integer> digits = new ArrayList<>();
        for (int i = nums.length - 1; i >= 0; i--) {
            for (int x = nums[i]; x > 0; x /= 10) {
                digits.add(x % 10);
            }
        }

        int m = digits.size();
        int[] ans = new int[m];
        for (int i = 0; i < m; i++) {
            ans[i] = digits.get(m - 1 - i);
        }
        return ans;
    }
}
class Solution {
public:
    vector<int> separateDigits(vector<int>& nums) {
        vector<int> ans;
        for (int x : nums | views::reverse) {
            for (; x > 0; x /= 10) {
                ans.push_back(x % 10);
            }
        }
        ranges::reverse(ans);
        return ans;
    }
};
func separateDigits(nums []int) (ans []int) {
for _, x := range slices.Backward(nums) {
for ; x > 0; x /= 10 {
ans = append(ans, x%10)
}
}
slices.Reverse(ans)
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log U)$,其中 $n$ 是 $\textit{nums}$ 的长度,$U=\max(\textit{nums})$。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-达到末尾下标所需的最大跳跃次数🟡

2026年5月10日 00:00

给你一个下标从 0 开始、由 n 个整数组成的数组 nums 和一个整数 target

你的初始位置在下标 0 。在一步操作中,你可以从下标 i 跳跃到任意满足下述条件的下标 j

  • 0 <= i < j < n
  • -target <= nums[j] - nums[i] <= target

返回到达下标 n - 1 处所需的 最大跳跃次数

如果无法到达下标 n - 1 ,返回 -1

 

示例 1:

输入:nums = [1,3,6,4,1,2], target = 2
输出:3
解释:要想以最大跳跃次数从下标 0 到下标 n - 1 ,可以按下述跳跃序列执行操作:
- 从下标 0 跳跃到下标 1 。 
- 从下标 1 跳跃到下标 3 。 
- 从下标 3 跳跃到下标 5 。 
可以证明,从 0 到 n - 1 的所有方案中,不存在比 3 步更长的跳跃序列。因此,答案是 3 。 

示例 2:

输入:nums = [1,3,6,4,1,2], target = 3
输出:5
解释:要想以最大跳跃次数从下标 0 到下标 n - 1 ,可以按下述跳跃序列执行操作:
- 从下标 0 跳跃到下标 1 。 
- 从下标 1 跳跃到下标 2 。 
- 从下标 2 跳跃到下标 3 。 
- 从下标 3 跳跃到下标 4 。 
- 从下标 4 跳跃到下标 5 。 
可以证明,从 0 到 n - 1 的所有方案中,不存在比 5 步更长的跳跃序列。因此,答案是 5 。 

示例 3:

输入:nums = [1,3,6,4,1,2], target = 0
输出:-1
解释:可以证明不存在从 0 到 n - 1 的跳跃序列。因此,答案是 -1 。 

 

提示:

  • 2 <= nums.length == n <= 1000
  • -109 <= nums[i] <= 109
  • 0 <= target <= 2 * 109

C++, 动态规划

作者 liu-xiang-3
2023年7月12日 12:57

思路

  1. f[i]表示从0到下标i的最大跳跃次数;
  2. f[0]=0, f[i]=f[j]+1, 可以从j跳过来;
class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> f(n, -1);
        f[0] = 0;
        /* 开始递推 */
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (f[j] != -1 && abs(nums[i] - nums[j]) <= target) {
                    f[i] = max(f[i], f[j] + 1);
                }
            }
        }
        return f[n - 1];
    }
};
class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        n = len(nums)
        f = [-1] * n
        f[0] = 0
        for j in range(1, n):
            for i in range(j):
                if f[i] != -1 and abs(nums[i] - nums[j]) <= target:
                    f[j] = max(f[j], f[i] + 1)
        return f[-1]

【什码情况】Java记忆化搜索

作者 smqk
2023年7月9日 15:37

image.png{:width=400}

解题思路

此处撰写解题思路

代码

###java

class Solution {
    // jump[i] 表示从下标 0 开始跳到 nums[i] 所需的最大跳跃次数
    int[] jump;

    public int maximumJumps(int[] nums, int target) {
        this.jump = new int[nums.length];
        Arrays.fill(jump, Integer.MIN_VALUE);
        jump[0] = 0;
        int maximumJumps = dfs(nums, nums.length - 1, target);
        return maximumJumps >= 0 ? maximumJumps : -1;
    }

    // 返回从下标 i 跳到下标 0 所需最大的跳跃次数
    private int dfs(int[] nums, int i, int target) {
        if (jump[i] != Integer.MIN_VALUE) {
            return jump[i];
        }

        int max = Integer.MIN_VALUE;
        for (int j = i - 1; j >= 0; j--) {
            int val = nums[j] - nums[i];
            if (-target <= val && val <= target) {
                int jump = dfs(nums, j, target) + 1;
                max = Math.max(max, jump);
            }
        }

        return jump[i] = max;
    }

}

最后

如果有帮助到你,请给题解点个收藏,让更多的人看到 ~ ("▔□▔)/

两种方法:普通 DP / 值域线段树优化 DP(Python/Java/C++/Go)

作者 endlesscheng
2023年7月9日 12:09

一、寻找子问题

想一想,最后一步发生了什么?

最后一步,我们从某个满足条件的下标 $i$ 跳到了下标 $n-1$。

枚举满足条件的 $i$,问题变成:

  • 从下标 $0$ 到达下标 $i$ 所需的最大跳跃次数。

这是和原问题相似的、规模更小的子问题,可以用递归解决。

注:从右往左思考,主要是方便把递归翻译成从左往右的递推。从左往右思考也是可以的。

二、状态定义与状态转移方程

根据上面的讨论,定义 $\textit{dfs}(j)$ 表示从下标 $0$ 到达下标 $j$ 所需的最大跳跃次数。

枚举满足 $0\le i<j$ 且 $|\textit{nums}[i]-\textit{nums}[j]|\le \textit{target}$ 的下标 $i$,问题变成从下标 $0$ 到达下标 $i$ 所需的最大跳跃次数,再加上从 $i$ 跳到 $j$ 的一次。

取最大值,得

$$
\textit{dfs}(j) = \max_{i} \textit{dfs}(i) + 1
$$

其中 $0\le i<j$ 且 $|\textit{nums}[i]-\textit{nums}[j]|\le \textit{target}$。

递归边界

  • $\textit{dfs}(0)=0$。从 $0$ 到 $0$ 不用跳。
  • 如果没有满足条件的 $i$,那么 $\textit{dfs}(j) = -\infty$。

递归入口:$\textit{dfs}(n-1)$,这是原问题,也是答案。

三、递归搜索 + 保存递归返回值 = 记忆化搜索

考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 $\textit{memo}$ 数组中。
  • 如果一个状态不是第一次遇到($\textit{memo}$ 中保存的结果不等于 $\textit{memo}$ 的初始值),那么可以直接返回 $\textit{memo}$ 中保存的结果。

Python 用户可以无视上面这段,直接用 @cache 装饰器。

关于记忆化搜索的原理,请看视频讲解 动态规划入门:从记忆化搜索到递推【基础算法精讲 17】,其中包含把记忆化搜索 1:1 翻译成递推的技巧。

class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        @cache  # 缓存装饰器,避免重复计算 dfs(一行代码实现记忆化)
        def dfs(j: int) -> int:
            if j == 0:  # 起点
                return 0
            res = -inf
            for i in range(j):
                if abs(nums[i] - nums[j]) <= target:  # 可以从 i 跳到 j
                    res = max(res, dfs(i) + 1)
            return res

        ans = dfs(len(nums) - 1)  # 终点
        return -1 if ans < 0 else ans
class Solution {
    public int maximumJumps(int[] nums, int target) {
        int n = nums.length;
        int[] memo = new int[n];
        int ans = dfs(n - 1, nums, target, memo);
        return ans < 0 ? -1 : ans;
    }

    private int dfs(int j, int[] nums, int target, int[] memo) {
        if (j == 0) { // 起点
            return 0;
        }

        if (memo[j] != 0) { // 之前计算过
            return memo[j];
        }

        int res = Integer.MIN_VALUE;
        for (int i = 0; i < j; i++) {
            if (Math.abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                res = Math.max(res, dfs(i, nums, target, memo) + 1);
            }
        }
        memo[j] = res; // 记忆化
        return res;
    }
}
class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> memo(n);

        auto dfs = [&](this auto&& dfs, int j) -> int {
            if (j == 0) { // 起点
                return 0;
            }

            int& res = memo[j]; // 注意这里是引用
            if (res) { // 之前计算过
                return res;
            }

            res = INT_MIN;
            for (int i = 0; i < j; i++) {
                if (abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                    res = max(res, dfs(i) + 1);
                }
            }
            return res;
        };

        int ans = dfs(n - 1); // 终点
        return ans < 0 ? -1 : ans;
    }
};
func maximumJumps(nums []int, target int) int {
n := len(nums)
memo := make([]int, n)

var dfs func(int) int
dfs = func(j int) int {
if j == 0 { // 起点
return 0
}

p := &memo[j]
if *p != 0 { // 之前计算过
return *p
}

res := math.MinInt
for i, x := range nums[:j] {
if abs(x-nums[j]) <= target { // 可以从 i 跳到 j
res = max(res, dfs(i)+1)
}
}
*p = res // 记忆化
return res
}

ans := dfs(n - 1) // 终点
if ans < 0 {
return -1
}
return ans
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 是 $\textit{nums}$ 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(n)$,单个状态的计算时间为 $\mathcal{O}(n)$,所以总的时间复杂度为 $\mathcal{O}(n^2)$。
  • 空间复杂度:$\mathcal{O}(n)$。保存多少状态,就需要多少空间。

四、1:1 翻译成递推

我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体来说,$f[j]$ 的定义和 $\textit{dfs}(j)$ 的定义是完全一样的,都表示从下标 $0$ 到达下标 $j$ 所需的最大跳跃次数。

相应的递推式(状态转移方程)也和 $\textit{dfs}$ 一样:

$$
f[j] = \max_{i} f[i] + 1
$$

其中 $0\le i<j$ 且 $|\textit{nums}[i]-\textit{nums}[j]|\le \textit{target}$。

如果没有满足条件的 $i$,那么 $f[j] = -\infty$。

初始值 $f[0]=0$,翻译自递归边界 $\textit{dfs}(0)=0$。

答案为 $f[n-1]$,翻译自递归入口 $\textit{dfs}(n-1)$。

class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        n = len(nums)
        f = [-inf] * n
        f[0] = 0
        for j in range(1, n):
            for i in range(j):
                if abs(nums[i] - nums[j]) <= target:  # 可以从 i 跳到 j
                    f[j] = max(f[j], f[i] + 1)
        return -1 if f[-1] < 0 else f[-1]
class Solution {
    public int maximumJumps(int[] nums, int target) {
        int n = nums.length;
        int[] f = new int[n];
        for (int j = 1; j < n; j++) {
            f[j] = Integer.MIN_VALUE;
            for (int i = 0; i < j; i++) {
                if (Math.abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                    f[j] = Math.max(f[j], f[i] + 1);
                }
            }
        }
        return f[n - 1] < 0 ? -1 : f[n - 1];
    }
}
class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        int n = nums.size();
        vector<int> f(n, INT_MIN);
        f[0] = 0;
        for (int j = 1; j < n; j++) {
            for (int i = 0; i < j; i++) {
                if (abs(nums[i] - nums[j]) <= target) { // 可以从 i 跳到 j
                    f[j] = max(f[j], f[i] + 1);
                }
            }
        }
        return f[n - 1] < 0 ? -1 : f[n - 1];
    }
};
func maximumJumps(nums []int, target int) int {
n := len(nums)
f := make([]int, n)

for j := 1; j < n; j++ {
f[j] = math.MinInt
for i, x := range nums[:j] {
if abs(x-nums[j]) <= target { // 可以从 i 跳到 j
f[j] = max(f[j], f[i]+1)
}
}
}

if f[n-1] < 0 {
return -1
}
return f[n-1]
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

五、值域线段树优化 DP

如果 $n=10^5$,上面的做法就超时了。

遍历到 $\textit{nums}[j]$ 时,我们需要知道满足 $\textit{nums}[j]-\textit{target} \le \textit{nums}[i] \le \textit{nums}[j]+\textit{target}$ 的最大的 $f[i]$。

这可以用一棵值域线段树维护。线段树的区间是值域区间,例如区间 $[20,23]$ 指的是 $\textit{nums}$ 中的元素 $20,21,22,23$。线段树的每个节点保存的是值域区间对应的最大的 $f[i]$。例如 $\textit{nums}[4]=20$ 且 $f[4] = 3$,那么线段树维护的位置 $20$ 更新为 $3$。

如此一来,满足 $\textit{nums}[j]-\textit{target} \le \textit{nums}[i] \le \textit{nums}[j]+\textit{target}$ 的最大的 $f[i]$,可以通过线段树的区间最值查询得到。

# 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
class SegmentTree:
    def __init__(self, n: int) -> None:
        self.t = [-inf] * (2 << (n - 1).bit_length())

    def update(self, node: int, l: int, r: int, i: int, val: int) -> None:
        if l == r:  # 叶子
            self.t[node] = val
            return
        m = (l + r) // 2
        if i <= m:  # i 在左子树
            self.update(node * 2, l, m, i, val)
        else:  # i 在右子树
            self.update(node * 2 + 1, m + 1, r, i, val)
        self.t[node] = max(self.t[node * 2], self.t[node * 2 + 1])

    def query(self, node: int, l: int, r: int, ql: int, qr: int) -> int:
        if ql <= l and r <= qr:  # 当前子树完全在 [ql, qr] 内
            return self.t[node]
        m = (l + r) // 2
        if qr <= m:  # [ql, qr] 在左子树
            return self.query(node * 2, l, m, ql, qr)
        if ql > m:  # [ql, qr] 在右子树
            return self.query(node * 2 + 1, m + 1, r, ql, qr)
        return max(self.query(node * 2, l, m, ql, qr), self.query(node * 2 + 1, m + 1, r, ql, qr))


class Solution:
    def maximumJumps(self, nums: List[int], target: int) -> int:
        # 去重排序,便于离散化
        sorted_nums = sorted(set(nums))

        n = len(nums)
        m = len(sorted_nums)
        t = SegmentTree(m)  # 值域线段树

        # nums[0] 对应的 f[0] = 0
        t.update(1, 0, m - 1, bisect_left(sorted_nums, nums[0]), 0)

        for j in range(1, n):
            l = bisect_left(sorted_nums, nums[j] - target)       # >= nums[j]-target 的第一个数
            r = bisect_right(sorted_nums, nums[j] + target) - 1  # <= nums[j]+target 的最后一个数
            # t.query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
            fj = t.query(1, 0, m - 1, l, r) + 1
            t.update(1, 0, m - 1, bisect_left(sorted_nums, nums[j]), fj)

        return -1 if fj < 0 else fj
class Solution {
    // 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
    private int[] tree;

    private void update(int node, int l, int r, int i, int val) {
        if (l == r) { // 叶子
            tree[node] = val;
            return;
        }
        int m = (l + r) / 2;
        if (i <= m) { // i 在左子树
            update(node * 2, l, m, i, val);
        } else { // i 在右子树
            update(node * 2 + 1, m + 1, r, i, val);
        }
        tree[node] = Math.max(tree[node * 2], tree[node * 2 + 1]);
    }

    private int query(int node, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
            return tree[node];
        }
        int m = (l + r) / 2;
        if (qr <= m) { // [ql, qr] 在左子树
            return query(node * 2, l, m, ql, qr);
        }
        if (ql > m) { // [ql, qr] 在右子树
            return query(node * 2 + 1, m + 1, r, ql, qr);
        }
        return Math.max(query(node * 2, l, m, ql, qr), query(node * 2 + 1, m + 1, r, ql, qr));
    }

    public int maximumJumps(int[] nums, int target) {
        int n = nums.length;
        int[] sorted = nums.clone(); // 用于离散化
        Arrays.sort(sorted);

        tree = new int[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
        Arrays.fill(tree, Integer.MIN_VALUE);

        // nums[0] 对应的 f[0] = 0
        update(1, 0, n - 1, lowerBound(sorted, nums[0]), 0);

        for (int j = 1; ; j++) {
            int l = lowerBound(sorted, (long) nums[j] - target);         // >= nums[j]-target 的第一个数
            int r = lowerBound(sorted, (long) nums[j] + target + 1) - 1; // <= nums[j]+target 的最后一个数
            // query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
            int fj = query(1, 0, n - 1, l, r) + 1;
            if (j == n - 1) {
                return fj < 0 ? -1 : fj;
            }
            update(1, 0, n - 1, lowerBound(sorted, nums[j]), fj);
        }
    }

    // 见 https://www.bilibili.com/video/BV1AP41137w7/
    private int lowerBound(int[] nums, long target) {
        int left = -1;
        int right = nums.length;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            if (nums[mid] >= target) {
                right = mid;
            } else {
                left = mid;
            }
        }
        return right;
    }
}
// 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
class SegmentTree {
    vector<int> tree;

public:
    SegmentTree(int n) : tree(2 << bit_width(n - 1u), INT_MIN) {}

    void update(int node, int l, int r, int i, int val) {
        if (l == r) { // 叶子
            tree[node] = val;
            return;
        }
        int m = (l + r) / 2;
        if (i <= m) { // i 在左子树
            update(node * 2, l, m, i, val);
        } else { // i 在右子树
            update(node * 2 + 1, m + 1, r, i, val);
        }
        tree[node] = max(tree[node * 2], tree[node * 2 + 1]);
    }

    int query(int node, int l, int r, int ql, int qr) const {
        if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
            return tree[node];
        }
        int m = (l + r) / 2;
        if (qr <= m) { // [ql, qr] 在左子树
            return query(node * 2, l, m, ql, qr);
        }
        if (ql > m) { // [ql, qr] 在右子树
            return query(node * 2 + 1, m + 1, r, ql, qr);
        }
        return max(query(node * 2, l, m, ql, qr), query(node * 2 + 1, m + 1, r, ql, qr));
    }
};

class Solution {
public:
    int maximumJumps(vector<int>& nums, int target) {
        // 排序去重,便于离散化
        auto sorted = nums;
        ranges::sort(sorted);
        sorted.erase(ranges::unique(sorted).begin(), sorted.end());

        int n = nums.size();
        int m = sorted.size();

        SegmentTree t(m); // 值域线段树

        // nums[0] 对应的 f[0] = 0
        int pos = ranges::lower_bound(sorted, nums[0]) - sorted.begin();
        t.update(1, 0, m - 1, pos, 0);

        long long tar = target;
        for (int j = 1; ; j++) {
            int l = ranges::lower_bound(sorted, nums[j] - tar) - sorted.begin();     // >= nums[j]-target 的第一个数
            int r = ranges::upper_bound(sorted, nums[j] + tar) - sorted.begin() - 1; // <= nums[j]+target 的最后一个数
            // t.query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
            int fj = t.query(1, 0, m - 1, l, r) + 1;
            if (j == n - 1) {
                return fj < 0 ? -1 : fj;
            }
            pos = ranges::lower_bound(sorted, nums[j]) - sorted.begin();
            t.update(1, 0, m - 1, pos, fj);
        }
    }
};
// 完整的线段树模板见 https://leetcode.cn/circle/discuss/mOr1u6/
type seg []int

func (t seg) update(node, l, r, i, val int) {
if l == r { // 叶子
t[node] = val
return
}
m := (l + r) / 2
if i <= m { // i 在左子树
t.update(node*2, l, m, i, val)
} else { // i 在右子树
t.update(node*2+1, m+1, r, i, val)
}
t[node] = max(t[node*2], t[node*2+1])
}

func (t seg) query(node, l, r, ql, qr int) int {
if ql <= l && r <= qr { // 当前子树完全在 [ql, qr] 内
return t[node]
}
m := (l + r) / 2
if qr <= m { // [ql, qr] 在左子树
return t.query(node*2, l, m, ql, qr)
}
if ql > m { // [ql, qr] 在右子树
return t.query(node*2+1, m+1, r, ql, qr)
}
return max(t.query(node*2, l, m, ql, qr), t.query(node*2+1, m+1, r, ql, qr))
}

func maximumJumps(nums []int, target int) int {
// 排序去重,便于离散化
sorted := slices.Clone(nums)
slices.Sort(sorted)
sorted = slices.Compact(sorted)

n := len(nums)
m := len(sorted)

t := make(seg, 2<<bits.Len(uint(m-1))) // 值域线段树
for i := range t {
t[i] = math.MinInt
}

// nums[0] 对应的 f[0] = 0
t.update(1, 0, m-1, sort.SearchInts(sorted, nums[0]), 0)

for j := 1; ; j++ {
l := sort.SearchInts(sorted, nums[j]-target)       // >= nums[j]-target 的第一个数
r := sort.SearchInts(sorted, nums[j]+target+1) - 1 // <= nums[j]+target 的最后一个数
// t.query 返回满足 nums[j]-target <= nums[i] <= nums[j]+target 的最大的 f[i]
fj := t.query(1, 0, m-1, l, r) + 1
if j == n-1 {
if fj < 0 {
return -1
}
return fj
}
t.update(1, 0, m-1, sort.SearchInts(sorted, nums[j]), fj)
}
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

相似题目

2407. 最长递增子序列 II

更多相似题目,见下面动态规划题单的「§11.4 树状数组/线段树优化 DP」和「专题:跳跃游戏」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

利用方向向量简化代码,O(1) 空间优化(Python/Java/C++/Go)

作者 endlesscheng
2021年6月27日 12:08

本题是模拟题,由于每一圈互相独立,可以分别处理。

对于每一圈:

  1. 从左上角开始,顺时针遍历这一圈,把遍历到的元素记录到一个数组 $a$ 中。
  2. 把 $a$ 循环左移 $k\bmod N$ 次,其中 $N$ 是数组 $a$ 的长度。这是因为,循环左移 $N$ 次等同于没有左移,循环左移 $N+1$ 次等同于循环左移 $1$ 次,依此类推,循环左移 $k$ 次等同于循环左移 $k\bmod N$ 次。
  3. 把 $a$ 中元素按照步骤 1 中的顺序,重新填回 $\textit{grid}$。

对于步骤 1,可以仿照 54. 螺旋矩阵方法二,使用一个方向向量数组分别表示顺时针的右、下、左、上。对于最外圈,分别移动 $n-1,m-1,n-1,m-1$ 次;对于次外圈,分别移动 $n-3,m-3,n-3,m-3$ 次;依此类推。

DIRS = (0, 1), (1, 0), (0, -1), (-1, 0)  # 右下左上

class Solution:
    def rotateGrid(self, grid: List[List[int]], k: int) -> List[List[int]]:
        m0, n0 = len(grid), len(grid[0])

        # 从外到内枚举圈
        for i in range(min(m0, n0) // 2):
            m, n = m0 - i * 2, n0 - i * 2  # 这一圈的行数和列数
            x, y = i, i  # 这一圈的左上角
            a = []
            for dx, dy in DIRS:
                for _ in range(n - 1):
                    a.append(grid[x][y])
                    x += dx
                    y += dy
                m, n = n, m  # 见 54. 螺旋矩阵 我的题解

            shift = k % len(a)
            a = a[shift:] + a[:shift]

            # 注意此时 (x, y) 又回到了左上角
            j = 0
            for dx, dy in DIRS:
                for _ in range(n - 1):
                    grid[x][y] = a[j]
                    j += 1
                    x += dx
                    y += dy
                m, n = n, m

        return grid
class Solution {
    private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右下左上

    public int[][] rotateGrid(int[][] grid, int k) {
        int m0 = grid.length, n0 = grid[0].length;
        List<Integer> a = new ArrayList<>((m0 + n0 - 2) * 2); // 预分配空间

        // 从外到内枚举圈
        for (int i = 0; i < Math.min(m0, n0) / 2; i++) {
            int m = m0 - i * 2, n = n0 - i * 2; // 这一圈的行数和列数
            int x = i, y = i; // 这一圈的左上角
            a.clear();
            for (int[] dir : DIRS) {
                for (int t = 0; t < n - 1; t++) {
                    a.add(grid[x][y]);
                    x += dir[0];
                    y += dir[1];
                }
                int tmp = m; // 见 54. 螺旋矩阵 我的题解
                m = n;
                n = tmp; 
            }

            int shift = k % a.size();
            Collections.rotate(a, -shift);

            // 注意此时 (x, y) 又回到了左上角
            int j = 0;
            for (int[] dir : DIRS) {
                for (int t = 0; t < n - 1; t++) {
                    grid[x][y] = a.get(j++);
                    x += dir[0];
                    y += dir[1];
                }
                int temp = m;
                m = n;
                n = temp;
            }
        }

        return grid;
    }
}
class Solution {
    static constexpr int DIRS[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右下左上

public:
    vector<vector<int>> rotateGrid(vector<vector<int>>& grid, int k) {
        int m0 = grid.size(), n0 = grid[0].size();
        vector<int> a;
        a.reserve((m0 + n0 - 2) * 2); // 预分配空间

        // 从外到内枚举圈
        for (int i = 0; i < min(m0, n0) / 2; i++) {
            int m = m0 - i * 2, n = n0 - i * 2; // 这一圈的行数和列数
            int x = i, y = i; // 这一圈的左上角
            a.resize(0);
            for (auto& [dx, dy] : DIRS) {
                for (int t = 0; t < n - 1; t++) {
                    a.push_back(grid[x][y]);
                    x += dx;
                    y += dy;
                }
                swap(m, n); // 见 54. 螺旋矩阵 我的题解
            }

            int shift = k % a.size();
            ranges::rotate(a, a.begin() + shift);

            // 注意此时 (x, y) 又回到了左上角
            int j = 0;
            for (auto& [dx, dy] : DIRS) {
                for (int t = 0; t < n - 1; t++) {
                    grid[x][y] = a[j++];
                    x += dx;
                    y += dy;
                }
                swap(m, n);
            }
        }

        return grid;
    }
};
var dirs = [4][2]int{{0, 1}, {1, 0}, {0, -1}, {-1, 0}} // 右下左上

func rotateGrid(grid [][]int, k int) [][]int {
m0, n0 := len(grid), len(grid[0])
a := make([]int, 0, (m0+n0-2)*2) // 预分配空间

// 从外到内枚举圈
for i := range min(m0, n0) / 2 {
m, n := m0-i*2, n0-i*2 // 这一圈的行数和列数
x, y := i, i // 这一圈的左上角
a = a[:0]
for _, dir := range dirs {
for range n - 1 {
a = append(a, grid[x][y])
x += dir[0]
y += dir[1]
}
m, n = n, m // 见 54. 螺旋矩阵 我的题解
}

shift := k % len(a)
a = append(a[shift:], a[:shift]...)

// 注意此时 (x, y) 又回到了左上角
j := 0
for _, dir := range dirs {
for range n - 1 {
grid[x][y] = a[j]
j++
x += dir[0]
y += dir[1]
}
m, n = n, m
}
}

return grid
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。
  • 空间复杂度:$\mathcal{O}(m+n)$。

空间优化

利用 189. 轮转数组 的技巧,可以做到 $\mathcal{O}(1)$ 空间。详见 我的题解

class Solution:
    def rotateGrid(self, grid: List[List[int]], k: int) -> List[List[int]]:
        m0, n0 = len(grid), len(grid[0])

        # 从外到内枚举圈
        for i in range(min(m0, n0) // 2):
            m, n = m0 - i * 2 - 1, n0 - i * 2 - 1  # 注意这里减一了

            # 返回这一圈顺时针下标 p 对应 grid 的位置 (x, y)
            def index(p: int) -> Tuple[int, int]:
                # 左上角在 (i, i)
                if p < n:
                    return i, i + p
                if p < n + m:
                    return i + p - n, i + n
                if p < n * 2 + m:
                    return i + m, i - p + n * 2 + m
                return i - p + (n + m) * 2, i

            def reverse(l: int, r: int) -> None:
                while l < r:
                    x1, y1 = index(l)
                    x2, y2 = index(r)
                    grid[x1][y1], grid[x2][y2] = grid[x2][y2], grid[x1][y1]
                    l += 1
                    r -= 1

            # 189. 轮转数组(改成向左轮转)
            size = (m + n) * 2
            shift = k % size
            reverse(0, shift - 1)
            reverse(shift, size - 1)
            reverse(0, size - 1)

        return grid
class Solution {
    public int[][] rotateGrid(int[][] grid, int k) {
        int m0 = grid.length, n0 = grid[0].length;

        // 从外到内枚举圈
        for (int i = 0; i < Math.min(m0, n0) / 2; i++) {
            int m = m0 - i * 2 - 1, n = n0 - i * 2 - 1; // 注意这里减一了

            // 189. 轮转数组(改成向左轮转)
            int size = (m + n) * 2;
            int shift = k % size;
            reverse(grid, i, m, n, 0, shift - 1);
            reverse(grid, i, m, n, shift, size - 1);
            reverse(grid, i, m, n, 0, size - 1);
        }

        return grid;
    }

    private void reverse(int[][] grid, int i, int m, int n, int l, int r) {
        while (l < r) {
            int[] p1 = index(i, m, n, l);
            int[] p2 = index(i, m, n, r);
            int x1 = p1[0], y1 = p1[1];
            int x2 = p2[0], y2 = p2[1];

            int tmp = grid[x1][y1];
            grid[x1][y1] = grid[x2][y2];
            grid[x2][y2] = tmp;

            l++;
            r--;
        }
    }

    private int[] index(int i, int m, int n, int p) {
        // 左上角在 (i, i)
        if (p < n) {
            return new int[]{i, i + p};
        }
        if (p < n + m) {
            return new int[]{i + p - n, i + n};
        }
        if (p < n * 2 + m) {
            return new int[]{i + m, i - p + n * 2 + m};
        }
        return new int[]{i - p + (n + m) * 2, i};
    }
}
class Solution {
public:
    vector<vector<int>> rotateGrid(vector<vector<int>>& grid, int k) {
        int m0 = grid.size(), n0 = grid[0].size();

        // 从外到内枚举圈
        for (int i = 0; i < min(m0, n0) / 2; i++) {
            int m = m0 - i * 2 - 1, n = n0 - i * 2 - 1; // 注意这里减一了

            // 返回这一圈顺时针下标 p 对应 grid 的位置 (x, y)
            auto index = [&](int p) -> pair<int, int> {
                // 左上角在 (i, i)
                if (p < n) {
                    return {i, i + p};
                }
                if (p < n + m) {
                    return {i + p - n, i + n};
                }
                if (p < n * 2 + m) {
                    return {i + m, i - p + n * 2 + m};
                }
                return {i - p + (n + m) * 2, i};
            };

            auto reverse = [&](int l, int r) {
                while (l < r) {
                    auto [x1, y1] = index(l);
                    auto [x2, y2] = index(r);
                    swap(grid[x1][y1], grid[x2][y2]);
                    l++;
                    r--;
                }
            };

            // 189. 轮转数组(改成向左轮转)
            int size = (m + n) * 2;
            int shift = k % size;
            reverse(0, shift - 1);
            reverse(shift, size - 1);
            reverse(0, size - 1);
        }

        return grid;
    }
};
func rotateGrid(grid [][]int, k int) [][]int {
m0, n0 := len(grid), len(grid[0])

// 从外到内枚举圈
for i := range min(m0, n0) / 2 {
m, n := m0-i*2-1, n0-i*2-1 // 注意这里减一了

// 返回这一圈顺时针下标 p 对应 grid 的位置 (x, y)
index := func(p int) (x, y int) {
// 左上角在 (i, i)
if p < n {
return i, i + p
}
if p < n+m {
return i + p - n, i + n
}
if p < n*2+m {
return i + m, i - p + n*2 + m
}
return i - p + (n+m)*2, i
}

reverse := func(l, r int) {
for l < r {
x1, y1 := index(l)
x2, y2 := index(r)
grid[x1][y1], grid[x2][y2] = grid[x2][y2], grid[x1][y1]
l++
r--
}
}

// 189. 轮转数组(改成向左轮转)
size := (m + n) * 2
shift := k % size
reverse(0, shift-1)
reverse(shift, size-1)
reverse(0, size-1)
}

return grid
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-循环轮转矩阵🟡

2026年5月9日 00:00

给你一个大小为 m x n 的整数矩阵 grid ,其中 mn 都是 偶数 ;另给你一个整数 k

矩阵由若干层组成,如下图所示,每种颜色代表一层:

矩阵的循环轮转是通过分别循环轮转矩阵中的每一层完成的。在对某一层进行一次循环旋转操作时,层中的每一个元素将会取代其 逆时针 方向的相邻元素。轮转示例如下:

返回执行 k 次循环轮转操作后的矩阵。

 

示例 1:

输入:grid = [[40,10],[30,20]], k = 1
输出:[[10,20],[40,30]]
解释:上图展示了矩阵在执行循环轮转操作时每一步的状态。

示例 2:

输入:grid = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]], k = 2
输出:[[3,4,8,12],[2,11,10,16],[1,7,6,15],[5,9,13,14]]
解释:上图展示了矩阵在执行循环轮转操作时每一步的状态。

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 2 <= m, n <= 50
  • mn 都是 偶数
  • 1 <= grid[i][j] <=5000
  • 1 <= k <= 109
❌
❌