T701955 mex
T701955 mex
题目链接
只找到多用例测试的题目,题目要求是一样的
https://vjudge.net/problem/CodeChef-MEXQUER
用户名 lzm
密码 123456
题目描述
有一个可重集合,其中值为 \(i(0 \le i < n)\) 的元素有 \(a_i\) 个。
定义 \(mex(S)\) 为集合 \(S\) 中最小的没有出现过的非负整数。
你可以进行若干次操作,每次操作形如将集合中的一个数 \(x\) 变为 \(2x\) 或 \(2x + 1\)。
请对于所有 \(0 \le k \le n\) 求出如果要使得整个可重集的 \(mex\) 等于 \(k\) 至少要进行多少 次操作。
输入格式
第一行一个整数 \(n\),表示值的范围。
接下来一行 \(n\) 个数,表示出现次数 \(a_0\)∼\(a_{n−1}\)。
输出格式
输出一行 \(n + 1\) 个数。
第 \(k + 1\) 个数表示如果要使得整个可重集的 \(mex\) 等于 \(k\) 至少要进行多少次操作。 如果不能变成 \(k\),输出 -1。
输入输出样例 #1
输入 #1
3
1 0 1
输出 #1
1 0 -1 -1
输入输出样例 #2
输入 #2
5
2 1 4 0 0
输出 #2
2 1 4 0 2 3
输入输出样例 #3
输入 #3
其余大样例见附件下载。
输出 #3
其余大样例见附件下载。
说明/提示
样例 \(2\) 解释:
如果 \(k = 4\),将 \(0\) 进行两次 \(x\) → \(2x + 1\) 的操作可以得到 \(3\),可以验证没有比这更优 的方案。
对于 \(20\%\) 的数据,\(n, a_i ≤ 10\)。
存在另外 \(10\%\) 的数据,\(n ≤ 300, a_i ≤ 1\)。
对于 \(60\%\) 的数据,\(n ≤ 300, a_i ≤ 300\)。
对于所有数据,\(1 \le n \le 10^5,0 \le a_i \le 10^9.\)
题解
这是一个非常经典的贪心与构造类问题。我们需要对于每一个 \(k \in [0, n]\),计算使得集合的 \(mex = k\) 所需的最少操作次数。
1. 题目分析
\(mex(S) = k\) 的定义:
- 集合 \(S\) 中必须包含 \(0, 1, \dots, k-1\) 所有数字。
- 集合 \(S\) 中不能包含数字 \(k\)。
- 集合中大于 \(k\) 的数字不影响 \(mex\) 为 \(k\)。
操作的性质:
- 操作是将 \(x\) 变为 \(2x\) 或 \(2x+1\)。
- 这意味着我们只能将数字变大。或者从二进制的角度看,一个数 \(u\) 可以变成 \(v\) 当且仅当 \(u\) 是 \(v\) 的二进制前缀(即 \(u\) 是 \(v\) 在二进制树上的祖先)。
- 从数值上看,\(u\) 能变成 \(v\) 的代价(操作次数)并不是简单的差值,而是它们在二进制树上的层数差,即二进制位长度的差。如果 \(u\) 能变成 \(v\),那么 \(u\) 可以通过反复除以 2(下取整)得到。
代价计算分解:
对于一个固定的 \(k\),我们需要满足两个条件:
- 构建集合 \(\{0, \dots, k-1\}\):我们需要从原始数组中选出一部分数,通过操作将它们变成 \(0, \dots, k-1\)。因为操作只能变大,所以我们只能用原始值 \(\le\) 目标值的数来生成目标。实际上,为了生成 \(t\),我们必须使用 \(t\) 的某个“祖先”(如 \(t, \lfloor t/2 \rfloor, \dots, 0\))。
- 消除数字 \(k\):如果原始集合中还有剩余的数字 \(k\),它们会破坏 \(mex=k\) 的条件(因为 \(k\) 不能存在)。既然操作只能变大,我们可以花 1 次操作将 \(k\) 变为 \(2k\) 或 \(2k+1\),从而消除 \(k\)。如果有 \(a_k\) 个 \(k\),这部分的代价就是 \(a_k\)。
综上,对于每个 \(k\),总代价 = (构建 \(\{0, \dots, k-1\}\) 的最小代价) + (如果 \(k < n\),则加上 \(a_k\),否则为 0)。
2. 增量贪心策略
我们可以从 \(k=0\) 到 \(n\) 依次计算。
注意到 \(\{0, \dots, k\}\) 是 \(\{0, \dots, k-1\} \cup \{k\}\)。这意味着我们可以利用 \(k-1\) 时的构建结果,再额外满足目标 \(k\),就能得到 \(k+1\) 时的构建状态。
维护的数据结构:
- 一个“资源池”(
pool),存储当前所有可用的、未被使用的原始数字。 - 一个变量
current_cost,记录构建当前 \(\{0, \dots, k-1\}\) 集合的累积代价。
流程推演:
- 初始化:\(k=0\),此时需要构建空集,代价为 0。
- 循环 \(k\) 从 0 到 \(n\):
- 输出答案:当前的
current_cost加上 \(a_k\)(消除现有 \(k\) 的代价)。 - 更新状态以备下一次循环(即满足目标 \(k\)):
- 首先,对于 \(mex=k+1\),原来的“坏数字” \(k\)(数量 \(a_k\))现在变成了可用的资源,我们将它们加入
pool。 - 然后,我们需要在
pool中找到一个数 \(u\),使得 \(u\) 可以通过操作变成 \(k\)。 - 贪心策略:为了让总代价最小,且保留更有用的数字(较小的数字如 0 可以生成任意数,而较大的数如 \(k\) 只能生成 \(k\) 及其子树),我们应该选择代价最小且数值最大的祖先。即优先检查 \(k\) 是否在
pool中,然后是 \(\lfloor k/2 \rfloor\),直到 0。 - 找到最佳祖先 \(u\) 后,将操作次数加到
current_cost,并从pool中消耗一个 \(u\)。 - 如果找不到这样的 \(u\),说明无法构成 \(0 \dots k\),那么对于当前及之后的所有 \(k\),答案都是 -1。
- 首先,对于 \(mex=k+1\),原来的“坏数字” \(k\)(数量 \(a_k\))现在变成了可用的资源,我们将它们加入
- 输出答案:当前的
3. C++ 代码实现
代码中使用了 vector<long long> pool 作为桶来计数。因为我们只关心 \(\le n\) 的数字,且每次只处理到 \(k\),加入的数字也都是 \(\le k\) 的,所以直接用数组索引即可,效率极高。
#include <iostream>
#include <vector>
using namespace std;
int main() {
// 优化输入输出效率
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
if (!(cin >> n)) return 0;
// 读入每个数字的初始数量
// 注意题目输入是 a_0 到 a_{n-1}
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
// pool[x] 表示当前未使用的数字 x 的数量
// 使用 long long 防止溢出(虽然题目中 a_i 很大,但在本题逻辑中减法只发生 n 次)
// 大小开 n 足够,因为我们只处理到 n-1 的输入
vector<long long> pool(n, 0);
long long current_cost = 0;
bool possible = true;
// 我们需要输出 n+1 个数,对应 k = 0 到 n
for (int k = 0; k <= n; ++k) {
// --- 1. 输出当前 k 的答案 ---
if (!possible) {
cout << -1 << (k == n ? "" : " ");
} else {
long long ans = current_cost;
// 如果 k < n,我们需要消除所有的 k,代价为 a[k]
// 如果 k = n,输入中没有 n,也就没有需要消除的数
if (k < n) {
ans += a[k];
}
cout << ans << (k == n ? "" : " ");
}
// --- 2. 为下一次循环做准备:满足目标 k ---
// 将 a[k] 个 k 加入资源池
// 因为在 mex=k+1 的情况下,这些 k 是构建集合 $\{0...k\}$ 的重要资源
if (k < n) {
pool[k] += a[k];
} else {
// k=n 时循环即将结束,不需要再处理目标 n
continue;
}
// 如果之前已经不可能了,就直接跳过
if (!possible) continue;
// 贪心匹配:寻找 k 的最佳祖先
// 最佳祖先是离 k 最近的(操作数最少),也就是数值最大的
int best_u = -1;
int best_cost = 0;
// 依次检查 k, k/2, k/4, ... 0
int curr = k;
int dist = 0;
while (true) {
if (pool[curr] > 0) {
best_u = curr;
best_cost = dist;
break;
}
if (curr == 0) break;
curr >>= 1; // 相当于除以 2
dist++;
}
if (best_u != -1) {
current_cost += best_cost;
pool[best_u]--;
} else {
// 如果找不到任何祖先能生成 k,说明无法构建集合
possible = false;
}
}
cout << endl;
return 0;
}
4. 详细解释与复杂度
-
时间复杂度:
- 主循环运行 \(n+1\) 次。
- 内部寻找祖先的
while循环,对于 \(10^5\) 范围内的 \(k\),最多执行约 \(\log_2(10^5) \approx 17\) 次。 - 数组访问是 \(O(1)\) 的。
- 总时间复杂度为 \(O(n \log n)\),完全可以处理 \(n=10^5\) 的数据(1秒限时内)。
-
空间复杂度:
- 需要存储输入数组
a和资源池pool,均为 \(O(n)\)。
- 需要存储输入数组
-
贪心正确性:
- 为什么选“最近的祖先”是对的?
- 代价角度:最近的祖先意味着操作次数最少。
- 资源保留角度:\(x\) 的祖先链是 \(x \to \lfloor x/2 \rfloor \to \dots \to 0\)。数值越大的祖先(越接近 \(x\)),其生成的数字范围越窄(即子树越小)。例如,0 可以生成任何数,而 \(x\) 只能生成 \(x\) 及其后代。因此,应该优先消耗“能力较弱”(数值较大)的资源,保留“万能”的 0 以备后用。
- 为什么选“最近的祖先”是对的?
-
边界与细节:
- 数据范围:\(a_i\) 可达 \(10^9\),累积代价可能超过
int,必须使用long long。 - 输出格式:注意最后一个数后面不要有多余空格。
- 无法达成的情况:一旦某一步无法构建出目标 \(k\),后续所有更大的 \(mex\) 都不可能达成,因为它们都需要包含 \(\{0, \dots, k\}\) 这个子集。
- 数据范围:\(a_i\) 可达 \(10^9\),累积代价可能超过
另一种写法
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve()
{
int n;
cin >> n;
vector<ll> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
// cnt:当前可用的每个值的份数(会在为 0..v 分配时被消耗)
vector<ll> cnt = a;
// pref_cost[k] 表示使得 0..k-1 每个至少出现一次的最小操作数
// pref_cost[0] = 0
vector<ll> pref_cost(n + 1, -1);
pref_cost[0] = 0;
ll acc = 0; // 累计代价,等于 pref_cost[cur_v+1] 的值
bool impossible = false;
for (int v = 0; v < n; ++v) {
if (impossible) {
pref_cost[v + 1] = -1;
continue;
}
int p = v;
int steps = 0;
// 向上寻找最近有剩余的祖先
while (p > 0 && cnt[p] == 0) {
p >>= 1; // p = p / 2
++steps;
}
// 如果到 p == 0 仍然没有(cnt[0] == 0),那就失败
if (p == 0 && cnt[0] == 0) {
impossible = true;
pref_cost[v + 1] = -1;
} else {
// 找到了 p(可能 p==v 或 p==0 或中间某个祖先),消耗一份
// 需要的步数是 steps(因为我们从 v 往上走了 steps 次到 p)
// 注意:如果最开始 p==v 且 cnt[v]>0 则 steps==0
// 同时要对 cnt[p] 做减一操作
cnt[p]--;
acc += steps;
pref_cost[v + 1] = acc;
}
}
// 输出答案:对于 k = 0..n
// 若 pref_cost[k] == -1 输出 -1
// 否则 输出 pref_cost[k] + a_k (注意 a_n 视为 0)
vector<ll> ans(n + 1);
for (int k = 0; k <= n; ++k) {
if (pref_cost[k] == -1) {
// 无法构造 0..k-1
cout << -1;
} else {
ll extra = 0;
if (k < n) extra = a[k]; // 需把所有等于 k 的元素移走,每个至少 1 次操作
cout << (pref_cost[k] + extra);
}
if (k < n) cout << ' ';
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin>>T;
while(T--) solve();
return 0;
}

浙公网安备 33010602011771号