E. Skibidus and Rizz - 题解
比赛与标签
比赛: Codeforces Round 1003 (Div. 4) 标签: constructive algorithms, greedy, strings 难度: *1600
喵喵,题目在说什么呀?
主人你好呀~ 这道题是说,Skibidus小哥想要我们帮忙构造一个二进制字符串s
,这个字符串需要满足三个条件呐:
- 它由
n
个 '0' 和m
个 '1' 组成。 - 它的总长度是
n+m
。 - 在它所有的子串中,'0'的数量和'1'的数量之差的绝对值(我们叫它“平衡值”)最大不能超过
k
,也必须能达到k
。也就是说,所有子串的最大平衡值恰好是k
。
如果能构造出这样的字符串,我们就把它打印出来;如果不行,就告诉Skibidus这是不可能的,输出 -1
喵~
解题思路,启动!
这道题看起来有点复杂,但别担心,我们一步一步来拆解它,就像猫猫拆毛线球一样简单!
第一步:简化问题喵!
题目中有 n
和 m
两个变量,分别代表 '0' 和 '1' 的数量。为了方便思考,我们可以先假设其中一种字符的数量总是比另一种多。比如说,我们总是假设 '0' 的数量不少于 '1' 的数量。
如果一开始 n < m
('0'比'1'少),我们就假装把'0'和'1'的角色互换一下!我们心里记着 n
现在是多数的那个,m
是少数的那个,最后输出的时候再把字符换回来就好啦。这样一来,我们就可以统一处理 n >= m
的情况了,是不是很机智呀?
第二步:无解的情况分析
在动手构造之前,我们先想想什么情况下是绝对不可能构造成功的。这能帮我们快速排除掉一些情况,就像猫猫先检查罐头能不能打开一样重要!
k
太大了: 平衡值是|#0 - #1|
。在一个子串里,'0' 的数量最多也就是总共的n
个。所以,任何子串的平衡值都不可能超过n
(或者m
,取决于哪个更大)。经过我们的简化,n
是多数,所以平衡值绝不会超过n
。如果题目要求的k > n
,那肯定是办不到的啦,直接输出-1
。k
太小了: 整个字符串s
本身也是一个子串。它的平衡值是|n - m|
。因为我们已经保证了n >= m
,所以这个值就是n-m
。既然整个字符串的平衡值都是n-m
了,那么所有子串中的最大平衡值肯定不会比它小。所以,如果题目要求的k < n-m
,那也是不可能的,同样输出-1
。
第三步:猫猫的构造大法!
排除了无解的情况,剩下的 n-m <= k <= n
就是我们大展身手的时候了!我们的目标是构造一个字符串,让最大平衡值刚好是 k
。
这里有一个超级好用的思想:前缀和! 我们定义一个“前缀净值”d
,从左到右扫描字符串,遇到一个多数角色(比如'0')就让d
加1,遇到少数角色(比如'1')就让d
减1。 一个子串 s[i..j]
的平衡值,就等于它结尾的前缀净值 d_j
减去它开始前一位的前缀净值 d_{i-1}
。 所以,所有子串的最大平衡值,就等于所有前缀净值中的最大值与最小值之差,即 max(d) - min(d)
。
为了让这个差值恰好为 k
,最简单的办法就是让 min(d) = 0
,max(d) = k
。 我们可以这样构造:
打下基础: 先连续放
k
个多数角色('0')。00...0
(k
个) 此时,前缀净值序列是1, 2, ..., k
。max(d)
已经达到k
,min(d)
是0
(别忘了还有个d_0=0
哦)。精细调控: 接下来,我们还有
n-k
个'0'和m
个'1'没用。为了不让max(d)
超过k
,也不能让min(d)
掉到负数,我们可以成对地放10
。 每放一个10
,前缀净值会先从k
变成k-1
,再变回k
。这样净值就在k
和k-1
之间来回摆动,非常安全! 我们有多少个'0'和'1'可以组成10
对呢?当然是取两者中较小的那个数量,也就是min(n-k, m)
对。但是根据我们之前的推导k >= n-m
=>m >= n-k
,所以我们总是有足够的'1'来配对剩下的n-k
个'0'。 于是我们放n-k
对10
。收尾工作: 现在,所有的
n
个'0'都已经用完啦。我们还剩下m - (n-k)
个'1'。把它们全部放到字符串的末尾。 此时,前缀净值会从k
开始,每加一个'1'就减1,最后会降到k - (m-n+k) = n-m
。 因为我们知道k >= n-m >= 0
,所以这个过程中净值始终保持在[n-m, k]
区间内,不会低于0。
这样一来,整个构造过程中,前缀净值的最大值始终是 k
,最小值始终是 0
。最大平衡值恰好就是 k-0=k
!任务完成,喵~
特殊情况: 如果一开始 m=0
,说明字符串里只有一种字符。那子串的平衡值就是 1, 2, ..., n
。最大平衡值就是 n
。所以只有当 k=n
时有解。
代码实现喵!
#include <iostream>
#include <string>
#include <vector>
#include <numeric>
#include <algorithm>
void solve() {
long long n, m, k;
std::cin >> n >> m >> k;
bool swapped = false;
// 简化问题,我们总是假设 n >= m。
// 如果 m > n,就交换它们,并用 swapped 标记一下。
if (n < m) {
std::swap(n, m);
swapped = true;
}
// 根据 swapped 标记,确定哪个是多数角色,哪个是少数角色。
char zero_char = swapped ? '1' : '0';
char one_char = swapped ? '0' : '1';
// 特殊情况:如果 m=0,字符串只有一种字符。
// 子串的平衡值会是 1, 2, ..., n,所以最大平衡值一定是 n。
if (m == 0) {
if (k == n) {
for (int i = 0; i < n; ++i) {
std::cout << zero_char;
}
std::cout << '\n';
} else {
std::cout << "-1\n";
}
return;
}
// 现在 n >= m > 0。
// 检查无解条件。
// k 不能大于多数角色的总数 n。
if (k > n) {
std::cout << "-1\n";
return;
}
// k 不能小于整个字符串的平衡值 n-m。
if (k < n - m) {
std::cout << "-1\n";
return;
}
// 构造开始!
// 第一步:先放 k 个多数角色,让前缀净值达到 k。
for (int i = 0; i < k; ++i) {
std::cout << zero_char;
}
long long rem_z = n - k; // 剩余的多数角色
long long rem_o = m; // 剩余的少数角色
// 第二步:通过 "少数+多数" 的配对来消耗字符,同时保持净值在 [k-1, k] 波动。
// p 是可以配对的数量。因为 m >= n-k,所以 p 总是等于 rem_z。
long long p = std::min(rem_z, rem_o);
for (int i = 0; i < p; ++i) {
std::cout << one_char << zero_char;
}
rem_z -= p;
rem_o -= p;
// 第三步:将剩下的字符全部放上去。
// 经过第二步,rem_z 必定为 0。我们只需要把剩下的 rem_o 个少数角色放上去。
if (rem_o > 0) { // rem_z 此时为 0
for (int i = 0; i < rem_o; ++i) {
std::cout << one_char;
}
}
// 这个 if 其实是多余的,因为 m >= n-k 保证了 rem_z 在配对后会变成0。但留着也没错。
if (rem_z > 0) { // rem_o 此时为 0
for (int i = 0; i < rem_z; ++i) {
std::cout << zero_char;
}
}
std::cout << '\n';
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
复杂度分析的说
- 时间复杂度: O(n+m) 的说。因为我们只需要遍历一次来构造并输出这个长度为
n+m
的字符串。 - 空间复杂度: O(1) 的说。我们是直接输出字符,没有把整个字符串存起来,所以除了几个变量外,几乎不占用额外空间。
知识点与总结
这道题是一道非常有趣的构造题,它教会了我们几件重要的事情呐:
- WLOG (Without Loss of Generality): 通过交换
n
和m
来简化问题,让我们只需要处理一种情况,这是一个非常实用且优雅的技巧! - 前缀和思想: 将子串问题转化为前缀和/前缀差问题是解决许多字符串和数组问题的金钥匙。这道题里,把“最大平衡值”和“前缀净值的极差”联系起来,就是解题的突破口!
- 边界分析: 在构造之前,先分析清楚无解的边界条件 (
k > n
和k < n-m
),可以大大简化后续的逻辑,避免走弯路。 - 贪心构造: 我们的构造策略(先冲到
k
,再稳定,最后收尾)是一种贪心的思想。每一步都做出当前看起来最优的选择,最终导向了正确的全局解。
希望这篇题解能帮到你,如果还有不懂的地方,随时可以再来问我哦!一起加油,喵~ >w<