把赛程看成满二叉树,预处理确定赢家与自由前缀,再用差分统计每个叶子可能夺冠的前缀区间。
OJ: luogu
题目 ID: P11234
难度:省选/NOI-
标签:树形结构动态规划
日期: 2026-06-22 18:45
题意
有 n 位已报名选手,第 i 位选手编号为 i,能力为 a_i。对某个前缀 c,只收到前 c 位选手,需要补充最少的人,使总人数变成 2 的幂。
比赛是固定赛程的淘汰赛。每场比赛会指定左边或右边的胜者作为擂主。若擂主能力至少为当前轮次,擂主获胜;否则另一方获胜。
补充选手能力可以任意选择。如果补充选手可能成为冠军,也要计入答案。对每个询问 c_i,求所有可能冠军编号之和,最后按题目要求输出加权 xor。
思路
先看一个小数据暴力:对补充选手枚举能力值,再模拟整棵比赛树,收集所有可能冠军。
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20;
int n, m, max_k;
int base_ability[MAXN], current_ability[MAXN], query_c[MAXN];
int draw_round[10][MAXN];
set<int> possible_winner;
int ceil_power_log(int x) {
int k = 0;
while ((1 << k) < x) {
k++;
}
return k;
}
int simulate_tournament(int total_players, int k, int ability[]) {
vector<int> player;
for (int i = 1; i <= total_players; i++) {
player.push_back(i);
}
for (int round_id = 1; round_id <= k; round_id++) {
vector<int> next_player;
int games = (int)player.size() / 2;
for (int g = 1; g <= games; g++) {
int left = player[(g - 1) * 2];
int right = player[(g - 1) * 2 + 1];
int lord, other;
if (draw_round[round_id][g] == 0) {
lord = left;
other = right;
} else {
lord = right;
other = left;
}
if (ability[lord] >= round_id) {
next_player.push_back(lord);
} else {
next_player.push_back(other);
}
}
player = next_player;
}
return player[0];
}
void enumerate_unknown(int pos, int total_players, int known_count, int k, int ability[]) {
if (pos > total_players) {
possible_winner.insert(simulate_tournament(total_players, k, ability));
return;
}
if (pos <= known_count) {
enumerate_unknown(pos + 1, total_players, known_count, k, ability);
return;
}
// 补充选手只需要枚举 0..k,能力超过 k 与 k 等价。
for (int value = 0; value <= k; value++) {
ability[pos] = value;
enumerate_unknown(pos + 1, total_players, known_count, k, ability);
}
}
long long champion_sum_for_prefix(int prefix_len) {
int k = ceil_power_log(prefix_len);
int total_players = 1 << k;
int ability[MAXN];
for (int i = 1; i <= total_players; i++) {
if (i <= prefix_len) {
ability[i] = current_ability[i];
} else {
ability[i] = 0;
}
}
possible_winner.clear();
enumerate_unknown(1, total_players, prefix_len, k, ability);
long long sum = 0;
for (set<int>::iterator it = possible_winner.begin(); it != possible_winner.end(); ++it) {
sum += *it;
}
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
max_k = ceil_power_log(n);
for (int i = 1; i <= n; i++) {
cin >> base_ability[i];
}
for (int i = 1; i <= m; i++) {
cin >> query_c[i];
}
for (int round_id = 1; round_id <= max_k; round_id++) {
string s;
cin >> s;
for (int j = 0; j < (int)s.size(); j++) {
draw_round[round_id][j + 1] = s[j] - '0';
}
}
int test_count;
cin >> test_count;
while (test_count--) {
int mask_value[4];
for (int i = 0; i < 4; i++) {
cin >> mask_value[i];
}
for (int i = 1; i <= n; i++) {
current_ability[i] = base_ability[i] ^ mask_value[i & 3];
}
long long ans = 0;
for (int i = 1; i <= m; i++) {
ans ^= 1LL * i * champion_sum_for_prefix(query_c[i]);
}
cout << ans << '\n';
}
return 0;
}补充选手能力超过总轮数 K 后没有区别,所以暴力只枚举 0..K。但补充人数可能很多,仍然是指数级。
正解把赛程看成一棵满二叉树。叶子是选手编号,内部结点是一场比赛。对某个前缀 c,前 c 个叶子是已知选手,后面的叶子是补充选手。
我们希望反过来统计:每个叶子在哪些前缀长度下可能成为冠军。若一个编号 id 能在前缀区间 [l,r] 中成为冠军,就把 id 加到这个区间上。最后对差分数组做前缀和,就得到每个 c 的答案。
需要维护几个树上信息:
| 变量 | 含义 |
|---|---|
fixed_winner[u] |
子树 u 完全确定时,最终赢家的能力 |
free_time[u] |
子树 u 最后仍可能受补充选手影响的前缀位置 |
known_limit[leaf] |
已知选手能力能支持它通过的最高补齐层级 |
front_limit[leaf] |
从根往下看,这个叶子不被确定赢家挡住的最大前缀 |
fixed_winner 和 free_time 可以自底向上计算。叶子如果是已知选手,它在前缀达到自己之前还不可用,所以自由时刻是 pos - 1;补充叶子一直可以自由选择能力。
known_limit 处理的是已知选手自身能力够不够。沿着叶子到根的路径,如果它所在的一侧被抽为擂主,就必须满足对应轮次的能力要求。
front_limit 处理的是“会不会被对面确定赢家挡住”。对每个补齐规模 2^W,从对应子树根向下传播限制:若某一侧确定赢家已经足够赢当前轮,就会压缩另一侧叶子的可行前缀范围。
最后枚举补齐规模 W 和叶子:
- 如果叶子是已知选手,且
known_limit支持它打到这一层,就把它能成为冠军的前缀区间加入差分; - 如果叶子是补充选手,只要它所在位置在当前前缀之后,且没有被
front_limit挡住,也把它的前缀区间加入差分。
对差分数组做前缀和后,answer_prefix[c] 就是前缀 c 的可能冠军编号和。
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
const int MAXNODE = 300005;
int n, m, max_k, base_size;
int ability[MAXN], query_c[MAXN];
int draw_side[MAXNODE]; // 内部结点抽到 0/1,0 表示左边为擂主
int log_floor_value[MAXNODE]; // 堆编号的 floor(log2)
int fixed_winner[MAXNODE]; // 子树完全确定时,赢家的能力值
int free_time[MAXNODE]; // 子树最后仍可能自由变化的前缀时刻
int father_need[MAXNODE]; // 从当前结点往上,选手作为擂主需要满足的最高轮次
int known_limit[MAXNODE]; // 已知选手能力能通过的祖先限制
int front_limit[MAXNODE]; // 从上往下 DP 得到的可贡献前缀上界
long long answer_prefix[MAXN + 5];
int min_int(int x, int y) {
return x < y ? x : y;
}
int max_int(int x, int y) {
return x > y ? x : y;
}
void prepare_tree_info() {
base_size = 1 << max_k;
for (int i = 2; i < base_size * 2; i++) {
log_floor_value[i] = log_floor_value[i >> 1] + 1;
}
father_need[1] = max_k;
for (int i = 2; i < base_size * 2; i++) {
int parent = i >> 1;
int is_right_child = i & 1;
// 如果这个孩子所在的半区是擂主,记录跳到这里后还需要满足的层级限制。
if (is_right_child == draw_side[parent]) {
father_need[i] = max_k - log_floor_value[i];
} else {
father_need[i] = father_need[parent];
}
}
}
void build_fixed_winner() {
for (int pos = 1; pos <= base_size; pos++) {
int node = base_size + pos - 1;
if (pos <= n) {
free_time[node] = pos - 1;
fixed_winner[node] = ability[pos];
} else {
free_time[node] = base_size;
fixed_winner[node] = 0;
}
}
for (int pos = 1; pos <= n; pos++) {
int node = base_size + pos - 1;
int climb = ability[pos] < max_k ? ability[pos] : max_k;
known_limit[node] = father_need[node >> climb];
}
for (int node = base_size - 1; node >= 1; node--) {
int left = node << 1;
int right = left | 1;
int round_id = max_k - log_floor_value[node];
if (draw_side[node] == 0) {
if (fixed_winner[left] < round_id) {
free_time[node] = free_time[right];
fixed_winner[node] = fixed_winner[right];
} else {
free_time[node] = free_time[left];
fixed_winner[node] = fixed_winner[left];
}
} else {
// 右边是擂主。自由性来自右边擂主是否能跨过当前轮,所以始终继承右子树的自由时刻。
free_time[node] = free_time[right];
if (fixed_winner[right] >= round_id) {
fixed_winner[node] = fixed_winner[right];
} else {
fixed_winner[node] = fixed_winner[left];
}
}
}
}
void solve_current_abilities() {
for (int i = 0; i <= n + 2; i++) {
answer_prefix[i] = 0;
}
build_fixed_winner();
// W 表示当前前缀补齐到 2^W 人。
for (int W = 0; W <= max_k; W++) {
int lower_bound_prefix = (W == 0) ? 0 : (1 << (W - 1));
int root_offset = (1 << (max_k - W)) - 1;
front_limit[root_offset + 1] = base_size;
for (int id = 1; id < (1 << W); id++) {
int node = (root_offset << log_floor_value[id]) + id;
int left = node << 1;
int right = left | 1;
int round_id = max_k - log_floor_value[node];
front_limit[left] = front_limit[node];
front_limit[right] = front_limit[node];
if (draw_side[node] == 0) {
if (fixed_winner[left] >= round_id) {
front_limit[right] = min_int(front_limit[right], free_time[node]);
} else {
front_limit[left] = min_int(front_limit[left], free_time[left]);
}
} else {
if (fixed_winner[right] >= round_id) {
front_limit[left] = min_int(front_limit[left], free_time[node]);
} else {
front_limit[right] = min_int(front_limit[right], free_time[right]);
}
}
}
for (int i = 0; i < (1 << W); i++) {
int player_id = i + 1;
int leaf = base_size + i;
// 已知选手成为冠军的前缀区间。
if (player_id <= n && known_limit[leaf] >= W && front_limit[leaf] >= player_id &&
front_limit[leaf] >= lower_bound_prefix + 1) {
int left = max_int(player_id, lower_bound_prefix + 1);
int right = min_int(front_limit[leaf], 1 << W);
if (left <= right && left <= n) {
right = min_int(right, n);
answer_prefix[left] += player_id;
answer_prefix[right + 1] -= player_id;
}
}
// 补充选手成为冠军的前缀区间。
if (W > 0 && i > lower_bound_prefix && front_limit[leaf] >= lower_bound_prefix) {
int left = lower_bound_prefix + 1;
int right = min_int(i, front_limit[leaf]);
if (left <= right && left <= n) {
right = min_int(right, n);
answer_prefix[left] += player_id;
answer_prefix[right + 1] -= player_id;
}
}
}
}
for (int i = 1; i <= n; i++) {
answer_prefix[i] += answer_prefix[i - 1];
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
while ((1 << max_k) < n) {
max_k++;
}
base_size = 1 << max_k;
for (int i = 1; i <= n; i++) {
cin >> ability[i];
}
for (int i = 1; i <= m; i++) {
cin >> query_c[i];
}
for (int round_id = 1; round_id <= max_k; round_id++) {
string s;
cin >> s;
int start = 1 << (max_k - round_id);
for (int j = 0; j < (int)s.size(); j++) {
draw_side[start + j] = s[j] - '0';
}
}
prepare_tree_info();
int test_count;
cin >> test_count;
while (test_count--) {
int mask_value[4];
for (int i = 0; i < 4; i++) {
cin >> mask_value[i];
}
for (int i = 1; i <= n; i++) {
ability[i] ^= mask_value[i & 3];
}
solve_current_abilities();
long long ans = 0;
for (int i = 1; i <= m; i++) {
ans ^= 1LL * i * answer_prefix[query_c[i]];
}
for (int i = 1; i <= n; i++) {
ability[i] ^= mask_value[i & 3];
}
cout << ans << '\n';
}
return 0;
}复杂度
设 K = ceil(log2 n)。每组 xor 测试数据的复杂度为
总结
这题难点不是模拟比赛,而是同时处理所有前缀。核心做法是把“某个选手能否夺冠”转成“某个叶子在哪些前缀中可行”,再用差分统一统计。
树上自底向上的 fixed_winner/free_time 负责确定子树信息,自顶向下的 front_limit 负责传播阻挡条件。两部分合起来,就能得到每个叶子的可行前缀区间。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
