洛谷 P14924:[GESP202512 八级] 宝石项链 ← 倍增 + 动态规划

【题目来源】
https://www.luogu.com.cn/problem/P14924

【题目描述】
小 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 出现的次数

● 逐段代码详细解析
第一段:输入处理 + 构造循环数组

int n,m;
cin>>n>>m;
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<=n; i++) a[n+i]=a[i];

(1)输入n:原数组长度;m:数组中不同元素的总类数(题目保证输入满足此条件);
(2)原数组存储在 a[1~n](下标从 1 开始,竞赛常用写法,避免边界问题);
(3)a[n+i] = a[i]:把原数组复制一遍接在后面,构造两倍长度的循环数组,解决「数组首尾相连」的循环问题。

第二段:滑动窗口(双指针)预处理 f[i][0] 数组 【核心 1】

int s=0;  // s:当前窗口内「不同元素的数量」
for(int i=1,j=1; i<=2*n; i++) {  // i=窗口左端点,j=窗口右端点(均只向右移动)
    // 右指针j向右扩展,直到窗口内包含所有m种元素,或j越界
    while(j<=2*n && s<m) {
        c[a[j]]++;  // 元素a[j]加入窗口,计数+1
        if(c[a[j]]==1) s++;  // 如果是第一次出现,不同元素数+1
        j++;
    }
    // 预处理倍增的「0阶」:如果当前窗口满足条件(包含所有m种元素)
    if(s==m) f[i][0]=j;  // 从i出发,走「1步(2^0)」,到达位置j
    // 左指针i向右移动,移出窗口,维护窗口计数
    c[a[i]]--;  // 元素a[i]移出窗口,计数-1
    if(c[a[i]]==0) s--;  // 如果计数变为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】

for(int j=1; j<=LOG; j++) {  // 枚举倍增的「阶数」,从1到20
    for(int i=1; i<=n; i++) {  // 枚举所有起点(只枚举原数组的1~n,因为是求一个完整周期)
        f[i][j]=f[f[i][j-1]][j-1];  // 倍增核心递推公式
    }
}

倍增法的本质是:用「二进制」表示所有整数,把「跳 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】

int ans=0;  // 存储最终答案:最大能取到的子段数量
for(int i=1; i<=n; i++) {  // 枚举原数组的每个起点i(1<=i<=n,循环数组的一个周期)
    int t=0, p=i, up=n+i;  // t:当前起点i能取到的子段数量;p:当前跳转的位置;up:i的最远边界(不超过n+i,即一个周期)
    // 从最高阶到0阶枚举,贪心大步数优先(二进制分解)
    for(int j=LOG; j>=0; j--) {
        // 条件1:f[p][j]不为0(该位置能跳转);条件2:跳转后不超过最远边界up(不超出一个周期)
        if(f[p][j]<=up && f[p][j]) {
            t+=(1<<j);  // 累加步数:1<<j 等价于 2^j
            p=f[p][j];  // 更新当前位置为跳转后的位置
        }
    }
    ans=max(ans,t);  // 更新全局最大值
}
cout<<ans<<endl;

关键变量 & 逻辑解释
(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 的最大值。
​​​​​​​
【算法代码】

#include <bits/stdc++.h>
using namespace std;

const int N=2e5+5;
const int LOG=20; //log2(N)<20
int f[N][LOG+5];
int a[N],c[N];

int main() {
    int n,m;
    cin>>n>>m;
    for(int i=1; i<=n; i++) cin>>a[i];
    for(int i=1; i<=n; i++) a[n+i]=a[i];

    int s=0;
    for(int i=1,j=1; i<=2*n; i++) {
        while(j<=2*n && s<m) {
            c[a[j]]++;
            if(c[a[j]]==1) s++;
            j++;
        }
        if(s==m) f[i][0]=j;
        c[a[i]]--;
        if(c[a[i]]==0) s--;
    }

    for(int j=1; j<=LOG; j++) {
        for(int i=1; i<=n; i++) {
            f[i][j]=f[f[i][j-1]][j-1];
        }
    }

    int ans=0;
    for(int i=1; i<=n; i++) {
        int t=0, p=i, up=n+i;
        for(int j=LOG; j>=0; j--) {
            if(f[p][j]<=up && f[p][j]) {
                t+=(1<<j);
                p=f[p][j];
            }
        }
        ans=max(ans,t);
    }
    cout<<ans<<endl;

    return 0;
}

/*
in:
7 3
3 1 3 1 2 1 2

out:
2
*/





【参考文献】
https://www.luogu.com.cn/article/gd3c03t7
https://mp.weixin.qq.com/s/GI4PgJ1wlK8xhckAFeb1uw
https://www.luogu.com.cn/problem/solution/P14924
https://gesp.ccf.org.cn/101/attach/1720331477712928.pdf


 

posted @ 2026-01-13 11:13  Triwa  阅读(5)  评论(0)    收藏  举报