[SCOI2009] 生日礼物

GitHub跳转原题关系图返回列表

先把所有彩珠按坐标打平成 `(位置, 颜色)` 序列并排序,再用双指针维护覆盖全部颜色的最短区间。

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

这个思路是对的,但它会重复扫描很多区间,最坏复杂度到 O(N2)O(N^2),肯定过不了。

关键观察是:排序之后,题目就变成了一个标准的“最短覆盖全部种类的连续区间”问题。

于是我们维护滑动窗口 [l, r]

  • cnt[c] 表示颜色 c 在窗口里出现多少次
  • covered 表示当前已经覆盖了多少种颜色

做法很直接:

  1. 右端点 r 不断右移,把新彩珠加入窗口
  2. 当窗口已经覆盖全部 K 种颜色时,就不断右移左端点 l
  3. 每收缩一次,都尝试用 a[r].pos - a[l].pos 更新答案
  4. 一旦删掉左端点后某种颜色消失,窗口就不再合法,停止收缩,继续扩张右端点

这样每个彩珠最多被左右指针各经过一次,窗口扫描本身就是线性的。

实现上最重要的对应关系有两点:

  • 先把输入的“按颜色分组”打平成统一数组,再排序
  • 再在这个排序数组上套标准双指针模板

代码

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;
}

复杂度

  • 排序:O(NlogN)O(N log N)
  • 双指针扫描:O(N)O(N)

总时间复杂度:

O(NlogN)O(N log N)

空间复杂度:

O(N)O(N)

总结

这题最容易卡住的地方不是双指针本身,而是前面的建模转换:

  • 原输入是按颜色分组
  • 但答案对应的是按坐标连续的一段彩带

只要先把所有彩珠打平成按坐标排序的一条线,后面就是标准的最短覆盖区间模型。