一、题目
你曾经有一棵 n 个节点的无向树 。你给树上的 n−1 条边随便指定了方向 ,把它变成了一棵有向树。
后来你把原来的树结构忘了,但你记下了一张表:对任意一对节点 (i,j),记录了 i 能否到达 j。
现在给你这张可达性矩阵,你需要:
- 判断是否存在一棵原来的树 + 一种边的定向方式,使得它的可达性完全符合这张表;
- 如果存在,输出任意一种合法的有向边方案。
可达性定义:
- 节点自己一定能到达自己;
- x 能到 y 当且仅当存在一条有向路径 x → ... → y。
输入
多组测试数据。每组数据:
- 第一行 n(节点数,2 ≤ n ≤ 8000)
- 接下来 n 行,每行一个长度为 n 的 01 串 sᵢsᵢⱼ = 1 表示 i 能到达 jsᵢⱼ = 0 表示不能
保证所有测试点的 n² 之和 ≤ 8000²。
输出
- 若无解,输出
No - 若有解,输出
Yes,再输出 n−1 条有向边 x→y
二、核心观察
这道题最关键的结论只有一句:
合法的结构一定是一棵外向树 / 内向树混合,但本质是:每条直接边 u→v 一定是 "一步直达" 的关键节点
更直白地说:
对每个 u,它的直接儿子 v 一定是 u 能到达的点里,能到达最多点的那个点。
因为:
-
v 能覆盖一大片节点;
-
只要连 u→v,u 就能通过 v 间接到达所有 v 能到达的点;
-
这些点就不需要 再和 u 连直接边了。
在 u 能到达的点里,选可达点数最大 的 v,u→v 这条边必须存在。
三、思路
1. 先给每个点算一个 "能力值"
对每个点 u:
- cnt [u] = 它能到达的点的数量(就是它那一行 1 的个数)
cnt 越大,说明这个点越 "强"、覆盖范围越广。
2. 按能力从强到弱排序所有点
得到一个顺序:强点在前,弱点在后。
3. 对每个 u,构造它的直接出边
对每个 u:
- 先标记 u 自己已经 "覆盖";
- 按从强到弱遍历所有点 v;
- 如果 v 没被覆盖,且 u 能到达 v:
- 连边 u → v
- 然后把 v 能到达的所有点全部标记为已覆盖(因为 u 可以通过 v 间接到达它们,不需要再连直接边)
- 重复直到所有可达点都被覆盖。
4. 最后验证三件事
- 边数必须正好是 n−1(必须是树);
- 这棵有向图必须连通;
- 重新跑一遍可达性,必须完全匹配输入矩阵。
满足就输出 Yes + 边,否则 No。
四、为什么这个思路是对的?
因为题目保证:如果有解,解唯一,且一定是这种 "贪心选最强可达点" 的结构。
- 强点能覆盖大片区域 → 连一条直接边就够;
- 弱点覆盖小 → 不需要直接连;
- 最终刚好形成一棵 n−1 条边的有向树。
五、流程
- 读入 n × n 的 01 矩阵
- 对每个点统计 cnt [u] = 可达点数
- 按 cnt 从大到小排序点(强 → 弱)
- 对每个 u 贪心建边
- 没覆盖 & u 能到 v → 连 u→v
- 把 v 能到的点全部标记覆盖
- 检查边数是不是 n−1
- 用并查集检查连通
- DFS 跑一遍可达性,验证是否完全匹配
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;
int n, cnt[N], id[N]; // cnt[u]:u的可达节点数 id:排序后的节点编号
bool e[N][N], vis[N]; // e[u][v]:u能否到v vis:标记是否被覆盖
bool reach[N]; // DFS验证时的可达标记
vector<int> tree[N]; // 构造的树的邻接表
vector<pair<int, int>> ans; // 存储最终的边
// 并查集
struct DSU {
int fa[N];
void init(int n) {
for(int i=1; i<=n; i++) fa[i] = i;
}
int find(int x) {
return fa[x] == x ? x : (fa[x] = find(fa[x]));
}
void merge(int x, int y) {
fa[find(x)] = find(y);
}
} dsu;
// DFS:标记从u出发的所有可达节点
void dfs(int u) {
reach[u] = 1;
for(int v : tree[u]) {
if(!reach[v]) dfs(v);
}
}
// 排序规则:按可达数从多到少
bool cmp(int u, int v) {
return cnt[u] > cnt[v];
}
void solve() {
cin >> n;
// 初始化
ans.clear();
memset(cnt, 0, sizeof cnt);
for(int i=1; i<=n; i++) tree[i].clear();
// 读入可达矩阵并统计cnt
for(int u=1; u<=n; u++) {
id[u] = u;
string s; cin >> s;
for(int v=1; v<=n; v++) {
e[u][v] = (s[v-1] == '1');
cnt[u] += e[u][v];
}
}
// 按可达数排序节点
sort(id+1, id+n+1, cmp);
// 贪心构造边
for(int u=1; u<=n; u++) {
memset(vis, 0, sizeof vis);
vis[u] = 1; // 自身标记为覆盖
for(int i=1; i<=n; i++) {
int v = id[i];
if(!vis[v] && e[u][v]) {
ans.push_back({u, v});
if(ans.size() >= n) { // 边数超了,无解
cout << "No\n";
return;
}
// 标记v的所有可达节点为覆盖
for(int w=1; w<=n; w++) {
if(e[v][w]) vis[w] = 1;
}
}
}
}
// 检查边数是否为n-1
if(ans.size() != n-1) {
cout << "No\n";
return;
}
// 验证连通性
dsu.init(n);
for(auto p : ans) {
int u = p.first, v = p.second;
dsu.merge(u, v);
tree[u].push_back(v);
}
// 检查所有节点是否连通
int root = dsu.find(1);
bool ok = 1;
for(int i=2; i<=n; i++) {
if(dsu.find(i) != root) {
ok = 0;
break;
}
}
if(!ok) {
cout << "No\n";
return;
}
// 验证可达性是否匹配
for(int u=1; u<=n; u++) {
memset(reach, 0, sizeof reach);
dfs(u);
for(int v=1; v<=n; v++) {
if(reach[v] != e[u][v]) {
ok = 0;
break;
}
}
if(!ok) break;
}
if(!ok) {
cout << "No\n";
return;
}
// 输出答案
cout << "Yes\n";
for(auto p : ans) {
cout << p.first << " " << p.second << "\n";
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T; cin >> T;
while(T--) solve();
return 0;
}