Tree Orientation (Hard Version)
Tree Orientation (Hard Version)
1. 问题概述
给定一个 \(n\) 个节点的有向图的可达矩阵 \(g\),其中 \(g_{i,j}=1\) 表示节点 \(i\) 可以到达节点 \(j\)。已知这个有向图是由一棵无向树将每条边任意定向后得到的。要求判断是否存在这样的树和定向方案,如果存在则输出任意一组 \(n-1\) 条有向边。
困难版中 \(n \le 8000\),且 \(\sum n^2 \le 8000^2\),要求时间复杂度 \(O(n^2)\)。
2. 核心思路
2.1 可达集合与大小
对于每个节点 \(u\),定义其可达集合:
[
R_u = { v \mid g_{u,v}=1 }
]
并令 \(s_u = |R_u|\),即节点 \(u\) 能到达的节点数量(包括自身)。
在树中,如果 \(u \to v\) 是一条有向边,那么:
[
R_v \subset R_u
]
并且 \(R_v\) 是 \(R_u\) 中“极大”的真子集之一,即不存在其他节点 \(w\) 使得 \(R_v \subset R_w \subset R_u\)。因此,直接孩子的可达集合大小一定大于其后代的可达集合大小。
2.2 确定直接孩子
对于固定的 \(u\),考虑其剩余可达节点集合 \(R_u \setminus \{u\}\)。我们按 \(s_v\) 从大到小扫描所有节点。第一个仍在 \(R_u\) 中的节点 \(v\),必然是 \(u\) 的直接孩子。
理由:如果 \(v\) 不是直接孩子,则从 \(u\) 到 \(v\) 的路径上存在中间节点 \(w\),使得 \(R_w \supset R_v\),从而 \(s_w > s_v\),这与 \(v\) 是当前 \(s\) 最大的节点矛盾。
确定 \(u \to v\) 后,所有 \(v\) 能到达的节点都可以通过 \(v\) 间接到达,因此它们不可能是 \(u\) 的直接孩子。于是从 \(R_u\) 中删除整个 \(R_v\),继续在剩余节点中找下一个 \(s\) 最大的节点,直到 \(R_u\) 为空。
2.3 处理顺序
- 对每个节点 \(u\) 独立处理,处理 \(u\) 的顺序任意(通常按 \(0\) 到 \(n-1\))。
- 全局预计算所有节点的 \(s_v\),并按 \(s_v\) 降序排序,得到一个全局顺序
order。 - 对每个 \(u\),按
order扫描,遇到还在 \(R_u\) 中的节点就选为直接孩子,并删除其可达集合。
2.4 最后的验证
由于困难版删除了显式的传递性检查,输入的矩阵可能不满足传递性,但构造过程仍可能找出 \(n-1\) 条边。因此必须用构造出的有向边建图,从每个节点出发做有向 BFS,计算实际可达集合,与输入矩阵逐项比较。只有完全一致才能输出 YES。
这一步同时覆盖了自反性、反对称性、传递性以及所有可达关系,且复杂度为 \(O(n^2)\)。
验证最后是否为一棵完整的树,即所有节点是否都在树上
3. 算法步骤
- 读入 \(n\) 和矩阵 \(g\)。
- 自反性检查:对每个 \(i\),若 \(g_{i,i} \ne '1'\),输出
NO。 - 反对称性检查:对每对 \(i < j\),若 \(g_{i,j}='1'\) 且 \(g_{j,i}='1'\),输出
NO。 - 计算每个节点的 \(s_i = \sum_j g_{i,j}\)。
- 将节点按 \(s_i\) 降序排序,得到数组
order。 - 初始化边集
edges为空。 - 对于每个 \(u\) 从 \(0\) 到 \(n-1\):
- 创建标记数组
vis,初始vis[j] = (j != u && g[u][j] == '1'),并统计剩余可达节点数cnt。 - 按
order顺序遍历每个节点 \(v\):- 若
cnt == 0,跳出。 - 若 \(v == u\) 或
!vis[v],跳过。 - 否则,\(v\) 是 \(u\) 的直接孩子,记录边 \((u, v)\)。
- 对于所有 \(w\),若 \(g_{v,w}='1'\) 且
vis[w]为真,则置vis[w]=0并cnt--。
- 若
- 创建标记数组
- 若
edges.size() != n-1,输出NO。 - 用
edges建有向图adj。 - 对每个起点 \(s\) 从 \(0\) 到 \(n-1\):
- 从 \(s\) 出发做有向 BFS,得到可达标记
vis。 - 对每个 \(t\),若
vis[t] != (g[s][t] - '0'),输出NO。
- 从 \(s\) 出发做有向 BFS,得到可达标记
- 输出
YES和所有边(节点编号 \(1\) 起始)。
4. 复杂度分析
- 计算 \(s_i\):\(O(n^2)\)。
- 排序:\(O(n \log n)\)。
- 对每个 \(u\) 扫描
order并删除节点:- 每个节点在某个 \(u\) 的处理中最多被删除一次。
- 所有 \(u\) 的剩余可达节点总数不超过矩阵中
'1'的总数,即 \(O(n^2)\)。 - 因此总删除和扫描代价为 \(O(n^2)\)。
- 有向 BFS 验证:从每个节点出发,每条边最多被遍历一次,总时间 \(O(n^2)\)。
- 总时间复杂度:\(O(n^2)\)。
- 空间复杂度:存储矩阵 \(O(n^2)\),其他数组 \(O(n)\)。
6. 关键点总结
- 利用传递闭包矩阵直接统计可达集合大小,无需动态计算。
- 按可达集合大小降序排列,保证每次选出的都是直接孩子。
- 删除已覆盖子树,避免重复检查,将找边复杂度降到 \(O(n^2)\)。
- 最后必须用有向 BFS 验证,替代简单版的传递性检查,确保构造的树与输入矩阵完全一致。
- 处理节点 \(u\) 的顺序任意,但挑选直接孩子必须按全局降序。
- 总时间复杂度 \(O(n^2)\),空间复杂度 \(O(n^2)\),可处理 \(n \le 8000\)。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve() {
int n; cin >> n;
vector<string> g(n);
for(int i = 0; i < n; i ++) cin >> g[i];
//自反性检查
for(int i = 0; i < n; i ++)
if(g[i][i] != '1') {cout << "NO\n"; return;}
//反对称性检查
for(int i = 0; i < n; i ++) {
for(int j = i + 1; j < n; j ++) {
if(g[i][j] == '1' && g[j][i] == '1') {cout << "NO\n"; return;}
}
}
//计算每个节点u的可达集合大小
vector<int> s(n, 0);
vector<vector<int>> r(n);//提前取出,减少遍历时间
for(int i = 0; i < n; i ++) {
for(int j = 0; j < n; j ++) {
if(g[i][j] == '1') s[i] ++, r[i].push_back(j);
}
}
vector<int> order(n);
iota(order.begin(), order.end(), 0);
sort(order.begin(), order.end(), [&](int a, int b) {
return s[a] > s[b];
});
vector<pair<int, int>> edges;
for(int u = 0; u < n; u ++) {
vector<char> vis(n, 0);
int cnt = 0;
for(int j = 0; j < n; j ++) {
if(j != u && g[u][j] == '1') {
cnt ++; vis[j] = 1;
}
}
for(int v : order) {
if(cnt == 0) break;
if(v == u || !vis[v]) continue;
edges.push_back({u, v});
for(int w : r[v]) {
if(g[v][w] == '1' && vis[w]) {
vis[w] = 0; cnt --;
}
}
}
}
if(edges.size() != n - 1) {cout << "NO\n"; return;}
vector<vector<int>> adj(n + 1);
for(auto [u, v] : edges) {
adj[u].push_back(v);
}
vector<int> vis(n + 1, 0);
for(int i = 0; i < n; i ++) {
queue<int> q; q.push(i);
fill(vis.begin(), vis.end(), 0); vis[i] = 1;//减少开辟空间的耗时
int cnt = 0;
while(!q.empty()) {
int u = q.front(); q.pop();
for(int v : adj[u]) {
if(!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
for(int j = 0; j < n; j ++) {
int exp = g[i][j] - '0';
if(vis[j] != exp) {cout << "NO\n"; return;}
}
}
//检查是否为树
for(auto [u, v] : edges) {
adj[v].push_back(u);
}
queue<int> q; q.push(0);
fill(vis.begin(), vis.end(), 0); vis[0] = 1;
int cnt = 0;
while(!q.empty()) {
int u = q.front(); q.pop();
cnt ++;
for(int v : adj[u]) {
if(!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
if(cnt != n) {cout << "NO\n"; return;}
cout << "YES\n";
for(auto [u, v] : edges) cout << u + 1 << " " << v + 1 << "\n";
}
int main() {
ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int _ = 1; cin >> _;
while(_--) solve();
return 0;
}

浙公网安备 33010602011771号