复赛二:刮刮乐(game)

洛谷: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,我们需要统计它的所有非空连续子串中,有多少个是“可消除”的。

“可消除”的定义是:通过若干次操作可以将字符串变为空串,每次操作可以删除两个相邻且相同的字符,删除后剩余的字符串会拼接在一起。

算法思路
  1. 动态规划
    • 状态定义
      • dp[i]表示以位置i 结尾的可消除子串的数量。
      • lst[i]表示位置 i 之前,与 s[i] 相同且可以与之匹配消除的最近位置。
  2. 状态转移方程
    • 当存在一个位置 j,使得 s[i]==s[j],并且可以通过消除中间的部分使得 s[j] 与 s[i] 相邻,那么: dp[i]=dp[lst[i]−1]+1
      • 其中,lst[i]−1是匹配位置的前一个位置。
      • dp[lst[i]−1] 表示以位置 lst[i]−1 结尾的可消除子串数量。
      • 加上当前匹配,形成新的可消除子串,所以加 1。
  3. 寻找匹配的位置
    • 为了高效地找到满足条件的j,我们使用类似 KMP 算法的“失配跳转”机制。
    • 在遍历过程中,我们从 j=i−1 开始,如果 s[i]≠s[j],则跳到 lst[j]−1 位置继续比较。
    • 这样可以避免重复遍历已经处理过的字符,降低时间复杂度。
  4. 算法步骤
    • 初始化
      • dp[0]=0
      • lst[0]=0
      • ans=0
    • 遍历字符串
      • 对于每个位置 i 从 1 到 n:
        • 从 j=i−1开始,使用 j=lst[j]−1进行跳跃式遍历。
        • 如果 s[i]==s[j],则找到匹配位置,设置 lst[i]=j,退出循环。
        • 如果未找到匹配,lst[i]保持为 0。
      • 更新状态
        • 如果 lst[i]>0,则计算 dp[i]=dp[lst[i]−1]+1,并将 dp[i]加入答案 ans中。

示例说明

以字符串 accabccb 为例:

  • 初始化
    • s = ' accabccb'(添加空格后,字符串下标从 1 开始)。
    • ans = 0
  • 遍历过程
    • i = 1
      • 内层循环:j = 0,终止循环。
      • lst[1] 未被更新,dp[1] 也未更新。
    • i = 2
      • j = 1s[2] = 'c's[1] = 'a',不匹配。
      • j = lst[1] - 1lst[1] = 0,所以 j = -1,循环终止。
      • lst[2] 未被更新,dp[2] 也未更新。
    • i = 3
      • j = 2s[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
    • i = 4
      • j = 3s[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
    • i = 5
      • j = 4s[5] = 'b's[4] = 'a',不匹配。
      • j = lst[4] - 1 = 1 - 1 = 0,循环终止。
      • lst[5] 未被更新,dp[5] 也未更新。
    • i = 6
      • j = 5s[6] = 'c's[5] = 'b',不匹配。
      • j = lst[5] - 1 = 0 - 1 = -1,循环终止。
      • lst[6] 未被更新,dp[6] 也未更新。
    • i = 7
      • j = 6s[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
    • i = 8
      • j = 7s[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,与示例输出一致。
时间复杂度分析
  • 时间复杂度:O(n)
    • 外层循环遍历 i从 1 到 n,共 n 次。
    • 内层循环每次通过 j=lst[j]−1向前跳跃,且每个位置最多被访问一次,因此总的时间复杂度为 O(n)。
  • 空间复杂度:O(n)
    • 需要额外的数组 dp和 lst 来存储状态。
算法总结
  • 该算法巧妙地利用了动态规划和类似于 KMP(Knuth-Morris-Pratt)算法的失配跳转机制,高效地解决了问题。
  • 通过记录每个位置能与当前字符匹配的最近位置 lst[i],并利用 dp数组累积可消除子串的数量,避免了重复计算和不必要的遍历。
  • 这种方法有效地将时间复杂度降低到了线性级别,适用于字符串长度较大的情况。
 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;
}
Scroll to Top