洛谷:P9753
OJ: P5960
方法一:暴力搜索 35分
维护一个栈,按顺序遍历字符串,若当前字符恰好等于栈顶,则弹出栈顶;反之则将该字符入栈。若遍历结束后,栈为空,则说明该字符串是“可消除的“。
题目要求对于一个字符串所有的子串是否为“可消除的”,那么最暴力的想法就是暴力枚举该字符串的每个子串 [l,r]并做上述括号匹配来判断,若该子串是“可消除的”,则方案数 +1。
时间复杂度 O(n^3),期望得分 35pts。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | /**************************************************************** * Description: 2023年CSP提高组复赛第二题,刮刮乐 35分 * Author: Alex Li * Date: 2024-07-16 16:53:33 * LastEditTime: 2024-09-27 22:17:14 ****************************************************************/ #include <iostream> #include <stack> using namespace std; // 判断一个字符串是否可被完全消除 bool isEliminable(const string &str) { stack<char> s; // 创建一个栈用于模拟消除过程 for (char ch : str) { if (!s.empty() && s.top() == ch) { // 如果栈不为空且栈顶元素等于当前字符 s.pop(); // 弹出栈顶元素,表示消除一对相同字符 } else { s.push(ch); // 否则,将当前字符压入栈中 } } return s.empty(); // 如果最终栈为空,表示字符串可被完全消除 } int main(){ int n, ans = 0; cin >> n; // 输入字符串长度(实际程序中未直接使用) string str; cin >> str; // 输入字符串 for(int i = 0; i < str.size(); i++) { for(int j = i + 1; j < str.size(); j++) { // 只有当子串长度为偶数时,才可能完全消除 if ((j - i + 1) % 2 == 0 && isEliminable(str.substr(i, j - i + 1))) ++ans; // 如果子串可被完全消除,计数器加一 } } cout << ans; // 输出满足条件的子串数量 } |
方法二:优化暴力 50分
考虑优化。注意到对于起点 l 相同的子串,只要维护过程中栈为空,那么就说明从 l到当前为止是个“可消除的”的字符串。所以我们不需要每次遍历子串中的每一个字符,维护多个栈,只需要遍历一次 l到 n,维护一个栈来解决,具体地:遍历 1 到 n 的每一个数作为子串的起点 l,每次维护一个栈,遍历从 l 到 n 的所有字符,做一遍括号匹配,同时在过程中维护栈被弹为空的次数 cnt,每次让答案 count++即可。
时间复杂度 O(n^2),期望得分 50pts。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | /**************************************************************** * Description: 2023年CSP提高组复赛第二题,刮刮乐 50分 * Author: Alex Li * Date: 2024-07-16 13:14:32 * LastEditTime: 2024-09-27 22:49:23 ****************************************************************/ #include <iostream> #include <string> #include <stack> using namespace std; int countEliminableSubstrings(const string &str) { int n = str.size(); int count = 0; // 枚举所有的子串的起始位置 for (int i = 0; i < n; ++i) { stack<char> s; // 以i为起始位置,向右扫描 for (int j = i; j < n; ++j) { if (!s.empty() && s.top() == str[j]) { s.pop(); } else { s.push(str[j]); } // 如果栈为空,说明当前子串可完全消除 if (s.empty()) { count++; } } } return count; } int main() { int n; cin >> n; string str; cin >> str; int result = countEliminableSubstrings(str); cout << result << endl; return 0; } |
方法三:动态规划 100分
给定一个长度为 n 的只包含小写字母的字符串 s,我们需要统计它的所有非空连续子串中,有多少个是“可消除”的。
“可消除”的定义是:通过若干次操作可以将字符串变为空串,每次操作可以删除两个相邻且相同的字符,删除后剩余的字符串会拼接在一起。
以字符串 accabccb
为例:
s = ' accabccb'
(添加空格后,字符串下标从 1 开始)。ans = 0
。j = 0
,终止循环。lst[1]
未被更新,dp[1]
也未更新。j = 1
,s[2] = 'c'
,s[1] = 'a'
,不匹配。j = lst[1] - 1
,lst[1] = 0
,所以 j = -1
,循环终止。lst[2]
未被更新,dp[2]
也未更新。j = 2
,s[3] = 'c'
,s[2] = 'c'
,匹配。lst[3] = 2
。dp[3] = dp[lst[3] - 1] + 1 = dp[1] + 1 = 0 + 1 = 1
。ans += dp[3] = 1
。j = 3
,s[4] = 'a'
,s[3] = 'c'
,不匹配。j = lst[3] - 1 = 2 - 1 = 1
。s[4] = 'a'
,s[1] = 'a'
,匹配。lst[4] = 1
。dp[4] = dp[lst[4] - 1] + 1 = dp[0] + 1 = 0 + 1 = 1
。ans += dp[4] = 2
。j = 4
,s[5] = 'b'
,s[4] = 'a'
,不匹配。j = lst[4] - 1 = 1 - 1 = 0
,循环终止。lst[5]
未被更新,dp[5]
也未更新。j = 5
,s[6] = 'c'
,s[5] = 'b'
,不匹配。j = lst[5] - 1 = 0 - 1 = -1
,循环终止。lst[6]
未被更新,dp[6]
也未更新。j = 6
,s[7] = 'c'
,s[6] = 'c'
,匹配。lst[7] = 6
。dp[7] = dp[lst[7] - 1] + 1 = dp[5] + 1 = 0 + 1 = 1
。ans += dp[7] = 3
。j = 7
,s[8] = 'b'
,s[7] = 'c'
,不匹配。j = lst[7] - 1 = 6 - 1 = 5
。s[8] = 'b'
,s[5] = 'b'
,匹配。lst[8] = 5
。dp[8] = dp[lst[8] - 1] + 1 = dp[4] + 1 = 1 + 1 = 2
。ans += dp[8] = 5
。ans = 5
,与示例输出一致。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /**************************************************************** * Description: 2023年提高组复赛第三题 刮刮乐 100分 * Author: Alex Li * Date: 2024-09-28 14:49:14 * LastEditTime: 2024-09-28 16:16:59 ****************************************************************/ #include <cstdio> #include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; vector<int> dp(n+1),lst(n+1); string s; cin >> s; s = ' ' + s; // 在字符串前添加一个空格,使字符串下标从1开始,方便处理 long long ans = 0; // 遍历字符串中的每一个字符 for (int i = 1; i <= n; i++) { // 从当前位置 i 向前寻找与 s[i] 相同的字符 // 使用 lst[j] 进行跳跃,避免重复遍历 for (int j = i - 1; j > 0; j = lst[j] - 1) { if (s[i] == s[j]) { lst[i] = j; // 记录匹配的位置 break; // 找到匹配字符后退出循环 } } // 如果找到了匹配的位置 if (lst[i]) { dp[i] = dp[lst[i] - 1] + 1; // 动态规划转移方程 ans += dp[i]; // 累加答案 } } cout << ans; return 0; } |