leetcode 第 461 场算法比赛

作者: | 更新日期:

比较简单

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

零、背景

这次比赛期间我在厦门旅游,工作也一直比较忙,所以拖更了一周。
周六天气比较热没出门,于是做了下题并补了题解。

A: 模拟
B: 贪心+动态规划
C: 二分
D: 枚举

排名:无
代码地址: https://github.com/tiankonguse/leetcode-solutions

一、三段式数组 I

题意:给一个数组,问是否可以找个两个位置 p 和 q,使得 [0,p] 是升序,[p,q]是降序,[q,n-1]是升序。

思路:模拟

先扫描数组,得到所有的边界位置。

// 第一步:计算出有序性的分割线
vector<int> nodes;
nodes.reserve(n);
int dir = 0;  // 0: 未知, 1: 上升, -1: 下降
nodes.push_back(0);
for (int i = 1; i < n; i++) {
  int newDir = 0;
  if (nums[i - 1] < nums[i]) {
    newDir = 1;
  } else if (nums[i - 1] > nums[i]) {
    newDir = -1;
  }
  if (dir != 0 && dir == newDir) {
    nodes.pop_back();
  }
  nodes.push_back(i);  // 相等的情况,当做分割线
  dir = newDir;
}

对于边界位置,预期应恰好有 4 个,且三个线段满足先升序,再降序,再升序

const int m = nodes.size();
if (m != 4) return false;
const int l = nodes[0];
const int p = nodes[1];  // 上升的结束点
const int q = nodes[2];  // 下降的结束点
const int r = nodes[3];  // 上升的结束点
return nums[l] < nums[p] && nums[p] > nums[q] && nums[q] < nums[r];

二、平衡装运的最大数量

题意:给一个数组,问最多可以拆分多少个不重叠子数组,使得子数组的最后一个数字严格小于子数组的最大值。

思路:贪心

子数组的最后一个数字严格小于子数组的最大值,等价于子数组的最后一个数字不是最大值。

假设子数组的右边界确定后,左边界有很多,由于允许某些数组不选择,显然子数组越短越好,即左边界越往右越好。
符合要求的最短的子数组,左边界是第一个大于右边界的位置。

故可以维护一个单调栈,预处理计算出所有位置为右边界时的最优左边界。

vector<int> preMax(n, -1);
vector<int> sta;
sta.reserve(n);
for (int i = 0; i < n; i++) {
  while (!sta.empty() && weight[sta.back()] <= weight[i]) {
    sta.pop_back();
  }
  preMax[i] = sta.empty() ? -1 : sta.back();
  sta.push_back(i);
}

左边界确定了,问题就转化为了最多线段不重叠的选择问题了,是一个典型的动态规划。

状态定义:f(i) 前i个数字的最大选择数量。
状态转移方程:f(i) = max(f(i-1), f(preMax[i]-1)+1)
方程解释:最后一个数字分为选择与不选择

int ans = 0;
vector<int> dp(n, 0);
for (int i = 1; i < n; i++) {
  dp[i] = dp[i - 1];
  if (preMax[i] != -1) {
    if (preMax[i] > 0) {
      dp[i] = max(dp[i], dp[preMax[i] - 1] + 1);
    } else {
      dp[i] = max(dp[i], 1);
    }
  }
}
return dp[n - 1];

三、变为活跃状态的最小时间

题意:给一个字符串,每个时间点修改字符串指定位置为特殊字符。问最少需要多少时间,可以使得含有特殊字符的子字符串个数至少为 k 个。

思路:二分

二分时间,判断是否满足条件。

int l = 0, r = n;
while (l < r) {
  int mid = (l + r) / 2;
  if (Check(mid)) {
    r = mid;
  } else {
    l = mid + 1;
  }
}
if (l == n) return -1;
return l;

检查时,暴力检查每个位置作为左边界时,有多少个子字符串满足条件,并累计求和。
显然,确定左边界后,第一个满足条件的位置是左边界右边的第一个特殊字符的位置,之后的也都满足。
复杂度:O(n log(n) log(n))

set<int> S;
auto Check = [&](const int mid) -> bool {
  S.clear();
  for (int i = 0; i <= mid; i++) {
    S.insert(order[i]);
  }
  ll ans = 0;
  for (int i = 0; i < n; i++) {
    auto it = S.lower_bound(i);
    if (it != S.end()) {
      ans += n - *it;
    }
  }
  return ans >= k;
};

优化1:S 集合中小于 i 的元素都无效,故可以实时删除小于 i 的元素。此时右边界就是 S 集合中的第一个元素,复杂度不变。
优化2:不使用集合,直接使用数组,排序后与集合等价,使用游标代替删除,复杂度不变。
优化3:数据范围[0,n-1],使用计数排序,复杂度可以降低到 O(n log(n))

优化4:递推。

分析每个时刻标记一个位置时新增的满足条件的子字符串,与之前与之后相邻的已经标记的位置相关。

如下图,行号代表子串的左边界位置,右边界依次递增到最后。
标记某个位置为特殊字符后,这个位置为左边界的所有子字符串都满足条件,另外左边界小于这个位置的子串也都满足答案,即新增的满足条件的子串可以组成一个矩阵。

假设位置0和位置3已经被标记,满足要求的子串为图中阴影部分。
此时新增位置2,则位置2为左边界的满足条件的子串个数为 W,即下一个标记位置减去当前位置。
位置2还会影响小于位置2的子串,影响的边界个数记为 H,即当前位置减去前一个标记位置。
两个相乘就是这次新增的满足条件的子串个数。

ll ans = 0;
set<int> S;
for (int t = 0; t < n; t++) {
  const int i = order[t];
  auto it = S.lower_bound(i);  // 不存在相等值
  ll w = n - i;
  if (it != S.end()) {
    w = *it - i;
  }
  ll h = i + 1;
  if (it != S.begin()) {
    --it;
    h = i - *it;
  }
  ans += w * h;
  if (ans >= k) {
    return t;
  }
  S.insert(i);
}

四、三段式数组 II

题意:给一个数组,需要找到一个三段式子数组,满足先严格升序再严格降序,后严格升序。
如果存在多个,返回子数组和最小的。

思路:枚举

与第一题一样,预处理出所有分割点。

然后枚举分割点,找到满足要求的三段式子数组。

可以发现,中间那一段和是固定的,左右两边的段可以缩短,所以需要分别找到左右端的最大和。
直接暴力搜索分别求最大和即可。
复杂度:O(n)

ll ans = -INFL;
const int m = nodes.size();
for (int i = 0; i + 3 < m; i++) {
  const int l = nodes[i];
  const int p = nodes[i + 1];  // 上升的结束点
  const int q = nodes[i + 2];  // 下降的结束点
  const int r = nodes[i + 3];  // 上升的结束点
  if (nums[l] < nums[p] && nums[p] > nums[q] && nums[q] < nums[r]) {
    // 计算三段的和
    ll sum = 0;
    ll tmp1 = nums[p - 1];
    ll sum1 = tmp1;
    for (int j = p - 2; j >= l; j--) {
      tmp1 += nums[j];
      sum1 = max(sum1, tmp1);
    }

    ll sum2 = 0;
    for (int j = p; j <= q; j++) {
      sum2 += nums[j];
    }
    
    ll tmp3 = nums[q + 1];
    ll sum3 = tmp3;
    for (int j = q + 2; j <= r; j++) {
      tmp3 += nums[j];
      sum3 = max(sum3, tmp3);
    }
    ans = max(ans, sum1 + sum2 + sum3);
  }
}

五、最后

这次比赛整体难度不大。

第一题按题意模拟,第二题是简单的动态规划,第三题用二分,第四题是枚举模拟。

《完》

-EOF-

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

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

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

tiankonguse +
穿越