leetcode 第 466 场算法比赛

作者: | 更新日期:

单调栈、数位DP

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

零、背景

这次比赛第三题二分单调栈,第四题数位DP,没想到过得人这么多。

A: 简单计算
B: 简单计算
C: 二分单调栈
D: 数位DP

排名:200
代码地址: https://github.com/tiankonguse/leetcode-solutions

一、数组元素相等的最小操作次数

题意:每次可以修改一个子数组的值,问修改多少次可以让数组所有元素相等。

思路:简单计算

显然,需要修改时,直接修改整个数组即可。
故需要修改则答案为1,否则答案为0。

二、转换字符串的最小操作次数

题意:给一个小写字母的字符串,每次可以选择值相同的字符全部修改为循环字典序下个字符。
问最少修改几次,所有字符修改为 a

思路:简单计算

由于每次可以修改值相同的字符,所以与字符个数与顺序无关,至于不同值有关。

如果存在两个不同值,显然需要修改小的,等到与大的值一样了,再一次修改。
故答案由非a得最小值决定。

求出这个最小值,再求需要修改的次数即可。

vector<int> count(26, 0);
for (char c : s) {
  count[c - 'a']++;
}
for (int i = 1; i < 26; i++) {
  if (count[i] > 0) {
    return 26 - i;
  }
}
return 0;

三、碗子数组的数目

题意:给一个数组,问有多少个连续子数组,使得子数组中间的值都不大于左右端点的值。

思路:二分单调栈

暴力做法是枚举所有子数组,然后使用线段树求区间最大值,判断是否满足要求,复杂度O(n^2 log(n))

子数组问题,显然需要枚举一个端点,然后在一个数据结构里,判断另一个端点集合里有多少满足要求的个数。

这里枚举右端点,维护左端点的集合。
由于中间的值都需要小于左右端点的值,所以可以使用单调栈,维护一个单调递减的栈。
具体来说,最后一个元素需要入栈,如果之前的元素小于最后一个元素,则都需要出栈。

然后分析这个栈,发现栈分两部分:

如下图,R 部分的值都小于当前右端点,故满足中间的值都小于左右端点的值,但需要排除最后一个元素。
而 L 部分的值,左端点大于右端点,只有最后一个满足要求,再往前的,就存在中间的值大于右端点了。

PS: 为了避免自己写二分查找,这里我使用负数储存,从而可以使用 lower_bound二分查找。

ll ans = 0;
for (int i = 0; i < n; i++) {
  const int v = nums[i];
  if (sta.size() > 1 && Abs(sta.back()) < v) {  // 长度至少为 3
    auto it = std::lower_bound(sta.begin(), sta.end(), -v);
    ans += sta.end() - it - 1;
    if (it != sta.begin()) {
      ans++;  // 只能选择一个大于 右端点的值
    }
  }
  while (!sta.empty() && Abs(sta.back()) < v) {
    sta.pop_back();
  }
  sta.push_back(-v);
}
return ans;

比赛的时候,我是假设右端点大于左端点,然后分别求出 R 部分的。

ll ans1 = bowlSubarraysEx(nums);
std::reverse(nums.begin(), nums.end());
ll ans2 = bowlSubarraysEx(nums);
return ans1 + ans2;

进一步分析单调栈,假设右半部有 R 个元素,则接下来维护单调栈时,需要出栈 R 个元素。
出栈后,如果堆栈还有元素,则显然这个元素就可以当做左端点答案。
故可以边出栈边计算答案。

ll ans = 0;
for (int i = 0; i < n; i++) {
  const int v = nums[i];
  while (!sta.empty() && Abs(sta.back()) < v) {
    sta.pop_back();
    if (!sta.empty()) {
      ans++; // [sta.back(), v] 区间可以组成一个答案
    }
  }
  sta.push_back(-v);
}
return ans;

神奇,答案个数竟然不大于 N。

四、统计二进制回文数字的数目

题意:给一个数字 n,求二进制表示下,有多少不大于n的二进制数字是回文数。

思路:经典的数位DP。

假设 n 的二进制数位为 B,根据状态可以划分为三部分:

1)数字二进制位数小于 B,则可以是任意回文二进制数字,答案固定,可以直接计算出来。
2)对于n位的二进制,从高位开始,如果 n 对应位值是 1, 则可以取值 0,接下来的剩余位数可以是带前缀0的任意回文二进制数字,答案固定,可以直接计算出来。
3)如果取值等于 n 对应的位值,则递归到下一位判断。
3)如果位数越过中界限,需要判断高位镜像过来后,是否不大于 n,不大于了也算一个答案。

允许前缀 0 的 B 位回文个数为 2^{(B+1)/2}
不允许前缀 0 的 B 位回文个数为 2^{(B-1)/2}

ans = 1;  // 0 答案是1
for (int i = 1; i < B; i++) {
  ans += one << ((i - 1) / 2);  // 各位任意值的答案,边界必须为1
}

数位DP 则是从高位开始,枚举取值,依次计算答案,复杂度 log(n)

void Dfs(int k) {
  if (k < B2) {
    // 需要检查低位一半是否大于高位一半,大于了也有一个答案
    for (int i = B2 - 1; i >= 0; i--) {
      if (bits[i] > bits[B - 1 - i]) {
        ans++; // 大于,有答案
        return;  
      } else if (bits[i] < bits[B - 1 - i]) {
        return;  // 小于没答案
      }
    }
    ans++;  // 相等也有一个答案
  } else {
    if (bits[k] == 1) {          // 取 0 时,后面的都是答案
      ans += (one << (k - B2));  // 各位任意值的答案,边界可以是0
    }
    Dfs(k - 1);  // 取值与 bits[k] 相同
  }
}

五、总结

这次比赛前两道题非常简单。
第三题单调栈竟然可以一步步推导出答案很小的结论。
第四题是一个经典的数位DP。
总的看下来,第三题应该比第四题难得,毕竟单调栈需要深度思考,而数位DP只是一个模版题。

《完》

-EOF-

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

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

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

tiankonguse +
穿越