给定一个不包含0的整数数组,检查是否存在一个起始索引,使得从这个索引开始,沿着数组循环遍历(遇到结尾后回到开头继续),所有的连续子序列和都 ≥ 0?

[1, -1, 2, 3, -9] 为例,测试每个起点:

  1. 起点 index=0: [1, -1, 2, 3, -9]

    • 前缀和: 1 → 0 → 2 → 5 → -4 ❌ (最后-4 < 0)
  2. 起点 index=1: [-1, 2, 3, -9, 1]

    • 前缀和: -1 ❌ (立即失败)
  3. 起点 index=2: [2, 3, -9, 1, -1]

    • 前缀和: 2 → 5 → -4 ❌
  4. 起点 index=3: [3, -9, 1, -1, 2]

    • 前缀和: 3 → -6 ❌
  5. 起点 index=4: [-9, 1, -1, 2, 3]

    • 前缀和: -9 ❌

所有起点都失败,返回 false

相反, [1, -1, 2, 3] 是 OK 的。

算法思路

这是一个经典的循环数组前缀和问题。可以用以下方法解决:

方法1:暴力法 (O(n²))

检查每个起点,计算循环前缀和。

方法2:优化法 (O(n))

核心观察:如果整个数组的总和 < 0,一定返回 false,因为无论如何循环,遍历完整个数组后的总和会是负数。所以更高效的算法思路:

  1. 将原数组复制一遍接在后面,形成 2n 的数组
  2. 问题转化为:在 2n 数组中是否存在长度为 n 的滑动窗口,使得窗口内的所有前缀和 ≥ 0
  3. 可以用前缀和和单调队列技巧

实现

实现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("---");
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值