1. 最大子段和问题
给定一个长度为 n n n 的数列 { A 1 , A 2 , ⋯ , A n } \{A_1, A_2, \cdots, A_n\} {A1,A2,⋯,An} 找到一对正整数 ( l 0 , r 0 ) (l_0, r_0) (l0,r0) 使得 sum ( l 0 , r 0 ) \text{sum}(l_0, r_0) sum(l0,r0) 尽可能大,其中
sum ( l , r ) = ∑ i = l r A i \text{sum}(l, r)=\sum_{i=l}^{r}A_i sum(l,r)=i=l∑rAi
最终,这个最大值(即 sum ( l , 0 r 0 ) \text{sum}(l,_0 r_0) sum(l,0r0))为所求的答案。
例如:对于数列 { − 3 , 1 , − 3 , 1 , 1 , − 1 , 4 , 5 , 3 , − 2 } \{-3, 1, -3, 1, 1, -1, 4, 5, 3, -2\} {−3,1,−3,1,1,−1,4,5,3,−2} 而言, l 0 = 4 , r 0 = 9 l_0=4, r_0=9 l0=4,r0=9 时, sum ( l 0 , r 0 ) = 1 + 1 + ( − 1 ) + 4 + 5 + 3 = 13 \text{sum}(l_0, r_0)=1 + 1 + (−1) + 4 + 5 + 3=13 sum(l0,r0)=1+1+(−1)+4+5+3=13 取到最大值。
我们允许区间长度为 0 0 0,因此当数组 A A A 中所有元素均为负数时,最终答案为 0 0 0。
1.1 朴素的思路
对于每一个区间右端点 r r r,我们都可以试着去找到与它相对应的最优左端点。这里所说的最优左端点就是使得 l ≤ r l\leq r l≤r 成立条件下能够使得 sum ( l , r ) \text{sum}(l, r) sum(l,r) 取到最大值的那个 l l l (如果有多个 l l l 都能使得 sum ( l , r ) \text{sum}(l, r) sum(l,r) 取到最大值,则记 best_left ( r ) \text{best\_left}(r) best_left(r) 为其中最大的一个)。
不妨记 best_left(r) \text{best\_left(r)} best_left(r) 表示右端点 r r r 对应的最优左端点,那么我们不难说明最终的答案 ( l 0 , r 0 ) (l_0, r_0) (l0,r0) 一定出自下列有序对:
( best_left(1) , 1 ) , ( best_left(2) , 2 ) , ⋯ , ( best_left(n) , n ) (\text{best\_left(1)}, 1), (\text{best\_left(2)}, 2), \cdots, (\text{best\_left(n)}, n) (best_left(1),1),(best_left(2),2),⋯,(best_left(n),n)
对于某个 r r r,我们可以使用 O ( n ) O(n) O(n) 的时间复杂度枚举地找到 best_left(r) \text{best\_left(r)} best_left(r),这样我们可以通过 O ( n 2 ) O(n^2) O(n2) 的时间复杂度得到最终答案。
#include <algorithm>
#include <cstdio>
const int maxn = 10000 + 5;
int A[maxn];
int P[maxn]; /* 计算 A 数组的前缀和,用于快速计算区间和 */
/* 为某个右端点计算最佳左端点 */
int best_left(int r);
/* 根据算出的前缀和计算区间和 */
int sum(int l, int r);
int main() {
/* 输入数组的元素个数 */
int n;
scanf("%d", &n);
/* 输入数组 A 中的全部内容,同时计算前缀和数组 */
P[0] = 0;
for(int i = 1; i <= n; i += 1) {
scanf("%d", &A[i]);
P[i] = P[i - 1] + A[i]; /* 计算前缀和数组 P */
}
/* 算法思想:暴力枚举确定每个右端点的最优左端点 */
int ans = 0;
for(int r = 1; r <= n; r += 1) {
int l = best_left(r);
ans = std::max(ans, sum(l, r));
}
/* 输出答案 */
printf("%d\n", ans);
return 0;
}
/* 计算区间和 */
int sum(int l, int r) {
return P[r] - P[l - 1];
}
/* 寻找最优左端点 */
int best_left(int r) {
int best_sum = A[r];
int best_l = r;
/* 枚举所有可能的左端点,如果比当前的最优左端点优,就更新答案 */
for(int l = 1; l <= r; l += 1) {
int now_sum = sum(l, r);
if(now_sum >= best_sum) { /* 这里取等是为了保证多解时取到最大的 l */
best_sum = now_sum;
best_l = l;
}
}
return best_l;
}
1.2 对朴素思路的优化
即使是细心的读者页很难发现, best_left ( r ) \text{best\_left}(r) best_left(r) 函数关于 r r r 是单调不下降的,这里可以给出一个简单的证明,证明 best_left ( r ) ≤ best_left ( r + 1 ) \text{best\_left}(r) \leq \text{best\_left}(r+1) best_left(r)≤best_left(r+1)。
这里其实有一个比较显然的结论,就是 best_left ( r + 1 ) \text{best\_left}(r+1) best_left(r+1) 要么等于 best_left ( r ) \text{best\_left}(r) best_left(r) 要么等于 r + 1 r+1 r+1。考虑所有以 r + 1 r+1 r+1 为右端点的区间,这个区间要么包含 r r r 要么不包含 r r r。对于那些所有包含了 r r r 的区间而言 best_left ( r ) \text{best\_left}(r) best_left(r) 一定是仅考虑这些区间时的最优左端点。而不包含 r r r 的区间只有一个,那就是 ( r + 1 , r + 1 ) (r+1, r+1) (r+1,r+1)。
这样可以将时间复杂度优化到 O ( n ) O(n) O(n)。
#include <algorithm>
#include <cstdio>
const int maxn = 1000000 + 7;
int A[maxn];
int P[maxn]; /* 计算 A 数组的前缀和,用于快速计算区间和 */
/* 根据算出的前缀和计算区间和 */
int sum(int l, int r);
int main() {
/* 输入数组的元素个数 */
int n;
scanf("%d", &n);
/* 输入数组 A 中的全部内容,同时计算前缀和数组 */
P[0] = 0;
for(int i = 1; i <= n; i += 1) {
scanf("%d", &A[i]);
P[i] = P[i - 1] + A[i]; /* 计算前缀和数组 P */
}
/* 算法思想:利用最优左端点的单调性优化时间复杂度 */
int best_l = 1;
int ans = A[1];
for(int r = 2; r <= n; r += 1) {
/* 利用 r-1 对应的 best_l 计算出 r 对应的 best_l */
if(A[r] >= sum(best_l, r)) {
best_l = r;
}
/* 更新最大子段和 */
ans = std::max(ans, sum(best_l, r));
}
/* 输出答案 */
printf("%d\n", std::max(ans, 0));
return 0;
}
/* 计算区间和 */
int sum(int l, int r) {
return P[r] - P[l - 1];
}
2. 寻找符合条件的最短子段
给定一个长度为 n n n 的数列 { A 1 , A 2 , ⋯ , A n } \{A_1, A_2, \cdots, A_n\} {A1,A2,⋯,An} 以及一个数 s s s,我们要找到一个 尽可能短的区间 使得该区间上 A A A 的区间和大于等于 s s s,最终答案为该区间的长度。如果这样的区间不存在,记答案为 0 0 0。
2023-07-06
隔壁魏教授给出了一种在单调队列上二分的做法,时间复杂度为 O ( n log n ) O(n\log n) O(nlogn),我觉得十分优雅。- 但是 SCQ 学弟给出了一种看起来是线性的做法,只可惜我还没研究明白他的正确性,我直觉上认为其正确性仍然应该从决策单调性入手思考(可惜他不具有决策单调性,我已我怀疑这个线性做法是错的)。
- 留坑待补 …