2025年12月GESP真题及题解(C++八级): 宝石项链

题目描述
小 A 有一串包含 n n n 枚宝石的宝石项链,这些宝石按照在项链中的顺序依次以 1 , 2 , ... , n 1,2,\ldots,n 1,2,...,n 编号,第 n n n 枚宝石与第 1 1 1 枚宝石相邻。项链由 m m m 种宝石组成,其中第 i i i 枚宝石种类为 t i t_i ti。
小 A 想将宝石项链分给他的好朋友们。具体而言,小 A 会将项链划分为若干连续段 ,并且需要保证每段都包含全部 m m m 种宝石。请帮小 A 计算在满足条件的前提下,宝石项链最多可以划分为多少段。
输入格式
第一行,两个正整数 n , m n,m n,m,分别表示宝石项链中的宝石的数量与种类数。
第二行, n n n 个正整数 t 1 , t 2 , ... , t n t_1,t_2,\ldots,t_n t1,t2,...,tn,表示每枚宝石的种类。
输出格式
输出一行,一个整数,表示宝石项链最多可以划分的段数。
输入输出样例 1
输入 1
6 2
1 2 1 2 1 2
输出 1
3
输入输出样例 2
输入 2
7 3
3 1 3 1 2 1 2
输出 2
2
说明/提示
对于 40 % 40\% 40% 的测试点,保证 2 ≤ n ≤ 1000 2\le n\le 1000 2≤n≤1000。
对于所有测试点,保证 2 ≤ n ≤ 10 5 2\le n\le 10^5 2≤n≤105, 2 ≤ m ≤ n 2\le m\le n 2≤m≤n, 1 ≤ t i ≤ m 1\le t_i\le m 1≤ti≤m,保证 1 , 2 , ... , m 1,2,\ldots,m 1,2,...,m 均在 t 1 , t 2 , ... , t n t_1,t_2,\ldots,t_n t1,t2,...,tn 中出现。
思路分析
这是一个环形数组划分问题。我们需要将一个环形宝石项链(第n个宝石与第1个宝石相邻)划分为若干连续段,每段必须包含全部m种宝石,求最多能划分多少段。
核心难点
- 环形结构:数组是环形的,需要考虑循环的情况
- 最短包含所有宝石的段 :要划分尽可能多的段,每段应该是包含所有m种宝石的最短连续段
- 贪心策略:每次找到最短的合法段,然后从下一位置继续寻找
算法思路
- 环形处理:将原数组复制一份接在后面,形成2n长度的数组,这样环形问题转化为线性问题
- 滑动窗口:使用双指针维护一个包含所有m种宝石的最短窗口
- 贪心划分 :
- 从起点开始,找到最短的包含所有宝石的段
- 记录这个段的结束位置,然后从下一个位置继续寻找
- 重复直到覆盖整个环形数组
- 最大化段数:由于是环形,不同起点可能导致不同结果,需要尝试所有起点
时间复杂度优化
- 朴素方法:对于每个起点O(n)寻找,总O(n²),会超时
- 优化方法:使用**倍增法(Binary Lifting)**预处理每个位置开始跳转的信息
- 预处理:对于每个位置i,计算从i开始取一段后下一个起点的位置
- 倍增表:f[i][k]表示从i开始取2^k段后到达的位置
- 查询:对于每个起点,用倍增快速计算最多能取多少段
具体步骤
- 数据准备:复制数组为2n长度
- 滑动窗口预处理 :
- 对于每个位置i,找到以i为起点的最短合法段的结束位置
- 记录next[i] = 结束位置 + 1(下一段的起点)
- 构建倍增表 :
- f[i][0] = next[i](跳1段)
- f[i][k] = f[f[i][k-1]][k-1](跳2^k段)
- 枚举起点计算 :
- 对每个起点i,用倍增法计算最多能跳多少段
- 保持结束位置不超过i+n(环形限制)
- 取最大值:所有起点结果的最大值
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 5; // 2倍长度
const int LOG = 20; // 2^20 > 1e6
int n, m;
int t[MAXN]; // 宝石数组(已复制为2倍长度)
int cnt[MAXN]; // 计数数组,记录当前窗口每种宝石数量
int nxt[MAXN]; // nxt[i]: 从i开始取一段后下一个起点位置
int f[MAXN][LOG]; // 倍增表:f[i][k]表示从i开始取2^k段后到达的位置
int main() {
// 读入数据
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> t[i];
t[i + n] = t[i]; // 复制一份,处理环形
}
// 滑动窗口预处理每个位置的下一个起点
int types = 0; // 当前窗口内宝石种类数
int r = 0; // 右指针
// 初始化计数数组
for (int i = 1; i <= m; i++) cnt[i] = 0;
// 遍历每个位置作为左端点
for (int l = 0; l < 2 * n; l++) {
// 扩展右指针,直到窗口包含所有m种宝石
while (r < 2 * n && types < m) {
cnt[t[r]]++;
if (cnt[t[r]] == 1) types++;
r++;
}
// 如果找到了包含所有宝石的窗口,记录下一个起点
if (types == m) {
nxt[l] = r; // 结束位置+1就是下一段起点
} else {
nxt[l] = 2 * n; // 标记为无效位置
}
// 移动左指针前,减少计数
cnt[t[l]]--;
if (cnt[t[l]] == 0) types--;
}
// 构建倍增表
// 初始化:跳2^0=1段
for (int i = 0; i < 2 * n; i++) {
f[i][0] = nxt[i];
}
// 计算2^k段
for (int k = 1; k < LOG; k++) {
for (int i = 0; i < 2 * n; i++) {
if (f[i][k - 1] < 2 * n) {
f[i][k] = f[f[i][k - 1]][k - 1];
} else {
f[i][k] = 2 * n; // 超出范围
}
}
}
// 枚举每个起点,计算最多段数
int ans = 0;
for (int start = 0; start < n; start++) {
int pos = start; // 当前位置
int seg = 0; // 段数
// 从大到小尝试跳转
for (int k = LOG - 1; k >= 0; k--) {
if (f[pos][k] <= start + n) { // 跳2^k段后不超过环形范围
seg += (1 << k); // 增加段数
pos = f[pos][k]; // 更新位置
}
}
ans = max(ans, seg);
}
cout << ans << endl;
return 0;
}
功能分析
1. 数据预处理部分
cpp
// 复制数组处理环形
for (int i = 0; i < n; i++) {
t[i + n] = t[i];
}
- 目的:将环形问题转化为线性问题
- 原理:在长度为2n的数组上,任何长度为n的窗口都对应原环形数组的一个起始位置
2. 滑动窗口预处理
cpp
while (r < 2 * n && types < m) {
cnt[t[r]]++;
if (cnt[t[r]] == 1) types++;
r++;
}
- 功能:对于每个左端点l,找到最短的包含所有m种宝石的右端点
- 变量说明 :
types:当前窗口内不同宝石的种类数cnt[]:记录每种宝石的出现次数nxt[l]:从l开始取一段后,下一段的起始位置
3. 倍增表构建
cpp
for (int k = 1; k < LOG; k++) {
for (int i = 0; i < 2 * n; i++) {
if (f[i][k - 1] < 2 * n) {
f[i][k] = f[f[i][k - 1]][k - 1];
}
}
}
- 原理 :
f[i][k] = f[f[i][k-1]][k-1]表示从i跳2k段等价于先跳2(k-1)段,再跳2^(k-1)段 - LOG选择:2^20 ≈ 1e6 > 2×1e5,足够覆盖所有可能跳转
4. 枚举起点计算
cpp
for (int k = LOG - 1; k >= 0; k--) {
if (f[pos][k] <= start + n) {
seg += (1 << k);
pos = f[pos][k];
}
}
- 贪心策略:从高位向低位尝试跳转
- 约束条件 :
f[pos][k] <= start + n确保跳转后不超过环形范围 - 时间复杂度:每个起点O(log n),总O(n log n)
5. 算法复杂度
- 时间复杂度 :O(n log n)
- 滑动窗口预处理:O(n)(每个元素进出各一次)
- 倍增表构建:O(n log n)
- 枚举起点计算:O(n log n)
- 空间复杂度:O(n log n)(主要存储倍增表)
6. 关键点总结
- 环形处理技巧:数组复制是处理环形问题的常用方法
- 滑动窗口优化:双指针法在线性时间内找到所有最短合法段
- 倍增法应用:将多次跳转压缩为对数级别查询
- 贪心正确性:每次取最短段能最大化段数,这是本题的关键性质
7. 示例解析
以样例1为例:
n=6, m=2
宝石序列: 1 2 1 2 1 2
- 最短合法段长度均为2(包含1和2)
- 可以划分为3段:[1,2]、[1,2]、[1,2]
- 代码会找到所有起点中的最优解
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新):https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
CSP信奥赛C++一等奖通关刷题题单及题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12673810.html 点击跳转
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}