leetcode 周赛 517 - 分组背包

作者: | 更新日期:

分组背包

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

零、背景

这次比赛比较简单,最后两道题还一模一样,核心算法不变,输入的构造调整下就可以通过了。

本场题型概览如下。

A 题:统计。
B 题:快速幂。
C 题:背包。
D 题:BFS+背包。

一、统计特殊整数个数

题意:给一个数组,问哪些数字在数组中的位置是连续的。

思路:统计

分别统计每个值出现的个数、左边界、右边界。
然后计算左右边界内的数字是否等于出现的个数。

for (int i = 0; i < n; i++) {
  cnt[nums[i]]++;
  if (leftPos.find(nums[i]) == leftPos.end()) {
    leftPos[nums[i]] = i;
  }
  rightPos[nums[i]] = i;
}
int ans = 0;
for (auto [v, c] : cnt) {
  int l = leftPos[v];
  int r = rightPos[v];
  if (r - l + 1 == c) {
    ans++;
  }
}
return ans;

二、解码值之和

题意:给一个数组,每个值都是二元组 (x,y) 编码后的值。
编码规则:假设数组的值为 v,最低位为宽度 w=v%10,其余位为 d=floor(v / 10)
x 的值为 d 的前 w 个数字,即 x = d[0:w]
y 的值为 d 的其余数字,即 y=d[w:]
求解码后所有 x^y 的之和。

思路:快速幂

按题意解码出 x 与 y,然后计算快速幂。

const ll w = v % 10;
const ll d = floor(v / 10);
const string s = to_string(d);
ll bit = s.size();
ll x = 0; // [0,w)
for (int i = 0; i < w; i++) {
  x = x * 10 + (s[i] - '0');
}
ll y = 0; // [w,bit)
for (int i = w; i < bit; i++) {
  y = y * 10 + (s[i] - '0');
}
ll V = qpow(x, y, mod);
ans = (ans + V) % mod;

三、构造子集和的最少操作次数 I

题意:给一个数组,每个数字可以操作变为其他数字,问最少操作多少次才能使得数组的一个子集之和等于 sum。

操作: 1)选择一个数字乘以2。
2)选择一个数字除2取整。

要求:对于同一个位置,如果有乘法与除法,乘法必须在前面。

数据范围:
数组大小:100
值大小:500
sum 大小:5000

思路:分组背包

首先注意要求,乘法在除法前面,乘与除抵消,意味着只能乘或者只能除,从而扩展出所有数字。
故,一个数字最多可以扩展出 log(V) 个数字。

g.resize(n);
for (int i = 0; i < n; i++) {
  const int v = nums[i];
  g[i].push_back({v, 0});
  for (int V = v * 2, t = 1; V <= sum; V *= 2, t++) {
    g[i].push_back({V, t});
  }
  for (int V = v / 2, t = 1; V > 0; V /= 2, t++) {
    g[i].push_back({V, t});
  }
  sort(g[i].begin(), g[i].end());
}

之后就是经典的分组背包了。
背包大小为 sum,每个数字一个分组,分组内有 log(V) 个数字。

状态定义:dp[i][sum]
含义:前 i 个分组,得到 sum 的最少操作次数。

状态转移方程:

dp(i,sum) = min(dp(i-1,sum), dp(i-1, sum - vj) + tj);  

解释:这个分组最多选择一个,所以分为不选择与选择某一个。
不选择就是 dp(i-1, sum)
选择一个 (vj, tj),其中 vj 是数字,tj 是得到这个数字的操作次数,则只能从上个状态 dp(i-1, sum-vj) 转移过来。

复杂度:O(n*sum*log(V))

vector<vector<int>> dp;
vector<vector<pair<int, int>>> g;  // <val, times>
// dp[0][0] = 0;

int Dfs(int i, int sum) {
  int& ret = dp[i][sum];
  if (ret != -1) return ret;
  ret = MaxVal;
  if (i == 0) {
    return ret;  // 只有 dp[0][0] 合法
  }
  // 不选择
  ret = min(ret, Dfs(i - 1, sum));
  for (auto [v, t] : g[i - 1]) {
    if (v > sum) break;
    ret = min(ret, Dfs(i - 1, sum - v) + t);
  }
  return ret;
}

四、构造子集和的最少操作次数 II

题意:同第三题,没有乘法在除法前面的限制。

思路:BFS + 分组背包。

可以发现,解除限制后,只是得到每组数字的方法变了。
原先数字是向上乘2与向下除2,得到的是一条链。

解除限制后,奇数向下除二后再向上乘二可以得到新的数字。
所以不断扩展,得到的是一个树。

要得到这些数字,并计算每个数字的最小操作次数,最简单的方法是使用 BFS 来搜索。

int maxVal;
void Bfs(const int i, const int v, const int sum) {
  queue<pair<int, int>> que;
  auto Add = [&](int V, int t) {
    if (V == 0 || V > maxVal) return;
    if (lastPos[V] == i) return;
    lastPos[V] = i;
    g[i].push_back({V, t});
    que.push({V, t});
  };
  Add(v, 0);
  while (!que.empty()) {
    auto [v, t] = que.front();
    que.pop();
    Add(v * 2, t + 1);
    Add(v / 2, t + 1);
  }
}

实际上,根据这个图,我们也可以直接不重不漏的构造出所有数字。
即向下除2时,判断是否是奇数,是奇数了,乘2就可以得到新的分支。

void Add(int i, int v, int sum, int t) {
  if (v == 0) return;
  for (int V = v; V <= sum; V *= 2, t++) {
    g[i].push_back({V, t});
  }
}
for (int i = 0; i < n; i++) {
  const int v = nums[i];
  Add(i, v * 2, sum, 1);
  for (int V = v, t = 0; V > 0; V /= 2, t++) {
    g[i].push_back({V, t});
    if (V % 2 == 1) {
      Add(i, V / 2 * 2, sum, t + 2);
    }
  }
  sort(g[i].begin(), g[i].end());
}

五、最后

这次比赛题目比较简单,最后两道题算法还一样,算是只有三道题了。

《完》

-EOF-

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

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

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

tiankonguse +
穿越