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. 算法步骤

  1. 读入 \(n\) 和矩阵 \(g\)。
  2. 自反性检查:对每个 \(i\),若 \(g_{i,i} \ne '1'\),输出 NO。
  3. 反对称性检查:对每对 \(i < j\),若 \(g_{i,j}='1'\) 且 \(g_{j,i}='1'\),输出 NO。
  4. 计算每个节点的 \(s_i = \sum_j g_{i,j}\)。
  5. 将节点按 \(s_i\) 降序排序,得到数组 order。
  6. 初始化边集 edges 为空。
  7. 对于每个 \(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--。
  8. 若 edges.size() != n-1,输出 NO。
  9. 用 edges 建有向图 adj。
  10. 对每个起点 \(s\) 从 \(0\) 到 \(n-1\):
    • 从 \(s\) 出发做有向 BFS,得到可达标记 vis。
    • 对每个 \(t\),若 vis[t] != (g[s][t] - '0'),输出 NO。
  11. 输出 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;
}
posted @ 2026-11-03 22:14  thenrify  阅读(2)  评论(0)    收藏  举报