leetcode 第 449 场算法比赛 - 贪心模拟

作者: | 更新日期:

翻车了

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

零、背景

这次比赛最后一题的边界一直没还清楚,最后差一秒才通过。

A: 统计
B: 前缀和
C: 贪心
D: 前缀和+二分

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

一、不同字符数量最多为 K 时的最少删除数

题意:给一个字符串,问最少删除多少个字符,才能使得剩余的不同字符个数最多为 K。

思路:统计。

保留的 K 个不同字符,一个都不能删除,其余的都需要删除。
显然,需要优先删除出现次数较少的字符。

算法:统计字符的频次,排序,保留频次最高的 K 个字符,其余字符都需要删除。

二、等和矩阵分割 I

题意:给一个矩阵,问是否可以水平或垂直拆分为两个矩阵,使得两个矩阵的和相等。

思路:前缀和。

预处理出每一行的前缀和与每一列的前缀和,然后枚举行与列的分割线,判断两个子矩阵和是否相等即可。

for (int i = 0; i < n; ++i) {
  for (int j = 0; j < m; ++j) {
    row[i] += grid[i][j];
    col[j] += grid[i][j];
    sum += grid[i][j];
  }
}
for (int i = 1; i < n; ++i) {
  if (sum % 2 == 0 && row[i - 1] == sum / 2) return true;
  row[i] += row[i - 1];
}
for (int j = 1; j < m; ++j) {
  if (sum % 2 == 0 && col[j - 1] == sum / 2) return true;
  col[j] += col[j - 1];
}

三、图中边值的最大和

题意:给一个无向图,每个节点 最多 与其他两个节点相连。
现在需要给图中节点分配权值,边的权值是两个节点权值之积,求所有边的权值之和的最大值。

思路:贪心。

每个节点最多连接两个节点,所以每个联通分支要么是一个环,要么是一个链。

之后就是贪心了。

假设只有一个环,则参考样例2,先填最大值,左右一次填写数字。

假设只有一个链,与环类似,参考样例1,在中间填最大值,然后最优依次填写数字。

假设有一个环和一个链,参考样例1,可以猜测优先选择环,答案更优。

假设有多个环,可以猜测,先选择节点更多的环答案更优。

假设有多个链,可以猜测,先选择节点更多的链答案更优。

综合就是,优先选择环,然后选择链。
不断对于环还是链,由于选择多的,再选择少的。

首先使用并查集,计算出所有环和链,以及对应的节点个数。

Dsu dsu;
dsu.Init(n);
unordered_set<int> loop;
for (auto& edge : edges) {
  int u = dsu.Find(edge[0]);
  int v = dsu.Find(edge[1]);
  if (u == v) {
    loop.insert(u);
  } else {
    dsu.Union(u, v);
  }
}
vector<ll> links;
vector<ll> loops;
for (int i = 0; i < n; i++) {
  int f = dsu.Find(i);
  if (i == f) {
    if (loop.count(f)) {
      loops.push_back(dsu.GetScore(f));
    } else {
      links.push_back(dsu.GetScore(f));
    }
  }
}

然后按照上面的结论进行贪心。

sort(links.begin(), links.end(), greater<ll>());
sort(loops.begin(), loops.end(), greater<ll>());
ll ans = 0;
ll index = n;
enum { LINK = 0, LOOP = 1 };
auto Next = [&](int v, int type) {
  ll l = index--;
  ll r = l;
  v--;
  while (v >= 2) {
    ll L = index--;
    ll R = index--;
    ans += l * L;
    ans += r * R;
    l = L;
    r = R;
    v -= 2;
  }
  if (v == 1) {
    ll L = index--;
    ll R = L;
    ans += l * L;
    if (type == LOOP) {
      ans += r * R;
    }
  } else {
    if (type == LOOP) {
      ans += l * r;
    }
  }
};
for (ll v : loops) {  // 长度为 v 的环
  Next(v, LOOP);
}
for (ll v : links) {  // 长度为 v 的链
  Next(v, LINK);
}
return ans;

四、等和矩阵分割 II

题意:与第二题,这里允许最多删除一个坐标,但是修改后两个子矩阵的剩余元素需要是连续的。

思路:前缀和与二分。

不修改的情况与第二题一模一样,可以直接使用第二题的代码。
之后枚举删除所有坐标,判断删除后,是否存在拆分,使得子矩阵和相等,子矩阵还联通。

以行为例,假设删除的是 (x,y),分割线可能在 x 的上边,也可能在 x 的下边。

如果分割线在 x 的上边,则说明分割线之前的前缀和都不变,直接二分查找即可。

特殊情况:找到后,由于是下半部进行删除,下半部可能不联通。
这里分三种情况讨论:

1)如果行和列都大于2,则删除肯定联调。
2)如果只有一列,删除点只能在两边,不能在中间。
3)如果只有一行,删除点只能在两边,不能在中间。

// 分割线 limit 在 x 之前 [0, x),即前缀和不需要减去 v,此时 row[limit] == target
if (x - 1 >= 0 && row[x - 1] >= target) {  // 肯定可以查找到
  auto limit = lower_bound(row.begin(), row.end() + x, target) - row.begin();
  if (row[limit] == target) {
    if (m >= 2 && n - limit > 2) return true;                   // 至少两行两列中删除,必然联通
    if (m == 1 && (limit + 1 == x || x + 1 == n)) return true;  // 只有一列,删除的元素在分割线的两侧
    if (n - limit == 2 && (y == 0 || y + 1 == m)) return true;  // 只有一行,删除的元素在分割线的两侧
  }
}

如果分割线在 x 的下边,下标的都需要减去 v,此时需要查找 row[limit] == target - v

如下代码,查找到目标值后,需要判断是否联通。
判断方法与上面类似,这里不再赘述。

// 分割线 limit 在 x 之后 [x, n),则需要 row[limit] - v == target
if (x < n - 1 && row[x] - v <= target) {
  auto limit = lower_bound(row.begin() + x, row.end(), target + v) - row.begin();
  if (row[limit] == target + v) {
    if (m >= 2 && limit + 1 >= 2) return true;              // 至少两行两列中删除,必然联通
    if (m == 1 && (x == 0 || x + 1 == limit)) return true;              // 只有一列,删除的元素在分割线的两侧
    if (limit == 0 && (y == 0 || y + 1 == m)) return true;  // 只有一行,删除的元素在分割线的两侧
  }
}

五、最后

这次比赛第三题需要大胆的贪心,第四题边界处理比较麻烦,我没处理好,结束比赛后的一秒才通过。

《完》

-EOF-

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

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

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

tiankonguse +
穿越