leetcode 第 240 场算法比赛

作者: | 更新日期:

四道题,背后涉及 11个不同的算法,可以学到不少知识点。

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

零、背景

这次五一调休加班,本来我以为是周日加班的。
没想到是周六加班,周日休息。
这样周日就可以正常的打比赛了。

但是,最近有不少事情要处理,这就导致周日不能完整的打比赛了。
做了几道题之后,就忙其他事情去了。

赛后看下题,发现这次比赛的题难度还可以,都可以当做面试题来考察大家。
建议大家把四道题都学习一下。

一、人口最多的年份

题意:给出每个人的出生年份与死亡年份。
问人口最多的年份。

思路:很容易想到,最优方法是左区间加一、右区间减一,扫描一遍即可。
但是怎么储存与扫描是一个问题,需要去思考的。

方法1、区间排序。

起初我打算对区间排序,发现没啥用,左区间有序了,右区间还是乱序的,无法进一步处理。

方法2、区间转数组。

后来,我发现区间的数据量不大,就决定定义一个完整区间的数组,然后左边界加一、有边界减一。
时间复杂度:O(n)
空间复杂度:O(V),V 是完整区间的大小。

方法3、区间离散化。

假设区间很大,区间转数组就面临存不下区间的问题。
思考题:这时候又该如何做呢?

由于区间个数只有 n 个,那区间边界顶多就是 2n 个。
对区间边界进行离散化,就可以转化为数组了。

方法4、区间转边界点,排序。

方法1 对区间排序无法解决这道题的本质原因是,二维数据无法在一维上比大小。

所以我们把二维数据转化为一维,再排序即可。
一个区间[l, r] 拆分为两个点 [l, +1][r, -1]
然后排序扫描一遍即可。

当然,喜欢 map的我当然直接使用 map 来代替排序了。

int maximumPopulation(vector<vector<int>>& logs) {
  map<int, int> m;
  for (auto& v : logs) {
    m[v[0]]++;
    m[v[1]]--;
  }

  int maxYear = 0, maxNum = 0, sum = 0;
  for (auto& p : m) {
    sum += p.second;
    if (sum > maxNum) {
      maxNum = sum;
      maxYear = p.first;
    }
  }

  return maxYear;
}

二、下标对中的最大距离

题意:给两个递减数组,分别取一个数字,假设下标是(i, j),如果满足i<=jnums1[i]<=nums2[j],则认为存在有效距离,值为 j-i
求最大的有效距离,如果不存在,输出 0。

思路:

首先理解题意,递减数组代表是降序数组,当然,可能存在相等的值。

方法1、二分查找。

由于数组已经有序了,很容易想到枚举每个j,二分查找找到最远的 i,然后求最大值即可。
复杂度:n log(n)

方法2:双指针。

由于两个数组都是降序。
如果nums1[i]<=nums2[j],那么可以确定对于所有的 0<=k<=j,都存在nums1[i]<=nums2[k]
因为 nums2 越靠前面的数字越大。

反过来,假设一个j,已经得到最远的 i
那么对于j+1,最远的i只会更大,即在范围 [i, j) 内。

利用这个性质,我们就可以使用双指针线性复杂度做这道题了。
复杂度:O(n)

int maxDistance(vector<int>& nums1, vector<int>& nums2) {
  int l = 0, r = 0;
  int n = nums1.size(), m = nums2.size();

  int ans = 0;
  for (r = 0; r < m; r++) {
    while (l < n && l <= r && nums1[l] > nums2[r]) {
      l++;
    }

    if (l == n) continue;
    ans = max(ans, r - l);
  }
  return ans;
}

三、子数组最小乘积的最大值

题意:给一个数组,有很多子数组。
对于一个子数组,最小值与数组和的乘积称之为最小乘积。
所有子数组里面,求值最大的最小乘积。

思路:

方法1:暴力计算。

枚举所有的子数组,并计算出最小乘积,最后取最优值。
复杂度:O(n^3)

优化:枚举过程中,就可以顺便计算出最小值和数组和。
复杂度:O(n^2)

方法2:二分查找下的线段树

分析所有的子数组,发现很多子数组明显没有其他子数组更优。

比如两个子数组有交集,最小值相等,那么两个子数组的并集的最小值不变,但是数组和可以更大。

根据这个特征,可以依次枚举每个下标元素是最小值,然后往两边寻找最远的距离,使得这个区间内的最小值就是枚举值。

那怎么找到一个位置两边最远的都不小于当前值的边界呢?

比如左边界,就是求枚举值左边最后一个值小于枚举值的位置。
而对于右边界,则是求枚举值右边,第一个值小于枚举值的位置。

面对这个问题,很容易想到使用二分查找加区间最值来解决。
区间最值一般使用线段树实现,用于快速得到一个区间的最小值。
二分查找用于快速找到最大的区间[l, r],且这个区间的最小值都不小于枚举值。

对于区间和,则使用线段树可以顺便求出来。

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

int maxSumMinProduct(vector<int>& nums) {
  n = nums.size();
  maxNM = n;

  memset(lineSegTree.str, 0, sizeof(int) * (n * 2 + 1));
  for (int i = 1; i <= n; i++) {
    lineSegTree.str[i] = nums[i - 1];
  }

  lineSegTree.bulid(1, n);

  ll ans = 0;

  for (int i = 1; i <= n; i++) {
    int v = nums[i - 1];
    int l = FindLeft(i, v);
    int r = FindRight(i, v);
    ll sum = lineSegTree.querySum(l, r, 1, n);
    ans = max(ans, sum * v);
  }

  return ans % mod;
}

方法3:预处理左右边界,线段树求区间和

上面求最边界和右边界的时候,使用二分查找+区间最值解决的。
分析一下,发现利用单调性预处理,使用 O(n)的复杂度得到所有边界。

所以,我们可以预处理左右边界,然后使用线段树求区间和得到答案。
复杂度:O(n log(n))

方法4:预处理左右边界和区间和

我们使用线段树的初衷是为了求左右边界,区间和只是顺便复用线段树而已。
现在左右边界预处理了,那区间和也可以预处理,这样就不需要线段树了。

复杂度:O(n)

int maxSumMinProduct(vector<int>& nums) {
  n = nums.size();

  InitLeft(nums);
  InitRight(nums);
  InitSum(nums);

  ll ans = 0;

  for (int i = 1; i <= n; i++) {
    int v = nums[i - 1];
    int l = leftPos[i];
    int r = rightPos[i];
    ll sum = sumPos[r] - sumPos[l - 1];
    ans = max(ans, sum * v);
  }

  return ans % mod;
}

四、有向图中最大颜色值

题意:给一个有向有环图,每个节点有一个颜色。
随便找一个路径,会有多个颜色在这个路径上,颜色最多的个数称为路径的颜色值。
问所有路径中,最大的颜色值是多少。
如果有环,直接输出 -1

思路:首先是判断是否有环。

如果是树,直接 DFS 即可判断。
对于拓扑图,需要通过删入度为0的点来判断是否有环。
如果最后删完后,删除的顶点等于图的顶点,代表没有环。

// 复杂度:O(m)
bool CheckOk() {
  stack<int> sta;
  for (auto v : zeroInDegNodes) {
    sta.push(v);
  }

  int delNodeNum = 0;

  // 找到入度为0 的边,依次删除
  while (!sta.empty()) {
    int from = sta.top();
    sta.pop();
    delNodeNum++;

    for (auto to : nextNodes[from]) {
      inDegs[to]--;
      if (inDegs[to] == 0) {
        sta.push(to);
      }
    }
  }

  return delNodeNum == n;
}

假设没有环了,接下来就是寻找一个算法来求最优答案了。

考虑到颜色全部是小写字母,那只有 26 个,突破口显然是枚举所有颜色,分别寻找最优答案。

int Dfs(int from, char c) {
  if (cache[from] != -1) return cache[from];

  int ans = 0;
  for (auto to : nextNodes[from]) {
    ans = max(ans, Dfs(to, c));
  }
  if (colors[from] == c) {
    ans++;
  }
  return cache[from] = ans;
}
int largestPathValue(string& colors_, vector<vector<int>>& edges) {
  colors.swap(colors_);

  Init(edges);

  if (!CheckOk()) {
    return -1;  // 有环
  }

  int ans = 0;
  for (char c = 'a'; c <= 'z'; c++) {
    cache.clear();
    cache.resize(n, -1);
    for (int from : zeroInDegNodes) {
      ans = max(ans, Dfs(from, c));
    }
  }
  return ans;
}

优化:实际上不需要枚举 26 个字母,只需要枚举图中出现的字母即可。

五、最后

这次比赛的题比较好。

四道题,背后涉及 11个不同的算法,可以学到不少知识点。

你还有其他解决思路吗?

加油,算法人。

《完》

-EOF-

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

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

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

tiankonguse +
穿越