回顾一下,前缀和数组 preSum 就是 nums 元素的累加和,preSum[i+1] - preSum[j] 其实就是子数组 nums[j..i] 之和(根据 preSum 数组的实现,索引 0 是占位符,所以 i 有一位索引偏移)。

那么反过来想,以 nums[i] 为结尾的最大子数组之和是多少?其实就是 preSum[i+1] - min(preSum[0..i])

所以,我们可以利用前缀和数组计算以每个元素结尾的子数组之和,进而得到和最大的子数组:

// 前缀和技巧解题
int maxSubArray(int[] nums) {
    int n = nums.length;
    int[] preSum = new int[n + 1];
    preSum[0] = 0;
    // 构造 nums 的前缀和数组
    for (int i = 1; i <= n; i++) {
        preSum[i] = preSum[i - 1] + nums[i - 1];
    }
    
    int res = Integer.MIN_VALUE;
    int minVal = Integer.MAX_VALUE;
    for (int i = 0; i < n; i++) {
        // 维护 minVal 是 preSum[0..i] 的最小值
        minVal = Math.min(minVal, preSum[i]);
        // 以 nums[i] 结尾的最大子数组和就是 preSum[i+1] - min(preSum[0..i])
        res = Math.max(res, preSum[i + 1] - minVal);
    }
    return res;
}

至此,前缀和解法也完成了。

简单总结下动态规划解法吧,虽然说状态转移方程确实有点玄学,但大部分还是有些规律可循的,跑不出那几个套路。像子数组、子序列这类问题,你就可以尝试定义 dp[i] 是以 nums[i] 为结尾的最大子数组和/最长递增子序列,因为这样定义更容易将 dp[i+1] 和 dp[i] 建立起联系,利用数学归纳法写出状态转移方程。