[USACO17OPEN] Modern Art 2 G

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

把每种颜色的首末出现位置看成区间,用栈扫描检查区间是否只存在包含关系;最大栈深就是最少轮数。

OJ: luogu

题目 ID: P3668

难度:提高+/省选-

标签:单调栈区间扫描线思维构造判定

日期: 2026-06-20 23:35

题意

给一个长度为 N 的最终颜色序列,0 表示空白。

原作者画画时,每种颜色只会刷一次,而且一次刷的是一个连续区间。

复制者要分若干轮来完成复制:

  • 每轮可以刷若干个互不相交区间
  • 每种颜色整个过程中也只能刷一次

要求最少需要多少轮;如果根本画不出来,输出 -1

思路

先看一个更直观的慢版:

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

const int MAXN = 205;

int n;
int a[MAXN];
int first_pos[MAXN], last_pos[MAXN];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;

    for (int i = 0; i <= n; i++) {
        first_pos[i] = 0;
        last_pos[i] = 0;
    }

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        if (a[i] == 0) {
            continue;
        }
        if (first_pos[a[i]] == 0) {
            first_pos[a[i]] = i;
        }
        last_pos[a[i]] = i;
    }

    // 先检查是否存在交叉区间。
    for (int x = 1; x <= n; x++) {
        if (first_pos[x] == 0) {
            continue;
        }
        for (int y = x + 1; y <= n; y++) {
            if (first_pos[y] == 0) {
                continue;
            }
            bool cross1 = first_pos[x] < first_pos[y] && first_pos[y] < last_pos[x] && last_pos[x] < last_pos[y];
            bool cross2 = first_pos[y] < first_pos[x] && first_pos[x] < last_pos[y] && last_pos[y] < last_pos[x];
            if (cross1 || cross2) {
                cout << -1 << '\n';
                return 0;
            }
        }
    }

    int ans = 0;

    // 慢版枚举每个位置,显式找“覆盖这个位置的最内层区间”。
    for (int i = 1; i <= n; i++) {
        int cover = 0;
        int top_color = 0;
        int best_first = -1;

        for (int color = 1; color <= n; color++) {
            if (first_pos[color] == 0) {
                continue;
            }
            if (first_pos[color] <= i && i <= last_pos[color]) {
                cover++;
                if (first_pos[color] > best_first) {
                    best_first = first_pos[color];
                    top_color = color;
                }
            }
        }

        if (a[i] == 0) {
            if (cover != 0) {
                cout << -1 << '\n';
                return 0;
            }
            continue;
        }

        if (top_color != a[i]) {
            cout << -1 << '\n';
            return 0;
        }

        ans = max(ans, cover);
    }

    cout << ans << '\n';
    return 0;
}

brute.cpp 直接按定义检查:

  • 任意两种颜色区间是否交叉
  • 每个位置真正露在最上层的颜色是不是当前颜色
  • 最大覆盖层数是多少

这个版本适合帮助理解题意,但正式解可以做到线性。

核心观察是:

对每种颜色,只看它在最终画面中的:

  • 第一次出现位置
  • 最后一次出现位置

就能把它看成一个区间。

合法画面中的这些颜色区间只能:

  • 互不相交
  • 或者一个完全包含另一个

如果出现:

text
l1 < l2 < r1 < r2

这样的交叉关系,就说明某种颜色会被切成两段,而它又只能刷一次连续区间,所以必定无解。

于是可以从左到右扫描整个序列,并用栈维护“当前已经开始但还没结束”的颜色区间。

扫描到位置 i 时:

  1. 如果是某颜色第一次出现,就把它入栈
  2. 当前颜色必须等于栈顶颜色,否则无解
  3. 如果是某颜色最后一次出现,就把它出栈

还要特别注意 0

  • 如果当前位置是 0,但栈还不空
  • 说明某个颜色区间内部出现了空白
  • 这同样无解

栈深度的最大值,就是同一时刻叠在一起的颜色层数,也就是复制者最少需要的轮数。

代码

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

const int MAXN = 100005;

int n;
int a[MAXN];
int first_pos[MAXN], last_pos[MAXN];
int stk[MAXN], top_idx;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;

    for (int i = 1; i <= n; i++) {
        first_pos[i] = 0;
        last_pos[i] = 0;
    }

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        if (a[i] == 0) {
            continue;
        }
        if (first_pos[a[i]] == 0) {
            first_pos[a[i]] = i;
        }
        last_pos[a[i]] = i;
    }

    top_idx = 0;
    int ans = 0;

    for (int i = 1; i <= n; i++) {
        if (a[i] == 0) {
            if (top_idx > 0) {
                cout << -1 << '\n';
                return 0;
            }
            continue;
        }

        if (i == first_pos[a[i]]) {
            stk[++top_idx] = a[i];
            ans = max(ans, top_idx);
        }

        if (stk[top_idx] != a[i]) {
            cout << -1 << '\n';
            return 0;
        }

        if (i == last_pos[a[i]]) {
            top_idx--;
        }
    }

    cout << ans << '\n';
    return 0;
}

复杂度

每种颜色最多入栈一次、出栈一次,整段扫描只做线性处理。

所以时间复杂度是:

O(N)O(N)

空间复杂度是:

O(N)O(N)

总结

这题的关键不是去想“每轮怎么刷”,而是先看最终颜色区间之间必须满足什么结构。

一旦看出:

  • 合法区间只能包含或分离,不能交叉
  • 扫描时最上层颜色就是当前打开区间里的最内层颜色

就能自然得到栈解法。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析