树的定向(dfs并查集贪心)

一、题目

Problem - D2 - Codeforces

你曾经有一棵 n 个节点的无向树 。你给树上的 n−1 条边随便指定了方向 ,把它变成了一棵有向树

后来你把原来的树结构忘了,但你记下了一张表:对任意一对节点 (i,j),记录了 i 能否到达 j

现在给你这张可达性矩阵,你需要:

  1. 判断是否存在一棵原来的树 + 一种边的定向方式,使得它的可达性完全符合这张表;
  2. 如果存在,输出任意一种合法的有向边方案

可达性定义:

  • 节点自己一定能到达自己;
  • 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:

  1. 先标记 u 自己已经 "覆盖";
  2. 按从强到弱遍历所有点 v;
  3. 如果 v 没被覆盖,且 u 能到达 v:
    • 连边 u → v
    • 然后把 v 能到达的所有点全部标记为已覆盖(因为 u 可以通过 v 间接到达它们,不需要再连直接边)
  4. 重复直到所有可达点都被覆盖。

4. 最后验证三件事

  1. 边数必须正好是 n−1(必须是树);
  2. 这棵有向图必须连通
  3. 重新跑一遍可达性,必须完全匹配输入矩阵

满足就输出 Yes + 边,否则 No。


四、为什么这个思路是对的?

因为题目保证:如果有解,解唯一,且一定是这种 "贪心选最强可达点" 的结构。

  • 强点能覆盖大片区域 → 连一条直接边就够;
  • 弱点覆盖小 → 不需要直接连;
  • 最终刚好形成一棵 n−1 条边的有向树。

五、流程

  1. 读入 n × n 的 01 矩阵
  2. 对每个点统计 cnt [u] = 可达点数
  3. 按 cnt 从大到小排序点(强 → 弱)
  4. 对每个 u 贪心建边
    • 没覆盖 & u 能到 v → 连 u→v
    • 把 v 能到的点全部标记覆盖
  5. 检查边数是不是 n−1
  6. 用并查集检查连通
  7. 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;
}
相关推荐
懒洋洋在睡觉2 小时前
Vulkan demo入门教程三:逻辑设备、队列与交换链
c++·图形渲染
YMH.2 小时前
Day3.14c++
开发语言·c++
花间相见2 小时前
【JAVA基础11】—— 吃透原码、反码、补码:计算机数值表示的底层逻辑
java·开发语言·笔记
特种加菲猫2 小时前
C++ std::list 完全指南:从入门到精通所有接口
开发语言·c++
mjhcsp2 小时前
C++ A* 算法:启发式路径搜索的黄金标准
android·c++·算法
仰泳的熊猫3 小时前
题目2281:蓝桥杯2018年第九届真题-次数差
数据结构·c++·算法·蓝桥杯
巧克力味的桃子3 小时前
最长连续因子问题 - C语言学习笔记
c语言·笔记·学习
blackicexs3 小时前
第九周第一天
数据结构·算法
实心儿儿3 小时前
C++ —— 多态
开发语言·c++