xxleyi / loop_invariants

记录以及整理「循环不变式」视角下的算法题解

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

leetcode 907. 子数组的最小值之和

xxleyi opened this issue · comments

题:

给定一个整数数组 A,找到 min(B) 的总和,其中 B 的范围为 A 的每个(连续)子数组。

由于答案可能很大,因此返回答案模 10^9 + 7。

 

示例:

输入:[3,1,2,4]
输出:17
解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
 

提示:

1 <= A <= 30000
1 <= A[i] <= 30000

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sum-of-subarray-minimums
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


解:此题暴力解法不难,难得是线性复杂度条件下解决问题。

// 暴力求解,临近超时边缘
var sumSubarrayMins = function(A) {
  const bigPrimeNumber = 10 ** 9 + 7
  // 「循环不变式」所需要的变量
  let stackTop = 0
  // 存储答案
  let ans = 0

  for (let i = 0; i < A.length; i++) {
    for (let j = i; j < A.length; j++) {
      // 子数组长度为 1 时,将子数组首元素直接入栈
      if (j === i) {
        stackTop = A[j]
      // 子数组长度大于 1 时,将子数组的最小值入栈
      } else {
        stackTop = Math.min(stackTop, A[j])
      }
      // 更新 ans
      ans += stackTop
    }
  }

  // 循环终止,返回答案
  return ans % bigPrimeNumber
};

下面这个线性复杂度解法是利用单调递增栈,总共需要维护 四个「循环不变式」 相关的变量。一般题目也就两三个,此题一共四个,而且思路巧妙。突破点是意识到对数组中的每一个元素进行计数,然后利用单调递增栈更新和维护计数,同时累计子数组最小元素之和

// 线性复杂度:单调递增栈
var sumSubarrayMins = function(A) {
  const bigPrimeNumber = 10 ** 9 + 7
  // 外层 for「循环不变式」相关的变量
  const stack = []
  let minSoFar = 0
  let subMinSoFar = 0

  for (let y of A) {
    // 内层 while「循环不变式」相关的变量,初始值为 1
    let countY = 1
    // 更新单调递增栈,保证「循环不变式」
    while (stack.length && stack[stack.length - 1][0] >= y) {
      // 弹出不再单调递增的部分
      const [x, c] = stack.pop()
      // 更新 countY: 弹出的 count 都应该归到当前元素 y 上
      countY += c
      // 更新 subMinSoFar: 将出栈部分剔除
      subMinSoFar -= x * c
    }
    // 内层 while 循环终止:将当前元素以及对应的 count 入栈
    stack.push([y, countY])
    // 更新 subMinSoFar: 计入入栈部分
    subMinSoFar += y * countY
    // 将当前稳定的 subMinSoFar 计入到总的 minSoFar
    minSoFar += subMinSoFar
  }

  // 外层循环终止:对大素数取模之后得到目标解
  return minSoFar % bigPrimeNumber
}

补充前驱递减数组(使用正向递增栈求解)和后继递增数组(使用反向递增栈求解)的方法

// Prev / Next Array
var sumSubarrayMins = function(A) {
  const bigPrimeNumber = 10 ** 9 + 7
  // 数组的左右边界
  const [lo, hi] = [-1, A.length]
  // 从左到右递增栈(记录索引)
  const stack = []
  const prev = Array(hi).fill(lo)
  for (let i = 0; i < hi; i++) {
    while (stack.length && A[i] <= A[stack[stack.length - 1]]) stack.pop()
    if (stack.length) prev[i] = stack[stack.length - 1]
    stack.push(i)
  }

  // 从右到左递增栈(记录索引)
  stack.length = 0
  const next = Array(hi).fill(hi)
  for (let k = hi - 1; k >= 0; k--) {
    while (stack.length && A[k] < A[stack[stack.length - 1]]) stack.pop()
    if (stack.length) next[k] = stack[stack.length - 1]
    stack.push(k)
  }
  // sum{A[i] * (left[i] + 1) * (right[i] + 1)} (lo < i < hi)
  let min = 0
  for (let i = lo + 1; i < hi; i++) {
    min += (i - prev[i]) * (next[i] - i) * A[i]
  }
  return min % bigPrimeNumber
}