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

作者: | 更新日期:

忘记定闹钟了

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

零、背景

上周回老家了一趟,临时把闹钟关闭了。
昨晚忘记打开闹钟了,一觉睡到了十一点多,起床后去买早餐,回来的时候就十二点结束比赛了。
中午吃完早餐,看了下四道题,都不难。

A: 排序
B: 并查集
C: 并查集
D: 贪心反推

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

一、优惠券校验器

题意:给一些优惠券,判断是否合法,最后按规则输出合法的优惠券。
合法条件:标识符合法、业务类别合法、当前有效。
输出规则:优先按业务类别,其次按标识符字典序。

思路:按顺序判断是否合法,然后按输出规则排序。

二、电网维护

题意:给一个无向图,然后若干操作和询问。
操作:一个顶点进行标记。
询问:顶点是否被标记,如果被标记,则返回连通图中尚未标记的最小顶点。

思路:并查集。

通过并查集对所有顶点进行联通分支的分组,根节点当做分组的编号,每个分组维护一个未标记的最小堆集合。

dsu.Init(c + 1);
for (auto& connection : connections) {
  int u = connection[0];
  int v = connection[1];
  dsu.Union(u, v);
}
unordered_map<int, set<int>> H;
for (int i = 1; i <= c; i++) {
  int p = dsu.Find(i);
  H[p].insert(i);
}

操作时,对顶点进行标记。
询问时:优先判断顶点是否标记。
如果已标记,则预处理最小堆,把已标记的堆顶删除,剩余的堆顶就是答案。

int t = q[0], x = q[1];
int p = dsu.Find(x);
auto& HP = H[p];
if (t == 1) {
  if (HP.empty()) {
    ans.push_back(-1);
  } else if (HP.count(x)) {
    ans.push_back(x);
  } else {
    ans.push_back(*HP.begin());
  }
} else {
  HP.erase(x);
}

PS:这里我使用 set 来模拟最小堆,这样就可以直接删除已标记的元素了。

复杂度:O(n log(n))

优化:进行分组的时候,其实我们已经知道顶点之间的顺序了,所以可以直接使用数组来储存。

vector<vector<int>> H(c + 1);
for (int i = 1; i <= c; i++) {
  int p = dsu.Find(i);
  H[p].push_back(i);
}
for (int i = 1; i <= c; i++) {
  std::reverse(H[i].begin(), H[i].end());
}

数组翻转后,数组是逆序递降的。
进行询问时,只需要判断数组最后一个元素即可。

vector<int> flag(c + 1, 0);
int t = q[0], x = q[1];
int p = dsu.Find(x);
auto& HP = H[p];
if (t == 1) {
  if (flag[x] == 0) {
    ans.push_back(x);
  } else {
    while (!HP.empty() && flag[HP.back()]) {
      HP.pop_back();
    }
    if (HP.empty()) {
      ans.push_back(-1);
    } else {
      ans.push_back(HP.back());
    }
  }
} else {
  flag[x] = 1;
}

三、包含 K 个连通分量需要的最小时间

题意:给n个顶点和m条边,每个边有一个删除时间,问哪个时刻期,图的联通分支至少有 k 个。

思路:并查集。

所有边按时间排序,然后时间从大到小一条条把边加回来,看什么时候不满足要求。

sort(edges.begin(), edges.end(), [](const auto& a, const auto& b) { return a[2] > b[2]; });
// 时间逆序判断
dsu.Init(n);
for (auto& e : edges) {
  int u = e[0], v = e[1], t = e[2];
  dsu.Union(u, v);
  if (dsu.Block() < k) {
    return t;
  }
}
return 0;

注意事项,某条边加回来首次不满足时,这个时刻就是答案。
因为正序来看,就是这个时刻删除边,即是首次满足的时刻。

四、到达目标点的最小移动次数

题意:对于一个坐标(x,y),令最大值为m=max(x,y),则可以得到新的坐标 (x+m,y)(x,y+m)
问一个坐标通过最少多少步,可以到达另外一个坐标。

分析:贪心反推

正向看,有两种选择。
逆向来看,其实只有一个选择。
即要么有答案,要么是固定的步数。

具体分析如下。
对于一个坐标,不妨假设 x<y,则可以得到 (x+y, y)(x,2y)
显然,加法在哪里,哪里就会更大。

如果是 x+y,则 (x+y)/y < 2
如果是 2y,则 2y/x > 2

换言之,如果一个坐标 (X, Y) 满足 Y > 2X, 那么一定是从 (Y/2, X)转换得到的。
反之,如果不满足,则是从 (Y-X, X)得到的。

故可以根据坐标的比例与2的关系,来逆向递推逼近初始坐标。
考虑到对称性,可以对坐标进行交换,保证 tx>=ty

if (tx < ty) {
  swap(tx, ty);
  swap(sx, sy);
}
if (tx >= ty * 2) {
  if (tx % 2 == 1) return -1;
  tx /= 2;
} else {
  tx -= ty;
}
ans++;

注意事项:如果 X==Y时,两个都可以选择。
故需要使用一个标记,如果存在相等的情况,则允许坐标交叉相等的情况。

if (tx == ty) eqFlag = true;
if (tx == sx && sy == ty) return ans;
if (eqFlag && tx == sy && ty == sx) return ans;

五、最后

这次比赛题目都不难,不过最后一题有一个注意事项,不注意的话就会翻车。

《完》

-EOF-

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

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

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

tiankonguse +
穿越