解题报告 smoj 2019初二创新班(2019.3.31)

Posted longlongzhu123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解题报告 smoj 2019初二创新班(2019.3.31)相关的知识,希望对你有一定的参考价值。

时间:2019.4.5

比赛网址

T1:单人游戏

题目描述

棋盘由N个格子排成一行,从左到右编号为1到N,每个格子都有一个相关的价值。
最初,棋子位于第1个格子上,当前方向是向右的。

在每个回合中,棋子在当前方向上行走零步或多步,每一步就是走一个格子。然后在下一回合中,棋子的方向反转。

一开始,玩家总得分是0分。

每次棋子从格子A移动到格子B,那么从A至B这一段连续的格子的得分都会累加到玩家的总得分去。

如果某次移动会使得玩家总得分小于0,那么玩家肯定不会进行这样的移动。

如果允许玩家最多进行k次移动,输出玩家获得的最高总得分。

分析

---前方大量证明预警---

大眼观察样例,我们发现:玩家跳跃多次后,总会在一个区间上反复跳跃。我们称这个区间为“环”。

举个例子:如图,区间([8, 10])(红色部分)就是“环”

技术图片

但是,对于所有情况游戏都有环吗?下面我们来证明这一点。

证明:游戏必定存在环

假如我们将每次跳跃后的得分贡献(就是题中的部分和)组成一个序列(Q),考虑(Q)的最后一项(Q_k)

首先,(Q_k)必定是整个序列中最大的。不然前面肯定存在一个最大值(Q_i),这样我们就可以在(i)这一步上反复跳跃,让序列(ge i)的位置都变成(Q_i),比原序列答案要优。

其次,与(Q_k)相同的数必定占据(Q)中最后连续的位置。不然前面也会存在一个(Q_i = Q_k),且(i)不在最后连续的位置,我们可以在(i)这一步上反复跳跃,让序列(ge i)的位置都变成(Q_i),仍比原序列答案要优。

综上所述,序列(Q)中必定存在一个最大值,且这个最大值占据了(Q?)最后连续的位置。这个最大值(和这些最大的位置)就是“环”。


在证明的过程中,我们发现环的得分贡献是最大的。因此可以发现以最少的跳跃到达环是最佳选择。

证明:以最短路径到达环必定最优

假设不以最短路径到达环,那么在到达环的路径上肯定会在某个地方多转几下。
又因为环的得分贡献最大,故这在别的地方多转的几下肯定不如跑到环上去转。
故以最短路径到达环必定最优(当然,在保证路径最短的情况下,要挑得分高的路径跳)。


另外,我们还发现一个性质:每一次移动都会在环前面的格子进行

即:若环是区间([l, r]?),那么我们不会走到(> r?)的位置去。这能为我们的程序提供一定的便利(例如无后效性)

证明:移动时不可能越过环的结尾

如图,假设环是区间([l, r])(j)是环后(> r)的一个位置。我们要从(i)跳到(l)上,并顺便在环上“蹭”一下,有两种选择:

  1. 直接从(i?)跳到(r?),再从(r?)跳到(l?)
  2. (i?)跳到(j?),再从(j?)跳到(l?)

这两种方案对得分的贡献分别是(sum (i, r) + sum (l, r))(sum (i, j) + sum (l, j) = sum (i, r) + sum (l, r) + sum(r + 1, j) imes 2)

由上面证明环的得分贡献最大这条性质知(sum (r + 1, j) le 0?),不然当前的环还可以向右“扩张”,不会对最终答案(即题目输出)造成影响。

故第二种方案不可能比第一种方案优,移动时越过环的结尾只会吃力不讨好。

技术图片


由上面这些证明可以得出一个贪心策略:若已知环的位置,求出到达环的最短路径,并在满足路径最短时得分最大。又有移动时不可能越过环的结尾,此题的解决已经很明显了。

DP实现

(suf(i)?)为以(i?)结尾的最大部分和。一边计算前缀和时一边维护(min1?),表示(le i?)中前缀和的最小值。(suf(i) = sum(i) - min1?)

(f(i))为从起点开始跳到(i)的最少步数。设(g(i))为在满足(f(i))最小时当前得分的最大值。

若我们要从(j)跳到(i),检查一下(g(j) + sum(j + 1, i))是否大于等于0。若为正数,直接更新答案即可。若为负数,说明无法一步从(j)跳到(i),需要在(suf(j))上跳若干(偶数)次。用(left lceil dfrac {-(g(j) + sum(j + 1, i))} {suf(j) imes 2} ight ceil imes 2)求出需要跳跃的次数,并更新答案。

更新答案时,若目前(f)(f(i))相同,取(g(i)?)大的答案。

环可能不是原棋盘中的最大部分和。因此我们需要枚举环。若环结尾是(i),根据贪心可以选择(suf(i))这一段。结果就是(g(i) + (k - f(i)) * suf(i))

---细节见代码---

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int kMaxN = 100 + 10;
const LL kInf = 9000000000000000000ll;
// 9e18
int T, n;
LL k, a[kMaxN];
LL min_sum;
LL sum[kMaxN], suf[kMaxN];
LL f[kMaxN], g[kMaxN];
LL ans;
inline LL GetSum(int l, int r) {
  return sum[r] - sum[l - 1];
}
inline LL DivCeil(LL a, LL b) {
  return (a + b - 1) / b;
}
void Debug() {
  printf("a:
  ");
  for (int i = 1; i <= n; i++) printf("%lld ", a[i]);
  printf("
sum:
  ");
  for (int i = 1; i <= n; i++) printf("%lld ", sum[i]);
  printf("
suf:
  ");
  for (int i = 1; i <= n; i++) printf("%lld ", suf[i]);
  printf("
f:
  ");
  for (int i = 1; i <= n; i++) printf("%lld ", f[i]);
  printf("
g:
  ");
  for (int i = 1; i <= n; i++) printf("%lld ", g[i]);
  printf("
");
}
int main() {
  freopen("2843.in", "r", stdin);
  freopen("2843.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    scanf("%d %lld", &n, &k);
    min_sum = 0;
    for (int i = 1; i <= n; i++) {
      scanf("%lld", &a[i]);
      sum[i] = sum[i - 1] + a[i];
      min_sum = min(min_sum, sum[i]);
      suf[i] = sum[i] - min_sum;
    }
    for (int i = 1; i <= n; i++) {
      if (sum[i] >= 0) {
        f[i] = 1; g[i] = sum[i];
      } else {
        f[i] = kInf; g[i] = 0;
      }
      for (int j = 1; j <= i - 1; j++) {
        LL score = g[j] + GetSum(j + 1, i); // 从j跳来后的得分
        if (score >= 0) { // 如果可以直接从j跳来
          if (f[j] < f[i]) { // 从j转移更优
            f[i] = f[j];
            g[i] = score;
          } else if (f[j] == f[i]) { // 尝试更新g数组
            g[i] = max(g[i], score);
          }
        } else if (suf[j]) {
          LL times = DivCeil(-score, suf[j] * 2) * 2;
          if (f[j] + times < f[i]) {
            f[i] = f[j] + times;
            g[i] = score + times * suf[j];
          } else if (f[j] + times == f[i]) {
            g[i] = max(g[i], score + times * suf[j]);
          }
        }
      }
    }
    // Debug();
    ans = 0;
    for (int i = 1; i <= n; i++) {
      if (f[i] <= k) {
        ans = max(ans, g[i] + (k - f[i]) * suf[i]);
      }
    }
    printf("%lld
", ans);
  }
  return 0;
}

T2:赚金币

题目描述

在游戏中,你刚刚建立了(a)个工厂并聘请了(b)专家。不幸的是,你现在还没有留下金币,你想以最快的速度赚到(target)金币。游戏进行多轮,在一轮中,您获得(a imes b)单位的黄金,其中(a)是工厂数量,(b)是您目前拥有的专家数量。在每轮结束时,您可以建立更多工厂并雇用更多专家。建立一个新工厂或雇用一个新的专家成本是(price)金币。只要您能负担得起,您拥有的工厂和专家数量就没有限制。至少要多少轮游戏,才能完成目标?

数据范围(Large 1 le a, b, price, target le 10^{12})

分析

观察发现数据范围都很大,且并没有单调性(游戏轮数并不能二分)

我们发现一个贪心策略:

  • 每次购买一件物资(工厂或专家),只购买数量少的一方。

    证明:显然,若(a le b),那么((a + 1) imes b ge a imes (b + 1)),而我们想让每局物资数量的乘积尽量高。

这个策略提示我们:完成目标时,物资中数量少的一方,数量最多为(f{sqrt n})

也就是说,物资购买的总数最多为(2sqrt n)。这启示我们枚举购买的物资数。

这时,我们又发现:如果决定要购买(k)件物资,那么这些物资越快买完越好。这样我们就可以空出时间来积攒金币了。

若当前拥有(a)个工厂和(b)位专家,当前金币数量是(money),我们可以通过(left lceil dfrac {max(price - money, 0)} {a imes b} ight ceil)算出下一次购买需要的回合数。

同理可以通过(left lceil dfrac {max(target - money, 0)} {a imes b} ight ceil)算出以当前的物资达到目标需要的回合数。

直接从(1)(2 imes 10^6)枚举购买的物资数即可。

代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL kInf = 1e13;
const int kMaxBuy = 2000000 + 10;
int T;
LL a, b, price, target;
LL money, round_;
LL ans;
inline LL DivCeil(LL a, LL b) {
  return (a + b - 1) / b;
}
int main() {
  freopen("2844.in", "r", stdin);
  freopen("2844.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    scanf("%lld %lld %lld %lld",
        &a, &b, &price, &target);
    if (a > b) swap(a, b);
    if (a > kMaxBuy) {
      printf("1
");
    } else {
      ans = kInf;
      round_ = 0;
      money = 0;
      if (a * b >= target) {
        printf("1
");
        continue;
      }
      for (int i = 0; i <= kMaxBuy; i++) {
        // 购买
        LL times = DivCeil(max(price - money, 0ll), a * b);
        round_ += times;
        money = money + a * b * times - price;
        a++;
        if (a > b) swap(a, b);
        // 更新答案
        if (money >= target) {
          ans = min(ans, round_);
          break;
        } else {
          ans = min(ans, round_ + DivCeil(target - money, a * b));
        }
      }
      printf("%lld
", ans);
    }
  }
  return 0;
}

T3:抽奖

题目描述

黑箱子里面有N种不同类型的彩球,每次你只能从箱子摸一个彩球出来,第i种彩球出现的频率是p[i](即概率为(dfrac {p[i]} {sum}))。问要摸多少次才能凑齐所有类型的彩球,输出期望值。

分析

移项期望裸题。

(F(S))表示已经摸到过了(S)集合中的彩球,凑齐所有类型的彩球期望需要的次数。

直接贴代码(滑稽)

(代码中为了方便传了一个cnt参数,表示(S)的大小)

代码

#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 20 + 10;
const int kMaxSet = 1 << 22;
int T;
int n, times[kMaxN], tot;
double p[kMaxN];
double arr_f[kMaxSet];
double F(int S, int cnt) { // Have got balls in S, cnt = S.size()
  if (cnt == n) {
    return 0;
  } else if (arr_f[S] != -1) {
    return arr_f[S];
  } else {
    double prob = 0;
    double sum = 0;
    for (int i = 0; i < n; i++) {
      if (S & (1 << i)) {
        prob = prob + p[i]; // !!!
      } else {
        sum += p[i] * ( 1 + F(S | (1 << i), cnt + 1) );
      }
    }
    return arr_f[S] = (prob + sum) / (1 - prob);
  }
}
int main() {
  freopen("2846.in", "r", stdin);
  freopen("2846.out", "w", stdout);
  scanf("%d", &T);
  while (T--) {
    scanf("%d", &n);
    tot = 0; // !!!
    for (int i = 0; i < n; i++) {
      scanf("%d", &times[i]);
      tot += times[i];
    }
    for (int i = 0; i < n; i++) {
      p[i] = 1.0 * times[i] / tot;
    }
    for (int S = 0; S < (1 << n); S++) {
      arr_f[S] = -1;
    }
    printf("%lf
", F(0, 0));
  }
  return 0;
}

以上是关于解题报告 smoj 2019初二创新班(2019.3.31)的主要内容,如果未能解决你的问题,请参考以下文章

比赛SMOJ 2019.4.21

2019模拟赛09场解题报告

解题报告-2019.12.16

「csp校内训练 2019-10-30」解题报告

2019.03.13 ZJOI2019模拟赛 解题报告

2019.11.09考试解题报告