洛谷 P14924:[GESP202512 八级] 宝石项链 ← 倍增 + 动态规划
【题目来源】
【题目描述】
小 A 有一串包含 n 枚宝石的宝石项链,这些宝石按照在项链中的顺序依次以 1,2,…,n 编号,第 n 枚宝石与第 1 枚宝石相邻。项链由 m 种宝石组成,其中第 i 枚宝石种类为 ti。
小 A 想将宝石项链分给他的好朋友们。具体而言,小 A 会将项链划分为若干连续段,并且需要保证每段都包含全部 m 种宝石。请帮小 A 计算在满足条件的前提下,宝石项链最多可以划分为多少段。
【输入格式】
第一行,两个正整数 n,m,分别表示宝石项链中的宝石的数量与种类数。
第二行,n 个正整数 t1,t2,…,tn,表示每枚宝石的种类。
【输出格式】
输出一行,一个整数,表示宝石项链最多可以划分的段数。
【输入样例一】
6 2
1 2 1 2 1 2
【输出样例一】
3
【输入样例二】
7 3
3 1 3 1 2 1 2
【输出样例二】
2
【数据范围】
对于 40% 的测试点,保证 2≤n≤1000。
对于所有测试点,保证 2≤n≤10^5,2≤m≤n,1≤ti≤m,保证 1,2,…,m 均在 t1,t2,…,tn 中出现。
【算法分析】
● 本文代码是 「滑动窗口(双指针)」 + 「倍增法(二进制跳跃 / ST 表)」 的经典结合,这是处理「区间最值 / 区间跳转 + 计数」问题的最优解,时间复杂度做到了 O(nlogn),能完美处理 n=2e5 的数据规模(暴力做法 O(n^2) 会超时)。
● 全局变量含义(重中之重)
所有变量都是全局定义,避免栈溢出,含义如下:
int f[N][LOG+5]; // 倍增核心数组:f[i][j] 表示「从位置 i 出发,跳 2^j 步后到达的位置」
//特别注意:此处「1步」的定义为取一个包含全部 m 种元素的最短连续子段
int a[N]; // 存储原数组。且利用 a[n+1~2n] = a[1~n],构造循环数组
int c[N]; // 计数数组:c[x] 表示当前窗口内元素 x 出现的次数
● 逐段代码详细解析
第一段:输入处理 + 构造循环数组
(1)输入n:原数组长度;m:数组中不同元素的总类数(题目保证输入满足此条件);
(2)原数组存储在 a[1~n](下标从 1 开始,竞赛常用写法,避免边界问题);
(3)a[n+i] = a[i]:把原数组复制一遍接在后面,构造两倍长度的循环数组,解决「数组首尾相连」的循环问题。
第二段:滑动窗口(双指针)预处理 f[i][0] 数组 【核心 1】
这段代码的核心意义:
f[i][0] 是整个算法的基石,定义是:从位置 i 作为起点,向右找「包含全部 m 种元素的最短连续子段」,这个子段的「下一个起点」就是 f[i][0]。
滑动窗口的性质:i 和 j 都只向右移动,不会回退,因此时间复杂度是 O(n),这是能处理 2e5 数据的关键;
窗口维护的逻辑:保证每个左端点 i,都能找到最小的右端点 j,使得区间 [i, j-1] 是「以 i 为起点、包含全部 m 种元素的最短连续子段」;
为什么是 [i, j-1] 不是 [i, j]?因为 while 循环中最后执行了 j++,所以有效区间是 [i, j-1],j 是这个子段的下一个位置,也是下一次的起点。
第三段:倍增预处理 f[i][j] 数组 【核心 2】
倍增法的本质是:用「二进制」表示所有整数,把「跳 k 步」拆解为「跳若干个 2 的幂次步」,比如跳 5 步 = 跳 4 步 (2²) + 跳 1 步 (2⁰)。
递推公式 f[i][j] = f[f[i][j-1] ][j-1] 的含义:从位置i出发,跳 2^j 步 → 等价于先从 i 跳
2^(j−1) 步到 f[i][j-1],再从这个位置继续跳 2^(j−1) 步。
O(nlogn) 的预处理后,任意起点、任意步数的跳转都能在 O(logn) 时间内完成,而不是暴力的 O(n)。
第四段:枚举所有起点,倍增查询最大值 【核心 3】
关键变量 & 逻辑解释
(1)up = n+i:因为是循环数组的一个完整周期,从起点 i 出发,最远只能到 n+i(超过则重复计算第二个周期),这是本题的核心约束;
(2)j 从 LOG 到 0:贪心策略,先尝试跳大步数(如 2^20、2^19...),能跳则跳,保证用最少的次数凑出最大的步数,这是倍增法的标准查询方式;
(3)1<<j:位运算,等价于 2^j,表示本次跳转的步数;
(4)t:统计从起点i出发,在一个周期内,最多能取到的「包含全部 m 种元素的连续子段」的数量。
核心目标就是枚举每个起点 i,计算该起点能取到的最大子段数 t,最终答案是所有 t 的最大值。
【算法代码】
【参考文献】

浙公网安备 33010602011771号