强连通基础与例题(Kosaraju算法与Tarjan算法) Kosaraju算法(转自Kosaraju算法) Tarjan算法(转自Tarjan算法) 例题
目录
B:HDU-2767 Proving Equivalences
先给个网址了解下强连通:(强连通算法)
连通分量:在无向图中,即为连通子图。
上图中,总共有四个连通分量。顶点A、B、C、D构成了一个连通分量,顶点E构成了一个连通分量,顶点F,G和H,I分别构成了两个连通分量。
强连通分量:有向图中,尽可能多的若干顶点组成的子图中,这些顶点都是相互可到达的,则这些顶点成为一个强连通分量。
上图中有三个强连通分量,分别是a、b、e以及f、g和c、d、h。
2. 连通分量的求解方法
对于一个无向图的连通分量,从连通分量的任意一个顶点开始,进行一次DFS,一定能遍历这个连通分量的所有顶点。所以,整个图的连通分量数应该等价于遍历整个图进行了几次(最外层的)DFS。一次DFS中遍历的所有顶点属于同一个连通分量。
下面我们将介绍有向图的强连通分量的求解方法。
3. Kosaraju算法的基本原理
我们用一个最简单的例子讲解Kosaraju算法
显然上图中有两个强连通分量,即强连通分量A和强连通分量B,分别由顶点A0-A1-A2和顶点B3-B4-B5构成。每个连通分量中有若干个可以相互访问的顶点(这里都是3个),强连通分量与强连通分量之间不会形成环,否则应该将这些连通分量看成一个整体,即看成同一个强连通分量。
我们现在试想能否按照无向图中求连通分量的思路求解有向图的强连通分量。我们假设,DFS从强连通分量B的任意一个顶点开始,那么恰好遍历整个图需要2次DFS,和连通分量的数量相等,而且每次DFS遍历的顶点恰好属于同一个连通分量。但是,我们若从连通分量A中任意一个顶点开始DFS,就不能得到正确的结果,因为此时我们只需要一次DFS就访问了所有的顶点。所以,我们不应该按照顶点编号的自然顺序(0,1,2,……)或者任意其它顺序进行DFS,而是应该按照被指向的强连通分量的顶点排在前面的顺序进行DFS。上图中由强连通分量A指向了强连通分量B。所以,我们按照
B3, B4, B5, A0, A1, A2
的顺序进行DFS,这样就可以达到我们的目的。但事实上这样的顺序太过严格,我们只需要保证被指向的强连通分量的至少一个顶点排在指向这个连通分量的所有顶点前面即可,比如
B3, A0, A1, A2, B4, B5
B3排在了强连通分量A所有顶点的前面。
现在我们的关键问题就是如何得到这样一个满足要求的顶点顺序,Kosaraju给出了这解决办法:对原图取反,然后从反向图的任意节点开始进行DFS的逆后序遍历,逆后序得到的顺序一定满足我们的要求。
DFS的逆后序遍历是指:如果当前顶点未访问,先遍历完与当前顶点相连的且未被访问的所有其它顶点,然后将当前顶点加入栈中,最后栈中从栈顶到栈底的顺序就是我们需要的顶点顺序。
上图表示原图的反向。
我们现在进行第一种假设:假设DFS从位于强连通分量A中的任意一个节点开始。那么第一次DFS完成后,栈中全部都是强连通分量A的顶点,第二次DFS完成后,栈顶一定是强连通分量B的顶点。保证了从栈顶到栈底的排序强连通分量B的顶点全部都在强连通分量A顶点之前。
我们现在进行第二种假设:假设DFS从位于强连通分量B中的任意一个顶点开始。显然我们只需要进行一次DFS就可以遍历整个图,由于是逆后续遍历,那么起始顶点一定最后完成,所以栈顶的顶点一定是强连通分量B中的顶点,这显然是我们希望得到的顶点排序的结果。
上面使用了最简单的例子说明Kosaraju算法的原理,对于有多个强连通分量,连接复杂的情况,仍然适用。大家可以自行举例验证。
综上可得,不论从哪个顶点开始,图中有多少个强连通分量,逆后续遍历的栈中顶点的顺序一定会保证:被指向的强连通分量的至少一个顶点排在指向这个连通分量的所有顶点前面。所以,我们求解强连通分量的步骤可以分为两步:
(1)对原图取反,从任意一个顶点开始对反向图进行逆后续DFS遍历
(2)按照逆后续遍历中栈中的顶点出栈顺序,对原图进行DFS遍历,一次DFS遍历中访问的所有顶点都属于同一强连通分量。
模板在下面例题中给出(不同题模板不同,可根据题理解模板)。
Tarjan算法(转自Tarjan算法)
tarjan算法,一个关于 图的联通性的神奇算法。基于DFS(迪法师)算法,深度优先搜索一张有向图。!注意!是有向图。根据树,堆栈,打标记等种种神(che)奇(dan)方法来完成剖析一个图的工作。而图的联通性,就是任督二脉通不通。。的问题。
了解tarjan算法之前你需要知道:
强连通,强连通图,强连通分量,解答树(解答树只是一种形式。了解即可)
不知道怎么办!!!
神奇海螺~:嘟噜噜~!
强连通(strongly connected): 在一个有向图G里,设两个点 a b 发现,由a有一条路可以走到b,由b又有一条路可以走到a,我们就叫这两个顶点(a,b)强连通。
强连通图: 如果 在一个有向图G中,每两个点都强连通,我们就叫这个图,强连通图。
强连通分量strongly connected components):在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量 [分量::把一个向量分解成几个方向的向量的和,那些方向上的向量就叫做该向量(未分解前的向量)的分量]
举个简单的栗子:
比如说这个图,在这个图中呢,点1与点2互相都有路径到达对方,所以它们强连通.
而在这个有向图中,点1 2 3组成的这个子图,是整个有向图中的强连通分量。
解答树:就是一个可以来表达出递归枚举的方式的树(图),其实也可以说是递归图。。反正都是一个作用,一个展示从“什么都没有做”开始到“所有结求出来”逐步完成的过程。“过程!”
神奇海螺结束!!!
tarjan算法,之所以用DFS就是因为它将每一个强连通分量作为搜索树上的一个子树。而这个图,就是一个完整的搜索树。
为了使这颗搜索树在遇到强连通分量的节点的时候能顺利进行。每个点都有两个参数。
1,DFN[]作为这个点搜索的次序编号(时间戳),简单来说就是 第几个被搜索到的。%每个点的时间戳都不一样%。
2,LOW[]作为每个点在这颗树中的,最小的子树的根,每次保证最小,like它的父亲结点的时间戳这种感觉。如果它自己的LOW[]最小,那这个点就应该从新分配,变成这个强连通分量子树的根节点。
ps:每次找到一个新点,这个点LOW[]=DFN[]。
而为了存储整个强连通分量,这里挑选的容器是,堆栈。每次一个新节点出现,就进站,如果这个点有 出度 就继续往下找。直到找到底,每次返回上来都看一看子节点与这个节点的LOW值,谁小就取谁,保证最小的子树根。如果找到DFN[]==LOW[]就说明这个节点是这个强连通分量的根节点(毕竟这个LOW[]值是这个强连通分量里最小的。)最后找到强连通分量的节点后,就将这个栈里,比此节点后进来的节点全部出栈,它们就组成一个全新的强连通分量。
先来一段伪代码压压惊:
tarjan(u){
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
for each (u, v) in E // 枚举每一条边
if (v is not visted) // 如果节点v未被访问过
tarjan(v) // 继续向下找
Low[u] = min(Low[u], Low[v])
else if (v in S) // 如果节点u还在栈内
Low[u] = min(Low[u], DFN[v])
if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点
print v
until (u== v)
}
首先来一张有向图。网上到处都是这个图。我们就一点一点来模拟整个算法。
从1进入 DFN[1]=LOW[1]= ++index ----1
入栈 1
由1进入2 DFN[2]=LOW[2]= ++index ----2
入栈 1 2
之后由2进入3 DFN[3]=LOW[3]= ++index ----3
入栈 1 2 3
之后由3进入 6 DFN[6]=LOW[6]=++index ----4
入栈 1 2 3 6
之后发现 嗯? 6无出度,之后判断 DFN[6]==LOW[6]
说明6是个强连通分量的根节点:6及6以后的点 出栈。
栈: 1 2 3
之后退回 节点3 Low[3] = min(Low[3], Low[6]) LOW[3]还是 3
节点3 也没有再能延伸的边了,判断 DFN[3]==LOW[3]
说明3是个强连通分量的根节点:3及3以后的点 出栈。
栈: 1 2
之后退回 节点2 嗯?!往下到节点5
DFN[5]=LOW[5]= ++index -----5
入栈 1 2 5
ps:你会发现在有向图旁边的那个丑的(划掉)搜索树 用红线剪掉的子树,那个就是强连通分量子树。每次找到一个。直接。一剪子下去。半个子树就没有了。。
结点5 往下找,发现节点6 DFN[6]有值,被访问过。就不管它。
继续 5往下找,找到了节点1 他爸爸的爸爸。。DFN[1]被访问过并且还在栈中,说明1还在这个强连通分量中,值得发现。 Low[5] = min(Low[5], DFN[1])
确定关系,在这棵强连通分量树中,5节点要比1节点出现的晚。所以5是1的子节点。so
LOW[5]= 1
由5继续回到2 Low[2] = min(Low[2], Low[5])
LOW[2]=1;
由2继续回到1 判断 Low[1] = min(Low[1], Low[2])
LOW[1]还是 1
1还有边没有走过。发现节点4,访问节点4
DFN[4]=LOW[4]=++index ----6
入栈 1 2 5 4
由节点4,走到5,发现5被访问过了,5还在栈里,
Low[4] = min(Low[4], DFN[5]) LOW[4]=5
说明4是5的一个子节点。
由4回到1.
回到1,判断 Low[1] = min(Low[1], Low[4])
LOW[1]还是 1 。
判断 LOW[1] == DFN[1]
诶?!相等了 说明以1为根节点的强连通分量已经找完了。
将栈中1以及1之后进栈的所有点,都出栈。
栈 :(鬼都没有了)
这个时候就完了吗?!
你以为就完了吗?!
然而并没有完,万一你只走了一遍tarjan整个图没有找完怎么办呢?!
所以。tarjan的调用最好在循环里解决。
like 如果这个点没有被访问过,那么就从这个点开始tarjan一遍。
因为这样好让每个点都被访问到。
来一道裸代码。
输入:
一个图有向图。
输出:
它每个强连通分量。
这个图就是刚才讲的那个图。一模一样。
input:
6 8
1 3
1 2
2 4
3 4
3 5
4 6
4 1
5 6
output:
6
5
3 4 2 1
#include<cstdio>
#include<algorithm>
#include<string.h>
using namespace std;
struct node {
int v,next;
}edge[1001];
int DFN[1001],LOW[1001];
int stack[1001],heads[1001],visit[1001],cnt,tot,index;
void add(int x,int y)
{
edge[++cnt].next=heads[x];
edge[cnt].v = y;
heads[x]=cnt;
return ;
}
void tarjan(int x)//代表第几个点在处理。递归的是点。
{
DFN[x]=LOW[x]=++tot;// 新进点的初始化。
stack[++index]=x;//进站
visit[x]=1;//表示在栈里
for(int i=heads[x];i!=-1;i=edge[i].next)
{
if(!DFN[edge[i].v]) {//如果没访问过
tarjan(edge[i].v);//往下进行延伸,开始递归
LOW[x]=min(LOW[x],LOW[edge[i].v]);//递归出来,比较谁是谁的儿子/父亲,就是树的对应关系,涉及到强连通分量子树最小根的事情。
}
else if(visit[edge[i].v ]){ //如果访问过,并且还在栈里。
LOW[x]=min(LOW[x],DFN[edge[i].v]);//比较谁是谁的儿子/父亲。就是链接对应关系
}
}
if(LOW[x]==DFN[x]) //发现是整个强连通分量子树里的最小根。
{
do{
printf("%d ",stack[index]);
visit[stack[index]]=0;
index--;
}while(x!=stack[index+1]);//出栈,并且输出。
printf("
");
}
return ;
}
int main()
{
memset(heads,-1,sizeof(heads));
int n,m;
scanf("%d%d",&n,&m);
int x,y;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
}
for(int i=1;i<=n;i++)
if(!DFN[i]) tarjan(1);//当这个点没有访问过,就从此点开始。防止图没走完
return 0;
}
例题
A:HDU-1269 迷宫城堡:强连通模板题,判断是否是强连通图,给出两种解法
Tarjan算法
#include<iostream>
#include<string.h>
#include<algorithm>
#include <cstdio>
#include <vector>
#include <stack>
using namespace std;
const int N = 10005;
int n, m, timer;
vector<int> adj[N];
bool visit[N], mark;
int number[N], low[N];
stack<int> st;
void tarjan(int rt)
{
if(!mark)
return;
number[rt]=low[rt]=++timer;
visit[rt]=true;
for (unsigned i=0; i<adj[rt].size();++i)
{
int t=adj[rt][i];
if (!visit[t]){
tarjan(t);
low[rt] = min(low[rt], low[t]);
}
low[rt] = min(low[rt], number[adj[rt][i]]);
}
if (low[rt] == number[rt] && rt != 1)
mark = false;
}
int main()
{
int a, b;
while (cin>>n>>m&&(m+n)){
for (int i = 0; i < m; ++i){
scanf("%d %d", &a, &b);
adj[a].push_back(b);
}
memset(visit, 0, sizeof(visit));
timer = 0;
mark = true;
tarjan(1);
if (mark&&timer==n)
printf("Yes
");
else
printf("No
");
for (int i = 1; i <= n; ++i)
adj[i].clear();
}
return 0;
}
Kosaraju算法
#include<cstdio>
#include<iostream>
#include<string.h>
#include<algorithm>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int maxn=1e4+5;
vector <int> adj[maxn],t_adj[maxn];
int n,m,cnt;
bool visit[maxn];
bool dfs(int rt,vector<int> *graph)
{
stack <int> st;
st.push(rt);
visit[rt]=true;
cnt=0;
while(!st.empty()){
int u=st.top();
cnt++;
st.pop();
for(unsigned i=0;i<graph[u].size();i++){
if(visit[graph[u][i]])
continue;
st.push(graph[u][i]);
visit[graph[u][i]]=true;
}
}
if(cnt==n)
return true;
return false;
}
void transform1()
{
for(int i=1;i<=n;i++)
for(unsigned j=0;j<adj[i].size();j++)
t_adj[adj[i][j]].push_back(i);
}
bool kosaraju()
{
memset(visit,0,sizeof(visit));
if(!dfs(1,adj))
return false;
transform1();
memset(visit,0,sizeof(visit));
if(dfs(1,t_adj))
return true;
return false;
}
int main()
{
while(cin>>n>>m&&(n+m)){
for(int i=0;i<m;i++){
int x,y;
cin>>x>>y;
adj[x].push_back(y);
}
if(kosaraju())
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
for (int i = 1; i <= n; ++i){
adj[i].clear();
t_adj[i].clear();
}
}
return 0;
}
B:HDU-2767 Proving Equivalences:求加几条边能把图变成强连通图,需要运用缩点的知识。(缩点在强连通中很重要)第一次接触这样的题可能会看不懂,要多看看代码,或者去网上查查。
缩点: 先通过tarjian算法把一个大的图分出来 好多强连通的小图 这样每个小图都有自己的编号(不会相同)
然后缩点就是将每个小图看成一个新的小点 然后重新将这些点连接起来 构成一个 有向无环图(DAG)
得到这个图之后通过判断 出、入度 就可以判断通添加几条边 可以让原来的大图变成前连通图
#include<cstdio>
#include<iostream>
#include<string.h>
#include<algorithm>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int maxn=1e5+5;
vector <int> G[maxn];
int pre[maxn],lowlink[maxn],sccno[maxn],dfs_clock,scc_cnt;
stack<int> S;
int in0[maxn],out0[maxn];
void dfs(int u)
{
pre[u]=lowlink[u]=++dfs_clock;
S.push(u);
for(unsigned i=0;i<G[u].size();i++){
int v=G[u][i];
if(!pre[v]){
dfs(v);
lowlink[u]=min(lowlink[u],lowlink[v]);
}
else if(!sccno[v]){
lowlink[u]=min(lowlink[u],pre[v]);
}
}
if(lowlink[u]==pre[u]){
scc_cnt++;
for(;;){
int x=S.top();S.pop();
sccno[x]=scc_cnt;
if(x==u)
break;
}
}
}
void find_scc(int n)
{
dfs_clock=scc_cnt=0;//scc_cnt为SCC计数器,sccno[i]为i所在的SCC编号
memset(sccno,0,sizeof(sccno));
memset(pre,0,sizeof(pre));
for(int i=0;i<n;i++)
if(!pre[i])
dfs(i);
}
int main()
{
int T,m,n;
cin>>T;
while(T--){
cin>>n>>m;
for(int i=0;i<n;i++)
G[i].clear();
for(int i=0;i<m;i++){
int u,v;
cin>>u>>v;
u--;v--;
G[u].push_back(v);
}
find_scc(n);
for(int i=1;i<=scc_cnt;i++)
in0[i]=out0[i]=1;
for(int u=0;u<n;u++)
for(unsigned i=0;i<G[u].size();i++){
int v=G[u][i];
if(sccno[u]!=sccno[v])
in0[sccno[v]]=out0[sccno[u]]=0;
}
int a=0,b=0;
for(int i=1;i<=scc_cnt;i++){
if(in0[i])
a++;
if(out0[i])
b++;
}
int ans=max(a,b);
if(scc_cnt==1)
ans=0;
cout<<ans<<endl;
}
return 0;
}
C:HDU-1827 Summer Holiday:也需要缩点的知识需要联系一些人,每联系一个人都有相应的代价,但这些人中相互认识的可以只联系一个然后让其互相联系。要用到强连通分量来找环。用Tarjan找到环之后,找到入度不为0的,这些环就不用联系了,会有其他环联系的。担心不同的环构成闭环的话那不就没有人联系了吗。后来想到这不就成了一个大的环了嘛,怎么会是几个小环呢
#include<cstdio>
#include<iostream>
#include<string.h>
#include<algorithm>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int maxn=1e3+5;
vector <int> G[maxn];
int value[maxn],pre[maxn],lowlink[maxn],sccno[maxn],dfs_clock,scc_cnt;
stack<int> S;
int in0[maxn],out0[maxn];
void dfs(int u)
{
pre[u]=lowlink[u]=++dfs_clock;
S.push(u);
for(unsigned i=0;i<G[u].size();i++){
int v=G[u][i];
if(!pre[v]){
dfs(v);
lowlink[u]=min(lowlink[u],lowlink[v]);
}
else if(!sccno[v]){
lowlink[u]=min(lowlink[u],pre[v]);
}
}
if(lowlink[u]==pre[u]){
scc_cnt++;
for(;;){
int x=S.top();S.pop();
sccno[x]=scc_cnt;
if(x==u)
break;
}
}
}
void find_scc(int n)
{
dfs_clock=scc_cnt=0;//scc_cnt为SCC计数器,sccno[i]为i所在的SCC编号
memset(sccno,0,sizeof(sccno));
memset(pre,0,sizeof(pre));
for(int i=0;i<n;i++)
if(!pre[i])
dfs(i);
}
int main()
{
int m,n;
while(scanf("%d%d",&n,&m)!=EOF){
for(int i=0;i<n;i++)
G[i].clear();
for(int i=0;i<n;i++)
scanf("%d",&value[i]);
for(int i=0;i<m;i++){
int u,v;
scanf("%d%d",&u,&v);
u--;v--;
G[u].push_back(v);
}
find_scc(n);
memset(in0,0,sizeof(in0));
for(int u=0;u<n;u++)
for(unsigned i=0;i<G[u].size();i++){
int v=G[u][i];
if(sccno[u]!=sccno[v])
in0[sccno[v]]=1;
}
int ans=0;
int a=0;
for(int i=1;i<=scc_cnt;i++){
int minn=0x3f3f3f3f;
if(in0[i])
continue;
a++;
for(int j=0;j<n;j++)
if(sccno[j]==i)
minn=min(minn,value[j]);
ans+=minn;
}
printf("%d %d
",a,ans);
}
return 0;
}
D:HDU-3639 Hawk-and-Chicken:给你t组数据,每组数据有n个点,m条有向边,表示小朋友a会把手绢丢给b,问谁可能获得最多的手绢,我们设定初始的时候每个人都有一个手绢,但是自己投自己的手绢不作数。一共输出两个:1、输出最大手绢数,2、输出可能获得最大手绢数的人编号。思路:1、因为有向图中可能有环存在,我们先用Tarjan算法染色缩点,得到一个DAG图。2、在DAG图中反向建边,然后找入度为0的点Dfs求累加能到达的点的手绢和,不难理解,如果有一个点u想把手绢丢给v,那么在反图中,从v一定能够找到u,因为在正向图中找到所有的u比较麻烦,所以我们建反图从v来找u。注意有重边。
#include<cstdio>
#include<iostream>
#include<string.h>
#include<algorithm>
#include<vector>
#include<queue>
#include<stack>
using namespace std;
const int maxn=1e5+5;
vector <int> G[maxn],G2[maxn];
int value[maxn],pre[maxn],lowlink[maxn],sccno[maxn],dfs_clock,scc_cnt,sum[maxn],vis[maxn],dp[maxn];
stack<int> S;
int in0[maxn],out0[maxn],ans;
void dfs(int u)
{
pre[u]=lowlink[u]=++dfs_clock;
S.push(u);
for(unsigned i=0;i<G[u].size();i++){
int v=G[u][i];
if(!pre[v]){
dfs(v);
lowlink[u]=min(lowlink[u],lowlink[v]);
}
else if(!sccno[v]){
lowlink[u]=min(lowlink[u],pre[v]);
}
}
if(lowlink[u]==pre[u]){
int cnt=0;
scc_cnt++;
for(;;){
int x=S.top();S.pop();
sccno[x]=scc_cnt;
cnt++;
if(x==u)
break;
}
sum[scc_cnt]=cnt;
}
}
void find_scc(int n)
{
dfs_clock=scc_cnt=0;//scc_cnt为SCC计数器,sccno[i]为i所在的SCC编号
memset(sccno,0,sizeof(sccno));
memset(pre,0,sizeof(pre));
memset(sum,0,sizeof(sum));//sum[i]为第i个强连通分量里的点数
memset(dp,0,sizeof(dp));//dp[i]为第i个强连通分量里的每个点的手帕数
for(int i=0;i<n;i++)
if(!pre[i])
dfs(i);
}
void dfs1(int u)
{
vis[u]=1;
ans+=sum[u];
for(unsigned i=0;i<G2[u].size();i++){
if(!vis[G2[u][i]])
dfs1(G2[u][i]);
}
}
int main()
{
int t=1,T,m,n;
cin>>T;
while(T--){
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++){
G[i].clear();
G2[i].clear();
}
for(int i=0;i<m;i++){
int u,v;
scanf("%d%d",&u,&v);
G[u].push_back(v);
}
find_scc(n);
memset(in0,0,sizeof(in0));
for(int i=0;i<n;i++)
for(unsigned j=0;j<G[i].size();j++){
int v=G[i][j];
if(sccno[i]!=sccno[v]){
in0[sccno[i]]=1;
G2[sccno[v]].push_back(sccno[i]);
}
}
for(int i=1;i<=scc_cnt;i++)
dp[i]=sum[i];
int res=-1;
for(int i=1;i<=scc_cnt;i++){
if(!in0[i]){
ans=0;
memset(vis,0,sizeof(vis));
dfs1(i);
dp[i]=max(dp[i],ans);
res=max(ans,res);
}
}
int flag=0;
printf("Case %d: %d
",t++,res-1);
for(int i=0;i<n;i++){
if(dp[sccno[i]]==res){
if(flag==0){
printf("%d",i);
flag=1;
}
else
printf(" %d",i);
}
}
printf("
");
}
return 0;
}
E:POJ-1523 SPF:给出边,无向图求割点,并求删去割点能把图分成几块。F题为割边问题,需要用到割点割边算法。模板问题,以后遇到可以直接用模板
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define MAXN 1010
int g[MAXN][MAXN];//邻接矩阵
int vis[MAXN];//记录每个节点是否访问过
int subnets[MAXN];//表示去掉该节点后的连通分量个数
int dfn[MAXN],low[MAXN];
int nodes,son,tarnum;
void init()
{
low[1]=dfn[1]=1;
tarnum=0;
son=0;
memset(dfn,0,sizeof(dfn));
vis[1]=1;
memset(subnets,0,sizeof(subnets));
}
void tarjan(int u)
{
int v;
dfn[u]=low[u]=++tarnum;
for(v=1; v<=nodes; v++)
{
if(g[u][v])
{
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
if(u!=1) subnets[u]++;
if(u==1) son++;
}
}
else low[u]=min(low[u],dfn[v]);
}
}
}
int main()
{
int i,u,v,cas=1;
while(1)
{
nodes=0;
memset(g,0,sizeof(g));
scanf("%d",&u);
if(u==0) break;
scanf("%d",&v);
nodes=max(nodes,u);
nodes=max(nodes,v);
g[u][v]=g[v][u]=1;
while(1)
{
scanf("%d",&u);
if(u==0) break;
scanf("%d",&v);
nodes=max(nodes,u);
nodes=max(nodes,v);
g[u][v]=g[v][u]=1;
}
init();
tarjan(1);
if(son>1) subnets[1]=son-1; //根节点有所不同
if(cas>1) printf("
");
printf("Network #%d
",cas++);
int flag=0;
for(i=1; i<=nodes; i++)
{
if(subnets[i])
{
flag=1;
printf(" SPF node %d leaves %d subnets
",i,subnets[i]+1);
}
}
if(!flag) printf(" No SPF nodes
");
}
return 0;
}
F:HDU-4738 Caocao's Bridges:
曹操在长江上建立了一些点,点之间有一些边连着。如果这些点构成的无向图变成了连通图,那么曹操就无敌了。刘备为了防止曹操变得无敌,就打算去摧毁连接曹操的点的桥。但是诸葛亮把所有炸弹都带走了,只留下一枚给刘备。所以刘备只能炸一条桥。
题目给出n,m。表示有n个点,m条桥。
接下来的m行每行给出a,b,c,表示a点和b点之间有一条桥,而且曹操派了c个人去守卫这条桥。
现在问刘备最少派多少人去炸桥。
如果无法使曹操的点成为多个连通图,则输出-1.
思路:
就是用tarjan算法算出桥的数量,再比较哪一个的值最小。
Tips:
注意三点:
①. 有重边,所以tarjan算法要处理重边。有两种处理方法,一种是先把所有的边存下,发现两点有重边的时候就只给这两个点连一条权值为无穷大的边。或者是在tarjan算法里处理重边,即使之求u或u的子树能够追溯到的最早的栈中节点的次序号时可访问父节点的次序号。(有重边代表一个炸弹不能摧毁这条边,即这条边不可取)
②. 如果无向图图本身已经有两个连通图了,就无需派人去炸桥,这时候输出0。
③. 如果求出来的最小权值桥的守卫人数为0时,也需要派出一个人去炸桥。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<string>
#include<vector>
#include<set>
#include<queue>
#include<stack>
#include<map>
#include<string>
#include<algorithm>
#include<sstream>
#include<iterator>
typedef long long ll;
const int inf=0x3f3f3f3f;
const int inn=0x80808080;
using namespace std;
const int maxm=1e3+5;
int dfn[maxm];
int low[maxm];
int cost[maxm][maxm];
int have[maxm][maxm];
vector<int>g[maxm];
int n,m;
int ans;
int cnt;
void init(){
for(int i=0;i<maxm;i++){
low[i]=dfn[i]=0;
g[i].clear();
}
memset(cost,inf,sizeof(cost));
memset(have,0,sizeof(have));
cnt=0;
ans=inf;
}
void dfs(int u,int fa){
dfn[u]=low[u]=++cnt;
for(unsigned i=0;i<g[u].size();i++){
int v=g[u][i];
if(dfn[v]==0){
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]){
if(have[v][u]==1){
ans=min(ans,cost[u][v]);
}
}
}else if(v!=fa){
low[u]=min(low[u],dfn[v]);
}
}
}
int main(){
while(cin>>n>>m&&(n+m)){
init();
for(int i=0;i<m;i++){
int aa,bb,cc;
cin>>aa>>bb>>cc;
cost[aa][bb]=cost[bb][aa]=min(cost[aa][bb],cc);
have[aa][bb]++;
have[bb][aa]++;
if(have[aa][bb]==1){
g[aa].push_back(bb);
g[bb].push_back(aa);
}
}
dfs(1,-1);
int ok=1;
for(int i=1;i<=n;i++){
if(dfn[i]==0){
ok=0;
break;
}
}
if(!ok){
cout<<0<<endl;
continue;
}
for(int i=1;i<=n;i++){
if(dfn[i]==0){
dfs(i,-1);
}
}
if(ans==inf){
cout<<-1<<endl;
}else if(ans==0){
cout<<1<<endl;
}else{
cout<<ans<<endl;
}
}
return 0;
}