leetcode 第 433 场算法比赛,两道题算法一样

作者: | 更新日期:

思维受限。

本文首发于公众号:天空的代码世界,微信号:tiankonguse

背景

这次比赛又翻车了,我第二题想复杂了,做了一个小时。
其实如果你第二题会做,第四题就也会做,两个思路一样。

A: 枚举、前缀和
B: 贪心枚举最值、数学公式
C: 动态规划
D: 贪心枚举

排名:196
代码地址: https://github.com/tiankonguse/leetcode-solutions/tree/master/contest

一、变长子数组求和

题意:给一个数组,求所有子数组 nums[i - nums[i], i] 之和的总和。

思路1:枚举所有子数组,遍历求和。
复杂度:O(nm)

思路2:预处理前缀和,枚举所有子数组,根据前缀和快速计算子数组和。
复杂度:O(n)

二、最多 K 个元素的子序列的最值之和

题意:给一个数组,返回所有长度最多为 k 的 子序列 中 最大值 与 最小值 之和的总和。

预处理:子序列不关心顺序,先对数组排序。
相等元素大小定义:右边的大于左边的。

思路1:贪心枚举最值。

分别枚举最大值和最小值为 nums[i] 的子序列个数,值乘以个数求和就是答案。

对于最大值,子序列只能选择左边的元素,方案数为分别选择 [0,k-1]个。
对于最小值,子序列只能选择右边的原则,方案数为分别选择 [0,k-1]个。

for (int i = 1; i <= n; i++) {
  ll v = nums[i - 1];
  for (int k = 1; k <= K; k++) {
    // 枚举最大值: [1, i-1]里面选择 k-1 个
    ans = (ans + v * C(i - 1, k-1)) % mod1e7;
    // 枚举最小值: [i+1, n] 里面选择 k-1个
    ans = (ans + v * C(n - (i + 1) + 1, k-1)) % mod1e7;
  }
}

思路2:数学公式

比赛的时候,我是同时枚举最大值和最小值的,然后推导出一个很复杂的数学公式。

假设最小值为 nums[l], 最大值为 nums[r],则我们可以在 (l+1, r-1) 里面分别选择 [0,k-2]个数字。
假设选择 kk 个数字,则枚举的一个答案就是: (nums[l] + nums[r]) * C(r-l-1, kk)

l 固定,r 属于 i=[l+1, n] 展开,答案如下:

  sum((nums[l] + nums[i]) * C(i-l-1, kk))
= nums[l] * sum(C(i-l-1, kk)) +  sum(nums[i] * C(i-l-1, kk))
=  nums[l] * sum(C(j, kk))  // j = [0, n-l-1]
  +sum(nums[i] * C(i-l-1, kk))

其中 nums[l] * sum(C(j, kk)) 是左边界相关的答案,可以通过预处理 C(j, kk)的前缀和,从而枚举左边界计算出答案。
其实这个公式转换一下,就是上面思路1枚举最小值的公式。
区别是,思路1是直接在右区间挑选 k-1 个数字,当前的公式是确定右边界,然后在区间内挑选k-2个数字,两个答案是等价的。

右有半部分 sum(nums[i] * C(i-l-1, kk)) 里面 il 耦合,我们需要对左边界 l 进行展开,然后基于 i 进行聚合,一通计算,推导出于左边界类似的公式。

  sum(nums[i] * C(i-l-1, kk)) // l=[1,n]
= nums[i] * sum(C(i-l-1, kk))

这个公式与左边界公式对称,是右边界公式,所以也可以通过前缀和优化快速解决。
通过数学公式,我通过这道题时已经 11点40了。

三、粉刷房子 IV

题意:给一个长度为2n的数组,以及每个位置三个颜色的代价,问相邻位置以及中心对称位置颜色不同,完成染色时最低成本是多少。

思路:动态规划。

把数组当做是 2*n 的二维数组,则题意转化为了上下左右颜色都不能相同的最低颜色成本。

状态定义:f(n,a,b) 第n个位置上面颜色为 a 下面颜色为 b 时,前面都完成染色的最低成本。

状态转移方程: f(n,a,b) = cost(n,a,b) + min(f(n-1,aa,bb))

四、最多 K 个元素的子数组的最值之和

题意:给一个数组,返回所有长度最多为 k 的 子数组 中 最大值 与 最小值 之和的总和。

思路:与第二题的差异是子序列变成了子数组。
我第二题的思路是枚举左右边界,此时再去枚举左右边界,发现无法推导出数学公式了。

赛后我看了下大家第二题的思路,发现是枚举最大值与最小值,那显然,第四题是一模一样的。

枚举最大值,则向左右找到最远的边界 L 与 R,使得区间 [L, R] 内当前值是最大值。
假设做区间有 A 个元素,右区间有 B 个元素,如果 K 不做限制的话,显然方案有 (1+A) * (1+B) 个。

这里 K 有限制,所以需要分情况讨论。
显然,A 和 B 的个数最多为 K-1,超过了需要进行截断。
左区间枚举[0,A],看右半部最多有多少个。
可以发现,右区间分为两部分:一开始由于B<K-1,所以最多都是可以选择 B 个。
知道左边为 k - (B + 1) 个时,右边开始从 B 递减,递减到 0 或者 k - (B + 1)-A

总结一下就是,枚举左区间,右区间的方案书分为两部分,一部分都相等,一部分是递减连续数列,分别求和即可。

如何根据一个位置找到左右边界呢?
最简单的方法是维护一个单调栈,即可均摊复杂度 O(1) 找到边界。
如果没想到单调栈的话,可以使用线段树+二分查找来找到边界。

综合复杂度:
单调栈 O(n)
线段树 O(n log(n) log(n))

五、最后

这次比赛又想复杂了,导致只做出三道题。
第二题与第四题在我看来是完全一样的,第二题能想到最大值与最小值分开处理,第四题也是一样的方法。

总结下如下:
1)谈到子序列,则不关心顺序,优先对数组排序。
2)谈到求和,如果可以分别求和,则分别处理,会更简单。
3)求最值覆盖的区间,可以使用单调栈。

《完》

-EOF-

本文公众号:天空的代码世界
个人微信号: tiankonguse
公众号ID: tiankonguse-code

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越