leetcode 周赛 480 - 高级线段树

作者: | 更新日期:

高级线段树,建议大家都学习一下

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

零、背景

这次比赛最后一题是经典的高级线段树,整体难度不算高,本篇主要记录四道题的解题思路。

A: 排序
B: 模拟
C: 贪心
D: 高级线段树

最终排名:95
代码仓库https://github.com/tiankonguse/leetcode-solutions

一、最大和最小 K 个元素的绝对差

题意:给一个数组,问最大 K 个数之和与最小 K 个数之和的绝对差。

思路:排序,然后枚举最大和最小的 K 个数,求和,求差。

sort(nums.begin(), nums.end());
int ans = 0;
for (int i = 0; i < k; i++) {
  ans += nums[n - 1 - i] - nums[i];
}

二、反转元音数相同的单词

题意:给一个字符串,空格分隔单词。对于后续的单词,如果元音字母个数与第一个单词相同,则反转。

思路:模拟

思路1:首先字符串分割为单词列表,然后遍历单词列表,统计元音字母个数,如果与第一个单词相同,则反转。

思路2:通过左右指针边分割单词,边在原地翻转。

int firstCnt = 0;
int vowelCnt = 0;
int l = 0;
for (int r = 0; r < n; r++) {
  if (s[r] == ' ') {
    if (l == 0) {
      firstCnt = vowelCnt;  // 记录第一个单词的元音数量
    } else {
      if (firstCnt == vowelCnt) {
        reverse(s.begin() + l, s.begin() + r);
      }
    }
    l = r + 1;
    vowelCnt = 0;
  } else {
    if (h.count(s[r])) vowelCnt++;
  }
}
if (firstCnt == vowelCnt) {
  reverse(s.begin() + l, s.end());
}

三、使循环数组余额非负的最少移动次数

题意:给一个循环数组,每次可以对相邻元素操作一次,其中一个元素加一,一个元素减一。
问最少多少次操作可以使数组余额非负。
限制:最多只有一个元素为负。

思路:贪心

如果元素和为负,显然没有答案。

如果所有元素都为非负,那么显然不需要操作,答案就是 0。

当有一个位置为负时,以该位置为起点,向两边扩散,把路径上的值移动到当前位置,直到当前位置为 0。
移动的操作次数等于值乘以距离。

注意事项1:数组是循环的,左右指针可能越界,越界后需要修正后取值。
注意事项2:最后一次操作,不需要把对应位置的所有值都移动到当前位置,剩多少就移动多少,即取最小值。
注意事项3:值乘以距离时,使用 int64,避免 int32 越界。

ll ans = 0;
int leftNum = -balance[pos];
for (int r = 1; leftNum > 0; r++) {
  int L = (pos - r + n) % n;
  int R = (pos + r) % n;
  ans += min(leftNum, balance[L]) * ll(r);
  leftNum -= min(leftNum, balance[L]);
  ans += min(leftNum, balance[R]) * ll(r);
  leftNum -= min(leftNum, balance[R]);
}
return ans;

四、使子字符串变交替的最少删除次数

题意:给一个 01 字符串,有修改和询问操作。
修改:对一个位置的值进行翻转。
询问:问一个区间内最少删除多少个字符,可以使该区间内的 01 交替。

思路:高级线段树

先来看询问操作:给定一个区间字符串,问最少删除多少个字符,使其满足 01 交替。

显然,删除的字符是由于相邻的字符相等,不满足 01 交替。
所以,最少删除的字符就是区间内相邻字符相等的数量。
即等价于问区间内相邻字符相等的数量。

单点更新,区间查询,典型的线段树问题。

SegTree segTree;
segTree.Init(nums);
segTree.Build();

vector<int> ans;
ans.reserve(queries.size());
for (auto& q : queries) {
  int op = q[0];
  if (op == 1) {
    int l = q[1] + 1;
    nums[l - 1] = 1 - nums[l - 1];  // 翻转
    segTree.Update(l, nums[l - 1]);
  } else {
    int l = q[1] + 1, r = q[2] + 1;
    auto [sum, left, right] = segTree.Query(l, r);
    ans.push_back(sum);
  }
}
return ans;

所以来看,线段树需要维护什么信息?

第一:需要存储区间内相邻字符相等的数量。
第二:两个区间合并时需要判断合并的边界是否相等,所以需要存左右边界的值。

由此,线段树需要维护的信息是:区间内相邻字符相等的数量,区间左右边界值。

vector<ll> sumVal;    // 区间内,相邻相等的个数
vector<ll> leftVal;   // 区间内,最左边的值
vector<ll> rightVal;  // 区间内,最右边的值

区间合并时,根据三个信息的定义,分别进行更新即可。

// 合并函数,按需进行合并
void PushUp(int rt, int l, int r) {
  leftVal[rt] = leftVal[rt << 1];
  rightVal[rt] = rightVal[rt << 1 | 1];
  sumVal[rt] = sumVal[rt << 1] + sumVal[rt << 1 | 1];
  if (rightVal[rt << 1] == leftVal[rt << 1 | 1]) {
    sumVal[rt]++;
  }
}

询问时,需要返回三个信息,方便合并左右两个区间。

// 返回区间内 sum, leftVal, rightVal
tuple<ll, ll, ll> Query(int L, int R, int l = 1, int r = maxNM, int rt = 1) {
  if (L <= l && r <= R) {
    return {sumVal[rt], leftVal[rt], rightVal[rt]};
  }
  int m = (l + r) >> 1;
  if (R <= m) {  // 区间都在左边
    return Query(L, R, l, m, rt << 1);
  } else if (m < L) {  // 区间都在右边
    return Query(L, R, m + 1, r, rt << 1 | 1);
  } else {  // 区间跨左右
    auto [leftSum, leftLeft, leftRight] = Query(L, R, l, m, rt << 1);
    auto [rightSum, rightLeft, rightRight] = Query(L, R, m + 1, r, rt << 1 | 1);
    return {leftSum + rightSum + (leftRight == rightLeft), leftLeft, rightRight};
  }
}

五、最后

这次比赛第三题有点坑,一开始没看到只有一个负数,想了好久好久,发现挺难的。
再一看题目难度等级,是中等的,所以继续读题,发现只有一个负数,直接贪心扫描即可。

第四题是经典的高级线段树的最基本用法,左右区间合并时有依赖,所以需要存下依赖信息,合并时根据依赖信息进行合并。

《完》

-EOF-

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

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

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

tiankonguse +
穿越