CH2101 可达性统计 (拓扑排序+状态压缩)

本文介绍了如何使用拓扑排序和bitset进行可达性统计。通过求解有向无环图的拓扑排序,利用bitset进行状态压缩,以判断图中节点的可达性。在空间复杂度上,bitset的使用减少了存储需求;在时间复杂度上,整体算法为O((n+m)*n/32)。引用了《算法竞赛进阶指南》--李煜东著作为参考资料。

题目链接:http://contest-hunter.org:83/contest/0x20%E3%80%8C%E6%90%9C%E7%B4%A2%E3%80%8D%E4%BE%8B%E9%A2%98/2101%20%E5%8F%AF%E8%BE%BE%E6%80%A7%E7%BB%9F%E8%AE%A1

以前看这道题的时候,拓扑排序搞出来就不会压缩状态了,这几天学会了用bitset这个神奇的东西就把这道题补了。

bitset每八位占一个字节。

既然是有向无环图,那么一定可以将这张图的拓扑排序求出来,那么对于拓扑序列中后面的点一定无法到达前面的点,前面的点能到达的点一定在他之后(但他之后的点不一定都能被他到达

故先求出拓扑排序,再按照拓扑排序倒序的顺序计算可达的点(用bitset状态压缩)。

上图:《算法竞赛进阶指南》 --李煜东著。

空间复杂度的话,八个bitset位占用一个字节,故空间为n*n/8;

时间复杂度:在计算的时候图的遍历是O(n+m),而每一次bitset的或运算为O(n/32),故总体复杂度为O(n+m)*n/32;

 

#include<bits/stdc++.h>
using namespace std;

const int maxn=3e4+7;
const int maxm=3e4+7;

struct Edge{
    int v,next;
}edge[maxm];

int head[maxn],top;
void init(){
    top=0;
    memset(head,-1,sizeof(head));
}

void add(int u,int v){
    edge[top].v=v;
    edge[top].next=head[u];
    head[u]=top++;
}

int tr[maxn];
int du[maxn];
queue<int> q;
int biao;

void top_sort(int n){
    for(int i=1;i<=n;++i)
        if(du[i]==0) q.push(i);
    int u,v;
    while(!q.empty()){
        u=q.front(); q.pop();
        tr[++biao]=u;
        for(int i=head[u];i!=-1;i=edge[i].next){
            v=edge[i].v;
            if(--du[v]==0) q.push(v);
        }
    }
}
bitset<30001> a[30001];

void jisuan(){
    int u,v;
    for(int i=biao;i>=1;--i){
        u=tr[i];
        a[u][u]=1;
        for(int j=head[u];j!=-1;j=edge[j].next){
            v=edge[j].v;
            a[u]|=a[v];
        }
    }
}

int main(){
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    int n,m,u,v;
    init();
    biao=0;
    scanf("%d%d",&n,&m);
    while(m--){
        scanf("%d%d",&u,&v);
        add(u,v);
        ++du[v];
    }
    top_sort(n);
    jisuan();
    for(int i=1;i<=n;++i)
        printf("%d\n",a[i].count());
    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。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值