题解:洛谷 P1203 坏掉的项链

【题目来源】

洛谷:P1203 [USACO1.1] 坏掉的项链 Broken Necklace - 洛谷

【题目描述】

你有一条由 \(n\) 个红色的,白色的,或蓝色的珠子组成的项链,珠子是随意安排的。这里是 \(n=29\) 的两个例子:

img/Pasted image 20260519125727.png

第一和第二个珠子在图片中已经被作记号。

图片 A 中的项链可以用下面的字符串表示:

brbrrrbbbrrrrrbrrbbrbbbbrrrrb

假如你要在一些点打破项链,展开成一条直线,然后从一端开始收集同颜色的珠子直到你遇到一个不同的颜色珠子,在另一端做同样的事(颜色可能与在这之前收集的不同)。确定应该在哪里打破项链来收集到最大数目的珠子。

例如,在图片 A 中的项链中,在珠子 9 和珠子 10 或珠子 24 和珠子 25 之间打断项链可以收集到 8 个珠子。

白色珠子什么意思?

在一些项链中还包括白色的珠子(如图片B)所示。

当收集珠子的时候,一个被遇到的白色珠子可以被当做红色也可以被当做蓝色。

表现含有白珠项链的字符串将会包括三个符号 rbw

写一个程序来确定从一条被给出的项链可以收集到的珠子最大数目。

【输入】

第一行一个正整数 \(n\) ,表示珠子

\(n\) 的字符串,每个字符是 rbw

【输出】

输出一行一个整数,表示从给出的项链中可以收集到的珠子的最大数量。

【输入样例】

29
wwwbbrwrbrbrrbrbrwrwwrbwrwrrb

【输出样例】

11

【核心思想】

  1. 问题分析:给定一条由 \(n\) 个红(r)、蓝(b)、白(w) 三种颜色珠子组成的环形项链。在某处断开后形成一条直线,从两端分别向中间收集同色珠子,白色珠子可视为红色或蓝色。目标是找到最优断开位置,使收集到的珠子总数最大。这是一个环形枚举 + 双端贪心模拟问题。

  2. 算法选择

    • 环形转线性:将原字符串 \(s\) 拼接成 \(s+s\),这样任意断开位置都对应一个长度为 \(n\) 的连续子串
    • 枚举断开位置:枚举起始位置 \(i \in [0, n-1]\),截取子串 \(s2 = (s+s)[i, i+n-1]\)
    • 双端贪心模拟:对每种线性表示,从左右两端分别向中间扩展,统计能收集的珠子数
    • 白色珠子处理:白色珠子作为通配符,可视为当前扩展段的颜色,从而延长同色段长度
    • 暴力枚举:由于断开位置只有 \(n\) 种可能,直接枚举所有情况并取最大值
  3. 关键步骤

    • 读取输入\(n\)(珠子数)、\(s\)(珠子颜色字符串)
    • 构造双倍串\(s1 = s + s\),将环形结构展开为线性结构
    • 枚举断开位置\(i\)\(0\)\(n-1\)):
      • 截取子串\(s2 = s1.substr(i, n)\),表示从位置 \(i\) 断开后得到的线性项链
      • 初始化\(ans = 2\)(首尾两个珠子),标记首尾已访问
      • 左端扩展:从左向右遍历,若珠子与左端当前颜色相同或为白色(且未访问),则加入
      • 右端扩展:从右向左遍历,若珠子与右端当前颜色相同或为白色(且未访问),则加入
      • 更新答案\(maxn = \max(maxn, ans)\)
    • 输出结果:最大可收集珠子数 \(maxn\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2)\),枚举 \(n\) 个断开位置,每个位置模拟扩展 \(O(n)\)
    • 空间复杂度:\(O(n)\),存储双倍串和访问标记数组
  5. 环形枚举 + 双端贪心的核心思想

    • 环形化线性:通过字符串复制一倍,将"在某处断开"转化为"取一个长度为 \(n\) 的子串"
    • 双端独立扩展:左右两端分别维护当前颜色,独立向中间贪心扩展
    • 通配符跟随策略:白色珠子不预先指定颜色,而是跟随当前扩展段的颜色,从而最大化同色连续段长度
    • 暴力枚举最优性:断开位置只有 \(n\) 种可能,枚举所有情况即可保证找到全局最优
    • 适用于环形结构的最优断开、环形子串枚举、双端贪心统计等问题

【解题思路】

【算法标签】

普及- #字符串入门

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int n, ans, maxn=-1e9;
string s, s1, s2;
int main()
{
    cin >> n >> s;  // 输入n和s
    s1 = s + s;  // 拼接2段字符串,方便后面截取
    for (int i=0; i<n; i++) {  // 从0开始遍历截取
        ans = 2;  // ans默认为2,即最少也有一头一尾2个字符
        int f[355]={0};  // 定义f数组,标记该位置是否被遍历过
        s2 = s1.substr(i, n);  // 截取字符串
        char last1 = s2[0];  // 获取左端第一个字符
        f[0] = 1;  // 标记已遍历
        char last2 = s2[s2.length()-1];  // 获取右端第一个字符
        f[s2.length()-1] = 1;  // 标记已遍历
        for (int j=1; j<s2.length(); j++) {  // 从第2个元素开始依次查找
            if (s2[j]!='w' && (s2[j]==last1) && f[j]==0) {  // 如果不是w,则一定要和last1一样,且没有被遍历过
                ans++;  // 数量自增1
                last1 = s2[j];  // 更新last1(其实不更新也可以,为了对仗)
                f[j] = 1;  // 标记已遍历
            } else if (s2[j]=='w' && f[j]==0) {  // 如果是w,且没有被遍历过
                ans++;  // 则直接自增1,不管w的前面是什么字符,且无需更新last1
                f[j] = 1;  // 标记已遍历
            } else if (last1=='w' && (s2[j]=='w'|| s2[j]=='b' || s2[j]=='r') && f[j]==0) {  // 如果last1是w,即第一个字符就是w,则无论是什么字符
                ans++;  // 直接自增1
                f[j] = 1;  // 标记已遍历
                last1 = s2[j];  // 更新last1
            } else break;  // 否则就是碰到不一样的,退出循环
        }
        for (int j=s2.length()-2; j>=0; j--) {  // 开始从后往前遍历
            if (s2[j]!='w' && (s2[j]==last2) && f[j]==0) {  // 如果不是w,则一定要和last2一样,且没有被遍历过
                ans++;  // 数量自增1
                last2 = s2[j];  // 更新last2
            } else if (s2[j]=='w' && f[j]==0) {  // 如果是w,且没有被遍历过
                ans++;  // 则直接自增1,不管w的前面是什么字符,且无需更新last1
            } else if (last1=='w' && (s2[j]=='w'|| s2[j]=='b' || s2[j]=='r') && f[j]==0) {  // 如果last2是w,则无论是什么字符
                ans++;  // 直接自增1
                last1 = s2[j];  // 更新last2,只要遇到不是w的,更新后下次遍历就进入其他分支中
            } else break;  // 否则就是碰到不一样的,退出循环
        }
        maxn = max(maxn, ans);  // 获取最大值
    }
    cout << maxn << endl;  // 打印最大值
    return 0;
}

【运行结果】

29
wwwbbrwrbrbrrbrbrwrwwrbwrwrrb
11
posted @ 2026-07-30 09:30  团爸讲算法  阅读(3)  评论(0)    收藏  举报