题解:洛谷 P1203 坏掉的项链
【题目来源】
洛谷:P1203 [USACO1.1] 坏掉的项链 Broken Necklace - 洛谷
【题目描述】
你有一条由 \(n\) 个红色的,白色的,或蓝色的珠子组成的项链,珠子是随意安排的。这里是 \(n=29\) 的两个例子:

第一和第二个珠子在图片中已经被作记号。
图片 A 中的项链可以用下面的字符串表示:
brbrrrbbbrrrrrbrrbbrbbbbrrrrb
假如你要在一些点打破项链,展开成一条直线,然后从一端开始收集同颜色的珠子直到你遇到一个不同的颜色珠子,在另一端做同样的事(颜色可能与在这之前收集的不同)。确定应该在哪里打破项链来收集到最大数目的珠子。
例如,在图片 A 中的项链中,在珠子 9 和珠子 10 或珠子 24 和珠子 25 之间打断项链可以收集到 8 个珠子。
白色珠子什么意思?
在一些项链中还包括白色的珠子(如图片B)所示。
当收集珠子的时候,一个被遇到的白色珠子可以被当做红色也可以被当做蓝色。
表现含有白珠项链的字符串将会包括三个符号 r,b,w。
写一个程序来确定从一条被给出的项链可以收集到的珠子最大数目。
【输入】
第一行一个正整数 \(n\) ,表示珠子
\(n\) 的字符串,每个字符是 r ,b 或 w。
【输出】
输出一行一个整数,表示从给出的项链中可以收集到的珠子的最大数量。
【输入样例】
29
wwwbbrwrbrbrrbrbrwrwwrbwrwrrb
【输出样例】
11
【核心思想】
-
问题分析:给定一条由 \(n\) 个红(
r)、蓝(b)、白(w) 三种颜色珠子组成的环形项链。在某处断开后形成一条直线,从两端分别向中间收集同色珠子,白色珠子可视为红色或蓝色。目标是找到最优断开位置,使收集到的珠子总数最大。这是一个环形枚举 + 双端贪心模拟问题。 -
算法选择:
- 环形转线性:将原字符串 \(s\) 拼接成 \(s+s\),这样任意断开位置都对应一个长度为 \(n\) 的连续子串
- 枚举断开位置:枚举起始位置 \(i \in [0, n-1]\),截取子串 \(s2 = (s+s)[i, i+n-1]\)
- 双端贪心模拟:对每种线性表示,从左右两端分别向中间扩展,统计能收集的珠子数
- 白色珠子处理:白色珠子作为通配符,可视为当前扩展段的颜色,从而延长同色段长度
- 暴力枚举:由于断开位置只有 \(n\) 种可能,直接枚举所有情况并取最大值
-
关键步骤:
- 读取输入:\(n\)(珠子数)、\(s\)(珠子颜色字符串)
- 构造双倍串:\(s1 = s + s\),将环形结构展开为线性结构
- 枚举断开位置(\(i\) 从 \(0\) 到 \(n-1\)):
- 截取子串:\(s2 = s1.substr(i, n)\),表示从位置 \(i\) 断开后得到的线性项链
- 初始化:\(ans = 2\)(首尾两个珠子),标记首尾已访问
- 左端扩展:从左向右遍历,若珠子与左端当前颜色相同或为白色(且未访问),则加入
- 右端扩展:从右向左遍历,若珠子与右端当前颜色相同或为白色(且未访问),则加入
- 更新答案:\(maxn = \max(maxn, ans)\)
- 输出结果:最大可收集珠子数 \(maxn\)
-
时间/空间复杂度:
- 时间复杂度:\(O(n^2)\),枚举 \(n\) 个断开位置,每个位置模拟扩展 \(O(n)\)
- 空间复杂度:\(O(n)\),存储双倍串和访问标记数组
-
环形枚举 + 双端贪心的核心思想:
- 环形化线性:通过字符串复制一倍,将"在某处断开"转化为"取一个长度为 \(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
浙公网安备 33010602011771号