可达性统计 —— 拓扑排序 / 记忆化搜索 + bitset状态压缩

36 篇文章 133 订阅
19 篇文章 0 订阅

可达性统计


题意:

给定一张 N 个点 M 条边的 有向无环图,分别统计从每个点出发能够到达的点的数量。
1 ≤ N , M ≤ 30000 1≤N,M≤30000 1N,M30000


法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 两种做法的相互转换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值