Tarjan 求强连通分量 O(n+m) ,缩点

Tarjan 求强连通分量 O ( n + m ) O(n+m) O(n+m) ,缩点


极其后悔当初没写博客

相关视频教程推荐:董老师邋遢大哥

强连通:若一张 有向图 的节点两两互相可达,则称这张图是强连通的。

强连通分量(SCC): 极大的强连通子图。


对图 dfs 时,每个节点只访问一次,被访问过的节点和边构成搜索树。

有向边分为四种情况:

  1. 树边: 访问节点走过的边。黑色。
  2. 返租边:指向祖先节点的边。红色。
  3. 横叉边:柚子树指向左子树的边。绿色。
  4. 前向边:指向子树中节点的边。蓝色。

返租边和树边一定构成环,横叉边可能与树边构成环,前向边无用

jjGRPK.png

jj8qc4.png


如果节点 X 是某个强连通分量在 搜索树 中遇到的第一个节点,则称 X 是这个强连通分量的根。

  1. 时间戳 d f n [   ] dfn [\ ] dfn[ ] :节点 X 第一次被访问的顺序。
  2. 追溯值 l o w [   ] low[\ ] low[ ]: 从节点 X 出发,所能访问到的最早的时间戳。
int dfn[N],low[N],cnt;
vector<int>scc[N];
int stk[N],top,tot,siz[N],id[N];//栈,栈顶,某个SCC节点数量
bool instk[N];
// 入、回、离、三部分。
void tarjan(int x){
    // x 【入】栈,盖戳
    dfn[x] = low[x] = ++cnt;
    stk[++top] = x;
    instk[x] = 1;
    
    // 遍历 x 邻接点,【回】X
    for(int i=h[x];~i;i=ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[x] = min(low[x],low[j]); // 如果j有返祖边,需要更新low[x]
        }
        else if(instk[j]){ // j已经被访问并且已经在栈中,横叉边和返祖边
            low[x] = min(low[x],dfn[j]);
        }
	}
    // 【离】开X,记录SCC
    if(dfn[x] == low[x]){ // x 是某个SCC的根节点
        int y; tot++;
        do{
            y = stk[top--];instk[y]=0;
            scc[tot].pb(y);
            id[y]=tot;
            siz[tot]++;
        }while(y!=x);
    }
}

jjNJQU.png


从未思考过的问题

  1. 为什么要有 l o w [   ] low[\ ] low[ ]
low[x] = min(low[x],low[y]) 如果去掉的话,遍历到最低端的节点,想通过祖先边返回的时候,会提前当成一个SCC出栈。
  1. l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x] = min(low[x],dfn[y]) low[x]=min(low[x],dfn[y]) 可以换成 l o w [ y ] low[y] low[y] 吗?

S C C SCC SCC 可以换,但是求 无向边的双连通分量就会出错。所以 不要换!


P3387 【模板】缩点

终于把这个坑给填上了

建图 -> tarjan 求SCC -> 新的SCC之间连边 -> topsort -> 遍历得最值。

一开始我还想原图上的点和SCC连边,贼乱,还只有60分。
还有就是直接在topsort中dp。
for(int i=1;i<=n;i++)
{
    for(int j=h[i];~j;j=ne[j])
    {
        int y=e[j];
        if(id[i]==id[y])continue;
        adds(id[i],id[y]);
        // 直接在新的连通分量之间连边。
        d[id[y]]++;
    }
}
// Problem: P3387 【模板】缩点
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3387
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <sstream>
#define pb push_back 
#define in insert
#define mem(f, x) memset(f,x,sizeof(f)) 
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;

template<typename T>
ostream& operator<<(ostream& os,const vector<T>&v){for(int i=0,j=0;i<v.size();i++,j++)if(j>=5){j=0;puts("");}else os<<v[i]<<" ";return os;}
template<typename T>
ostream& operator<<(ostream& os,const set<T>&v){for(auto c:v)os<<c<<" ";return os;}

typedef pair<int,int>PII;
typedef pair<long,long>PLL;

typedef long long ll;
typedef unsigned long long ull; 
const int N=2e5+10,M=1e5+10;
int n,m;
int w[N];//缩点之前的权值

int h[N],e[N],ne[N],idx;
int hs[N],es[N],nes[N],idxs;

int dfn[N],low[N],tim;
int stack[N],top;
bool is_in[N];

int cnt;//存储SCC个数
int id[N]; // 存储每个节点在哪个SCC中
vector<int>scc[N];
int W[N];//缩点之后的权值

void add(int a,int b){
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;	
}

void adds(int a,int b){
	es[idxs]=b,nes[idxs]=hs[a],hs[a]=idxs++;	
}

void tarjan(int u){
	dfn[u]=low[u]=++tim;
	stack[++top]=u;
	is_in[u]=1;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(is_in[j]){
			low[u]=min(low[u],dfn[j]);
		}
	}
	if(dfn[u]==low[u]){
		int y;
		cnt++;
		do{
			y=stack[top--];
			is_in[y]=false;
			scc[cnt].pb(y);
			W[cnt]+=w[y];
			id[y]=cnt;	
		}while(u!=y);
	}
}

int d[N],Topo[N];
int Cnt;//拓扑序下标
ll dis[N];

void topo()
{
	queue<int>que;
	for(int i=1;i<=cnt;i++)
		if(!d[i]){
			que.push(i);
			Topo[++Cnt]=i;
			dis[i] = W[i];
		}
	while(que.size())
	{
		int t=que.front();
		que.pop();
		for(int i=hs[t];~i;i=nes[i])
		{
			int j=es[i];
			d[j]--;
			if(!d[j])
			{
				que.push(j);
				dis[j] = max(dis[j] , dis[t] + W[j]);
				Topo[++Cnt]=j;
			}
		}

	}
}

void solve()
{
	mem(h,-1);
	mem(hs,-1);
	cin>>n>>m;
	fo(i,1,n)cin>>w[i];
	fo(i,1,m){
		int a,b;cin>>a>>b;
		add(a,b);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i);
	
	for(int i=1;i<=n;i++)
	{
		for(int j=h[i];~j;j=ne[j])
		{
			int y=e[j];
			if(id[i]==id[y])continue;
			adds(id[i],id[y]);
			// 直接在新的连通分量之间连边。
			d[id[y]]++;
		}
	}
	topo();
	
	ll ans = 0;
	fo(i,1,Cnt){
		ans = max(ans,dis[i]);
	}
	cout<<ans<<endl;
}

int main()
{
	solve();
	return 0;
}

P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G

题目描述

每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果 A A A 喜欢 B B B B B B 喜欢 C C C,那么 A A A 也喜欢 C C C。牛栏里共有 N N N 头奶牛,给定一些奶牛之间的爱慕关系,请你算出有多少头奶牛可以当明星。

输入格式

第一行:两个用空格分开的整数: N N N M M M

接下来 M M M 行:每行两个用空格分开的整数: A A A B B B,表示 A A A 喜欢 B B B

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 4 1\le N\le10^4 1N104 1 ≤ M ≤ 5 × 1 0 4 1\le M\le5\times 10^4 1M5×104

输出格式

一行单独一个整数,表示明星奶牛的数量。


容易发现,只有一个SCC可能是答案。然后我想着缩点(不用建图)统计SCC的入度,当入度 == cnt-1时统计答案,如果有多个满足条件ans = 0。

然后被hack了

因为可能SCC有间接入度。所以应该统计出度。AC!

// Problem: P3387 【模板】缩点
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3387
// Memory Limit: 125 MB
// Time Limit: 1000 ms

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <sstream>
#define pb push_back 
#define in insert
#define mem(f, x) memset(f,x,sizeof(f)) 
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;

template<typename T>
ostream& operator<<(ostream& os,const vector<T>&v){for(int i=0,j=0;i<v.size();i++,j++)if(j>=5){j=0;puts("");}else os<<v[i]<<" ";return os;}
template<typename T>
ostream& operator<<(ostream& os,const set<T>&v){for(auto c:v)os<<c<<" ";return os;}

typedef pair<int,int>PII;
typedef pair<long,long>PLL;

typedef long long ll;
typedef unsigned long long ull; 
const int N=1e4+10,M=1e5+10;
int n,m;
int h[N],e[10*N],ne[10*N],idx;
vector<int>scc[N];
int sz[N],cnt;//存储SCC个数
int dfn[N],low[N],tim;
int stack[N],top;
int id[N],d[N];
bool is_in[N];

void tarjan(int u){
	dfn[u] = low[u] = ++tim;
	stack[top++] = u;
	is_in[u] = true;
	for(int i=h[u];i!=-1;i=ne[i]){
		int v = e[i];
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u],low[v]);
		}
		else if(is_in[v])
			low[u] = min(low[u],dfn[v]);
	}
	if(dfn[u] == low[u]){
		int v;
		cnt++;
		do{
			v = stack[--top];
			is_in[v] = false;
			scc[cnt].pb(v);
			sz[cnt]++;
			id[v] = cnt;
		}while(v != u);
	}
}

void add(int a,int b){
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;	
}

void solve(){
	mem(h,-1);
	cin>>n>>m;
	fo(i,1,m){
		int a,b;cin>>a>>b;
		add(a,b);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		for(int k=h[i];~k;k=ne[k]){
			int j=e[k];
			if(id[i] == id[j])continue;
			d[id[i]]++;
		}
	}
	ll ans = 0;
	int num = 0;
	fo(i,1,cnt){
		if(!d[i]){
			num++;
			ans += sz[i];
		}
	}
	if(num>1)ans = 0;
	cout<<ans;
}

int main()
{
	solve();
	return 0;
}

采蘑菇 (缩点应用topo最长路(带边权和点权) + 卡double精度!)

题目描述

小胖和 ZYR 要去 ESQMS 森林采蘑菇。

ESQMS 森林间有 N N N 个小树丛, M M M 条小径,每条小径都是单向的,连接两个小树丛,上面都有一定数量的蘑菇。小胖和 ZYR 经过某条小径一次,可以采走这条路上所有的蘑菇。由于 ESQMS 森林是一片神奇的沃土,所以一条路上的蘑菇被采过后,又会长出一些新的蘑菇,数量为原来蘑菇的数量乘上这条路的“恢复系数”,再下取整。

比如,一条路上有 4 4 4 个蘑菇,这条路的“恢复系数”为 0.7 0.7 0.7,则第一~四次经过这条路径所能采到的蘑菇数量分别为 4 , 2 , 1 , 0 4,2,1,0 4,2,1,0

现在,小胖和 ZYR 从 S S S 号小树丛出发,求他们最多能采到多少蘑菇。

输入格式

第一行两个整数, N N N M M M

第二行到第 M + 1 M+1 M+1 行,每行四个数,分别表示一条小路的起点,终点,初始蘑菇数,恢复系数。

M + 2 M+2 M+2 行,一个整数 S S S

输出格式

一行一个整数,表示最多能采到多少蘑菇,保证答案不超过 ( 2 31 − 1 ) (2^{31}-1) (2311)

样例输入
3 3
1 2 4 0.5
1 3 7 0.1
2 3 4 0.6
1
样例输出
8

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 8 × 1 0 4 1 \le N\le 8\times 10^4 1N8×104 1 ≤ M ≤ 2 × 1 0 5 1\le M\le 2\times 10^5 1M2×105 0 ≤ 恢复系数 ≤ 0.8 0\le\text{恢复系数}\le 0.8 0恢复系数0.8 且最多有一位小数, 1 ≤ S ≤ N 1\le S\le N 1SN


和缩点求最长路板子唯一的不同就是点权变成了边权。缩点,对每个SCC暴力求他的内部边权和记作他的点权。然后跑Topo求最长路。

终于调出了样例,信心满满,一发上去10分qwq

后来终于30分了,想查看样例1发现竟然乱码了?!?在谷友和管理帮助下得到了样例1的数据,写字板竟然可以看到正确数据。发现是我先 * 10 再读入的,麻了。

这题卡精度,需要先乘10再/10,全程除了读入不用double!

还有就是怎么用拓扑排序dp。

拓扑求最长路

初始点不一样是0读入点,把他先塞进去,其他的零入读点也赛进入,然后初始成 -0x3f3f3f3f

topo 的时候,更新一定要将所有临界点更新,不能放里边更新!

void topo()
{
	queue<int>que;
	for(int i=1;i<=cnt;i++){
		if(!d[i]){
			que.push(i);
		}
		dis[i] = -0x3f3f3f3f3f3f3f3f;
	}
	
	root = id[root];
	dis[root] = W[root];	
	
	while(que.size()){
		int t=que.front();
		que.pop();
		for(int i=hs[t];~i;i=nes[i])
		{
			int j=es[i];
			int edge_value = Ws[i];
			// ? 放if里边和外边竟然不同 , 一定要放外卖,里边会wa!
			dis[j] = max(dis[j] , dis[t] + W[j] + edge_value);
			d[j]--;
			if(!d[j]){
				que.push(j);
			}
		}

	}
}
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <sstream>
#define pb push_back 
#define in insert
#define mem(f, x) memset(f,x,sizeof(f)) 
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;

typedef long long ll;
typedef unsigned long long ull; 
const int N=2e5+10,M=1e5+10;
int n,m;

int h[N],e[N],ne[N],w[N],idx,p[N];
int hs[N],es[N],nes[N],idxs;
int Ws[N];

int dfn[N],low[N],tim;
int stack[N],top;
bool is_in[N];

int cnt;//存储SCC个数
int id[N]; // 存储每个节点在哪个SCC中
vector<int>scc[N];
int W[N];//缩点之后的权值
int root;

void add(int a,int b,int c,int d){
	e[idx]=b,ne[idx]=h[a],w[idx]=c,p[idx]=d,h[a]=idx++;	
}

void adds(int a,int b,int c){
	es[idxs]=b,nes[idxs]=hs[a];
	Ws[idxs]=c;
	hs[a]=idxs++;	
}

void tarjan(int u){
	dfn[u]=low[u]=++tim;
	stack[++top]=u;
	is_in[u]=1;
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(!dfn[j]){
			tarjan(j);
			low[u]=min(low[u],low[j]);
		}
		else if(is_in[j]){
			low[u]=min(low[u],dfn[j]);
		}
	}
	if(dfn[u]==low[u]){
		int y;
		cnt++;
		do{
			y=stack[top--];
			is_in[y]=false;
			scc[cnt].pb(y);
			id[y]=cnt;	
		}while(u!=y);

	}
}

int d[N];
ll dis[N];

void topo()
{
	queue<int>que;
	for(int i=1;i<=cnt;i++){
		if(!d[i]){
			que.push(i);
		}
		dis[i] = -0x3f3f3f3f3f3f3f3f;
	}
	
	root = id[root];
	dis[root] = W[root];	
	
	while(que.size()){
		int t=que.front();
		que.pop();
		for(int i=hs[t];~i;i=nes[i])
		{
			int j=es[i];
			int edge_value = Ws[i];
			// ? 放if里边和外边竟然不同 , 一定要放外卖,里边会wa!
			dis[j] = max(dis[j] , dis[t] + W[j] + edge_value);
			d[j]--;
			if(!d[j]){
				que.push(j);
			}
		}

	}
}

void solve()
{
	mem(h,-1);
	mem(hs,-1);
	cin>>n>>m;
	fo(i,1,m){
		int a,b,c;
		double d;
		cin>>a>>b>>c>>d;
		d*=10;
		add(a,b,c,d);
	}
	cin>>root;
	for(int i=1;i<=n;i++)
		if(!dfn[i])
			tarjan(i);
	for(int i=1;i<=n;i++)
	{
		for(int j=h[i];~j;j=ne[j])
		{
			int y=e[j];
			int value=w[j];
			int cost=p[j];
			ll sum = 0;
			
			if(id[i]==id[y]){
				// i,y value
				while(value){
					sum += value;
					value = value*cost/10;
				}	
				W[id[i]] += sum;
				continue;
			}

			adds(id[i],id[y],value); // 只能走一遍了
			d[id[y]]++;
			// 直接在新的连通分量之间连边。
		}
	}
	topo();
	
	ll ans = 0;
	fo(i,1,cnt){
		ans = max(ans,dis[i]);
	}
	cout<<ans<<endl;
}

int main()
{
	solve();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值