题目描述
给定一个长度为 n 的数组 a 和一个整数 k。
你需要将数组 a 分割成 k 个非空且不重叠的连续子数组 b1,b2,…,bk,使得这些子数组的并集恰好是整个数组 a。
你需要找到一种分割方案,使得所有子数组 bi 的 MEX 值的最小值尽可能大。输出这个最大的最小值。
MEX(v) 指的是数组(或集合) v 中未出现的最小非负整数。例如:
MEX([0,1,3])=2
MEX([1,2,3])=0
* MEX([0,1,2])=3
样例
输入 #1:
7
1 1
0
5 1
0 1 3 2 4
6 2
2 1 0 0 1 2
5 5
0 0 0 0 0
5 2
2 3 4 5 6
6 2
0 0 1 1 2 2
4 4
1 0 0 0
输出 #1:
1
5
3
1
0
1
0
样例 1 解释 (第 3 个测试用例):
n=6, k=2, a = [2, 1, 0, 0, 1, 2]
一种分割方案是: b₁ = [2, 1, 0], b₂ = [0, 1, 2]
MEX(b₁) = MEX([2, 1, 0]) = 3 (包含 0, 1, 2)
MEX(b₂) = MEX([0, 1, 2]) = 3 (包含 0, 1, 2)
该方案的最小 MEX 是 min(3, 3) = 3。
可以证明,无法找到一种分割方案使得最小 MEX 大于 3。因此答案是 3。
算法 (二分答案 + 贪心)
O(nlogn)
思路分析
-
问题目标: 我们要最大化分割后所有子数组 MEX 的最小值。设这个最大的最小值为 X。
-
二分答案的适用性 (单调性):
考虑目标值 X。如果我们能够找到一种分割方案,使得所有子数组的 MEX 都 ≥X,那么是否也能找到一种方案使得所有子数组的 MEX 都 ≥X−1 呢?
答案是肯定的。如果一个子数组的 MEX ≥X,那么它必然包含了 0,1,…,X−1 这些数。既然它包含了 0,…,X−1,那它也一定包含了 0,1,…,(X−1)−1。所以,它的 MEX 也必然 ≥X−1。
因此,如果目标值 X 是可行的(存在一种分割使得最小 MEX ≥X),那么所有小于 X 的值 X′ 也一定是可行的。
反之,如果目标值 X 是不可行的,那么所有大于 X 的值 X′ 也一定是不可行的。
这种单调性表明我们可以使用 二分查找 来寻找最大的可行 X。 -
二分查找的范围:
MEX 的最小值是 0(例如,如果某个子数组不包含 0)。
MEX 的最大可能值是多少?如果一个子数组包含了 0,1,…,n−1,那么它的 MEX 就是 n。如果它包含了 0,1,…,n,它的 MEX 就是 n+1。所以,MEX 的值可能在 [0,n+1] 范围内。一个安全的二分上界可以是 n 或 n+1。代码中使用了 [0,n] 作为二分范围。 -
check(x)
函数:
二分查找的核心是实现一个check(x)
函数,该函数判断:是否存在一种将数组 a 分割成 至少 k 个子数组的方案,使得每个子数组的 MEX 都 ≥x?- 为什么是“至少 k 个”? 如果我们能将数组分割成 k′>k 个满足条件的子数组,我们可以通过合并相邻的子数组来减少子数组的数量,最终得到恰好 k 个子数组。合并后的子数组包含原来两个子数组的所有元素,如果原来两个子数组的 MEX 都 ≥x,合并后的子数组也必然包含 0,1,…,x−1,所以其 MEX 也 ≥x。因此,检查能否分成 至少 k 个是等价且更方便的。
- MEX ≥x 的条件: 一个子数组 b 的 MEX ≥x 当且仅当该子数组包含了所有非负整数 0,1,2,…,x−1。
- 贪心策略: 为了尽可能多地分割出满足条件的子数组,我们应该让每个子数组尽可能短。我们从左到右扫描原数组 a,尝试构建第一个满足 MEX ≥x 的子数组。一旦找到了这样一个最短的前缀子数组,我们就将其作为一个分割出的块,然后从下一个位置开始,继续寻找下一个满足条件的最短子数组。
- 实现
check(x)
:- 如果 x=0,任何非空子数组的 MEX 都 ≥0,所以一定可行,直接返回
true
。(虽然代码中没有显式处理,但在逻辑上是正确的,因为x=0
时vis
数组大小为 0,cur == x
立刻成立)。 - 使用一个布尔数组
vis
(大小为 x) 来标记当前正在构建的子数组中是否已经出现了数字 0,1,…,x−1。 - 使用一个计数器
cur
记录当前子数组中已经找到的不同数字(范围在 [0,x−1] 内)的数量。 - 使用一个计数器
cnt
记录已经成功分割出的满足条件的子数组数量。 - 遍历数组 a 的每个元素
a[i]
:- 如果
a[i]
在范围 [0,x−1] 内,并且在当前子数组中还未出现过 (!vis[a[i]]
):- 标记
vis[a[i]] = true
。 cur++
。
- 标记
- 当
cur == x
时,表示当前子数组已经包含了 0,…,x−1 所有数字。我们成功找到了一个满足条件的子数组。cnt++
。- 重置
cur = 0
。 - 重置
vis
数组(所有元素设为false
),为构建下一个子数组做准备。
- 如果
- 遍历结束后,如果
cnt >= k
,则说明存在一种分割方案满足条件,返回true
。否则返回false
。
- 如果 x=0,任何非空子数组的 MEX 都 ≥0,所以一定可行,直接返回
-
二分查找实现:
- 设置二分查找的下界
lo = 0
,上界hi = n
。 - 循环进行直到
lo == hi
。 - 计算中间值
mid = lo + (hi - lo + 1) / 2
(这种写法向上取整,配合lo = mid
和hi = mid - 1
的更新方式,是查找最大可行值的常用模板)。 - 调用
check(mid)
:- 如果
check(mid)
返回true
,说明mid
是一个可能的答案,或者答案可能更大。更新下界lo = mid
。 - 如果
check(mid)
返回false
,说明mid
太大了,答案一定比mid
小。更新上界hi = mid - 1
。
- 如果
- 循环结束后,
lo
(或hi
) 就是最大的满足check(x)
为true
的 x,即为所求答案。
- 设置二分查找的下界
时间复杂度
check(x)
函数:遍历数组 a 一次,时间复杂度为 O(n)。内部对vis
数组的操作(访问、fill
)总共也是 O(n) 级别(因为fill
的总调用次数与cnt
相关,而cnt * x
不会超过n
太多)。所以check(x)
的复杂度是 O(n)。- 二分查找:执行 logn 次
check
函数调用。 - 总时间复杂度:O(nlogn)。
空间复杂度
check(x)
函数中使用了一个大小为 x 的vis
数组。由于 x≤n,空间复杂度为 O(n)。- 总空间复杂度:O(n)。
C++ 代码
#include <bits/stdc++.h> // 引入所有标准库
// 定义常用类型别名
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
// 处理单个测试用例的函数
void solve() {
int n, k; // 数组长度 n, 需要分割的子数组数量 k
std::cin >> n >> k;
std::vector<int> a(n); // 存储输入的数组 a
for (int i = 0; i < n; i++) {
std::cin >> a[i];
}
// check(x) 函数:判断是否能将数组 a 分割成至少 k 个子数组,
// 使得每个子数组的 MEX 值都 >= x
auto check = [&](int x) {
// vis[j] = true 表示数字 j (0 <= j < x) 在当前子数组中已出现
std::vector<bool> vis(x);
int cnt = 0; // 记录成功分割出的满足条件的子数组数量
int cur = 0; // 记录当前子数组中已出现的不同数字 (0..x-1) 的数量
// 从左到右遍历数组 a
for (int i = 0; i < n; i++) {
// 如果当前元素 a[i] 在目标范围 [0, x-1] 内,
// 并且在当前子数组中尚未出现过
if (a[i] < x && !vis[a[i]]) {
vis[a[i]] = true; // 标记为已出现
cur++; // 增加计数
}
// 如果当前子数组已包含 0 到 x-1 所有数字
if (cur == x) {
cur = 0; // 重置计数器,开始下一个子数组
// 重置 vis 数组,为下一个子数组做准备
std::fill(vis.begin(), vis.end(), false);
cnt++; // 成功分割出一个子数组,增加总数
}
}
// 判断成功分割的子数组数量是否 >= k
return cnt >= k;
};
// 二分查找最大的可行 x
int lo = 0, hi = n; // 二分范围 [0, n] (包含 n 是安全的上界)
while (lo < hi) {
// 计算中间值 x,向上取整
// 这种写法等价于 ceil((lo + hi) / 2.0)
int x = (lo + hi + 1) / 2;
if (check(x)) {
// 如果 x 可行,说明答案可能是 x 或者更大
// 移动下界 lo 到 x
lo = x;
} else {
// 如果 x 不可行,说明答案必须小于 x
// 移动上界 hi 到 x - 1
hi = x - 1;
}
}
// 循环结束时,lo == hi,即为最大的可行 x
std::cout << lo << "\n";
}
int main() {
// 关闭 C++ 标准 IO 流与 C stdio 的同步,提高 cin/cout 速度
std::ios::sync_with_stdio(false);
// 解除 cin 和 cout 的绑定,进一步提速
std::cin.tie(nullptr);
int t; // 测试用例数量
std::cin >> t;
// 处理每个测试用例
while (t--) {
solve();
}
return 0; // 程序正常结束
}
非常好题解使我的大脑旋转。૮₍ˊᗜˋ₎ა