ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

连通性问题学习笔记

2022-07-10 13:37:10  阅读:194  来源: 互联网

标签:连通 连通性 int ++ 笔记 学习 vis dfn low


基本概念

下面介绍几个概念:

  • 强连通(Strongly Connected),如果这个有向图任意两点连通,那么这个图是强连通的。
  • (有向图的)强连通分量(Strongly Connected Components,SCC),指一个图中,极大的强连通的子图。

例子:

图片来源:初探Trajan算法(求强连通分量) - Styx 的博客 - 洛谷博客

这个图中,最大的红圈圈中的是强连通分量,其它的是连通分量。

  • (无向图的)割点(Cut Vertex):又称割顶,如果把一个点删除后原来该点所在的极大连通分量不再是一个连通分量,那么这个点是该图的割点。

例子:

图片来源:割点和桥 - OI Wiki

这个图中,割点是 \(2\) 且本图有且仅有一个割点。

  • (无向图的)桥(Bridge):又称割边,如果把一条边删除后原来该边所在的极大连通分量不再是一个连通分量,那么这条边是该图的桥。

例子:

图片来源:割点和桥 - OI Wiki

这个图中,红色的边即为桥。

  • 点双连通(Biconnected):如果这个连通图没有割点,那么它是点双连通的。
  • 边双连通(2-Edge-Connected):如果这个连通图没有割边,那么它是边双连通的。
  • 点双连通分量(Biconnected Component):一个图的极大点双连通子图。
  • 边双连通分量(2-Edge-Connected Component):一个图的极大边双连通子图。

上述定义部分参考自:图论相关概念 - OI Wiki

Tajan算法——求解强连通问题的利器

小资料

Robert E. Tarjan(Tarjan读音类似“塔扬”, \(1948-\textbf{至今}\)),美国计算机科学家,它的主要贡献有求解连通性问题的Tarjan算法、离线求解LCA的Tarjan算法、并查集(Union Find)、伸展树(Splay)等。

DFS树

了解Tarjan算法之前,先来学习一下DFS树。

图片来源:强连通分量 - OI Wiki

强连通分量

与DFS树的关系

如果结点 \(u\) 是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以 \(u\) 为根的子树中。结点 \(u\) 被称为这个强连通分量的根。参考资料:强连通分量 - OI Wiki

算法流程

首先需要维护一个栈 \(S\)。

然后我们维护这样几个东西:

  • \(\operatorname{DFN}[u]\):DFS时 \(u\) 的遍历次序。(为了书写方便,以后简写为 \(D[u]\))

  • \(\operatorname{LOW}[u]\):从节点 \(u\) 出发,能够回溯到的最早位于栈中的节点。(为了书写方便,以后简写为 \(L[u]\))

我们的步骤是这样子的:

  1. 从一个没有被DFS过的点 \(u\) 出发,DFS遍历这张图。
  2. 然后将这个点标记为一个强连通分量的根。(也就是说,\(L[u]=D[u]=\textbf{当前的DFS次序}\))
  3. 将这个节点入栈。
  4. 遍历这个节点的所有出边(令该边为 \(u \to v\))
  5. 如果 \(v\) 还没有访问过(可以判断 \(D[v]=0\)),那么回到操作 \(2\)(当然现在 \(u\) 是 \(v\) 了),然后 \(L[u]=\min\{L[u],L[v]\}\)。
  6. 如果 \(v\) 访问过了,那么 \(L[u]=\min\{D[v],L[u]\}\)。
  7. 如果此时 \(u\) 还是一个强连通分量的根,那么弹出元素,直到 \(u\)为止,对弹出的元素进行染色(其实就是打标记),最后对 \(u\) 本身染色。

(七步Tarjan法)

模板:

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

模板题:P2863 [USACO06JAN]The Cow Prom S 牛的舞会

有一个 \(n\) 个点,\(m\) 条边的有向图,请求出这个图点数大于 \(1\) 的强联通分量个数。

对于全部的测试点,保证 \(2\le n \le 10^4\),\(2\le m\le 5\times 10^4\),\(1 \leq a, b \leq n\)。

先来一遍Tarjan,然后统计答案。

代码:

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

int n,m;
const int N = 2e4+5,M = 5e4+5;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

int main(){
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		add(u,v,114);
	}	
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}
	int ans=0;
	for(int i=1;i<=cn;i++){
		if(num[i]>1){
			ans++;
		}
	}
	cout<<ans;
	return 0;
} 

Record in Luogu

复杂度分析

Tarjan算法,每个点都会遍历一遍,每个边也都会遍历一遍,所以时间复杂度 \(O(n+m)\),由于需要一个栈以及一大堆数组,因此有空间复杂度 \(O(n)\)。

Kosaraju算法——强连通分量专用算法

我们先求出这个图的DFS序,然后遍历DFS序,对于每一个没有经过DFS的节点,出栈DFS,遍历的节点若没有DFS过,属于这个强连通分量,最后在DFS中入栈。

很难用文字解释如此精妙的算法,还是用代码吧。例题:P2863 [USACO06JAN]The Cow Prom S

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

int n,m;
const int SIZE = 114514;
vector<int> g[SIZE],rg[SIZE];
bool vis[SIZE];
int sta[SIZE],top;

void add(int u,int v){
	g[u].push_back(v);
	rg[v].push_back(u);
}

void dfsspantree(int u){
	vis[u]=1;
	for(int i:g[u]){
		if(!vis[i]){
			dfsspantree(i);
		}
	}
	sta[++top]=u;
}
int siz[SIZE];

void qltfl(int u,int id){
	vis[u]=1;
	siz[id]++;
	for(int i:rg[u]){
		if(!vis[i]){
			qltfl(i,id);
		}
	}
}

int main(){
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		add(u,v);
	}	
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			dfsspantree(i);
		}
	}
	memset(vis,false,sizeof(vis));
	int idx=0;
	while(top){
		int u=sta[top--];
		if(!vis[u]){
			qltfl(u,++idx);
		}
	}
	int ans=0;
	for(int i=1;i<=idx;i++){
		ans+=(siz[i]>1);
	}
	cout<<ans<<endl;
}

Record in Luogu

时间复杂度 \(O(n+m)\),空间复杂度 \(O(n)\)。

强连通分量例题

P2002 消息扩散(By C_SUNSHINE)

题面

有 \(n\) 个城市,中间有 \(m\) 条单向道路连接,消息会沿着道路扩散,现在给出 \(n\) 个城市及其之间的道路,问至少需要在几个城市发布消息才能让这所有 \(n\) 个城市都得到消息。

【数据范围】

对于 \(20 \%\) 的数据,\(n \le 200\);
对于 \(40 \%\) 的数据,\(n \le 2000\);
对于 \(100 \%\) 的数据,\(1 \le n \le {10}^5\),\(1 \le m \le 5 \times {10}^5\)。

思路

样例:

5 4
1 2
2 1
2 3
5 1

建的图如下:

不难想到,缩点后,入度为 \(0\) 的点不能接受消息。

所以其实就是求入度为 \(0\) 的强连通分量个数,可以用 Tarjan。

时间复杂度 \(O(n+m)\)。

代码

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

int n,m;
const int N = 1e5+5,M = 5e5+5;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

int need[N];

int main(){
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		add(u,v,114);
	}	
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=head[i];j;j=g[j].nxt){
			int v=g[j].to;
			if(c[i]!=c[v]){
				need[c[v]]++;
			}
		}
	}
	int ans=0;
	for(int i=1;i<=cn;i++){
		if(need[i]==0){
			ans++;
		}
	}
	cout<<ans;
	return 0;
} 

P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G | LibreOJ10091. 「一本通 3.5 例 1」受欢迎的牛

题面

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

对于 \(10\%\) 的数据,\(N\le20\),\(M\le50\)。

对于 \(30\%\) 的数据,\(N\le10^3\),\(M\le2\times 10^4\)。

对于 \(70\%\) 的数据,\(N\le5\times 10^3\),\(M\le5\times 10^4\)。

对于 \(100\%\) 的数据,\(1\le N\le10^4\),\(1\le M\le5\times 10^4\)。

思路

样例:

3 3
1 2
2 1
2 3

图如下:

样例中 \(3\) 是明星奶牛。

可以发现,如果存在一个出度为 \(0\) 的连通分量,那么存在该连通分量的点个数的明星奶牛(如果有出度,那么它指向的奶牛就是新的明星奶牛),否则不存在。

时间复杂度 \(O(n+m)\)。

代码

出度变量名写成入度了,不要在意,反正答案也是对的。

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

int n,m;
const int N = 2e4+5,M = 5e4+5;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

int ru[N];

int main(){
	cin>>n>>m;
	for(int i=1,a,b;i<=m;i++){
		cin>>a>>b;
		add(a,b,114);
	}
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=head[i];j;j=g[j].nxt){
			int v=g[j].to;
			if(c[i]!=c[v]){
				ru[c[i]]++;
			}
		}
	}
	int ans=0;
	int Cnt=0;
	for(int i=1;i<=cn;i++){
		if(ru[i]==0){
			Cnt++;
			ans=i;
		}
	}
	if(Cnt==1){
		cout<<num[ans];
	}
	else{
		cout<<0;
	}
	return 0;
}

Record in Luogu

Record in LibreOJ

P2835 刻录光盘 | 一本通1383 刻录光盘(cdrom)

题面

在JSOI2005夏令营快要结束的时候,很多营员提出来要把整个夏令营期间的资料刻录成一张光盘给大家,以便大家回去后继续学习。组委会觉得这个主意不错!可是组委会一时没有足够的空光盘,没法保证每个人都能拿到刻录上资料的光盘,又来不及去买了,怎么办呢?!

组委会把这个难题交给了LHC,LHC分析了一下所有营员的地域关系,发现有些营员是一个城市的,其实他们只需要一张就可以了,因为一个人拿到光盘后,其他人可以带着U盘之类的东西去拷贝啊!

可是,LHC调查后发现,由于种种原因,有些营员并不是那么的合作,他们愿意某一些人到他那儿拷贝资料,当然也可能不愿意让另外一些人到他那儿拷贝资料,这与我们JSOI宣扬的团队合作精神格格不入!!!

现在假设总共有 \(N\) 个营员(\(2 \le N \le 200\)),每个营员的编号为1~N。LHC给每个人发了一张调查表,让每个营员填上自己愿意让哪些人到他那儿拷贝资料。当然,如果A愿意把资料拷贝给B,而B又愿意把资料拷贝给C,则一旦A获得了资料,则B,C都会获得资料。

现在,请你编写一个程序,根据回收上来的调查表,帮助LHC计算出组委会至少要刻录多少张光盘,才能保证所有营员回去后都能得到夏令营资料?

思路

其实本题就是消息扩散那题的双倍经验,只是输入格式要改一改(如果不改亲测 \(9\) 分,雾)。

代码

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

int n,m;
const int N = 1e5+5,M = 5e5+5;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

int need[N];

int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
	    while(1){
	        int u;
	        cin>>u;
	        if(u==0)break;
	        add(i,u,114);
	    }
	}
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=head[i];j;j=g[j].nxt){
			int v=g[j].to;
			if(c[i]!=c[v]){
				need[c[v]]++;
			}
		}
	}
	int ans=0;
	for(int i=1;i<=cn;i++){
		if(need[i]==0){
			ans++;
		}
	}
	cout<<ans;
	return 0;
} 

Record in Luogu

Record in 一本通OJ

P1407 [国家集训队] 稳定婚姻 | BZOJ2140 稳定婚姻

题面

我们已知 \(n\) 对夫妻的婚姻状况,称第 \(i\) 对夫妻的男方为 \(B_i\),女方为 \(G_i\)。若某男 \(B_i\) 与某女 \(G_j\) 曾经交往过(无论是大学,高中,亦或是幼儿园阶段,\(i \le j\)),则当某方与其配偶(即 \(B_i\) 与 \(G_i\) 或 \(B_j\) 与 \(G_j\))感情出现问题时,他们有私奔的可能性。不妨设 \(B_i\) 和其配偶 \(G_i\) 感情不和,于是 \(B_i\) 和 \(G_j\) 旧情复燃,进而 \(B_j\) 因被戴绿帽而感到不爽,联系上了他的初恋情人 \(Gk\) ……一串串的离婚事件像多米诺骨牌一般接踵而至。若在 \(B_i\) 和 \(G_i\) 离婚的前提下,这 \(2n\) 个人最终依然能够结合成 \(n\) 对情侣,那么我们称婚姻 \(i\) 为不安全的,否则婚姻 \(i\) 就是安全的。

记住,\(m\) 是相互喜欢的男女的对数。

给定所需信息,你的任务是判断每对婚姻是否安全(安全 Safe,不安全,Unsafe

对于 \(20\%\) 的数据,\(n \le 20\);

对于 \(40\%\) 的数据,\(n \le 100\),\(m \le 400\);

对于 \(100\%\) 的数据,所有姓名字符串中只包含英文大小写字母,大小写敏感,长度不大于 \(8\),保证每对关系只在输入文件中出现一次,输入文件的最后 \(m\) 行不会出现未在之前出现过的姓名,这 \(2n\) 个人的姓名各不相同,\(1 \le n \le 4000\),\(0 \le m \le 20000\)。

思路

我们可以夫妻 \(\textbf{女} \to \text{男}\),情人 \(\textbf{男} \to \text{女}\)(其实顺序随意),如果该男女在同一个强连通分量里面,那么就是不安全的,否则是安全的。

比如这个样例:

2
Melanie Ashley
Scarlett Charles
2
Scarlett Ashley
Melanie Charles

Melanie 为 \(1\),Scarlett 为 \(2\),Ashley 为 \(3\),Charles 为 \(4\),那么图如下:

\(1\) 与 \(3\),\(2\) 与 \(4\) 位于同一个强连通分量,因此两对夫妻关系都是不安全的。

同理,下面这组样例两个都是安全的。

2
Melanie Ashley
Scarlett Charles
1
Scarlett Ashley

实现代码时注意,由于节点是字符串,可以使用 std::map 离散化。

时间复杂度 \(O(n\log n+m\log m)\)。

代码

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

int n,m;
const int N = 2e4+5,M = 5e4+5;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

map<string,int> Map;

int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		string g,b;
		cin>>g>>b;
		Map[g]=i;
		Map[b]=i+n;
		add(i,i+n,114);
	}
	cin>>m;
	for(int i=1;i<=m;i++){
		string g,b;
		cin>>g>>b;
		add(Map[b],Map[g],514);
	}
	for(int i=1;i<=(n<<1);i++){
		if(!dfsed[i]){
			tarjan(i);
		} 
	}
	for(int i=1;i<=n;i++){
		if(c[i]==c[i+n]){
			cout<<"Unsafe"<<'\n';
		}
		else{
			cout<<"Safe"<<'\n';
		}
	}
	return 0;
} 

Record in Luogu

Record in BZOJ(Sub Domain in Hydro)

P2746 [USACO5.3]校园网Network of Schools | Hydro USACO533. Network of Schools

题面

一些学校连入一个电脑网络。那些学校已订立了协议:每个学校都会给其它的一些学校分发软件(称作“接受学校”)。注意即使 \(B\) 在 \(A\) 学校的分发列表中,\(A\) 也不一定在 \(B\) 学校的列表中。

你要写一个程序计算,根据协议,为了让网络中所有的学校都用上新软件,必须接受新软件副本的最少学校数目(子任务 A)。更进一步,我们想要确定通过给任意一个学校发送新软件,这个软件就会分发到网络中的所有学校。为了完成这个任务,我们可能必须扩展接收学校列表,使其加入新成员。计算最少需要增加几个扩展,使得不论我们给哪个学校发送新软件,它都会到达其余所有的学校(子任务 B)。一个扩展就是在一个学校的接收学校列表中引入一个新成员。

你需要求出子任务 A 和子任务 B 的解。

思路

首先对于子任务 A,就是求连通分量入度为 \(0\) 的点的个数。

子任务 B,就是让我们把图变成一个强连通分量,也就是入度至少要为 \(1\),所以是子任务 A与出度为 \(0\) 的个数的最大值。

注意,如果整图为连通分量,那么子任务 B就是 \(0\)(不言而喻)。

时间复杂度 \(O(n+m)\)。

代码

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

int n,m;
const int N = 1e3+5,M = 1e3+5;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

int ru[N],chu[N];

int A,B;

int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		while(1){
			int school;
			cin>>school;
			if(school==0){
				break;
			}
			add(i,school,114);
		}
	}
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}	
	for(int i=1;i<=n;i++){
		for(int j=head[i];j;j=g[j].nxt){
			int v=g[j].to;
			if(c[i]!=c[v]){
				ru[c[v]]++;
				chu[c[i]]++;
			}
		}
	}
	for(int i=1;i<=cn;i++){
		A+=(ru[i]==0);
		B+=(chu[i]==0);
	}
	cout<<A<<'\n';
	if(cn==1){
		cout<<0;
	}
	else{
		cout<<max(A,B);
	}
	return 0;
} 

Record in Luogu

P2812 校园网络【[USACO]Network of Schools加强版】

题面

共有 \(n\) 所学校 \((1 \leq n \leq 10000)\) 已知他们实现设计好的网络共 \(m\) 条线路,为了保证高速,网络是单向的。现在请你告诉他们至少选几所学校作为共享软件的母机,能使每所学校都可以用上。再告诉他们至少要添加几条线路能使任意一所学校作为母机都可以使别的学校使用上软件。

原题。数据扩大了 \(100\) 倍。

$1 \leq $ 边数 \(\leq5000000\),\(1 \leq n \leq 10000\) 。

思路

其实只要把数组大小改改就好。

代码

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

int n,m;
const int N = 1000000,M = 1000000;

struct edge{
    int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
    g[++ec].nxt=head[from];
    g[ec].to=to;
    g[ec].w=weight;
    head[from]=ec;
}

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N];
stack<int> s;

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

int ru[N],chu[N];

int A,B;

int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		while(1){
			int school;
			cin>>school;
			if(school==0){
				break;
			}
			add(i,school,114);
		}
	}
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}	
	for(int i=1;i<=n;i++){
		for(int j=head[i];j;j=g[j].nxt){
			int v=g[j].to;
			if(c[i]!=c[v]){
				ru[c[v]]++;
				chu[c[i]]++;
			}
		}
	}
	for(int i=1;i<=cn;i++){
		A+=(ru[i]==0);
		B+=(chu[i]==0);
	}
	cout<<A<<'\n';
	if(cn==1){
		cout<<0;
	}
	else{
		cout<<max(A,B);
	}
	return 0;
} 

P3627 [APIO2009]抢掠计划 | LibreOJ10096. 「一本通 3.5 练习 4」抢掠计划

题面

Siruseri 城中的 \(M\) 条道路都是单向的。不同的道路由 \(N\) 个路口连接。按照法律的规定,在每个路口都设立了一个 Siruseri 银行的 ATM 取款机。令人奇怪的是,Siruseri 的 \(p\) 个酒吧也都设在路口,虽然并不是每个路口都设有酒吧。

Banditji 计划实施 Siruseri 有史以来最惊天动地的 ATM 抢劫。他将从市中心 \(s\) 出发,沿着单向道路行驶,抢劫所有他途径的 ATM 机,最终他将在一个酒吧庆祝他的胜利。

使用高超的黑客技术,他获知了每个 ATM 机中可以掠取的现金数额。他希望你帮助他计算从市中心出发最后到达某个酒吧时最多能抢劫的现金总数。他可以经过同一路口或道路任意多次。但只要他抢劫过某个 ATM 机后,该 ATM 机里面就不会再有钱了。 例如,假设该城中有 \(6\) 个路口,道路的连接情况如下图所示:

市中心在路口 \(1\),由一个入口符号 → 来标识,那些有酒吧的路口用双圈来表示。每个 ATM 机中可取的钱数标在了路口的上方。在这个例子中,Banditji 能抢劫的现金总数为 \(47\),实施的抢劫路线是:\(1-2-4-1-2-3-5\)。

给出 \(n,m,s,p\) 和Siruseri城市地图,求能抢劫的现金总数的最大值。

对于 \(50\%\) 的数据,保证 \(N, M \le 3000\)。

对于 \(100\%\) 的数据,保证 \(N, M \le 5\times 10^5\),\(0 \le a_i \le 4000\)。保证可以从市中心沿着 Siruseri 的单向的道路到达其中的至少一个酒吧。

思路

其实就是求单源多汇最长路。

如果直接用SPFA,一定会T和WA,我们可以发现,一个强联通分量内的所有ATM都可以被抢一遍,因此只要缩点成为一个DAG之后再跑最长路即可。

代码

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

int n,m,start,p;
const int N = 5e5+5,M = 5e5+5;

struct edge{
	int nxt,to,w;
} g[M<<1];
int head[M],ec;
void add(int from,int to,int weight){
	g[++ec].nxt=head[from];
	g[ec].to=to;
	g[ec].w=weight;
	head[from]=ec;
}

struct edge2{
	int u,v;
} edges[M];

int dfn[N],low[N], cnt, vis[N], dfsed[N];
int cn,c[N],num[N],sum[N];
int dis[N];
stack<int> s;

int a[N],bar[N];

void mark(int u){
	s.pop();
	c[u]=cn;
	num[cn]++;
	vis[u]=0;
	sum[cn]+=a[u];
}

void tarjan(int u){
	dfn[u]=low[u]=(++cnt);
	vis[u]=dfsed[u]=1;
	s.push(u);
	for(int i=head[u];i;i=g[i].nxt){
		int v=g[i].to;
		if(dfn[v]==0){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if(vis[v]){
				low[u]=min(low[u],dfn[v]);
			}
		}
	}
	if(low[u]==dfn[u]){
		cn++;
		while(s.top()!=u){
			mark(s.top());
		}
		mark(u);
	}
}

queue<int> q;

void spfa(int ss){
	memset(vis,0,sizeof(vis));
	memset(dis,0,sizeof(dis));
	int gs = c[ss];
	vis[gs]=1;
	dis[gs]=sum[gs];
	q.push(gs);
	while(!q.empty()){
		int v = q.front();
		vis[v]=0;
		q.pop();
		for(int i=head[v];i;i=g[i].nxt){
			int w = g[i].to;
			if(dis[w]<dis[v]+g[i].w){
				dis[w]=dis[v]+g[i].w;
				if(!vis[w]){
					q.push(w);
					vis[w]=1;
				}
			}
		}
	}
}

int main(){
	cin>>n>>m;
	for(int i=1,u,v;i<=m;i++){
		cin>>u>>v;
		edges[i]={u,v};
		add(u,v,114);
	}
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	cin>>start>>p;
	for(int i=1;i<=p;i++){
		cin>>bar[i];
	}
	for(int i=1;i<=n;i++){
		if(!dfsed[i]){
			tarjan(i);
		}
	}
	ec=0;
	memset(head,0,sizeof(head));
	memset(g,0,sizeof(g));
	for(int i=1;i<=m;i++){
		if(c[edges[i].u]!=c[edges[i].v]){
			add(c[edges[i].u],c[edges[i].v],sum[c[edges[i].v]]);
		}
	}
	spfa(start);
	int ans=INT_MIN;
	for(int i=1;i<=p;i++){
		ans=max(ans,dis[c[bar[i]]]);
	}
	cout<<ans;
	return 0;
}

标签:连通,连通性,int,++,笔记,学习,vis,dfn,low
来源: https://www.cnblogs.com/zheyuanxie/p/connected-problems.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有