leetcode 第 456 场算法比赛-比较简单

作者: | 更新日期:

太简单了

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

零、背景

这场比赛的时候我还在老家,当时正在走亲戚,所以就没参加比赛。

现在回头做了一下,发现四道题都很简单。

A: 模拟
B: 前后缀
C: 动态规划
D: 二分+并查集

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

一、分割字符串

题意:给一个字符串,按顺序得到最短的前面没出现的子串。输出最终得到的所有子串。

思路:模拟

已经得到的子串储存在 hash 表中,新的子串判断 hash 表中是否存在,存在了则增加子串的长度。

unordered_set<string> H;
vector<string> ans;
string buf;
for (auto c : s) {
  buf.push_back(c);
  if (!H.count(buf)) {
    H.insert(buf);
    ans.push_back(buf);
    buf.clear();
  }
}
return ans;

二、相邻字符串之间的最长公共前缀

题意:给一个字符串数组,问删除第 i 个字符串后,剩余的字符串中相邻字符串的最长公共前缀。

思路:前后缀

第一步:预处理出相邻字符串的最长公共前缀。

vector<int> commonLen(n + 2, 0);
for (int i = 1; i < n; i++) {
  auto& s1 = words[i];
  auto& s2 = words[i - 1];
  commonLen[i] = Common(s1, s2);
}

第二步:预处理出数组前缀的最大公共前缀长度。

vector<int> leftMax(n + 2, 0);
for (int i = 1; i < n - 1; i++) {
  leftMax[i] = max(leftMax[i - 1], commonLen[i]);
}

第三步:预处理出数组后缀的最大公共前缀长度。

vector<int> rightMax(n + 2, 0);
for (int i = n - 1; i >= 0; i--) {
  rightMax[i] = max(rightMax[i + 1], commonLen[i + 1]);
}

第四步:枚举删除第i个字符串,模拟计算新的相邻子串的公共前缀,求最大值。

ans[0] = rightMax[1];
ans[n - 1] = leftMax[n - 2];
for (int i = 1; i + 1 < n; i++) {
  ans[i] = max(leftMax[i - 1], rightMax[i + 1]);
  auto& s1 = words[i + 1];
  auto& s2 = words[i - 1];
  ans[i] = max(ans[i], Common(s1, s2));
}

三、划分数组得到最小 XOR

题意:给一个数组,求分割为k个子数组,每个子数组求异或值。
问如何拆分,才能使得 k 个异或值的最大值最小。

思路:动态规划

求最大值的最小,第一时间想到的是二分。
但是在枚举第一段子数组后,剩余的子数组存在大量的重复问题,需要使用动态规划来合并重复状态。
而动态规划是可以直接储存答案的,这样就不需要二分了。

状态定义:f(n,k)前 n 个元素拆分为 k 个子数组的所有方案中,异或值最大值的最小值。

状态转移方程:

f(n,k) = min(max(f(n-i,k-1), xor(n-i+1, n)))

其中 xor(l,r) 代表求一个子数组的异或值,可以通过前缀异或值快速求差得到。

int Dfs(int n, int k) {
  int& ret = dp[n][k];
  if (ret != -1) return ret;
  if (k == 1) return ret = preXOR[n];
  ret = INT_MAX;
  for (int i = n; i >= k; i--) {
    ret = min(ret, max(preXOR[n] ^ preXOR[i - 1], Dfs(i - 1, k - 1)));
  }
  return ret;
}

四、升级后最大生成树稳定性

题意:给一个无向有权图,部分边必选,部分可选的边允许权值翻倍一次,最多对k条边权值翻倍,问可以得到的生成树的最小边权的最大值是多少。

思路:二分+并查集

二分答案,判断是否可以构造最小边权不小于指定权值的生成树。

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

判断方法需要使用并查集。
首先,先加入所有必选的边,并判断必选的边是否满足要求(权值满足要求,不能有环)。

dsu.Init(n);
for (auto& e : edges) {
  int u = e[0], v = e[1], s = e[2], m = e[3];
  if (m == 0) continue;
  // 必须加入到生成树中
  if (s < minVal) {
    return false;
  }
  if (dsu.Find(u) == dsu.Find(v)) {
    return false;  // 形成环
  }
  dsu.Union(u, v);
}

之后,可选边从大到小枚举,判断是否可以加入进来,以及是否需要权值翻倍。

int K = k;
for (auto& e : edges) {
  int u = e[0], v = e[1], s = e[2], m = e[3];
  if (m == 1) continue;
  if (dsu.Find(u) == dsu.Find(v)) continue;
  if (s >= minVal) {
    dsu.Union(u, v);
  } else if (K > 0 && s * 2 >= minVal) {
    dsu.Union(u, v);
    K--;
  }
}

最后,判断并查集是否是连通的即可。

return dsu.GetBlock() == 1;

五、最后

这次比赛整体比较简单。
第一题模拟,第二题前后缀,第三题动态规划,第四题二分+并查集。

《完》

-EOF-

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

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

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

tiankonguse +
穿越