leetcode 第 470 场算法比赛

作者: | 更新日期:

多维数位 DP

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

零、背景

这次比赛是国庆期间,那天正在重庆金佛山爬山,所以没参加比赛。
国庆后做了一下题目,发现这次比赛比较简单。

A: 循环
B: 贪心
C: 栈
D: 数位DP

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

一、计算交替和

题意:给一个数组,求偶数位数字之和减去奇数位数字之和。

思路:循环处理,偶数加奇数减即可。

二、按位异或非零的最长子序列

题意:给一个数组,求最长的子序列的长度,使得子序列的元素异或值非0。

思路:贪心。

题目只要求最长的异或非 0 子序列,并不要求最大的异或值。

异或的性质为相同元素异或得0,不同元素异或得非0。

将所有元素当做选择的子序列,进行异或。
如果非 0,则找到最长的子序列,就是数组的长度。
如果为 0,且存在非 0 元素 v,则去掉 v 后剩下的元素异或值为 v,满足非 0,此时最长的子序列长度为数组长度减一。
如果为 0,没找到非 0 元素,说明所有元素都为 0,不存在子序列,答案就是 0。

所以这里除了对所有元素求异或外,还需要判断是否存在非0元素。

l sum = 0;
ll num = 0;
for (ll v : nums) {
  sum ^= v;
  if (v != 0) {
    num = 1;
  }
}

int n = nums.size();
if (sum != 0) {
  return n;
}
if (num == 1) {
  return n - 1;
}
return 0;

三、移除K-平衡子字符串

题意:给一个括号序列,连续 k 个左括号与右括号可以消除掉。问不断进行消除,最终剩余哪些字符。

思路:栈

如果是 1 个匹配的括号进行消除,是典型的栈匹配问题。
如果是 k 个匹配的括号,则需要使用带计数的栈。

当遇到 k 个右括号时,尝试去判断上个括号的个数是否大于等于 k 个。

vector<pair<char, int>> nums;
nums.reserve(n);
for (auto c : s) {
  // 更新计数栈
  if (nums.empty() || nums.back().first != c) {
    nums.emplace_back(c, 1);
  } else {
    nums.back().second++;
  }
  // 尝试匹配
  const int sz = nums.size();
  if (sz >= 2) {
    auto [rightC, rightCount] = nums[sz - 1];
    auto [leftC, leftCount] = nums[sz - 2];
    if (rightC == ')' && leftC == '(' && rightCount == k && leftCount >= k) {
      nums.pop_back();
      nums.pop_back();
      if (leftCount > k) {
        nums.emplace_back(leftC, leftCount - k);
      }
    }
  }
}

最后计数栈转化为字符串。

string ans;
ans.reserve(n);
for (auto [c, count] : nums) {
  ans.append(count, c);
}
return ans;

四、统计和为 N 的无零数对

题意:给一个数字 n,问存在多少二元组 (a,b),满足 a+b=n
要求 a 和 b 的 10 进制数字里面不存在 0。

思路:数位DP

n 很多,所有没法枚举所有二元组。

不过也很容易发现,每一位的答案只与相邻两位有关。

上一位关系:当前位可能进位,上一位需要减一。
下一位关系:下一位可能进位,当前位需要减一。

故可以定义状态 f(i, flag) 代表上一位进位状态为 flag 时,i 位之后的数字的答案数。

状态转移方程:枚举 a 和 b 的所有可能,判断考虑进位的情况下,求和是否等于当前数字。

const int v = s[p] - '0';
ll& ans = dp[flag][p];
ans = 0;
for (int i = 1; i <= 9; i++) {
  for (int j = 1; j <= 9; j++) {
    // 情况1: 下一位不进位
    if (i + j == v + flag * 10) {
      ans += Dfs(p + 1, 0);
    }
    // 情况2: 下一位进位
    if (i + j == v + flag * 10 - 1) {
      ans += Dfs(p + 1, 1);
    }
  }
}

这个代码会发现第三个样例无法通过。
分析原因,当前代码没有考虑前导0, 即要求 a 和 b 的位数与 n 完全相等。

考虑前导 0 的情况,需要再增加一个维度来标识 a 或 b 当前是否处于前导 0 状态。

状态定义:f(i, flag, mask) 代表上一位进位状态为 flag,a 和 b 的前导 0 状态为 mask 时,i 位之后的数字的答案数。

mask 的值分为四种情况:0 表示都是前导 0,3 表示都没有前导 0,1 和 2 分别代表一个有前导 0,一个没有前导 0。

此时,a 和 b 需要从 0 开始枚举,需要根据 mask 与枚举值,决定当前是否合法。
合法了,计算出新的 mask,传递给子状态。

const int v = s[p] - '0';
ll& ans = dp[flag][p][mask];
ans = 0;
for (int i = 0; i <= 9; i++) {
  for (int j = 0; j <= 9; j++) {
    int newMask = mask;
    if (i != 0) newMask |= 1;
    if (j != 0) newMask |= 2;
    // 情况1: 下一位不进位
    if (i + j == v + flag * 10 && Check(i, j, mask)) {
      ans += Dfs(p + 1, 0, newMask);
    }
    // 情况2: 下一位进位
    if (i + j == v + flag * 10 - 1 && Check(i, j, mask)) {
      ans += Dfs(p + 1, 1, newMask);
    }
  }
}

只有两种情况不合法:当一个数字已经不处于前导 0 状态时,选择了 0,其他情况都是合法的。

bool Check(int i, int j, int mask) {
  int iMask = mask & 1;
  int jMask = (mask >> 1) & 1;
  if (iMask == 1 && i == 0) return false;
  if (jMask == 1 && j == 0) return false;
  return true;
}

五、最后

这次比赛整体比较简单,第二题是贪心,第三题是计数栈,第四题是数位 DP,都属于基础题。

《完》

-EOF-

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

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

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

tiankonguse +
穿越