NOIP 2003 基础组算法题解

作者: | 更新日期:

涉及动态规划、高精度等算法

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

零、背景

今天继续分享 2003 年 NOIP 普及组题解。

一、乒乓球

题意:告诉你两个人乒乓球每个球的胜负情况。
问在 11 分制和 21 分制情况下的比分。
规则:对于 X 分制,至少打 X 个球,且需要满足分差大于或等于 2 一局才算结束。

思路:循环

这道题最难的是读懂题意。
对于 11 分制,每局结束的规则是先打 11 个球,然后看是否满足分差大于等于 2,不满足就一直打。

读懂了题意,按题意计算即可。

注意事项:由于比赛每打完,最后一局都没得分就是 0:0

void Solver(int B) {
  int W = 0, L = 0;
  for (auto c : S) {
    if (c == 'W') {
      W++;
    } else if (c == 'L') {
      L++;
    }
    if (max(W, L) >= B && abs(W - L) >= 2) {
      printf("%d:%d\n", W, L);
      L = 0, W = 0;
    }
  }
  printf("%d:%d\n", W, L);
}

二、数字游戏

题意:n 个数字摆成一个圆圈,问将数字划分为 m 连续的部分。
每部分求和后模 10 得到 m 个非负数,最后这些非负数相乘。
求可以得到的最大值和最小值。
数据范围:n=50m=9

思路:动态规划

数字个数很小,枚举边界起始位置,问题就转化为了 n 个数字一排分成 m 组,求最值。

状态定义:dp(n,m) 前 n 个数字分成 m 组的最值。
状态转移方程:

dpMax(n,m) = max(dpMax(n-i, m-1) * Mod(sum(n-i+1, n)));
dpMin(n,m) = min(dpMin(n-i, m-1) * Mod(sum(n-i+1, n)));

复杂度:O(n^3 * m)

pair<ll, ll> Dfs(const int l, const int r, const int m) {
  if (dp[l][r][m][0] != -1) return {dp[l][r][m][0], dp[l][r][m][1]};
  if (m == 1) {
    dp[l][r][m][0] = dp[l][r][m][1] = Sum(l, r);
    return {dp[l][r][m][0], dp[l][r][m][1]};
  }
  ll minAns = INFL, maxAns = -INFL;
  for (int i = r; i - l + 1 >= m; i--) {
    auto [minV, maxV] = Dfs(l, i - 1, m - 1);
    ll sum = Sum(i, r);
    minAns = min(minAns, minV * sum);
    maxAns = max(maxAns, maxV * sum);
  }
  dp[l][r][m][0] = minAns;
  dp[l][r][m][1] = maxAns;
  return {minAns, maxAns};
}

三、栈

题意:给 n 个数字在栈中,现在可以使用一个中转栈,最终把数字移动到第三个栈中。
问最终第三个栈有多少种不同的排列。

思路:动态规划

状态定义:dp(n) n 个不同数字最终的排列数。

状态转移方程:
第 1 个数字可以借助中转栈,位于排列的第 i 个位置。
此时,第二个到第 i 个数字都需要先到达第三个栈,剩余的数字是独立的子问题。

dp(n) = sum(dp(i-1) * dp(n-i))  

复杂度:O(n^2)

ll Dfs(int n) {
  ll& ret = dp[n];
  if (ret != -1) {
    return ret;
  }
  ret = 0;
  for (int i = 0; i < n; i++) {
    ret += Dfs(i) * Dfs(n - i - 1);
  }
  return ret;
}

四、麦森数

题意:求 2^p-1 的位数与最低 500 位数字。
p 范围 [1000,3100000]

思路:高精度乘法

如果直接使用高精度乘法,只能得 50 分。
使用快速幂与高精度乘法,只能得 60 分。

所以,我就思考为何 p 最小值是 1000 而不是 1。

首先可以发现,2^p 的个位肯定不是 0,故减一不影响位数。
2^p 的位数等价于 log10(2^p),转化一下就是 p * log10(2)
这个值就是十进制的位数。

p 太小时,会存在精度问题。
p 最小值限制为 1000,显然是大于 1000 时就没有精度问题。

故直接使用公式计算位数。
而高精度计算时,剪枝只保留最低 500 位即可。

BigNum& Smp() {
  while (data.size() > 1 && data.back() == 0) {
    data.pop_back();
  }
  while(data.size() > 500){
      data.pop_back();
  }
  return *this;
}
BigNum operator*(const BigNum& other) const {
  BigNum res;
  res.data.resize(data.size() + other.data.size() + 1, 0);
  for (int i = 0; i < data.size(); i++) {
    ll carry = 0;
    int pos = i;
    for (int j = 0; j < other.data.size(); j++) {
      res.data[pos] += data[i] * other.data[j] + carry;
      carry = res.data[pos] / 10;
      res.data[pos] %= 10;
      pos++;
    }
    while (carry) {
      res.data[pos] += carry;
      carry = res.data[pos] / 10;
      res.data[pos] %= 10;
      pos++;
    }
  }
  return res.Smp();
}

五、最后

这次比赛有点坑,直接写高精度会超时。
必须本地手动验证下 1000 左右数学公式计算的位数是否正确,然后再直接使用数学公式。

《完》

-EOF-

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

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

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

tiankonguse +
穿越