leetcode 周赛 502 第 64 名

作者: | 更新日期:

字符串HASH与二分

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

零、背景

这次比赛不难,做完快 11:30 了,以为排名几百名之外了,没想到进入前百名,最终是第 64 名。

本场题型概览如下。
A 题:循环比较。
B 题:数学计算或二分。
C 题:DP。
D 题:字符串hash与二分。

一、检查相邻数字差

题意:给一个数组,问是否相邻数字之差至多为2。

思路:循环判断即可。

小技巧:提前定义一个满足条件的 pre 值,则不需要特殊判断,直接循环比较。
如果要求的差不大于 X,可以定义 pre 值与第一个值相同。
如果要求的差不小于 X,可以把 pre 值定义为无穷大或者无穷小。

二、统计区间内的完全 K 次幂数量

题意:问区间 [l,r] 内有多少个数字,是 k 次幂,即存在一个整数 x 满足 x^k 在这个区间内。

思路:数学计算或者二分

显然,数据范围太大,不能直接循环判断。
如果可以快速计算出满足条件的最小 x 和最大 x,则左右边界即可得到满足条件的个数。

其实,区间统计问题,往往可以转化为前缀和问题。
故问题可以简化为 f(r+1) - f(l) 问题。

怎么找到 [1,r] 内满足要求的个数呢?

方法1:数学计算。
直接对 r 开 k 次方,即可得到答案。
为了避免精度导致答案错误,可以加一再判断一次。

实际写代码时,为了避免精度问题,我的经验是使用循环多加几次。

ll x = floor(pow(r, 1.0 / k));
while (powl(x + 1 , k) <= r) x++;

方法2:二分。
小于答案的都满足,大于答案的都不满足,典型的二分问题。

注意事项:可能没有答案。

三、矩阵中的局部最大值 II

题意:给一个矩阵,如果一个单元格的值是非零 x,则要求在上下左右距离 x 行 x 列的范围内,x 是最大值。
特殊条件:不包含行距离和列距离恰好都为 x 的位置。
数据范围:200

思路:动态规划

如果暴力计算,思路为枚举每个单元格,以及枚举这个单元格的所有覆盖范围。
此时复杂度为 O(n^4),显然会超时。

显然,如果可以预处理出每一行的任意区间的最大值,则可以少一次循环。

状态定义:dp[i][l][r]
含义:第 i 行区间 [l,r] 内的区间最大值。

状态转移方程:

dp[i][l][r] = max(dp[i][l][r-1], nums[i][r])

对于特殊条件,进行特殊判断即可。

auto getMax = [&](int row, int col) -> int {
  const int x = matrix[row][col];
  int maxVal = -1;
  for (int i = row - x; i <= row + x; i++) {
    if (i < 0 || i >= n) continue;
    if (i == row - x || i == row + x) {
      int l = max(0, col - matrix[row][col] + 1);
      int r = min(m - 1, col + matrix[row][col] - 1);
      maxVal = max(maxVal, dp[i][l][r]);
    } else {
      int l = max(0, col - matrix[row][col]);
      int r = min(m - 1, col + matrix[row][col]);
      maxVal = max(maxVal, dp[i][l][r]);
    }
  }
  return maxVal;
};

复杂度:O(n^3)

四、最短唯一子数组

题意:给一个数组,求一个最短的子数组,满足这个子数组在所有子数组中只出现一次。

思路:字符串hash与二分

如果换成字符串,则是经典的字符串问题,可以使用后缀自动机或者后缀数组。

分析答案的性质,子数组只出现一次,且求最短的。

如果一个子数组是答案,这个子数组往两边扩展,得到的更长的子数组肯定也都是答案。
故答案满足二分的性质,长度小于答案时不存在只出现一次的子数组,长度大于等于答案时,至少存在一个子数组只出现一次。

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

假设子数组长度固定了,怎么判断是否存在只出现一次的子数组呢?
典型的做法是滑动窗口+哈希统计。

由于是整数 hash,为了避免哈希冲突,我采用了双hash。
之前在文章《字符串 hash》介绍过这个算法。

H1.Init(nums, n);
H2.Init(nums, n);
auto Check = [&](int len) -> bool {
  unordered_map<ll, int> seen;
  for (int i = 0; i + len <= n; i++) {
    ll hash1 = H1.H(i, i + len - 1);
    ll hash2 = H2.H(i, i + len - 1);
    ll combinedHash = (hash1 << 32) | hash2;
    seen[combinedHash]++;
  }
  for (const auto& [hash, count] : seen) {
    if (count == 1) {
      return true;
    }
  }
  return false;
};

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

最后再贴一下我的字符串 hash 模板。

typedef long long ll;
const int mod1e7 = 1000000007, mod1e9 = 1000000009;
const int max3 = 2010, max4 = 20010, max5 = 100010, max6 = 2000010;

const ll BASE = 29, BASE1 = 100003, BASE2 = 100019;

class StringHash {
  ll h[max5];
  ll qpowCache[max5];
  const ll BASE;
  const ll MOD;

 public:
  StringHash(ll base, ll mod) : BASE(base), MOD(mod) {}
  ll H(int l, int r) { // [l,r], 0 base
    if (l == 0) return h[r];
    ll pre = h[l - 1] * qpowCache[r - l + 1] % MOD;
    return (h[r] - pre + MOD) % MOD;
  }

  void Init(const vector<int>& str, int n) {
    qpowCache[0] = 1;
    for (int i = 1; i <= n; i++) {
      qpowCache[i] = (qpowCache[i - 1] * BASE) % MOD;
    }

    ll pre = 0;
    for (int i = 0; i < n; i++) {
      pre = (pre * BASE + str[i]) % MOD;
      h[i] = pre;
    }
  }
  void Init(const string& str, int n) {
    qpowCache[0] = 1;
    for (int i = 1; i <= n; i++) {
      qpowCache[i] = (qpowCache[i - 1] * BASE) % MOD;
    }

    ll pre = 0;
    for (int i = 0; i < n; i++) {
      pre = (pre * BASE + str[i] - '0' + 1) % MOD;
      h[i] = pre;
    }
  }
};

StringHash H1(BASE1, mod1e7);
StringHash H2(BASE2, mod1e9);

五、最后

这次比赛不难,都是基础算法。
第二题数学计算或二分,第三题动态规划,第四题二分与字符串 hash。

《完》

-EOF-

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

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

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

tiankonguse +
穿越