leetcode 第 447 场算法比赛 - 状压+倍增

作者: | 更新日期:

上班日还这么多人参赛

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

零、背景

这次比赛是上班日,没有参加比赛。
现在已经休假了,做一下比赛。

A: 分组+二分查找
B: 并查集
C: 状态压缩动态规划
D: 倍增动态规划

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

一、统计被覆盖的建筑

题意:给若干坐标,问有哪些坐标,上下左右四个方向都有建筑。

思路:分组+二分查找

分别对 x 坐标和 y 坐标分组,排序。
然后枚举每个坐标,在 x 坐标和 y 坐标里二分查找,看自身是否在中间。

unordered_map<int, vector<int>> hx, hy;
for (auto& building : buildings) {
  int x = building[0], y = building[1];
  hx[x].push_back(y);
  hy[y].push_back(x);
}
for (auto& [x, ys] : hx) {
  sort(ys.begin(), ys.end());
}
for (auto& [y, xs] : hy) {
  sort(xs.begin(), xs.end());
}
for (auto& building : buildings) {
  int x = building[0], y = building[1];
  auto& hxy = hx[x];
  auto& hyx = hy[y];
  if (y != hxy.front() && y != hxy.back() && x != hyx.front() && x != hyx.back()) {
    ans++;
  }
}

二、针对图的路径存在性查询 I

题意:给n个点,如果两点距离小于等于d,则可以连通。问两点之间是否存在路径。

思路:并查集

技巧:一个点距离在 d 范围内的点有很多,直接遍历会超时, 只需要对相邻的点进行合并即可。

贪心:如果合并的两个点是随机的,需要使用并查集。
这道题对点排序后,是连续一段的点需要合并在一起,所以直接遍历合并,指向第一个点即可。

for (int i = 0; i < n; i++) {
  pre[i] = i;
}
for (int i = 1; i < n; i++) {
  if (abs(nums[i] - nums[i - 1]) <= maxDiff) {
    pre[i] = pre[i - 1];
  }
}
for (int i = 0; i < queries.size(); i++) {
  int l = queries[i][0], r = queries[i][1];
  if (pre[r] == pre[l]) {
    ans[i] = true;
  }
}

三、判断连接可整除性

题意:给n个数字,求一个排列,使得所有数字按十进制拼接在一起后得到的数字能整除 k。
如果存在多个,求字典序最小的排列。

思路:状态压缩动态规划

取模有一些性质。

1)(a + b) % k = (a % k + b % k) % k
2) (a * b) % k = (a % k * b % k) % k

故,多个数字拼接时,可以从前到后两两拼接取模。

状态定义:dp[pre][mask] 已选择数字拼接的前缀取模为 pre,剩余数字为 mask 的情况下,是否存在可以整除 k。

状态转移方程: 枚举下一个数字,更新状态。

int dfs(const int pre, const int mask) {
  int& ret = dp[pre][mask];
  if (ret != -1) return ret;
  if (mask == 0) {
    return ret = (pre == 0);
  }
  for (int i = 0; i < nums.size(); ++i) { // 按字典序枚举
    if(!(mask & (1 << i))) continue; // 前面已选择
    int nextPre = (pre * pow10[bits[i]] + nums[i]) % k;
    int nextMask = mask ^ (1 << i);
    if (dfs(nextPre, nextMask)) {
      path[pre][mask] = i; // 记录最优路径
      return ret = 1;
    }
  }
  return ret = 0;
};

四、针对图的路径存在性查询 II

题意:给n个点,如果两点距离小于等于d,则可以连通。问两点之间是否存在路径,如果存在,求最短路。

思路:倍增算法

基本思路与第二题类似,排序。
由于是求最短路径,每个边向前跳时需要尽量跳的远一些,故需要二分找到第一跳最远可以到达的点。

如果想要判断一个点是否可以到达另外一个点,需要不断的向前跳,如果能跳到目标点或者超过目标点,则可以到达。

默认每次查询的复杂度:O(n)

优化:预处理每个点向前跳2^02^12^2,…,2^k 次的位置,则可以通过 log(n) 的时间跳到离目标点足够近。
每个点都通过这个方法向前跳,则一次查询的综合复杂度可以降低为 O(log(n))

如何得到一个点的前 2^k 次跳跃位置?
假设当前点向前跳 2^(k-1) 次的位置已经计算为 x,则 p(i, 2^k) = p(x, 2^(k-1))
基于这个思想,可以通过 O(n log(n))的复杂度预处理出所有点的前 2^k 次跳跃位置。

倍增初始化如下。

vector<vector<int>> dp(m, vector<int>(20, 0));
for (int i = 0; i < m; i++) {
  dp[i][0] = lower_bound(datas.begin(), datas.end(), datas[i] - maxDiff) - datas.begin();
  for (int j = 1; j < 20; j++) {
    dp[i][j] = dp[dp[i][j - 1]][j - 1];
  }
}

查询的时候有几个注意事项:

1)查询位置相同,则不需要跳跃。
2)有可能一次跳跃超过目标点,这个与跳跃无数次超过目标点无法区分。
故无法到达目标点时,需要特殊判断,这样在处理跳跃时,我们需要跳到目标点的前一个位置。

代码如下,跳的位置永远在目标点之前 dp[r][j] > l,最后再加 1 即可。

if (dp[r][19] > l) {  // 不联通,没答案
  result.push_back(-1);
  continue;
}
int ans = 0;
for (int j = 19; l < r && j >= 0; j--) {
  if (dp[r][j] > l) {
    ans += (1 << j);
    r = dp[r][j];
  }
}
ans++;

五、最后

这次比赛难度加大了,第一题数据范围就很大,第二题贪心或并查集,第三题状态压缩,第四题倍增。
后两题都属于高级算法了。

《完》

-EOF-

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

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

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

tiankonguse +
穿越