leetcode 周赛 521

作者: | 更新日期:

前缀DP

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

零、背景

这次比赛中秋回家了,所以没参加比赛。
晚上抽空做了下题,我怎么感觉第三题比第四题难呢。

本场题型概览如下。

A 题:统计排序
B 题:贪心统计
C 题:构造前缀最小值
D 题:前缀DP

一、移除不同值重排数组

题意:给你一个整数数组 nums。
初始时,你有一个 空 数组 ans。重复执行以下操作,直到 nums 变为 空 :

  • 找出当前 nums 中 所有不同 的值。
  • 将当前 nums 中每个 不同 的值各移除一个,并按 升序 将这些值依次添加到 ans 中。

返回数组 ans。

思路:统计排序。

所有数据放入有序集合,按题意,每次选择互不相同的元素,按升序排列。

vector<int> rearrangeArray(vector<int>& nums) {
  map<int, int> mp;
  for (int v : nums) {
    mp[v]++;
  }
  vector<int> ans;
  while (!mp.empty()) {
    for (auto it = mp.begin(); it != mp.end();) {
      ans.push_back(it->first);
      if (--it->second == 0) {
        it = mp.erase(it);
      } else {
        ++it;
      }
    }
  }
  return ans;
}

另外,根据答案的特征,如果我们对相同数字进行编号,然后按编号排序,编号相同时再按数值排序,则可以直接构造出答案。

vector<int> rearrangeArray(vector<int>& nums) {
  vector<pair<int, int>> indexNums;
  unordered_map<int, int> mp;
  for (int v : nums) {
    mp[v]++;
    indexNums.push_back({mp[v], v});
  }
  sort(indexNums.begin(), indexNums.end());
  vector<int> ans;
  for (auto [_, v] : indexNums) {
    ans.push_back(v);
  }
  return ans;
}

二、至多一次替换后的最大相邻相等元素对数

题意:给你一个 下标从 1 开始 的整数数组 nums。
你可以选择两个 不同 的值 x 和 y,并 最多 执行一次以下操作:将 nums 中所有值为 x 的元素替换为 y。
返回执行操作后,相邻且相等的元素对数量的 最大值 。

思路:贪心统计

答案是统计相等的相邻二元组,先统计不修改的答案。

题目要求找到一个不相等二元组,修改为相等的值,则可以使得答案更大。

要想答案最大,显然统计所有相邻的二元组的个数,修改个数最多的那个,则可以使得答案最大。

int maxEqualAdjacentPairs(vector<int>& nums) {
  map<pair<int, int>, int> mp;
  int n = nums.size();
  int ans = 0;
  int maxCount = 0;
  for (int i = 0; i < n - 1; ++i) {
    int a = nums[i], b = nums[i + 1];
    if (a > b) swap(a, b);
    if (a == b) {
      ans++;
    } else {
      mp[{a, b}]++;
      maxCount = max(maxCount, mp[{a, b}]);
    }
  }
  return ans + maxCount;
}

三、数对和受限的最长子数组

题意:给你一个整数数组 nums。
如果不存在三个 互不相同 的下标 i、j 和 k,满足 l <= i, j, k <= r 且:nums[i] + nums[j] == nums[k],则子数组 nums[l..r] 是 有效 子数组。

返回 nums 中有效子数组的 最大 长度。
子数组 是数组中一个连续 非空 元素序列。

思路:构造前缀最小值

对于子数组问题,要么枚举左端点,求满足右端点的个数;要么枚举右端点,求满足左端点的个数。

假设左端点 L 固定,如何判断一个右端点 R 是不是答案呢?
如果存在一个 i,j,k 满足 L <= min(i,j,k) <= max(i,j,k) <= R,则 [L,R] 显然不可能是有效子数组。

由此还可以进一步得到两个推论:
1)对于所有的 Y>=R,[L,Y] 都不可能是有效子数组。
2)对于所有的 X<=L,[X,R] 都不可能是有效子数组。

结论 2,用大白话理解就是 L 前面的点作为左端点时,有效子数组边界不可能超过 R。

由此,可以想到一个贪心算法:枚举所有满足条件的 i,j,k,然后确定一批前缀左端点 [1,L] 的右边界。
对一个左端点的所有右边界取最小值,就可以得到这个左端点的最长有效子数组的长度。

如何对前缀批量设置最小值呢?
线段树可能会超时,而离线逆向遍历一遍,可以线性求出所有最小值。

int ans = 0;
int pre = n;
for (int i = n - 1; i >= 0; i--) {
  pre = min(pre, preMax[i]);
  ans = max(ans, pre - i);
}
return ans;

如何枚举找到所有满足条件的 i,j,k 呢?
可以枚举 i 和 j,然后二分查找 k,即可找到所有的三元组。

unordered_map<ll, vector<int>> valPos;
int n = nums.size();
for (int i = 0; i < n; ++i) {
  int v = nums[i];
  valPos[v].push_back(i);
}
vector<int> preMax(n, n);  // [i, preMax[i])
for (int i = 0; i < n; i++) {
  for (int j = i + 1; j < n; j++) {
    int v = nums[i] + nums[j];
    auto itV = valPos.find(v);
    if (itV == valPos.end()) {
      continue;
    }
    auto& pos = itV->second;
    // ...
  }
}

对于同一个 (i,j),满足条件的 k 可能很多,这样还会超时。
可以发现,根据 [i,j],可以将有序的 k 分三种情况讨论。

1)在右边 k>j,前缀为 [1,i] 这些点有一个右边界 k。
此时需要得到最小的 k。

2)在中间 i<k<j,前缀为 [1,i] 这些点有一个右边界 j。

3)在左边 k<i,前缀 [1,k] 这些点有一个右边界 j。
此时,需要得到最大的 k。

由此,对于每个 (i,j) 只需要判断三次即可。
复杂度:O(n^2 log(n))

写代码时,情况 1 与情况 2 可以合并,从而只需要分两种情况。

auto& pos = itV->second;
auto itI = lower_bound(pos.begin(), pos.end(), i);  // [i,j]
if (itI != pos.end()) {
  int idx = max(*itI, j);
  preMax[i] = min(preMax[i], idx);
}
if (itI != pos.begin()) {
  --itI;
  int idx = *itI;
  preMax[idx] = min(preMax[idx], j);
}

四、考虑空闲时间的会议最大收益

题意:给你一个二维整数数组 meetings,其中 meetings[i] = [starti, endi, revenuei] 表示一场会议从时间 starti 开始,在时间 endi 结束,并可获得 revenuei 的收益。
所有会议均采用 左闭右开区间 [start, end) 表示,因此仅在端点处相接的会议 不视为 重叠。

你可以选择任意一个会议 非空子集,所选会议两两不重叠。每选择一场会议,你都可以获得该会议对应的收益。
将所选会议按照 开始时间递增 的顺序排列。
对于该顺序中每一对相邻会议,你还可以根据它们之间的空闲时间获得额外收益,每单位空闲时间获得 1 单位收益。
空闲时间等于后一场会议的开始时间减去前一场会议的结束时间。

最早一场所选会议开始之前,以及最晚一场所选会议结束之后的空闲时间不会产生收益。
如果只选择一场会议,则不会获得任何空闲时间收益。

返回可以获得的 最大总收益 。

思路:前缀DP

很容易想到动态规划。
状态定义:dp[t] 截止到时间 t 获取的最大收益。

难点:dp 状态是否要包含最后一段空闲时间的收益。

如果包含,则状态转移方程只依赖上一个时间,方程如下:

dp[t] = max(dp[t], dp[t-1] + 1);

此时状态 dp[i] 的含义是假设后面还有一个会议,截止到时间 i,前缀的最大收益。

题目中时间数据范围是 10^9,所以需要离散化。

离散化后如何求前缀最大值呢?
可以使用一个游标来累计计算当前时刻的最大收益。

vector<ll> dp(n, 0);
ll ans = 0;
int p = 0;
for (auto& m : meetings) {
  int l = valToIdx[m[0]], r = valToIdx[m[1]];
  ll v = m[2];
  while (p < l) {
    if (dp[p] > 0) {
      dp[p + 1] = max(dp[p + 1], dp[p] + vals[p + 1] - vals[p]);
    }
    p++;
  }
  dp[r] = max(dp[r], dp[l] + v);
  ans = max(ans, dp[l] + v);
}
return ans;

如果状态中不包含最后的空闲时间,则状态转移方程需要加上空闲时间的收益,方程如下。

dp[t] = max(dp[T] + t - T);
      = max(dp[T] - T) + t;

这个就使得状态比较复杂了。
而且还不好写出正确的状态转移方程,就不展开介绍了。

五、最后

这次比赛,第四题很容易想到状态包含空闲时间,从而很快写出代码。
而第三题,需要先根据左端点推导出右端点上限,再反向推导出左端点前缀。
这样对比,第三题的思维难度其实更高的。

《完》

-EOF-

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

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

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

tiankonguse + ≡
穿越