先把所有彩珠按坐标打平成 `(位置, 颜色)` 序列并排序,再用双指针维护覆盖全部颜色的最短区间。
OJ: luogu
题目 ID: P2564
难度:普及+/提高
标签:双指针滑动窗口排序思维
日期: 2026-06-21 00:56
题意
有一条彩带,上面挂着 N 颗彩珠,一共分成 K 种颜色。
输入不是按坐标顺序给出的,而是按颜色分组给出每一种彩珠出现的位置。
现在要剪出一段连续彩带,使它至少包含每一种颜色各一颗,求最短长度。
这里长度定义为:
右端点坐标 - 左端点坐标
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
const int MAXN = 205;
const int MAXK = 65;
struct Node {
int pos;
int kind;
};
int n, k;
Node a[MAXN];
int cnt[MAXK];
bool cmp_node(const Node &x, const Node &y) {
if (x.pos != y.pos) {
return x.pos < y.pos;
}
return x.kind < y.kind;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
int tot = 0;
for (int i = 1; i <= k; i++) {
int t;
cin >> t;
for (int j = 1; j <= t; j++) {
++tot;
cin >> a[tot].pos;
a[tot].kind = i;
}
}
sort(a + 1, a + tot + 1, cmp_node);
int ans = INT_MAX;
// brute.cpp:枚举排序后彩珠序列上的左右端点。
// 只要这个区间已经覆盖了所有种类,就用坐标差更新答案。
for (int l = 1; l <= tot; l++) {
for (int i = 1; i <= k; i++) {
cnt[i] = 0;
}
int covered = 0;
for (int r = l; r <= tot; r++) {
if (cnt[a[r].kind] == 0) {
covered++;
}
cnt[a[r].kind]++;
if (covered == k) {
ans = min(ans, a[r].pos - a[l].pos);
break;
}
}
}
cout << ans << '\n';
return 0;
}brute.cpp 先把所有彩珠整理成 (位置, 颜色),按位置排序,然后枚举左端点 l,再向右找到第一个能覆盖全部颜色的右端点 r。
这个思路是对的,但它会重复扫描很多区间,最坏复杂度到
关键观察是:排序之后,题目就变成了一个标准的“最短覆盖全部种类的连续区间”问题。
于是我们维护滑动窗口 [l, r]:
cnt[c]表示颜色c在窗口里出现多少次covered表示当前已经覆盖了多少种颜色
做法很直接:
- 右端点
r不断右移,把新彩珠加入窗口 - 当窗口已经覆盖全部
K种颜色时,就不断右移左端点l - 每收缩一次,都尝试用
a[r].pos - a[l].pos更新答案 - 一旦删掉左端点后某种颜色消失,窗口就不再合法,停止收缩,继续扩张右端点
这样每个彩珠最多被左右指针各经过一次,窗口扫描本身就是线性的。
实现上最重要的对应关系有两点:
- 先把输入的“按颜色分组”打平成统一数组,再排序
- 再在这个排序数组上套标准双指针模板
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
const int MAXK = 65;
struct Node {
int pos; // 彩珠所在位置
int kind; // 彩珠的种类编号
};
int n, k;
Node a[MAXN];
int cnt[MAXK]; // 当前窗口里每一种彩珠出现了多少次
bool cmp_node(const Node &x, const Node &y) {
if (x.pos != y.pos) {
return x.pos < y.pos;
}
return x.kind < y.kind;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
int tot = 0;
for (int i = 1; i <= k; i++) {
int t;
cin >> t;
for (int j = 1; j <= t; j++) {
++tot;
cin >> a[tot].pos;
a[tot].kind = i;
}
}
// 先把所有彩珠按坐标排成一条线,问题就变成:
// 找到一个最短的连续彩珠段,使它覆盖全部 k 种颜色。
sort(a + 1, a + tot + 1, cmp_node);
int covered = 0; // 当前窗口里已经覆盖了多少种彩珠
int l = 1;
int ans = INT_MAX;
for (int r = 1; r <= tot; r++) {
if (cnt[a[r].kind] == 0) {
covered++;
}
cnt[a[r].kind]++;
while (covered == k) {
// 当前窗口已经合法,尽量用它更新最短长度。
ans = min(ans, a[r].pos - a[l].pos);
// 左端点右移,尝试继续缩短区间。
cnt[a[l].kind]--;
if (cnt[a[l].kind] == 0) {
covered--;
}
l++;
}
}
cout << ans << '\n';
return 0;
}复杂度
- 排序:
- 双指针扫描:
总时间复杂度:
空间复杂度:
总结
这题最容易卡住的地方不是双指针本身,而是前面的建模转换:
- 原输入是按颜色分组
- 但答案对应的是按坐标连续的一段彩带
只要先把所有彩珠打平成按坐标排序的一条线,后面就是标准的最短覆盖区间模型。