以 [1, -1, 2, 3, -9] 为例,测试每个起点:
-
起点 index=0:
[1, -1, 2, 3, -9]- 前缀和: 1 → 0 → 2 → 5 → -4 ❌ (最后-4 < 0)
-
起点 index=1:
[-1, 2, 3, -9, 1]- 前缀和: -1 ❌ (立即失败)
-
起点 index=2:
[2, 3, -9, 1, -1]- 前缀和: 2 → 5 → -4 ❌
-
起点 index=3:
[3, -9, 1, -1, 2]- 前缀和: 3 → -6 ❌
-
起点 index=4:
[-9, 1, -1, 2, 3]- 前缀和: -9 ❌
所有起点都失败,返回 false。
相反, [1, -1, 2, 3] 是 OK 的。
算法思路
这是一个经典的循环数组前缀和问题。可以用以下方法解决:
方法1:暴力法 (O(n²))
检查每个起点,计算循环前缀和。
方法2:优化法 (O(n))
核心观察:如果整个数组的总和 < 0,一定返回 false,因为无论如何循环,遍历完整个数组后的总和会是负数。所以更高效的算法思路:
- 将原数组复制一遍接在后面,形成
2n的数组 - 问题转化为:在
2n数组中是否存在长度为n的滑动窗口,使得窗口内的所有前缀和 ≥ 0 - 可以用前缀和和单调队列技巧
实现
实现1:暴力解法(清晰但低效)
public class Solution {
public boolean hasValidStart(int[] nums) {
int n = nums.length;
// 检查每个起点
for (int start = 0; start < n; start++) {
int sum = 0;
boolean valid = true;
// 从start开始遍历n个元素(循环)
for (int i = 0; i < n; i++) {
int idx = (start + i) % n;
sum += nums[idx];
if (sum < 0) {
valid = false;
break;
}
}
if (valid) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] arr = {1, -1, 2, 3, -9};
System.out.println(sol.hasValidStart(arr)); // false
int[] arr2 = {2, -1, 2};
System.out.println(sol.hasValidStart(arr2)); // true? 验证一下
// 起点0: [2, -1, 2] -> 2, 1, 3 ✓
}
}
实现2:优化解法 O(n)
实际上,这个问题等价于:是否存在一个起点,使得循环数组nums 的所有前缀和 ≥ 0。
对于每个可能的起点 start,我们需要检查对于所有 k = 0, 1, ..., n-1,是否
sum(nums[start], nums[start+1], ..., nums[(start+k) % n]) ≥ 0
前缀和
定义扩展数组的前缀和数组 prefix:
prefix[0] = 0
prefix[1] = nums[0]
prefix[2] = nums[0] + nums[1]
...
prefix[i] = nums[0] + nums[1] + ... + nums[i-1]
对于起点 start,第 k 个前缀和是:
nums[start] + ... + nums[(start+k) % n] = prefix[(start+k+1) % n] - prefix[start] (需要处理循环)
问题转化
是否存在一个 start (0 ≤ start < n),使得在区间 [start+1, start+n] 中,所有的 prefix[j] 都 ≥ prefix[start]?
换句话说,是否存在一个 start,使得 prefix[start] 是区间 [start+1, start+n] 中所有 prefix 值的最小值?
import java.util.*;
public class CircularArrayPrefixSum {
/**
* 使用单调队列检查是否存在起点,使得所有循环前缀和非负
* 时间复杂度: O(n)
* 空间复杂度: O(n)
*/
public static boolean hasValidStart(int[] nums) {
int n = nums.length;
// 1. 构建扩展数组
int[] extended = new int[2 * n];
for (int i = 0; i < 2 * n; i++) {
extended[i] = nums[i % n];
}
// 2. 计算前缀和
int[] prefix = new int[2 * n + 1];
for (int i = 0; i < 2 * n; i++) {
prefix[i + 1] = prefix[i] + extended[i];
}
// 3. 单调递增队列
Deque<Integer> deque = new ArrayDeque<>();
// 4. 遍历所有位置
for (int i = 0; i <= 2 * n; i++) {
// 移除超出窗口的元素(窗口大小为n)
while (!deque.isEmpty() && deque.peekFirst() < i - n) {
deque.pollFirst();
}
// 检查是否找到解
if (i >= n) {
int start = i - n;
int minPrefix = prefix[deque.peekFirst()];
// 如果窗口 [start+1, start+n] 的最小值 ≥ prefix[start]
if (minPrefix >= prefix[start]) {
return true;
}
}
// 维护单调递增队列
while (!deque.isEmpty() && prefix[deque.peekLast()] >= prefix[i]) {
deque.pollLast();
}
deque.offerLast(i);
}
return false;
}
/**
* 验证函数:暴力解法用于对比
*/
public static boolean bruteForce(int[] nums) {
int n = nums.length;
for (int start = 0; start < n; start++) {
int sum = 0;
boolean valid = true;
for (int i = 0; i < n; i++) {
int idx = (start + i) % n;
sum += nums[idx];
if (sum < 0) {
valid = false;
break;
}
}
if (valid) return true;
}
return false;
}
public static void main(String[] args) {
// 测试用例
int[][] testCases = {
{1, -1, 2, 3, -9}, // false
{2, -1, 2}, // true
{1, 2, 3}, // true
{-1, -2, -3}, // false
{3, -2, 2}, // true
{-1, -2, 3}, // true
{5, -3, 2, -1, 4}, // true
{-1, 2, -3, 4, -5}, // 需要计算
};
for (int[] testCase : testCases) {
boolean result1 = bruteForce(testCase);
boolean result2 = hasValidStart(testCase);
System.out.println("数组: " + Arrays.toString(testCase));
System.out.println("暴力解法: " + result1);
System.out.println("单调队列: " + result2);
System.out.println("结果一致: " + (result1 == result2));
System.out.println("---");
}
}
}

被折叠的 条评论
为什么被折叠?



