leetcode 第 427 场算法比赛

作者: | 更新日期:

线段树大模拟。

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

零、背景

这次比赛求坐标系最大矩阵,需要离散化+线段树+二分查找,相当复杂。

A: 模拟
B: 枚举
C: 分段最大子数组和
D: 离散化线段树+二分

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

一、转换数组

题意:给一个循环数组,求构造一个映射数组。
映射规则:如果元数组值是正数v,则取右边第v个位置的值,否则取左边第v个位置的值。

思路:按题意模拟即可。

优化:可以直接相加取模即可。

注意事项:负数可能是n的很多倍,取模后再加n,再取模即可。

for (int i = 0; i < n; i++) {
  ans[i] = nums[((i + nums[i]) % n + n) % n];
}

二、用点构造面积最大的矩形 I

题意:坐标系给n个点,求挑选4个点组成与坐标轴平行的面积最大的矩阵,且矩阵的内部和边界上没有其他点。

思路:枚举。

枚举 4 个点,再判断其他点是否在矩阵内或边界上。
复杂度:O(n^5)

优化:所有坐标优先 x轴其次y轴排序。

边界1:枚举第一个点后,下个点肯定是第二个点,且 x 轴相同。
边界2:第三个点肯定在 x 轴右边,且 y 轴与第一个点相等。
边界3:在找到第三个点之前,中间的点的 y 轴都不能在中间。
边界4:找到第三个点后,第四个点肯定是下个点,判断是否可以组成矩阵。

复杂度:O(n^2)

进一步优化见第四题。

三、长度可被 K 整除的子数组的最大元素和

题意:给一个个数组,问所有长度可以整除 k 的子数组的最大数组和。

思路:分段。

可以先划分出所有长度为 k 的子数组,相邻的长度为 k 的子数组之和就是长度为 2k 的子数组。

显然,可以根据起始位置,将长度为 k 的没有交集的子数组进行分组。
每组内连续的子数组可以组成 k 倍长度的子数组。
分组个数为 K,每组内线段个数为 n/k

对于这样每个分组,我们需要找到一个连续的多个分段,使得分段和最大。
这个是典型的最大子数组和。
复杂度:O(K * n/K) = O(n)

for (int i = 1; i <= k; i++) {
  ll pre = 0;
  for (int j = i; j <= n; j += k) {
    int l = j;
    int r = j + k - 1;
    if (r <= n) {
      ll now = preSum[r] - preSum[l - 1];
      pre += now;
      Update(pre);
      if (pre < 0) {
        pre = 0;
      }
    }
  }
}

四、用点构造面积最大的矩形 II

题意:坐标系给n个点,求挑选4个点组成与坐标轴平行的面积最大的矩阵,且矩阵的内部和边界上没有其他点。

思路:与第二题一摸一样,数据范围变大了。
第二题已经优化到O(n),这里需要继续优化。

先回头看下第二题的优化。

优化:所有坐标优先 x轴其次y轴排序。
边界1:枚举第一个点后,下个点肯定是第二个点,且 x 轴相同。
边界2:第三个点肯定在 x 轴右边,且 y 轴与第一个点相等。
边界3:在找到第三个点之前,中间的点的 y 轴都不能在中间。
边界4:找到第三个点后,第四个点肯定是下个点,判断是否可以组成矩阵。

复杂度分析:
寻找第二个点:第一个点的下一个就是第二个点,复杂度O(1)
寻找第三个点:需要跳过边界外的点,寻找边界内x轴最小的时最小的y,复杂度O(n)
寻找第四个点:第三个点的下一个点就是第四个点,复杂度O(1)

可以发现,关键在于确定前两个点后,无法快速找到第三个点,所以只能一个个枚举判断,导致复杂度较低。

p0 对应 X 轴右边的所有点如果都储存在线段树里,则查询区间[P0_y, P1_y]的最小坐标就是第三个点。

问题:X 轴的点枚举完了,如何从线段树中删除 X 轴的点。
方法:所有点按 X 轴分组,即每个 X轴对应一个数组,相同 X 轴的点放在数组里。

问题:Y 轴相同时,线段树里只能储存 X 轴最小的那个点。
方法:删除一个点后,把相同 Y 轴的右边的第一个点加入进来。

问题:删除一个点时,怎么找到相同 Y 轴右边的第一个点。
方法:所有点按 Y 轴分组,分组内按 X 轴排序,即可二分查找得到。

小技巧:所有点按 X 轴与 Y 轴分组后,第一个点找第二个点,以及第三个点找第四个点,都可以二分查找得分。

离散化代码:

vector<ll> yh = {yCoord.begin(), yCoord.end()};
sort(yh.begin(), yh.end());
yh.erase(unique(yh.begin(), yh.end()), yh.end());

const int yn = yh.size();
unordered_map<ll, ll> YH;
for (int i = 0; i < yn; i++) {
  YH[yh[i]] = i;
}

分组代码:

vector<vector<ll>> Yarray(yn);
for (int i = 0; i < n; i++) {
  auto [x, y] = points[i];
  Yarray[YH[y]].push_back(XH[x]);
}
for (int i = 0; i < yn; i++) {
  sort(Yarray[i].begin(), Yarray[i].end());
}

删除 X 轴的所有点代码

if (X0 != preX) {
  // 处理到新的第 X 列,X 列的所有 Y 肯定在线段树里, 更新线段树
  for (ll XY : Xarray[X0]) {
    auto it = upper_bound(Yarray[XY].begin(), Yarray[XY].end(), X0);
    Point p = {INT_MAX, INT_MAX};
    if (it != Yarray[XY].end()) {
      p = {*it, XY}; // Y 轴下个点
    }
    segTree.Update(XY + 1, p);
  }
  preX = X0;
}

查找三个点,计算答案:

// 查找 X1
auto it1 = upper_bound(Xarray[X0].begin(), Xarray[X0].end(), Y0);
if (it1 == Xarray[X0].end()) {
  continue;  // 同一列没有下一个
}
const auto [X1, Y1] = Point{X0, *it1};

// 根据大小顺序关系,区间最小值肯定是右下角坐标
auto [X2, Y2] = segTree.QueryMin(Y0 + 1, Y1 + 1);
if (X2 == INT_MAX || Y2 != Y0) {
  continue;  // 右边没有点了
}

auto it3 = upper_bound(Xarray[X2].begin(), Xarray[X2].end(), Y2);
if (it3 == Xarray[X2].end() || *it3 != Y1) {
  continue;  // 同一列没有下一个
}
const auto [X3, Y3] = Point{X2, *it3};

ans = max(ans, (xh[X3] - xh[X1]) * (yh[Y1] - yh[Y0]));

五、最后

这次比赛第四题其实不难吗,但是离散化 + 线段树 + 各种二分,代码量很是很大的,相当于一个大的模拟题了。

《完》

-EOF-

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

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

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

tiankonguse +
穿越