前端算法第一八五弹-巫师的总力量和

125 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情

作为国王的统治者,你有一支巫师军队听你指挥。

给你一个下标从 0 开始的整数数组 strength ,其中 strength[i] 表示第 i 位巫师的力量值。对于连续的一组巫师(也就是这些巫师的力量值是 strength 的 子数组),总力量 定义为以下两个值的 乘积 :

  • 巫师中 最弱 的能力值。
  • 组中所有巫师的个人力量值 之和

请你返回 所有 巫师组的  力量之和。由于答案可能很大,请将答案对 109+7`10^9 + 7` 取余 后返回。

子数组 是一个数组里 非空 连续子序列。

示例 1:

输入:strength = [1,3,1,2]
输出:44
解释:以下是所有连续巫师组:
- [1,3,1,2][1] ,总力量值为 min([1]) * sum([1]) = 1 * 1 = 1
- [1,3,1,2][3] ,总力量值为 min([3]) * sum([3]) = 3 * 3 = 9
- [1,3,1,2][1] ,总力量值为 min([1]) * sum([1]) = 1 * 1 = 1
- [1,3,1,2][2] ,总力量值为 min([2]) * sum([2]) = 2 * 2 = 4
- [1,3,1,2][1,3] ,总力量值为 min([1,3]) * sum([1,3]) = 1 * 4 = 4
- [1,3,1,2][3,1] ,总力量值为 min([3,1]) * sum([3,1]) = 1 * 4 = 4
- [1,3,1,2][1,2] ,总力量值为 min([1,2]) * sum([1,2]) = 1 * 3 = 3
- [1,3,1,2][1,3,1] ,总力量值为 min([1,3,1]) * sum([1,3,1]) = 1 * 5 = 5
- [1,3,1,2][3,1,2] ,总力量值为 min([3,1,2]) * sum([3,1,2]) = 1 * 6 = 6
- [1,3,1,2][1,3,1,2] ,总力量值为 min([1,3,1,2]) * sum([1,3,1,2]) = 1 * 7 = 7
所有力量值之和为 1 + 9 + 1 + 4 + 4 + 4 + 3 + 5 + 6 + 7 = 44

示例 2:

输入:strength = [5,4,6]
输出:213
解释:以下是所有连续巫师组:
- [5,4,6][5] ,总力量值为 min([5]) * sum([5]) = 5 * 5 = 25
- [5,4,6][4] ,总力量值为 min([4]) * sum([4]) = 4 * 4 = 16
- [5,4,6][6] ,总力量值为 min([6]) * sum([6]) = 6 * 6 = 36
- [5,4,6][5,4] ,总力量值为 min([5,4]) * sum([5,4]) = 4 * 9 = 36
- [5,4,6][4,6] ,总力量值为 min([4,6]) * sum([4,6]) = 4 * 10 = 40
- [5,4,6][5,4,6] ,总力量值为 min([5,4,6]) * sum([5,4,6]) = 4 * 15 = 60
所有力量值之和为 25 + 16 + 36 + 36 + 40 + 60 = 213

提示 1-1

枚举每位巫师,假设他是最弱的巫师,那么他能在哪些子数组中? 提示 1-2

左右边界最远能到哪?具体地,这些子数组的左边界的最小值是多少,右边界的最大值是多少? 提示 1-3

用单调栈来计算左右边界。 提示 1-4

注意本题是可能有重复元素的,这会对最终答案的计算产生什么影响? 提示 1-5

设左右边界为 [L,R][L,R]

为了避免重复计算,我们可以考虑左侧严格小于当前元素的最近元素位置 L1L−1,以及右侧小于等于当前元素的最近元素位置 R+1R+1

以示例 1 中的数组 [1,3,1,2][1,3,1,2] 为例,如果左右两侧都是找严格小于,那么第一个 1 和第二个 1 算出来的边界范围都是一样的(都是整个数组),这就重复统计了,为了避免这种情况,可以把某一侧改为小于等于,比如把右侧改成小于等于,那么第一个 1 算出来的右边界不会触及或越过第二个 1,这样就能避免重复统计同一个子数组。

提示 2-1

设当前枚举的巫师的能力值为 v,那么他对答案产生的贡献是 v 乘上在左右边界 [L,R] 内的所有包含 v 的子数组的元素和的和。 提示 2-2

如何计算子数组的元素和?

用前缀和来计算。 提示 2-3

如何计算子数组的元素和的和?

不妨将子数组的右端点固定,子数组左端点的范围是多少?

对于多个不同的右端点,其对应的左端点的范围是否均相同?

/**
 * @param {number[]} strength
 * @return {number}
 */
var totalStrength = function(strength) {
  const mod = BigInt(1e9 + 7);
  // 使用单调栈求前后边界
  const stack = [], lefts = [], rights = [];
  for (let i = 0;i < strength.length;i++) {
    while (stack.length && strength[stack[stack.length - 1]] >= strength[i]) {
      stack.pop();
    }
    if (stack.length) {
      lefts[i] = stack[stack.length - 1] + 1;
    } else {
      lefts[i] = 0;
    }
    stack.push(i);
  }

  stack.splice(0);
  for (let i = strength.length - 1;i >= 0;i--) {
    while (stack.length && strength[stack[stack.length - 1]] > strength[i]) {
      stack.pop();
    }
    if (stack.length) {
      rights[i] = stack[stack.length - 1] - 1;
    } else {
      rights[i] = strength.length - 1;
    }
    stack.push(i);
  }

  // 计算前缀和
  let s = 0n, preSum = [0n, 0n], prePreSum = [];
  for (let i = 0; i < strength.length; i++) {
    s += BigInt(strength[i]);
    preSum[i + 2] = s;
  }

  // 计算前缀和的前缀和
  s = 0n;
  for (let i = 0; i < preSum.length; i++) {
    s += BigInt(preSum[i]);
    prePreSum[i] = s;
  }

  let target = 0n;
  for (let i = 0;i < strength.length;i++) {
    const current = BigInt(strength[i]), left = lefts[i], right = rights[i];
    const preValue = (preSum[i + 1] * BigInt(i) - prePreSum[i]) - (preSum[i + 1] * BigInt(left) - prePreSum[left]);
    const currentValue = current * BigInt((i - left + 1) * (right - i + 1));
    const lastValue = prePreSum[right + 2] - prePreSum[i + 2] - preSum[i + 2] * BigInt(right - i);
    target += current * (preValue * BigInt(right - i + 1) + currentValue + lastValue * BigInt(i - left + 1));
  }
  return target % mod;
};