[HNOI2015] 菜肴制作

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

把限制反向建图,用大根堆在反图上做拓扑排序,再把得到的序列倒过来输出,就能得到题目要求的最优顺序。

OJ: luogu

题目 ID: P3243

难度:提高+/省选-

标签:图论拓扑排序贪心

日期: 2026-06-19 23:49

题意

给出若干道菜以及一些先后限制 (u, v),表示菜 u 必须在菜 v 之前制作。

现在要在满足所有限制的前提下,找出一个“最优”的制作顺序。这里的“最优”不是普通的字典序最小,而是:

  • 先让 1 号菜尽量早
  • 在此基础上让 2 号菜尽量早
  • 再在前面都最优的基础上让 3 号菜尽量早
  • 依此类推

如果无解,就输出 Impossible!

样例图

这张图展示第三组样例中的限制关系:

digraph G {
  rankdir=LR;
  5 -> 2;
  4 -> 3;
}

如果只看普通的“当前可选编号最小”,会得到 1 4 3 5 2,这不是题目想要的答案。 题目真正想要的是先尽量让 1 靠前,再尽量让 2 靠前,所以正确顺序是 1 5 2 4 3。 这说明它不是普通的最小字典序拓扑序。

思路

先看一个小数据暴力:

cpp
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 12;

int T;
int n, m;
vector<int> graph[MAXN];
int indeg[MAXN];
bool used[MAXN];
int cur_order[MAXN], best_order[MAXN];
int best_pos[MAXN];
bool found;

bool better_than_best() {
    int cur_pos[MAXN];
    for (int i = 1; i <= n; i++) {
        cur_pos[cur_order[i]] = i;
    }

    if (!found) {
        return true;
    }

    for (int x = 1; x <= n; x++) {
        if (cur_pos[x] != best_pos[x]) {
            return cur_pos[x] < best_pos[x];
        }
    }
    return false;
}

// 直接枚举所有拓扑序,然后按题目要求比较哪个更优。
void dfs(int dep) {
    if (dep > n) {
        if (better_than_best()) {
            found = true;
            for (int i = 1; i <= n; i++) {
                best_order[i] = cur_order[i];
                best_pos[cur_order[i]] = i;
            }
        }
        return;
    }

    for (int i = 1; i <= n; i++) {
        if (used[i] || indeg[i] != 0) {
            continue;
        }

        used[i] = true;
        cur_order[dep] = i;
        for (int v : graph[i]) {
            indeg[v]--;
        }

        dfs(dep + 1);

        for (int v : graph[i]) {
            indeg[v]++;
        }
        used[i] = false;
    }
}

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

    cin >> T;
    while (T--) {
        cin >> n >> m;

        for (int i = 1; i <= n; i++) {
            graph[i].clear();
            indeg[i] = 0;
            used[i] = false;
        }

        for (int i = 1; i <= m; i++) {
            int u, v;
            cin >> u >> v;
            graph[u].push_back(v);
            indeg[v]++;
        }

        found = false;
        dfs(1);

        if (!found) {
            cout << "Impossible!\n";
        } else {
            for (int i = 1; i <= n; i++) {
                if (i > 1) {
                    cout << ' ';
                }
                cout << best_order[i];
            }
            cout << '\n';
        }
    }

    return 0;
}

暴力会枚举所有拓扑序,然后按照题目给出的比较规则选最优的那个:

  • 先比较 1 的位置
  • 若相同,再比较 2 的位置
  • 继续往后比

这个方法可以帮助理解题意,但正解不能真的枚举所有拓扑序。

关键观察是:

题目要求“小编号尽量早”,等价于反过来说:

  • 在还没确定前面位置时,应该尽量把“大编号且当前不影响别人”的点放到后面去

因此可以换一个角度做:

  1. 把原图限制 u -> v 反过来,建成 v -> u
  2. 在反图上做拓扑排序
  3. 每次从当前入度为 0 的点里,选编号最大的那个
  4. 最后把得到的序列整体反过来输出

为什么这样是对的?

  • 反图里入度为 0,等价于原图里出度为 0
  • 也就是这些点在原图里已经可以尽量往后放,不会卡住别人
  • 此时把编号大的点优先放到后面,等价于把编号小的点尽量留在前面

于是:

  • 反图 + 大根堆:决定“后面的位置怎么放”
  • 最终倒序输出:得到“前面的位置怎么尽量优”

如果反图拓扑排序做不满 n 个点,说明原图有环,无解。

代码

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

const int MAXN = 100005;
const int MAXM = 100005;

int T;
int n, m;
int head[MAXN], to[MAXM], nxt[MAXM], indeg[MAXN], edge_cnt;
int ans[MAXN];

// 在反图上加一条 u -> v 的边。
void add_edge(int u, int v) {
    edge_cnt++;
    to[edge_cnt] = v;
    nxt[edge_cnt] = head[u];
    head[u] = edge_cnt;
    indeg[v]++;
}

void init_graph() {
    edge_cnt = 0;
    for (int i = 1; i <= n; i++) {
        head[i] = 0;
        indeg[i] = 0;
    }
}

bool solve_case() {
    priority_queue<int> pq; // 反图里每次优先取编号最大的点

    for (int i = 1; i <= n; i++) {
        if (indeg[i] == 0) {
            pq.push(i);
        }
    }

    int tot = 0;
    while (!pq.empty()) {
        int u = pq.top();
        pq.pop();
        ans[++tot] = u;

        for (int i = head[u]; i != 0; i = nxt[i]) {
            int v = to[i];
            indeg[v]--;
            if (indeg[v] == 0) {
                pq.push(v);
            }
        }
    }

    if (tot != n) {
        return false;
    }

    for (int i = n; i >= 1; i--) {
        cout << ans[i];
        if (i > 1) {
            cout << ' ';
        }
    }
    cout << '\n';
    return true;
}

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

    cin >> T;
    while (T--) {
        cin >> n >> m;
        init_graph();

        for (int i = 1; i <= m; i++) {
            int u, v;
            cin >> u >> v;
            // 原图要求 u 在 v 前面,因此在反图上连 v -> u。
            add_edge(v, u);
        }

        if (!solve_case()) {
            cout << "Impossible!\n";
        }
    }

    return 0;
}

复杂度

设点数为 n,边数为 m

  • 建图 O(n+m)O(n + m)
  • 大根堆拓扑排序 O((n+m)logn)O((n + m)\log n)

总时间复杂度 O((n+m)logn)O((n + m)\log n),空间复杂度 O(n+m)O(n + m)

总结

这题最容易误判成“字典序最小拓扑序”。真正的技巧是把视角倒过来:先决定谁应该尽量靠后,再把答案翻转回来。于是题目就落成了“反图上的大根堆拓扑排序”。

一图流解析

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

一图流解析