可达性统计
题意:
给定一张 N 个点 M 条边的 有向无环图,分别统计从每个点出发能够到达的点的数量。
1
≤
N
,
M
≤
30000
1≤N,M≤30000
1≤N,M≤30000
法1:拓扑序+dp
1、按照什么顺序来更新呢?
注意到该图是有向无环图,那么其中的节点就满足拓扑序。
对于拓扑序列来说,前面的点一定会指向后面的点。
所以我们可以从后往前处理,对于遍历到点 x,其指向的节点的可达点就已经固定了。于是当前点 x 的可达点就是指向节点的所有可达点。
最后要求的是以每个点为起点的到达点个数,所以从后往前转移,所以要反向更新,反向建边。
2、如何记录状态?
但是不能直接将子节点的可达点个数相加,因为子节点间的可达点可能有重复。对于重复的元素只取一个,我们可以想到 或(|)操作。
一个点可到达点的数量 等于 其指向的点的所有可达点取或。
所以可以将每个点的所有可达点进行状态压缩,用二进制中的 0 和 1 表示对于一个点可不可达。如果点 x 可达的话,那么第 x 个位置就置为 1,否则就置为 0;
为了方便操作,这里采用 STL 中的 bitset
(用法)来存储这个二进制。
从后往前遍历拓扑序列,对于当前位置 x,其可达点状态压缩对应二进制 f[x]
就可以直接和其所指向节点所对应的二进制 f[tx]
直接取或。
bitset<N> f[N];
topsort();
for(int i=n;i>=1;i--) //从后到前遍历拓扑序列
{
int x = top[i];
f[x][x] = 1; //对于x点来说,本身肯定是可达的,所以对应二进制的第x个位置就要置为1;
for(auto tx:e[x]) //遍历其指向的节点
{
f[x] |= f[tx]; //二进制取或
}
}
完整Code:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<int,int> mp;
const int N = 30010, mod = 1e9+7;
int T, n, m, k;
int a[N], ru[N];
vector<int> e[N];
bitset<N> f[N];
void topsort()
{
queue<int> que;
for(int i=1;i<=n;i++) if(!ru[i]) que.push(i);
while(que.size())
{
int x = que.front();
que.pop();
f[x][x] = 1;
for(auto tx:e[x])
{
f[tx] |= f[x];
ru[tx] -- ;
if(ru[tx] == 0) que.push(tx);
}
}
}
signed main(){
Ios;
cin>>n>>m;
while(m--)
{
int x,y;cin>>x>>y;
e[y].pb(x);
ru[x]++;
}
topsort();
for(int i=1;i<=n;i++)
{
cout << f[i].count() << endl;
}
return 0;
}
法2:记忆化搜索
我们知道,对于 记忆化搜索 可以转化为在 DAG 上 dp
,也就是按照拓扑序进行 dp,即上一种做法。
同理,
用 拓扑序+dp 解决的问题,记忆化搜索 也可以解决。(只是复杂度较高)
因为是求每个点作为起点能够到达的点数,所以就从每个点出发,向外走,由起点往外更新。那么用记忆化搜索就要正常建边。
由此可见:
一般记忆化搜索
是正常从前往后走,正常建边;
而拓扑序+dp
却是将后面点的处理完毕之后,再用后面的状态更新前面状态。那么,拓扑序+dp 的做法一般就要反向建边。
步骤:
从一点 x 出发:
- 如果走到一个点 tx 发现之前没有走过,那么从这个点递归,返回来的结果就是从这个点出发的答案;
- 否则,该位置已经存储了从该点 tx 出发的答案,直接用即可。不需要继续递归。
- 将当前点 x 更新,传回从当前点出发的答案。
bitset<N> dfs(int x)
{
f[x][x] = 1;
for(auto tx:e[x])
{
if(f[tx].count()) f[x] |= f[tx]; //有存储,之前走过
else f[x] |= dfs(tx); //没走过,递归一遍
}
return f[x];
}
完整Code:
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
#define PII pair<int,int>
#define pb push_back
#define fi first
#define se second
#define endl '\n'
map<int,int> mp;
const int N = 30010, mod = 1e9+7;
int T, n, m, k;
int a[N];
vector<int> e[N];
bitset<N> f[N];
bitset<N> dfs(int x)
{
f[x][x] = 1;
for(auto tx:e[x])
{
if(f[tx].count()) f[x] |= f[tx];
else f[x] |= dfs(tx);
}
return f[x];
}
signed main(){
Ios;
cin>>n>>m;
while(m--)
{
int x,y;cin>>x>>y;
e[x].pb(y);
}
for(int i=1;i<=n;i++)
if(!f[i].count()) dfs(i); //保证每个点都要走过
for(int i=1;i<=n;i++)
{
cout << f[i].count() << endl;
}
return 0;
}
时间复杂度比较:
经验:
1、了解到 bitset 的用法;
2、掌握 记忆化搜索 和 拓扑序+dp 两种做法的相互转换。