2025“钉耙编程”中国大学生算法设计春季联赛(1) 1002个人题解
水手当船长的概率问题
问题描述
考虑一场淘汰赛制的船长竞选,共有 \(n\) 名水手参与(编号从 \(1\) 到 \(n\)),通过两两对决的方式逐轮淘汰,最终决出唯一的船长。规则如下:
- 每轮中,水手两两配对对决,胜者晋级;若水手数量为奇数,则最后一人轮空,直接晋级。
- 主角染染(编号 \(p_0\))与其他普通水手对决时必胜。
- 有 \(k\) 个特定的威胁者(编号 \(p_1, p_2, \ldots, p_k\)),若染染在任何一轮中与其中之一对决,则必败。
- 除染染外的其他水手之间对决,胜负概率均为 \(\frac{1}{2}\)。
目标是计算染染在整个比赛中不与任何威胁者对决并最终成为船长的概率,结果对模 \(998244353\) 输出。
数据范围
- \(1 \leq n \leq 10^9\)
- \(0 \leq k < \min\{n, 10^5\}\)
输入格式
- 第一行:测试用例数 \(T\)。
- 每组测试数据:
- 第一行:\(n\)(水手总数),\(k\)(威胁者数量),\(p_0\)(染染编号)。
- 第二行:\(k\) 个整数 \(p_1, p_2, \ldots, p_k\)(威胁者编号)。
输出格式
- 每组数据输出一个整数,表示染染获胜的概率对 \(998244353\) 取模。
解题思路
问题建模
淘汰赛可以抽象为一个二叉树形过程:
- 初始有 \(n\) 个节点(水手),每轮通过配对减少节点数,奇数时轮空一人,直至剩一个节点(船长)。
- 每轮中,水手的位置 \(p\)(从 \(0\) 开始计数)更新为 \(\lfloor p / 2 \rfloor\)(父节点位置),反映了对决后的晋级。
- 染染的胜利条件是:在每一轮中,其配对对手不是威胁者。
由于 \(n\) 可达 \(10^9\),直接模拟比赛树不可行,但 \(k < 10^5\) 提示我们可以聚焦于威胁者的动态。
核心洞察
染染获胜的概率等价于所有威胁者在比赛中要么被淘汰,要么从未与染染配对。我们可以通过以下步骤计算:
-
模拟轮次:
- 每轮水手数变为 \(\lceil n / 2 \rceil\),总轮数约为 \(\lceil \log_2 n \rceil\)。
- 染染位置更新:\(p_0 \gets \lfloor p_0 / 2 \rfloor\)。
- 威胁者位置更新:\(p_i \gets \lfloor p_i / 2 \rfloor\)。
-
威胁者存活概率:
- 初始时,每个威胁者存活概率为 \(1\)。
- 每轮更新:
- 若威胁者 \(p_i\) 与另一威胁者 \(p_j\) 对决(即 \(\lfloor p_i / 2 \rfloor = \lfloor p_j / 2 \rfloor\)),存活概率为两者平均值(各 \(\frac{1}{2}\) 胜出)。
- 若与普通水手对决,存活概率减半(\(\times \frac{1}{2}\))。
- 若轮空(位置 \(p_i \oplus 1 \geq n\)),概率不变。
-
染染的胜利条件:
- 若某威胁者 \(p_i\) 的晋级位置与染染相同(\(\lfloor p_i / 2 \rfloor = \lfloor p_0 / 2 \rfloor\)),则该轮染染与之对决。
- 染染获胜要求该威胁者在此轮前已淘汰,其概率为 \(1 - P(\text{威胁者存活})\)。
-
概率计算:
- 答案为每一轮中染染不遇到存活威胁者的概率乘积。
- 在模 \(998244353\) 下,\(\frac{1}{2}\) 的乘法逆元为 \(499122177\)。
算法设计
- 初始化:记录染染位置 \(p_0\) 和威胁者位置 \(\{p_1, p_2, \ldots, p_k\}\)(转为 0-based),每个威胁者存活概率为 \(1\)。
- 轮次迭代:
- 更新所有位置并计算威胁者存活概率。
- 检查是否有威胁者与染染对决,若有,答案乘以该威胁者未存活的概率,并移除该威胁者。
- 继续下一轮,直到威胁者为空或比赛结束。
- 复杂度:轮次 \(O(\log n)\),每轮处理威胁者 \(O(k)\),总时间 \(O(k \log n)\)。
参考代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353;
const int inv2 = 499122177; // 2 的逆元
void solve() {
int n, k, winner;
cin >> n >> k >> winner;
winner--; // 转为 0-based
vector<int> pos(k), prob(k, 1); // 威胁者位置和存活概率
for (int &x : pos) {
cin >> x;
x--;
}
sort(pos.begin(), pos.end());
int ans = 1;
while (!pos.empty()) {
vector<int> next_pos, next_prob;
winner >>= 1; // 更新染染位置
for (int i = 0; i < pos.size();) {
int curr = pos[i] >> 1; // 当前威胁者晋级位置
if (curr == winner) {
// 染染与威胁者对决,需威胁者未存活
ans = ans * (1 - prob[i] + mod) % mod;
i++;
} else if (i + 1 < pos.size() && (pos[i + 1] >> 1) == curr) {
// 两个威胁者对决
next_pos.push_back(curr);
next_prob.push_back(inv2 * (prob[i] + prob[i + 1]) % mod);
i += 2;
} else {
// 威胁者与普通水手对决或轮空
next_pos.push_back(curr);
next_prob.push_back((pos[i] ^ 1) < n ? prob[i] * inv2 % mod : prob[i]);
i++;
}
}
pos = next_pos;
prob = next_prob;
n = (n + 1) >> 1; // 更新水手总数
}
cout << ans << "\n";
}

浙公网安备 33010602011771号