leetcode 周赛 477 - 子集和与容斥

作者: | 更新日期:

很有难度的问题

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

零、背景

这场比赛最后一题比较有难度,需要使用容斥原理来求解。
容斥的时候需要统计子集和,所以还需要使用 DP 来预处理计算子集和。

A: 循环
B: 前缀和
C: 前缀和
D: 容斥+DP

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

一、连接非零数字并乘以其数字和 I

题意:给一个整数,问删除所有数字 0 后,剩余数字拼成的数与所有数字和的乘积是多少?

思路:循环模拟

按题意一位位判断,遇到 0 就跳过,遇到其他数字就进行数字拼接,以及计算数字和。

二、最大平衡异或子数组的长度

题意:给一个数组,求一个最长的子数组,使得子数组的元素异或值为 0,且子数组中奇数和偶数个数相等。

思路:前缀和

前缀不仅需要记录异或值,还需要记录奇数与偶数的差值。

满足要求的前缀:异或值相同,奇数与偶数的差值相同。

map<pair<ll, ll>, ll> H;  // <xor, oddEven> -> index
ll preXor = 0, PreOddEven = 0;
H[{preXor, PreOddEven}] = 0;
int n = nums.size();
ll ans = 0;
for (int i = 1; i <= n; i++) {
  ll v = nums[i - 1];
  preXor ^= v;
  if (v % 2 == 0) {
    PreOddEven++;
  } else {
    PreOddEven--;
  }
  pair<ll, ll> key = {preXor, PreOddEven};
  if (H.count(key)) {  // 最长,不覆盖
    ans = max(ans, i - H[key]);
  } else {
    H[key] = i;
  }
}

三、连接非零数字并乘以其数字和 II

题意:给一个字符串和若干询问。
问对每个区间子串,删除其中所有数字 0 后,剩余数字拼成的数与所有数字和的乘积是多少?

思路:前缀和

先预处理字符串,预处理出每个位置的前缀和 preSum[i],以及非 0 数字的前缀值与个数 preNum[i]={val, count}

vector<ll> preSum(n + 1, 0);
vector<pair<ll, ll>> preNum(n + 1, {0, 0});  // <val, count>
vector<ll> base(n + 1, 1);
for (int i = 1; i <= n; i++) {
  ll v = s[i - 1] - '0';
  preSum[i] = (preSum[i - 1] + v) % mod;
  preNum[i] = preNum[i - 1];
  base[i] = (base[i - 1] * 10) % mod;
  if (v == 0) continue;
  preNum[i].first = (preNum[i].first * 10 + v) % mod;
  preNum[i].second++;
}

假设询问的是 [l,r]
则区间内数字和为 sum = preSum[r] - preSum[l - 1]
则区间内数字个数为 k = preNum[r].count - preNum[l-1].count
区间内的数字值为 val = preNum[r] - preNum[l-1] * 10^k
答案为 val * sum

vector<int> ans;
ans.reserve(queries.size());
for (auto& q : queries) {
  int l = q[0] + 1, r = q[1] + 1;
  ll sum = (preSum[r] - preSum[l - 1] + mod) % mod;
  auto [leftVal, leftCount] = preNum[l - 1];
  auto [rightVal, rightCount] = preNum[r];
  ll leftBaseVal = (leftVal * base[rightCount - leftCount]) % mod;
  ll val = (rightVal - leftBaseVal + mod) % mod;
  ans.push_back((sum * val) % mod);
}
return ans;

四、有效子序列的数量

题意:给一个数组,数组的强度定义为所有元素的按位或值,问存在多少个非空子序列,删除后可以使得数组的强度降低。

思路:容斥+DP

显然,要使数组的强度降低,至少把某个 Bit 位为 1 的元素全部删除。

这个是一个典型的容斥问题。
先把所有 1 个 Bit 位的元素都删除,此时所有 2 位 Bit 位的元素被重复删除,需要加回来。
依次递推,奇数个 Bit 位的元素进行删除,偶数个 Bit 位的元素进行添加。

有了这个规律,我们可以通过枚举有效位子集的方式,来计算容斥。
假设当前有效位是 mask,计算出所有包含这些 Bit 位的元素个数 count_A
count_A 个元素删除后就可以满足要求,故其他元素可以任意选择,共 2^(n - count_A) 种方案。

复杂度:O(n*2^n)
通过的测试用例数为 987 / 1041 个。

ll ans = 0;
for (int realMask = 1; realMask < kRealMaxMask; realMask++) {
  ll mask = getMask(realMask);
  ll oneCount = __builtin_popcount(mask);
  ll count_A = 0;  // 子集个数
  for (ll x : nums) {
    if ((x & mask) != 0) {
      count_A++;
    }
  }
  ll count_B = n - count_A;
  ll tmp = powers[count_B];
  if (oneCount % 2 == 1) {
    ans = (ans + tmp) % mod;
  } else {
    ans = (ans - tmp + mod) % mod;
  }
}
return ans;

可以发现,容斥是没办法优化了。
但是计算一个子集的个数是可以预处理优化的。

包含 Bit 位的元素个数 count_A 与不包含 Bit 位的元素个数 count_B 之和为 n

如何计算至少包含某些 Bit 位的元素个数?
这是一个典型的子集 DP 问题。

定义状态:dp[mask] 表示包含 mask Bit 位的元素个数。

状态转移方程如下:

// mask & (1 << i) != 0
dp[mask] = dp[mask] + dp[mask ^ (1 << i)];

故,预处理出所有子集的个数,即可优化到 O(n*log(n))

const ll kMaxMask = 1 << kMaxBit;
vector<ll> preMaskCount(kMaxMask, 0);
for (ll x : nums) {
  preMaskCount[x]++;
}
for (int i = 0; i < kMaxBit; i++) {
  for (int mask = 0; mask < kMaxMask; mask++) {
    if (mask & (1 << i)) {
      preMaskCount[mask] += preMaskCount[mask ^ (1 << i)];
    }
  }
}

五、最后

这次比赛的第四题比较有难度,需要使用容斥原理来求解。
容斥的时候需要统计子集和,所以还需要使用 DP 来预处理计算子集和。
两个子问题都比较难,加起来就更难了。

《完》

-EOF-

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

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

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

tiankonguse +
穿越