0. 批斗
难度排序:F<<A=J=I<L=G=E<C=K
赛后批斗,很遗憾最后只开出了 \(6\) 题无缘晋级赛。
\(F\) 题签到。队友写的。一发过。
\(I\) 开赛就被我秒了,我尝试让队友抢一血,但是队友说会 \(F\) 和 \(J\) 所以放他先写完,反正没手速也抢不了一血。
于是我继续和另一个队友去讨论了 \(I\) 的正确性,讨论出来发现显然是对的。我们打算写 \(I\) 的时候已经过了四十多队了,由于机器下面的俩人没有代码能力,口述做法给了主码手让他实现,很快一发过了。(赛后我自己去实现甚至花了半个小时,而且这时候我还不困)
\(J\) 题我们队刷题少的人在这场比赛之前完全不会微扰法,还好队友会。我当时没看。队友一发过。
\(A\) 题是另一个队友秒了的,和我讨论了正确性后发现很对。但是他手速很慢,实现花了很久。也一发过了。
这时候队伍排名已经遥遥领先了。接下来才是惨败。
\(L\) 题队友猜了一个贪心,甚至我也觉得大概率是对的,但是错了。事后队友给了一个几乎正确的做法,我第一次检查感觉没问题,WA 了一发后,我发现队友的期望多算了 \(1\) ,深入询问后确定他看错了题。
重新读题后更新了公式,但公式莫名其妙被约空了,这时候队友怀疑思路是错的,我提出了坚信这个思路是对的观点,然后独自去推公式了。
不久后推出了存在一个 \(v\) 满足左边的贡献为 \(\frac{1}{n} i\) ,右边为 \(1 + E(X)\) 。这时候队友也同时发现之前的公式期望又少算了 \(1\) ,检验后发现和我刚想到的公式一样,于是笃定是完全对的。
上面两个错误的观察卡了我们几乎两个多小时。当时我为了追罚时不想求导,劝队友上机三分秒掉,结果手速过慢写了十几分钟。实际上求个导找极点估计只要三五分钟。
\(G\) 题是我们卡 \(L\) 的时候队友写的。赛后观察了一下显然需要强制连胜才能导致游戏继续,考虑维护概率前缀暴力 \(dfs\) 模后的状态,容易检验正确性为每次只会让状态乘以 \(2\) 且最多有 \(\log n\) 层状态。因为去年多校写过这种题所以很容易想,如果先开 \(G\) 而不是 \(L\) 我们应该能半小时内想完+写完。
\(E\) 题一眼看过去完全没思路,询问队友后发现他的思路正确性显然。于是三个人讨论一下后发现维护奇偶染色是显然的,但是有两个人不会写代码,只能干瞪眼等主码手写。
但还是写 \(WA\) 了,后来找 \(bug\) 的时候我提出 \(bfs\) 染色的部分是对的,但是时间还剩二十分钟估计队友没精力听我说话。虽然怀疑是路径维护出了问题,但是我自己也没学过路径维护的正确做法,没敢强行让队友重写路径维护,而是寄托于主码手自己调。
最后还是没开出来。
很遗憾晋级失败了,也不一定能成为外卡选手了。如果被宣告拿不到卡,就是有些人九月已经死了,十一月才闭掉眼睛。
前期的手速也没有任何作用,遇到 \(L\) 和 \(E\) 这种调不出 \(bug\) 的题,最后总是会体现成被其他队伍题数碾压。
F
题意:
给 \(a_0 = 1500\) ,询问第一个 \(i\) 使得 \(\sum_{i = 0}^{i} a_i \geq 4000\) 。
题解:
线性能过,那就秒了,没什么说的。
Code
int n; std::cin >> n;std::vector<i64> a(n + 1);a[0] = 1500;int v = -1;for (int i = 1; i <= n; i++) {std::cin >> a[i];a[i] += a[i - 1];if (v == -1 && a[i] >= 4000) {v = i;}}std::cout << v << "\n";
A
题意:
给 \(n\) 支队伍,每支队伍能力值为 \(a_i \ (1 \leq i \leq n)\) ,属于学校 \(b_i \ (1 \leq i \leq n)\) 。有 \(k\) 场区域赛,第 $ \ (1 \leq i \leq k)$ 个赛区限制每个学校最多派遣 \(c_i\) 队。每支队伍最多选择两个赛区。
每支队伍不知道其他队伍的选赛区情况(哪怕是自己学校的),询问第 \(i, i = 1, 2, \cdots, n\) 支队伍在最优选择情况下的最坏的最高排名。
题解:
容易发现这是一个欺骗题,我们并不期望能够获得的排名率尽可能优,而是希望排名尽可能高。
于是最高排名一定会选择最小的赛区。直接 \(ban\) 掉 \(c\) 数组和可选两个赛区的操作。
维护出每个学校的队伍,假设有 \(m\) 个学校分别有 \(t_i \ (1 \leq i \leq m)\) 支队伍。
考虑最优选择下的最坏情况。
令 \(mn = \mathbf{min}\{c_i\}\) ,这个赛区最坏会被所有学校最强的 \(\mathbf{min}(mn, t_i)\) 支队伍选择。维护出这些队伍并进行一个升序排序,假设存储在 \(vec\) 。
考虑每个队伍,不妨是第 \(i \ (1 \leq i \leq n)\) 个。
- 这个队伍如果在这些强队中,只需 \(lower\_bound\) 出比不弱于它的队伍的位置 \(p\) ,这个队伍一定是它,于是它在 \(p\) 这个位置。
- 这个队伍如果不在这些强队中,考虑\(lower\_bound\) 出比不弱于它的队伍的位置 \(p\) ,挤掉右边一个同校的队伍,最坏会让从 \(p\) 开始的队伍右移一位,于是它也在 \(p\) 这个位置。
答案是 \(|vec| - 1 - p + 1 = |vec| - p\) 。
Code
int n, k; std::cin >> n >> k;const int INF = 1 << 30;int mi = INF;for (int i = 1; i <= k; i++) {int x; std::cin >> x;mi = std::min(mi, x);}std::map<std::string, std::vector<int> > sch;std::vector<int> a(n + 1);for (int i = 1; i <= n; i++) {std::string s; int x;std::cin >> x >> s;sch[s].push_back(x);a[i] = x;}std::vector<int> teams;for (auto &t : sch) {std::vector<int> vec = t.second;std::sort(vec.begin(), vec.end(), std::greater<>());for (int i = 0; i < std::min(mi, (int)vec.size()); i++) {teams.push_back(vec[i]);}}int tot = teams.size();std::sort(teams.begin(), teams.end());// for (auto x : teams) std::cout << x << " "; std::cout << "\n";for (int i = 1; i <= n; i++) {auto it = std::lower_bound(teams.begin(), teams.end(), a[i]);int rk = it - teams.begin();std::cout << tot - rk << "\n";}
时间复杂度依赖于排序的 \(O(n \log n)\) 和容器调用的 \(O(n \log n)\) 。
J
题意:
给 \(n\) 个物品属性为 \(v_i, c_i, w_i\) 。可以用任意顺序堆叠它们。从上往下第 \(i\) 个物品体积会被压缩为 \(v_i - c_i \times (\sum_{j = 1}^{i - 1} w_j)\) ,询问这 \(n\) 个物品总共的最小体积。
数据保证物品不会被压缩成负体积。(哪怕真有这种情况也不影响做法)
题解:
对于寻求最优排列的问题,可以尝试考虑微扰法。
任选第个 \(i \ (i < n)\) 位的物品,有
考虑仅调换两个物品的顺序,它们的体积之和为
如果第一种情况更优,则应该满足
排序的微扰法正确性容易构造性证明:
从增量法构造偏序的角度考虑:若可以让任意相邻两个物品满足偏序,则任意两个物品满足偏序。
\(\square\)
存在两个相邻物品不具有偏序时,微扰法是失效的。
不难发现 \(\forall i \in [1, n - 1]\) 都和后一个位置满足偏序,于是按偏序 \(w_i \times c_j \geq w_j \times c_i\) 排序能获得最优偏序。
Code
int n; std::cin >> n;std::vector<std::array<i64, 3> > a(n + 1);for (int i = 1; i <= n; i++) {i64 w, v, c; std::cin >> w >> v >> c;a[i] = {w, v, c};}std::sort(a.begin() + 1, a.begin() + 1 + n, [&](std::array<i64, 3> A, std::array<i64, 3> B){// w_1 / c_1 > w_2 / c_2 -> w_1 * c_2 > w_2 * c_1return A[0] * B[2] > B[0] * A[2];});i64 sum = 0, pre = 0;for (int i = 1; i <= n; i++) {sum += a[i][1] - a[i][2] * pre;pre += a[i][0];}std::cout << sum << "\n";
时间复杂度 \(O(n \log n)\) 。
I
题意:
给一个十进制的无符号 \(32\) 位正整数 \(n\) ,询问 \(n\) 能否构造成多项式:
如果能,输出一个多项式。
题解:
显然我们要构造一个相邻位不能同时为 \(0\) 的多项式。
注意到多项式 \(\sum_{i = 1}^{n} a_i x^{i}\) 若满足 \(|a_i| < x\) 则一定唯一。(如果有人读到请自行反证)
由于二进制与这个多项式的数位存在大量交集,于是从二进制形态考虑通过微调构造出这个多项式。
显然:
- \(1 = 2 - 1 = 4 - 2 - 1 = 8 - 4 - 2 - 1 \cdots\)
- \(2 = 4 - 2 = 8 - 4 - 2 = 16 - 8 - 4 - 2 \cdots\)
- 通过归纳法可以证明出 \(2^{i} = 2^{k} - \sum_{j = 1}^{k} 2^{j} , \ \forall k > i\) 。
于是从 \(lowbit(n)\) 开始的所有数位都可以构造成符合条件的情况。当且仅当 \(lowbit(n) > 2\) 时(\(lowbit\) 不在最低两位上)违反条件。
显然二进制一个 \(0\) 如果要变为 \(1\) ,必须从更低位进行补位。而 \(lowbit\) 右边的任意低位 \(0\) 无位可补,必然导致相邻 \(0\) 存在。
于是先判断是否存在多项式,如果存在,则在二进制上找极大的 \(00 \cdots 1\) 串进行修改,双指针可以实现。
Code
const int M = 32;
void solve() {int n; std::cin >> n;if ((n >> 0 & 1) == 0 && (n >> 1 & 1) == 0) {std::cout << "NO\n";return;}std::cout << "YES\n";std::vector<int> ans(32);int O = __builtin_ctz(n & -n);for (int i = O, j = O; i < M; i++) {while (j < i) {j++;}while (j + 1 < M && (n >> j + 1 & 1) == 0) {j++;}if ((n >> j & 1) == 1) {ans[j] = 1;continue;}// i, j -> 100...0 -> -1 -1 -1 ... 1ans[j] = 1;for (int k = i; k < j; k++) {ans[k] = -1;}// std::cout << i << " " << j << "\n";i = j;}for (int i = 0; i < M; i++) {std::cout << ans[i] << " \n"[(i + 1) % 8 == 0];}
}
时间复杂度 \(O(Tw)\) 。注意输出格式为 \(\overline{a_{31}a_{30} \cdots a_{0}}\) 每行空格隔开 \(8\) 个共四行。
副机位刚启动,刚好点开了这题,然后三分钟内确定了正确做法。但队友选择了先跟榜写掉签到题。由于签到都被队友秒了所以拖到第三道题才写。
L
题意:
开局在 \([1, n]\) 随机一个 \(t\) ,每一秒执行一次以下两个操作之一
- 待机,\(t\) 会自然减少 \(1\) 。
- 让 \(t\) 在 \([1, n]\) 重新随机。
询问使用最优操作让 \(t\) 到 \(0\) 的最小时间期望,答案输出最简分数下的分子和分母。
题解:
设游戏开始到游戏结束的最优期望时间是 \(E(X)\) 。
根据期望的线性性 \(E(X) = \sum_{i = 1}^{n} E(Y_i)\) (第 \(i\) 个点到 \(0\) 的期望)。
去考虑 \(i \in [1, n]\) 上每个点的期望。
- 要么是直接花费 \(i\) 秒走到 \(0\) ,时间贡献是 \(i\) 。
- 要么是使用随机,对时间贡献是 \(1 + E(X)\) 。
考虑期望为“随机变量的取值”乘以“出现这个取值的概率”,\(E(Y_i) = \frac{1}{n} \mathbf{min}(i, 1 + E(X))\) 。
于是有
由于 \(i\) 单调,\(1 + E(X)\) 为常数,一定存在一个 \(v\) 使得
不难证明(比如反证)一个宽松边界是 \(v \in [1, n]\) ,于是
若 \(E(X)\) 为最优则一定最小,显然 \(E(X)\) 在 \(v\) 的定义域上是对勾函数,于是在 \((0, +\infty]\) 存在极小值。
这时候可以掂量一下是自己求导求极值快还是写三分快。问题可以解决。
如果求导则有
在 \(\sqrt{2n}\) 的上下取整两个点进行函数比较即可得到极小值。
Code
i64 gcd(i64 a, i64 b) { return b ? gcd(b, a % b) : a; }
void solve() {i64 n; std::cin >> n;// E(X) = \frac{v - 1}{2} + \frac{n}{v} -> \sqrt{2v}i64 v1 = std::sqrt(2 * n), v2 = std::sqrt(2 * n) + 1;// std::cout << "v " << v1 << " " << v2 << "\n";auto calc = [&] (i64 v) -> std::array<i64, 2> {i64 O = v * v - v + 2LL * n, P = 2LL * v;i64 g = gcd(O, P);return {O / g, P / g};};std::array<i64, 2> F1 = calc(v1), F2 = calc(v2);if (F1[0] * F2[1] < F2[0] * F1[1]) {std::cout << F1[0] << " " << F1[1] << "\n";} else {std::cout << F2[0] << " " << F2[1] << "\n";}
}
时间复杂度 \(O(\log n)\) ,需要求 \(O(1)\) 次 \(gcd\) 。如果三分只会增加一个 \(O(\log n)\) 。
最后说说我队如何唐掉的 \(L\) ,队友先猜了一发只待机是最优选择,当时看榜上过了七十队,刷新一下直接过了一百多队,于是示意冲了一发贪心,结果是错了。
后续我们大概宕机了半个小时,才意识到期望的和等于和的期望 + 每个位置在游戏开始时的地位是等价的这件事情。然后搞半天发现有人读错题,再然后重读题把公式改错了,于是卡了几乎两个小时。
G
题意:
一局游戏,\(Alice\) 开局有 \(x\) 个筹码,\(Bob\) 开局有 \(y\) 个筹码。 \(Alice\) 赢的概率是 \(p_0\) ,\(Bob\) 赢的概率是 \(p_1\) ,平局的概率是 \(1 - p_0 - p_1\) 。
- 如果有人赢了一局,则败者会减少胜者的筹码数量。若败者筹码数量因此 \(\leq 0\) ,这轮的胜者直接获得整局胜利。否则游戏立刻进入下一轮。
- 如果平局,游戏立刻进入下一轮。
给出 \(a_0, a_1, b\) 以表示 \(p_0 = \frac{a_0}{b}, p_1 = \frac{a_1}{b}\) 。
询问 \(Alice\) 能获得全局胜利的概率,答案模 \(998244353\) 。
题解:
因为平局会导致游戏直接进入下一轮,所以平局实际上是一个状态的自环。因为只考虑获胜的后继和失败的后继,所以自环是无效状态。
于是重定义胜败的概率,\(p_0 = \frac{a_0}{a_0 + a_1}\) \(p_1 = \frac{a_1}{a_0 + a_1}\) 。
分析状态:
- 若 \(x = y\) ,有 \(p_0\) 的概率 \(Alice\) 转移向胜态,\(p_1\) 的概率 \(Bob\) 转移向胜态。
- \(Alice\) 获得全局胜利的概率为,到达当前局面的概率 \(cur\) 乘以 \(p_0\) 。
- 若 \(x < y\) ,\(Alice\) 必须连赢后续 \(d = \lceil \frac{y - x}{x} \rceil\) 轮才能使游戏继续。游戏继续的概率为 \(p_{0}^{d}\) ,\(y := y - d x\) 。
- 若 \(x > y\) ,\(Alice\) 必须连输后续 \(d = \lceil \frac{x - y}{y} \rceil\) 轮才能使游戏继续。游戏继续的概率为 \(p_{1}^{d}\) ,\(x := x - d y\) 。
- 但凡 \(Alice\) 没有连输 \(d\) 次,都能获得全局胜利并终止游戏。\(Alice\) 在后续连续 \(d\) 次中存在胜利的概率,可以方便地用容斥计算,为 \(1\) 减去 \(Alice\) 连输 \(d\) 次的概率,这个概率为到达当前局面的概率 \(cur\) 乘以 \(p_1^{d}\) 。
尝试暴力搜索这些状态,按轮次的不独立性以乘法原理维护一个搜索树前缀的概率。
不难分析出每个状态要么 \(x\) 至少减半,要么 \(y\) 至少减半。最多会有 \(2 \log n\) 个状态。于是复杂度是对的。
或者可以注意到
- \(y := y - dx \Leftrightarrow y := y \bmod x\) 。
- \(x := x - dy \Leftrightarrow x := x \bmod y\) 。
这个状态递降速度等于辗转相除的递降速度,也可以认为状态个数是 \(O(\log n)\) 的。
Code
const int MOD = 998244353;
i64 ksm(i64 a, i64 n) {i64 res = 1; a = (a + MOD) % MOD;for(;n;n>>=1,a=a*a%MOD)if(n&1)res=res*a%MOD;return res;
}
void solve() {i64 x, y; std::cin >> x >> y;i64 a0, a1, b; std::cin >> a0 >> a1 >> b;b = a0 + a1;i64 p0 = a0 * ksm(b, MOD - 2) % MOD;i64 p1 = a1 * ksm(b, MOD - 2) % MOD;i64 ans = 0;std::function<void(i64, i64, i64)> dfs = [&] (i64 cx, i64 cy, i64 cur) {// std::cout << cx << " " << cy << "\n";if (cx == cy) {ans = (ans + cur * p0) % MOD;return;} else if (cx < cy) {i64 d = (cy + cx - 1) / cx - 1; // ceil( (cy - cx) / cx )dfs(cx, cy - d * cx, cur * ksm(p0, d)) % MOD;);} else { // cx > cyi64 d = (cx + cy - 1) / cy - 1; // ceil( (cx - cy) / cy )ans = (ans + cur * (1 - ksm(p1, d)) ) % MOD;dfs(cx - d * cy, cy, (cur * ksm(p1, d)) % MOD;);}};dfs(x, y, 1);std::cout << ans << "\n";
}
E
后面三题明天再写。