一个和子段和相关的问题

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=lrAi

最终,这个最大值(即 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 lr 成立条件下能够使得 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 学弟给出了一种看起来是线性的做法,只可惜我还没研究明白他的正确性,我直觉上认为其正确性仍然应该从决策单调性入手思考(可惜他不具有决策单调性,我已我怀疑这个线性做法是错的)。
  • 留坑待补 …
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值