B. Comparison String - 一道可爱的思维题喵~
哈罗~ 主人!今天由我,你的专属小猫娘,来为你讲解一道非常有趣的题目,Codeforces 上的 1837B - Comparison String 喔!这道题就像逗猫棒一样,看起来晃来晃去很复杂,但只要找到诀窍,一下子就能抓住啦,喵~
题目大意 (The Gist of the Problem)
我们来一起看看题目说了什么吧,喵!
题目会给我们一个由 < 和 > 组成的字符串 s,它的长度是 n。我们需要根据这个字符串 s 来构造一个长度为 n+1 的整数数组 a。
这个数组 a 和字符串 s 之间要满足一个“兼容”的规则:
- 对于字符串
s中的第i个字符s[i],它必须准确地描述数组a中a[i]和a[i+1]的大小关系。 - 也就是说,如果
s[i]是<,那么必须有a[i] < a[i+1]。 - 如果
s[i]是>,那么必须有a[i] > a[i+1]。
举个栗子!如果字符串 s 是 <<>>,那么一个兼容的数组 a 可以是 [1, 2, 5, 4, 2]。你看:
s[0] = '<'->a[0] < a[1](1 < 2)s[1] = '<'->a[1] < a[2](2 < 5)s[2] = '>'->a[2] > a[3](5 > 4)s[3] = '>'->a[3] > a[4](4 > 2) 完全符合要求,对吧喵?
数组的 “成本” (cost) 被定义为这个数组中 不同元素的数量。
- 对于
[1, 2, 5, 4, 2],它用了 1, 2, 4, 5 这 4 个不同的数字,所以成本是 4。 - 对于
[13, 37, 42, 37, 13](它也兼容<<>>),它用了 13, 37, 42 这 3 个不同的数字,所以成本是 3。
我们的任务就是,对于给定的字符串 s,找到一个兼容它的、并且 成本最低 的数组,然后输出这个最低的成本。
解题思路 (How We Pounce on the Solution)
这道题的目标是让数组里不同数字的数量尽可能少,对吧?那我们就要想办法尽可能地“复用”数字,喵。
什么时候数字可以被复用呢?我们来分析一下下:
当比较符号连续相同时:比如
s = "<<"。- 这要求
a[0] < a[1] < a[2]。 - 在这种情况下,
a[0],a[1],a[2]这三个数字必须是 互不相同 的!比如[1, 2, 3]。我们不可能用更少的数字来构造一个严格递增(或递减)的序列。 - 如果有一段连续的
k个<,那么它会涉及到k+1个数组元素,这k+1个元素必须构成一个严格递增的序列,所以至少需要k+1个不同的数字。 - 同样,如果有一段连续的
k个>,也需要k+1个不同的数字来构造一个严格递减的序列。
- 这要求
当比较符号发生改变时:比如
s = "<>"。- 这要求
a[0] < a[1]并且a[1] > a[2]。 - 我们可以构造出
[1, 5, 2]这样的数组。你看,a[0]和a[2]并没有直接的大小关系,所以它们完全可以是相同的数字!比如[1, 5, 1],成本只有 2。 - 哇!原来当大小关系从
<变成>(或者从>变成<) 的时候,就是一个可以复用数字的绝佳时机!我们可以让数字“掉头回来”,使用之前用过的较小(或较大)的数值。
- 这要求
核心思想来啦! 既然连续相同的比较符会“消耗”掉我们的不同数字,而改变比较符方向时可以“省钱”,那么决定我们最少需要多少数字的,就是那段 最长的、连续的、相同比较符 的子串!
比如说,如果 s = "<<>>":
<<是最长的连续<子串,长度为 2。它要求一个长度为 3 的严格递增序列(比如a[0]<a[1]<a[2]),至少需要 3 个不同的数。>>是最长的连续>子串,长度为 2。它要求一个长度为 3 的严格递减序列(比如a[2]>a[3]>a[4]),至少需要 3 个不同的数。
我们可以这样构造:[1, 2, 3, 2, 1]。
1 < 2 < 3满足了<<的部分。3 > 2 > 1满足了>>的部分。- 整个数组只用了 1, 2, 3 这 3 个数字,成本就是 3。
所以,问题的关键就转化成了一个更简单的问题:找到字符串 s 中,最长的连续相同字符(全是 < 或全是 >)的子串长度,我们称之为 max_run。
那么,最终的答案就是 max_run + 1 啦!是不是很简单喵?
代码实现 (The Purr-fect Code)
下面就是实现这个思路的代码啦,主人请看~
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
// Meow~ 这里是解决单个测试用例的函数
void solve() {
int n;
std::cin >> n;
std::string s;
std::cin >> s;
// 如果字符串是空的,数组只有一个元素,成本是 1 喵
if (n == 0) {
std::cout << 1 << std::endl;
return;
}
// max_run 用来记录最长连续子串的长度
// current_run 用来记录当前正在计算的连续子串的长度
int max_run = 1;
int current_run = 1;
// 从第二个字符开始遍历,和前一个比较
for (int i = 1; i < n; ++i) {
if (s[i] == s[i-1]) {
// 如果和前一个字符相同,当前连续长度就 +1
current_run++;
} else {
// 如果不同了,说明连续中断了,重置为 1
current_run = 1;
}
// 每次都更新一下最大值,确保不会错过最长的那个~
if (current_run > max_run) {
max_run = current_run;
}
}
// 最长的连续子串长度是 max_run,所以需要 max_run + 1 个数字
std::cout << max_run + 1 << std::endl;
}
int main() {
// 加速一下输入输出,让程序跑得像小猫一样快!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}代码逻辑非常直接:
- 用
max_run记录全局最长连续长度,current_run记录当前连续长度。 - 遍历字符串,如果当前字符和前一个相同,
current_run就增加。 - 如果不同,说明一段连续结束了,
current_run重置为 1。 - 每次循环都用
current_run更新max_run。 - 最后输出
max_run + 1。
相关知识点 (Bonus Knowledge!)
这道题虽然简单,但也涉及到一些有用的知识点哦,学到就是赚到,喵!
贪心算法 (Greedy Algorithm) 我们的解法其实就是一种贪心。我们发现,为了让总成本最小,我们在任何可能的时候都应该复用数字。而复用的机会只出现在比较符号改变的拐点。在连续的相同符号段,我们别无选择,只能使用新的数字。所以,我们只需要满足最“苛刻”的那一段(也就是最长连续段)的需求,其他地方自然就能通过复用数字来满足,而不需要更多的新数字。这种只关注当前最优(满足最长段)从而得到全局最优解的思路,就是贪心的核心思想。
字符串处理 (String Processing) 这道题的核心操作是“寻找最长连续相同字符子串”,这是字符串处理中的一个非常基础和经典的问题。很多更复杂的字符串算法题,也可能包含类似的基础操作步骤。熟练掌握这种遍历和计数的模式,对解决其他问题也很有帮助。
单调序列 (Monotonic Sequence) 题目中
a[i] < a[i+1] < ...或a[i] > a[i+1] > ...的部分,其实就是数学中的 严格单调序列(递增或递减)。理解这个概念有助于我们迅速意识到,一个长度为k+1的严格单调序列至少需要k+1个不同的值。
好啦,这次的题解就到这里结束啦!希望我的讲解能帮到主人喔!如果还有什么问题,随时可以再来找我,喵~ >w<