可达性统计(拓扑排序)

博客围绕有向无环图可达性统计问题展开。给定N个点M条边的有向无环图,要统计从每个点出发能到达的点的数量。解题思路是建图后进行拓扑排序,再反向遍历,用bitset标记可走的点,并给出了相应代码。

题目:可达性统计

题意:给定一张N个点M条边的有向无环图,分别统计从每个点出发能够到达的点的数量。
思路:建图之后采用拓扑排序,然后反向遍历,使用bitset去标记所有可以走的点
代码:

#include<iostream>
#include<bitset>
#include<queue>
#include<string.h>
using namespace std;
const int maxn = 30010;
int n, m;
int h[maxn], e[maxn], ne[maxn], id, degree[maxn], rk[maxn];

bitset<maxn>vis[maxn];
int cn = 0;
void add(int u, int v) //前向星建图
{
    e[id] = v;
    ne[id] = h[u];
    h[u] = id++;
}
void TopSort() //拓扑排序
{
    queue<int> q;
    for(int i = 1; i <= n; i++)
    {
        if(degree[i] == 0)q.push(i);
    }
    while(!q.empty())
    {
        int t = q.front();
        q.pop();
        rk[cn++] = t;//保存拓扑排序的结果
        for(int i = h[t]; ~i; i = ne[i])
        {
            int k = e[i];
            degree[k]--;
            if(degree[k] == 0)q.push(k);
        }
    }

}
int main()
{
    cin>>n>>m;
    int id = 0;
    memset(h, -1, sizeof(h));//初始化h
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin>>u>>v;
        add(u, v);
        degree[v]++;
    }
    TopSort();
    for(int i = n-1; ~i; i --)
    {
        int j = rk[i];
        vis[j][j] = 1;
        for(int k = h[j]; ~k; k = ne[k])
            vis[j] |= vis[e[k]];
    }
    for(int i = 1; i <= n; i++)cout<<vis[i].count()<<endl;

    return 0;
}


当然可以!**即使不用显式的拓扑排序**,我们仍然可以通过其他方法来解决“在有向无环图(DAG)中统计每个到达数量”这个问题。 但需要注意:由于题目明确是 **有向无环图(DAG)**,所以我们可以利用其特性设计多种替代方案。下面我们详细分析几种 **不使用显式拓扑排序** 的解法,并说明它们的优劣和适用场景。 --- ### ✅ 方法一:DFS + 记忆化(推荐,简洁高效) 这是最自然的替代方法。对每个进行深度优先搜索,记录它能到达的所有节集合。为了避免重复计算,使用 `bitset` 或记忆化 DP。 #### 原理: - 在 DAG 上做 DFS 不会出现无限递归(无环) - 使用记忆化:`dp[u] = 所有后继 v 的 dp[v] 的并集` - 最终结果就是 `dp[u].count()` ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 30010; vector<int> graph[MAXN]; bitset<MAXN> reachable[MAXN]; bool visited[MAXN]; // 返回从 u 出发到达的所有节的 bitset bitset<MAXN> dfs(int u) { if (visited[u]) return reachable[u]; visited[u] = true; reachable[u][u] = 1; // 自己可达自己 for (int v : graph[u]) { if (!visited[v]) { dfs(v); } reachable[u] |= reachable[v]; // 合并所有后继的可达集合 } return reachable[u]; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < m; ++i) { int x, y; cin >> x >> y; graph[x].push_back(y); } // 对每个未访问节调用 DFS for (int i = 1; i <= n; ++i) { if (!visited[i]) { dfs(i); } } // 输出结果 for (int i = 1; i <= n; ++i) { cout << reachable[i].count() << '\n'; } return 0; } ``` #### 🔍 优: - 不需要手动实现拓扑排序 - 利用了 DAG 的结构特性自动完成状态传递 - 代码简洁,易于理解 #### ⚠️ 注意事项: - 必须保证图是 DAG,否则 DFS 可能陷入循环(本题满足) - `bitset` 是关键优化,否则时间/空间爆炸 --- ### ✅ 方法二:Floyd-Warshall(传递闭包)——适用于小规模图 直接求整个图的**可达性闭包**。 ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 30010; bool reach[MAXN][MAXN]; // reach[i][j] 表示 i 能否到达 j int main() { int n, m; cin >> n >> m; // 初始化 for (int i = 1; i <= n; ++i) reach[i][i] = true; // 自己能到自己 for (int i = 0; i < m; ++i) { int x, y; cin >> x >> y; reach[x][y] = true; } // Floyd-Warshall 求传递闭包 for (int k = 1; k <= n; ++k) for (int i = 1; i <= n; ++i) for (int j = 1; j <= n; ++j) if (reach[i][k] && reach[k][j]) reach[i][j] = true; // 输出每个到达数 for (int i = 1; i <= n; ++i) { int cnt = 0; for (int j = 1; j <= n; ++j) if (reach[i][j]) cnt++; cout << cnt << '\n'; } return 0; } ``` #### ❌ 缺: - 时间复杂度 O() = 30000³ ≈ 2.7e13 —— **完全不可行** - 空间复杂度 O() ≈ 900e6 个布尔值 ≈ 900MB,超内存 👉 所以 **Floyd-Warshall 仅适用于 N ≤ 500 左右的小图** --- ### ✅ 方法三:多次 BFS/DFS(暴力枚举起) 对每个单独运行一次 BFS/DFS,标记所有可达。 ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 30010; vector<int> graph[MAXN]; int ans[MAXN]; void bfs(int start) { vector<bool> visited(MAXN, false); queue<int> q; q.push(start); visited[start] = true; int count = 0; while (!q.empty()) { int u = q.front(); q.pop(); count++; for (int v : graph[u]) { if (!visited[v]) { visited[v] = true; q.push(v); } } } ans[start] = count; } int main() { int n, m; cin >> n >> m; for (int i = 0; i < m; ++i) { int x, y; cin >> x >> y; graph[x].push_back(y); } for (int i = 1; i <= n; ++i) { bfs(i); } for (int i = 1; i <= n; ++i) { cout << ans[i] << '\n'; } return 0; } ``` #### ❌ 缺: - 时间复杂度 O(N*(N+M)) ≈ 30000 * 60000 = 1.8e9,在极限情况下会 TLE - 虽然常数较小,但在稠密图上仍可能超时 👉 适合数据较弱或 N 较小的情况 --- ### ✅ 总结:是否可以不用拓扑排序? | 方法 | 是否可用 | 时间复杂度 | 推荐程度 | |------|----------|------------|----------| | DFS + 记忆化 + bitset | ✅ 是,推荐 | O((N+M) × N / 32) | ⭐⭐⭐⭐☆ | | 多次 BFS/DFS | ✅ 可用但慢 | O((N+M)) | ⭐⭐☆☆☆ | | Floyd-Warshall | ❌ N太大时不行 | O() | ⭐☆☆☆☆ | | 显式拓扑排序 + bitset | ✅ 最稳 | O((N+M) × N / 32) | ⭐⭐⭐⭐⭐ | > 虽然你可以“不用显式拓扑排序”,但 **DFS + 记忆化本质上是在隐式地按逆拓扑序处理节** —— 因为函数返回时才合并状态,这与逆拓扑序一致。 --- ### 💡 结论: - ✅ **可以不用显式写拓扑排序**,用 DFS 记忆化即可; - ✅ **最优解仍然是基于 DAG 的逆序传播思想**(无论是拓扑排序还是 DFS 返回栈); - ❌ 暴力 BFS 和 Floyd 在 N=30000 下都不可行; - ✅ 使用 `bitset` 是关键优化,避免 TLE/MLE。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值