把限制反向建图,用大根堆在反图上做拓扑排序,再把得到的序列倒过来输出,就能得到题目要求的最优顺序。
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的位置 - 继续往后比
这个方法可以帮助理解题意,但正解不能真的枚举所有拓扑序。
关键观察是:
题目要求“小编号尽量早”,等价于反过来说:
- 在还没确定前面位置时,应该尽量把“大编号且当前不影响别人”的点放到后面去
因此可以换一个角度做:
- 把原图限制
u -> v反过来,建成v -> u - 在反图上做拓扑排序
- 每次从当前入度为
0的点里,选编号最大的那个 - 最后把得到的序列整体反过来输出
为什么这样是对的?
- 反图里入度为
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。
- 建图
- 大根堆拓扑排序
总时间复杂度
总结
这题最容易误判成“字典序最小拓扑序”。真正的技巧是把视角倒过来:先决定谁应该尽量靠后,再把答案翻转回来。于是题目就落成了“反图上的大根堆拓扑排序”。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
