子数组和排序后的区间和 图解题解
这道题到底在问什么
- 输入
- nums=[1,2,3,4], left=1, right=5
- 输出
- 13
- 输入
- nums=[1,2,3,4], left=3, right=4
- 输出
- 6
先想最直接的笨办法
记住三步走:枚举出全部子数组和、升序排序、把下标 left 到 right 这一段加起来取模。下面每一帧都在走这三步。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 1508 子数组和排序后的区间和:n≤1000 数据小,直接枚举全部 n×(n+1)/2 个子数组和、升序排序,再取下标 left 到 right 这段求和取模,时间 O(n²log n)、空间 O(n²)。
排序后取第 left 到 right 个,要返回哪个和
给一个全是正整数的数组 nums,先把它所有非空连续子数组各自求和——长度 n 的数组一共有 n×(n+1)/2 个这样的和。把这些和从小到大排成一个新序列,返回新序列里下标 left 到 right(从 1 数起、含两端)这一段的总和,对 10⁹+7 取模。题面 nums=[1,2,3,4]、left=1、right=5 时答案是 13:排序后前 5 个数正好加到 13。
n 最多 1000,把全部子数组和排一遍贵不贵
n 最大只有 1000,子数组和总共 n×(n+1)/2 个,满打满算约 50 万个数。把它们全枚举出来是双重循环、约 O(n²);对这约 n² 个数排序是 O(n²log n),大概两千万次比较;再扫一段区间求和。三步里排序最重,合起来 O(n²log n),这个量级现代机器一秒内跑得完。所以不必绕开枚举去凑更巧的公式,照题意把全部子数组和摆出来、排好、取区间,就够通过。
枚举每个子数组和,真要每段从头加一遍吗
枚举时有个白费力气的地方:固定起点 i,子数组 [i..j] 的和其实就是 [i..j−1] 的和再加上 nums[j] 一个数。要是每延伸一格都从 i 重新累加到 j,同一段前缀会被反复加许多遍。用一个累加变量 s 顶着:起点 i 处 s 归零,内层 j 从 i 往右走,每步 s 加上 nums[j] 就是当前子数组的和,直接记下来。这样每个子数组和只多做一次加法,枚举这步稳定在 O(n²)。
排好序之后,取哪一段、下标怎么减
排好序只是普通升序,真正要小心的是取哪一段。题目的 left、right 从 1 数起,数组下标却从 0 起,要取的是下标 left−1 到 right−1 这一段,忘了减 1 整段区间就整体偏一位,取到的是错的几个数。取出这段后把它们加起来,就是排序前那些子数组和里、第 left 小到第 right 小的总和。
[1,2,3,4] 上 left=1、right=5 算出什么
第一个数当起点:累加变量从 0 起,依次加成 1、3、6、10(对应 [1]、[1,2]、[1,2,3]、[1,2,3,4])。换第二个数起头,归零后加成 2、5、9;从第三个起得 3、7;从第四个起得 4。收齐 10 个和:1、3、6、10、2、5、9、3、7、4。升序排好是 1、2、3、3、4、5、6、7、9、10,两个 3 各占一格。left=1、right=5 换成下标 0 到 4,取出 1、2、3、3、4,加起来 13,没超过取模上限,答案就是 13。
相等的和别去重,取模也别拖到最后
整套下来时间 O(n²log n)、空间 O(n²),大头是装下约 n² 个子数组和的那个数组。边界想清三种:子数组只剩单个元素时和就是它自己;left=right 时区间只取排序后那一个数;left=1、right=n×(n+1)/2 时区间盖住全部,答案是所有子数组和之和。还有两处松了就悄悄错:排序时把相等的和去掉一个,长度就少一格、整段跟着错位——本例两个 3 必须都留;取模若拖到最后一次做,区间数一多累计和会先超出整型上限,得边加边对 10⁹+7 取模。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住三步走:枚举出全部子数组和、升序排序、把下标 left 到 right 这一段加起来取模。下面每一帧都在走这三步。
- 4上面这排是 nums = [1, 2, 3, 4]。右边面板用来收集所有子数组的和,现在还是空的。我们要枚举的是连续子数组,做法是固定一个起点 i,从 i 往右逐格延伸。一共会得到 4 乘 5 除以 2,也就是 10 个子数组和。
- 5换一个新起点,i = 0。累加变量先归零,加上 nums[0] = 1,得到只含一个元素的子数组 [1],和是 1。把 1 记进面板。
- 6从起点 0 继续往右延伸到下标 1。新进来的数是 nums[1] = 2,直接接到上一段的和 1 上,1 加 2 等于 3。这就是子数组 [1, 2] 的和,记进面板。注意这里复用了上一段的结果,没有从头重算。
- 7从起点 0 继续往右延伸到下标 2。新进来的数是 nums[2] = 3,直接接到上一段的和 3 上,3 加 3 等于 6。这就是子数组 [1, 2, 3] 的和,记进面板。注意这里复用了上一段的结果,没有从头重算。
- 8从起点 0 继续往右延伸到下标 3。新进来的数是 nums[3] = 4,直接接到上一段的和 6 上,6 加 4 等于 10。这就是子数组 [1, 2, 3, 4] 的和,记进面板。注意这里复用了上一段的结果,没有从头重算。
- 9换一个新起点,i = 1。累加变量先归零,加上 nums[1] = 2,得到只含一个元素的子数组 [2],和是 2。把 2 记进面板。
- 10从起点 1 继续往右延伸到下标 2。新进来的数是 nums[2] = 3,直接接到上一段的和 2 上,2 加 3 等于 5。这就是子数组 [2, 3] 的和,记进面板。注意这里复用了上一段的结果,没有从头重算。
- 11从起点 1 继续往右延伸到下标 3。新进来的数是 nums[3] = 4,直接接到上一段的和 5 上,5 加 4 等于 9。这就是子数组 [2, 3, 4] 的和,记进面板。注意这里复用了上一段的结果,没有从头重算。
- 12换一个新起点,i = 2。累加变量先归零,加上 nums[2] = 3,得到只含一个元素的子数组 [3],和是 3。把 3 记进面板。
- 13从起点 2 继续往右延伸到下标 3。新进来的数是 nums[3] = 4,直接接到上一段的和 3 上,3 加 4 等于 7。这就是子数组 [3, 4] 的和,记进面板。注意这里复用了上一段的结果,没有从头重算。
- 14换一个新起点,i = 3。累加变量先归零,加上 nums[3] = 4,得到只含一个元素的子数组 [4],和是 4。把 4 记进面板。
- 15枚举结束。把面板里的 10 个子数组和按生成顺序铺到舞台上,是 1、3、6、10、2、5、9、3、7、4。从这一帧起,舞台上摆的就是这排子数组和,原来的 nums 退到幕后。下一步要把它们排序。
- 16把这 10 个数从小到大排好。原来的顺序 1、3、6、10、2、5、9、3、7、4,排完变成 1、2、3、3、4、5、6、7、9、10。这一步就是普通的升序排序,语言自带的排序函数即可。排好之后,我们才能按题目要求用下标去取区间。
- 17提醒一个容易忽略的点:子数组 [3] 和子数组 [1, 2] 的和都是 3,排序后这两个 3 各自占一格,不会去重。绿色高亮的就是这两个 3,它们落在下标 2 和下标 3。题目要的是带重复的完整序列,千万别当成集合去掉重复。
- 18现在做第三步,区间求和。题目的下标从 1 开始,left = 1、right = 5。换算到我们这从 0 开始的数组,就是下标 0 到下标 4,一共 5 个数。底色框住的就是要相加的这一段。这个减 1 的换算很关键,别搞错。
- 19把下标 0 上的数 1 加进来。之前累计是 0,加上 1 得到 1。绿色部分就是已经累加进去的范围,紫色是这一帧刚加入的格子。继续往右加下一格。
- 20把下标 1 上的数 2 加进来。之前累计是 1,加上 2 得到 3。绿色部分就是已经累加进去的范围,紫色是这一帧刚加入的格子。继续往右加下一格。
- 21把下标 2 上的数 3 加进来。之前累计是 3,加上 3 得到 6。绿色部分就是已经累加进去的范围,紫色是这一帧刚加入的格子。继续往右加下一格。
- 22把下标 3 上的数 3 加进来。之前累计是 6,加上 3 得到 9。绿色部分就是已经累加进去的范围,紫色是这一帧刚加入的格子。继续往右加下一格。
- 23把下标 4 上的数 4 加进来。之前累计是 9,加上 4 得到 13。绿色部分就是已经累加进去的范围,紫色是这一帧刚加入的格子。到这,区间里 5 个数全部加完。
- 24区间求和结束。排序后下标 1 到 5 是 1、2、3、3、4,加起来是 13。本例没有超过取模上限,所以对 10 的 9 次方加 7 取模后还是 13。这就是最终答案。回顾一下全流程:枚举全部子数组和、升序排序、再把指定区间加起来取模,三步走完。
⚠️ 容易写错的地方
✗ 错:把下标 left、right 当成从 0 开始直接取
✓ 对:题目下标从 1 开始,取数组时要减 1
区间对应数组下标是 left 减 1 到 right 减 1;不减 1 会整体偏移一位,取错区间
✗ 错:排序时把相等的子数组和去重了
✓ 对:保留所有重复值,新数组长度恒为 n×(n+1)/2
不同子数组可能和相等(如本例两个 3),题目要的是带重复的完整序列,去重会少算
✗ 错:累加到最后才取一次模,中途用 int 累计
✓ 对:每加一步就对 10^9 + 7 取模
区间内的数可能很多,累计和会超出 int 上限;边加边取模或用更大类型才不溢出
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def rangeSum(self, nums: List[int], n: int, left: int, right: int) -> int:
arr = []
for i in range(n):
s = 0
for j in range(i, n):
s += nums[j]
arr.append(s)
arr.sort()
mod = 10**9 + 7
return sum(arr[left - 1 : right]) % modC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int rangeSum(vector<int>& nums, int n, int left, int right) {
int arr[n * (n + 1) / 2];
for (int i = 0, k = 0; i < n; ++i) {
int s = 0;
for (int j = i; j < n; ++j) {
s += nums[j];
arr[k++] = s;
}
}
sort(arr, arr + n * (n + 1) / 2);
int ans = 0;
const int mod = 1e9 + 7;
for (int i = left - 1; i < right; ++i) {
ans = (ans + arr[i]) % mod;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int rangeSum(int[] nums, int n, int left, int right) {
int[] arr = new int[n * (n + 1) / 2];
for (int i = 0, k = 0; i < n; ++i) {
int s = 0;
for (int j = i; j < n; ++j) {
s += nums[j];
arr[k++] = s;
}
}
Arrays.sort(arr);
int ans = 0;
final int mod = (int) 1e9 + 7;
for (int i = left - 1; i < right; ++i) {
ans = (ans + arr[i]) % mod;
}
return ans;
}
}复杂度
时间
O(n² log n)
枚举全部子数组和是双重循环,共 n×(n+1)/2 个,约 O(n²);对这约 n² 个数排序是 O(n² log n);区间求和最多扫一遍 O(n²)。三步相加,排序占主导,总体 O(n² log n)
空间
O(n²)
主要开销是存放全部子数组和的数组,长度 n×(n+1)/2,峰值 O(n²)。排序本身的额外开销因语言实现而异:C++/Java 递归栈约 O(log n),Python 的 Timsort 最坏 O(n²),但都不会把总空间推到超过 O(n²)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子数组和排序后的区间和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么直接枚举加排序就够,不用更巧的写法?+
看数据范围:n 最大 1000,子数组和的个数 n×(n+1)/2 约 50 万个。枚举它们是 O(n²) 百万级,排序 O(n²log n) 约两千万次比较,现代机器一秒内轻松跑完。面试和竞赛里,先估一下最大规模落在什么量级,只要在能过的范围内,照题意直接模拟往往比硬想优化解更稳、也更不容易写错。这题就属于直接枚举排序足够通过的那类。
要是 n 再大很多,有没有不枚举全部的办法?+
有,可以不把所有子数组和真的列出来。借助前缀和加二分:二分一个阈值,数出有多少个子数组和不超过它、同时把这些和累计起来,就能求出前 k 小的子数组和之和;对 right 和 left−1 各求一次再相减,就是区间答案。这样把复杂度压到 O(n·log(总和)) 量级。也可以用小根堆逐个弹出第 k 小的和。本题规模小,直接枚举更好写,才没上这些。
为什么累加时要取模,排序和比较时反而不能取?+
排序靠的是子数组和之间真实的大小关系,而取模会把大数绕回小数、打乱这个关系——排序前先取模,选出来的区间就整个错了。到最后累加时,区间里是哪些数已经定死,边加边取模和全加完再取一次结果完全一样,又能挡住累计和冲破整型上限。所以取模只该落在最后这一步,早一步都不行。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 子数组和排序后的区间和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。