题目描述

✅ 769. 最多能完成排序的块

image-20260928224729477

image-20260928224729478

题意分析

数组恰好包含 0 到 n - 1,每个数出现一次。把它切成若干连续块,只允许在各块内部排序,块与块的先后顺序不变,要求拼接结果与整个数组排序后的结果一致,并让块数最多。

完全排好序后,下标 i 处的值就是 i。因此要判断能否在 i 后面切开,只需检查前 i + 1 个位置是否已经收齐最终属于这个前缀的全部值。

解法:前缀最大值

核心思路

[!blue]

用 max 维护 arr[0..i] 的最大值。当前前缀有 i + 1 个互不相同的非负整数;如果 max == i,所有值都来自只有 i + 1 个数的集合 {0, 1, ..., i},所以这个集合必然已经被完整收齐。前缀内部排序后就能放到正确位置,后面的值都大于 i,在这里切开不会妨碍全局有序。

反过来,如果这里能够切开,前缀中的数就无法再移动到后缀中,必须恰好是 0 到 i,最大值也就一定等于 i。所以 max == i 是合法切点的充要条件,而不只是一个足够条件。max < i 在本题中不可能出现,因为 i + 1 个不同非负数无法都塞进更小的值域。

所有合法切点还可以同时选取:两个相邻切点对应的前缀分别收齐两个连续值域,它们的差集正好就是中间那一块应该包含的值。把这一块单独排序,仍能占据正确的位置。因此每遇到一个合法切点就增加一块,既不会破坏其他切点,又取得了所有可能的切点,块数最多。

遍历时先把当前元素计入 max,再检查是否等于当前下标。max 始终表示整个前缀,不必在切块后清零。最后一个位置的前缀包含全部排列,最大值一定是 n - 1,会自然结算最后一块。

解题步骤

  • 维护包含当前值的前缀最大值。
  • 最大值等于当前下标时,块数加一。
  • 遍历结束直接返回,不额外加块。

数组只有一个元素时返回一块;原数组已经升序时,每个位置都可以成为一块。这个判定依赖“元素互异且值域恰好为 0 到 n - 1”,不能直接用于包含重复值或任意整数的数组。

代码实现

class Solution {
    public int maxChunksToSorted(int[] arr) {
        int max = 0;
        int chunks = 0;

        for (int i = 0; i < arr.length; i++) {
            max = Math.max(max, arr[i]);

            // 互异排列的前缀最大值等于下标,说明已占满对应值域
            if (max == i) {
                chunks++;
            }
        }

        return chunks;
    }
}
func maxChunksToSorted(arr []int) int {
    maxVal := 0
    chunks := 0

    for i, v := range arr {
        if v > maxVal {
            maxVal = v
        }
        // 互异排列的前缀最大值等于下标,说明已占满对应值域
        if maxVal == i {
            chunks++
        }
    }

    return chunks
}

复杂度分析

  • 时间复杂度:$O(n)$,一次扫描。
  • 空间复杂度:$O(1)$,前缀最大值与计数。

关键点总结

[!green]

  • 判定同时依赖值域与互异性,不能直接用于任意含重复数组。
  • 判断的是整个前缀最大值,不是当前值。

易错点总结

[!yellow]

  • 先判定再更新,会把当前更大值漏出前缀。
  • 最后再加一,会重复计算已经结算的末块。
  • 直接把最大值改成当前值,会遗忘前面尚未归位的大元素。

相似题目

题目 难度 关联与区别
768. 最多能完成排序的块 II 困难 去掉排列前提后,下标与数值不再直接对应,需要更一般的前后缀边界或多重集合判定。
915. 分割数组 中等 同样要求左段最大值不大于右段最小值,原题只找一个最早分割,本题找尽可能多的切点。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/92338141
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!