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

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种宝石,求最多能划分多少段。

核心难点
  1. 环形结构:数组是环形的,需要考虑循环的情况
  2. 最短包含所有宝石的段 :要划分尽可能多的段,每段应该是包含所有m种宝石的最短连续段
  3. 贪心策略:每次找到最短的合法段,然后从下一位置继续寻找
算法思路
  1. 环形处理:将原数组复制一份接在后面,形成2n长度的数组,这样环形问题转化为线性问题
  2. 滑动窗口:使用双指针维护一个包含所有m种宝石的最短窗口
  3. 贪心划分
    • 从起点开始,找到最短的包含所有宝石的段
    • 记录这个段的结束位置,然后从下一个位置继续寻找
    • 重复直到覆盖整个环形数组
  4. 最大化段数:由于是环形,不同起点可能导致不同结果,需要尝试所有起点
时间复杂度优化
  • 朴素方法:对于每个起点O(n)寻找,总O(n²),会超时
  • 优化方法:使用**倍增法(Binary Lifting)**预处理每个位置开始跳转的信息
    • 预处理:对于每个位置i,计算从i开始取一段后下一个起点的位置
    • 倍增表:f[i][k]表示从i开始取2^k段后到达的位置
    • 查询:对于每个起点,用倍增快速计算最多能取多少段
具体步骤
  1. 数据准备:复制数组为2n长度
  2. 滑动窗口预处理
    • 对于每个位置i,找到以i为起点的最短合法段的结束位置
    • 记录next[i] = 结束位置 + 1(下一段的起点)
  3. 构建倍增表
    • f[i][0] = next[i](跳1段)
    • f[i][k] = f[f[i][k-1]][k-1](跳2^k段)
  4. 枚举起点计算
    • 对每个起点i,用倍增法计算最多能跳多少段
    • 保持结束位置不超过i+n(环形限制)
  5. 取最大值:所有起点结果的最大值

代码实现

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. 关键点总结
  1. 环形处理技巧:数组复制是处理环形问题的常用方法
  2. 滑动窗口优化:双指针法在线性时间内找到所有最短合法段
  3. 倍增法应用:将多次跳转压缩为对数级别查询
  4. 贪心正确性:每次取最短段能最大化段数,这是本题的关键性质
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;
}
相关推荐
郝学胜-神的一滴2 小时前
深入理解Qt中的坐标系统:鼠标、窗口与控件位置详解
开发语言·c++·qt·程序人生
福楠2 小时前
C++ | 继承
c语言·开发语言·数据结构·c++·算法
程序员zgh2 小时前
汽车以太网协议 —— DDS
c语言·开发语言·c++·网络协议·udp·汽车·信息与通信
王老师青少年编程2 小时前
2025年12月GESP真题及题解(C++八级): 猫和老鼠
c++·gesp·csp·信奥赛·八级·csp-s·提高组
_OP_CHEN2 小时前
【算法基础篇】(四十六)同余方程终极攻略:从基础转化到实战破解
c++·算法·蓝桥杯·数论·同余方程·扩展欧几里得算法·acm/icpc
程序员泡椒4 小时前
二分查找Go版本实现
数据结构·c++·算法·leetcode·go·二分
txinyu的博客10 小时前
解析业务层的key冲突问题
开发语言·c++·分布式
SmartRadio11 小时前
ESP32添加修改蓝牙名称和获取蓝牙连接状态的AT命令-完整UART BLE服务功能后的完整`main.c`代码
c语言·开发语言·c++·esp32·ble
charlie11451419113 小时前
嵌入式的现代C++教程——constexpr与设计技巧
开发语言·c++·笔记·单片机·学习·算法·嵌入式