强连通分量&双连通(桥,割点)

割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。

桥:无向连通图中,如果删除某边后,图变成不连通,则称该边为桥。

强连通:  强连通其实就是指图中有两点u,v使得能够找到有向路径从u到v并且也能够找到有向路径从v到u,则称u,v是强连通的.

强连通分量:在同一强连通分量中的每个点之间都是互相可达的,即任意两点是强连通的

 

Tarjan算法中,有如下定义:

dfn[ ],表示这个点在dfs时是第几个被搜到的
low[ ],表示这个点以及其子孙节点连的所有点中dfn最小的值
stack[ ],表示当前所有可能能构成强连通分量的点。
vis[ ],表示一个点是否在stack[ ]数组中。

 

tarjan算法给求连通分量模板:

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

vector<int> vc[MAXN];
int dfn[MAXN];//被搜索到的次序
int low[MAXN];//值为:i或i的子树中最小的次序号
int sck[MAXN];//模拟栈
int vis[MAXN];//标记是否在栈中
int color[MAXN];//染色
int deep=0,top=0,sum=0;//分别为时间戳,栈顶,强连通

void tarjan(int x)
{
    low[x]=dfn[x]=++deep;
    vis[x]=1;
    sck[++top]=x;
    int gz=vc[x].size();
    for(int i=0;i<gz;i++)
    {
        int v=vc[x][i];
        if(!dfn[v])//没有被访问过
        {
            tarjan(v);
            low[x]=min(low[x],low[v]);
        }
        else if(vis[v]){//访问过且在栈中
            low[x]=min(low[x],dfn[v]);
        }
    }
    if(dfn[x]==low[x])//这是整个强连通分量的最小根
    {
        color[x]=++sum;//染色
        vis[x]=0;      //清除标记
        while(sck[top]!=x)//将同一连通分量里的全部出栈,并标记
        {
            color[sck[top]]=sum;
            vis[sck[top--]]=0;
        }
        top--;
    }
}


int main()
{
    int n,m,a,b;
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)
    {
        scanf("%d%d",&a,&b);
        vc[a].push_back(b);
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
            tarjan(i);
    }
    for(int i=1;i<=n;i++)
        printf("%d ",color[i]);
    printf("\n");
    return 0;
}

链接:https://codeforces.com/problemset/problem/962/F

 

tarjan求强连通分量:

约翰的N (2 <= N <= 10,000)只奶牛非常兴奋,因为这是舞会之夜!她们穿上礼服和新鞋子,别 上鲜花,她们要表演圆舞.

只有奶牛才能表演这种圆舞.圆舞需要一些绳索和一个圆形的水池.奶牛们围在池边站好, 顺时针顺序由1到N编号.每只奶牛都面对水池,这样她就能看到其他的每一只奶牛.

为了跳这种圆舞,她们找了 M(2<M< 50000)条绳索.若干只奶牛的蹄上握着绳索的一端, 绳索沿顺时针方绕过水池,另一端则捆在另一些奶牛身上.这样,一些奶牛就可以牵引另一些奶 牛.有的奶牛可能握有很多绳索,也有的奶牛可能一条绳索都没有.

对于一只奶牛,比如说贝茜,她的圆舞跳得是否成功,可以这样检验:沿着她牵引的绳索, 找到她牵引的奶牛,再沿着这只奶牛牵引的绳索,又找到一只被牵引的奶牛,如此下去,若最终 能回到贝茜,则她的圆舞跳得成功,因为这一个环上的奶牛可以逆时针牵引而跳起旋转的圆舞. 如果这样的检验无法完成,那她的圆舞是不成功的.

如果两只成功跳圆舞的奶牛有绳索相连,那她们可以同属一个组合.

给出每一条绳索的描述,请找出,成功跳了圆舞的奶牛有多少个组合?

ac:

#include<bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f

vector<int> g[10010];
int color[10010],dfn[20020],low[20020],stck[20020],vis[10010],cnt[10010];
int deep,top,n,m,sum,ans;

void tarjan(int u)
{
    dfn[u]=++deep;
    low[u]=deep;
    vis[u]=1;
    stck[++top]=u;
    int sz=g[u].size();
    for(int i=0;i<sz;i++)
    {
        int v=g[u][i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else
        {
            if(vis[v])
                low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u])
    {
        color[u]=++sum;
        vis[u]=0;
        while(stck[top]!=u)
        {
            color[stck[top]]=sum;
            vis[stck[top--]]=0;
        }
        top--;
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int from,to;
        scanf("%d%d",&from,&to);
        g[from].push_back(to);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);

    for(int i=1;i<=n;i++)
        cnt[color[i]]++;

    for(int i=1;i<=sum;i++)//求强连通分量中点数目大于1的个数
        if(cnt[i]>1)
            ans++;
    printf("%d\n",ans);
}

tarjan求桥:

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

struct edge
{
    int u,v;
};
vector<edge> bridge;

struct node
{
    int to,next;
}ee[MAXN<<1];
int head[MAXN],dfn[MAXN],low[MAXN];
int n,m,cnt,tot,ans;

void add(int u,int v)
{
    ee[++cnt].to=v;
    ee[cnt].next=head[u];
    head[u]=cnt;
}

void tarjan(int u,int id)
{
    dfn[u]=low[u]=++tot;
    for(int i=head[u];i;i=ee[i].next)
    {
        int v=ee[i].to;
        if(!dfn[v])
        {
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])
                bridge.push_back({u,v});
        }
        else if(i!=(id^1))
            low[u]=min(low[u],dfn[v]);
    }
}

int main()
{
    int u,v;
    cnt=1;
    tot=0;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&u,&v);
        add(u,v);
        add(v,u);
    }
    tarjan(1,0);
    int len=bridge.size();
    printf("%d\n",len);
    for(int i=0;i<len;i++)
        printf("%d %d\n",bridge[i].u,bridge[i].v);
    return 0;
}

tarjan算法求割点:

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

struct node
{
    int to,val,next;
}mp[MAXN<<2];
int head[MAXN<<2],tot;

void init()
{
    tot=0;
    memset(head,-1,sizeof(head));
    memset(mp,-1,sizeof(mp));
}

void add(int u,int v)
{
    mp[tot]={v,0,head[u]};
    head[u]=tot++;
    mp[tot]={u,0,head[v]};
    head[v]=tot++;
}

int dfn[MAXN],low[MAXN],dfs_clock;
bool cut[MAXN];
void tarjan(int u,int root)
{
	dfn[u]=low[u]=++dfs_clock;
	int son=0;
	for(int i=head[u];i!=-1;i=mp[i].next)
    {
        int v=mp[i].to;
		if(!dfn[v])
		{
			tarjan(v,root);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]&&u!=root)
                cut[u]=true;
			if(u==root)++son;
		}
		low[u]=min(low[u],dfn[v]);
	}
	if(son>=2&&u==root)
        cut[u]=true;
}

int n,m,u,v;
int main()
{
    init();
    scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i)
    {
        scanf("%d%d",&u,&v);
		add(u,v);
	}
	int cnt=0;
	for(int i=1;i<=n;++i)
    {
		if(!dfn[i])
            tarjan(i,i);
		if(cut[i])
            ++cnt;
	}
	printf("%d\n",cnt);
	for(int i=1;i<=n;++i)
        if(cut[i])
            printf("%d ",i);
	return 0;
}

缩点:

题目背景

缩点+DP

题目描述

给定一个n个点m条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。

允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

输入输出格式

输入格式:

 

第一行,n,m

第二行,n个整数,依次代表点权

第三至m+2行,每行两个整数u,v,表示u->v有一条有向边

 

输出格式:

 

共一行,最大的点权之和。

 

输入输出样例

输入样例#1: 

2 2
1 1
1 2
2 1

输出样例#1: 

2

说明

n<=10^4,m<=10^5,0<=点权<=1000

算法:Tarjan缩点+DAGdp

ac:

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

struct node
{
	int to,val,next;
} mp[MAXN];
int n,m,p[MAXN],dp[MAXN],ans;
int tot,head[MAXN];
int top,stck[MAXN];
int indexx,num,dfn[MAXN],low[MAXN],color[MAXN],sum[MAXN];
int heads=1,tail,indegree[MAXN],que[MAXN];
bool instack[MAXN];

void init()
{
    tot=0;
    memset(mp,0,sizeof(mp));
    memset(head,-1,sizeof(head));
	tot=0;
}

void add(int u, int v)
{
    mp[tot]={v,0,head[u]};
    head[u]=tot++;
}

//缩点可以看模板中的解析
void tarjan(int u)
{
	int i;
	dfn[u] = low[u] = ++indexx;
	stck[++top] = u;
	instack[u] = true;
	for (i=head[u];i!=-1;i=mp[i].next)
    {
		int v=mp[i].to;
		if(!dfn[v])
        {
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(instack[v])
            low[u]=min(low[u],dfn[v]);
	}
	if (dfn[u]==low[u])
    {
		num++;
		do{
			color[stck[top]] = num;
			sum[num] += p[stck[top]];
			instack[stck[top]] = false;
        } while (stck[top--] != u);
	}
	return ;
}

int topo()//用topo排序解决dp无后继性,也可以用记忆化搜索
{
	int i;
	for (i=1;i<=num;i++)
	{
		if(!indegree[i])
        {
			que[++tail]=i;
			dp[i]=sum[i];
		}
	}
	while (tail>=heads)
    {
		int u=que[heads];
		heads++;
		for (i=head[u];i+1;i=mp[i].next)
        {
			int v=mp[i].to;
			dp[v]=max(dp[v],dp[u]+sum[v]);
			indegree[v]--;
			if (!indegree[v])
                que[++tail]=v;
		}
	}
	for (i=1;i<=num;i++)
		ans=max(ans,dp[i]);
	return ans;
}

int main()
{
	init();
	int i,a[MAXN],b[MAXN];
	scanf("%d%d",&n,&m);
	for(i=1;i<=n;i++)
        scanf("%d",&p[i]);
	for(i=1;i<=m;i++)
    {
        scanf("%d%d",&a[i],&b[i]);
		add(a[i],b[i]);
	}
	for (i=1;i<=n;i++)
		if (!dfn[i])
            tarjan(i);
    init();//已经缩完点,准备重建边,先重置
	for(i=1;i<=m;i++)
	{
		if(color[a[i]]!=color[b[i]])//不是同一强连通分量,建一条边
		{
			add(color[a[i]],color[b[i]]);
			indegree[color[b[i]]]++;
		}
	}
	printf("%d\n",topo());
	return 0;
}

链接:https://ac.nowcoder.com/acm/contest/81/C
来源:牛客网
 

时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld

题目描述

给出一个 0 ≤ N ≤ 105 点数、0 ≤ M ≤ 105 边数的有向图,
输出一个尽可能小的点集,使得从这些点出发能够到达任意一点,如果有多个这样的集合,输出这些集合升序排序后字典序最小的。

输入描述:

第一行为两个整数 1 ≤ n, m ≤ 105,
接下来 M 行,每行两个整数 1 ≤ u, v ≤ 105 表示从点 u 至点 v 有一条有向边。
数据保证没有重边、自环。

输出描述:

第一行输出一个整数 z,表示作为答案的点集的大小;
第二行输出 z 个整数,升序排序,表示作为答案的点集。

示例1

输入

7 10
4 5
5 1
2 5
6 5
7 2
4 2
1 2
5 3
3 5
3 6

输出

2
4 7

解析:

简单的缩点

ac:

#include<bits/stdc++.h>
#define inf 9999999
#define MAXN 100005
using namespace std;

struct node
{
    int to,val,next;
} mp[MAXN];
int tot,head[MAXN];

int n,m,p[MAXN],dp[MAXN],ans;
int top,stck[MAXN];
int indexx,num,dfn[MAXN],low[MAXN],color[MAXN],sum[MAXN],in[MAXN];
bool instack[MAXN];

void init()
{
    num=tot=0;
    memset(mp,0,sizeof(mp));
    memset(head,-1,sizeof(head));
    memset(in,0,sizeof(in));
}

void add(int u, int v)
{
    mp[tot]={v,0,head[u]};
    head[u]=tot++;
}

void tarjan(int u)
{
    dfn[u]=low[u]=++indexx;
    stck[++top]=u;
    instack[u]=true;
    for(int i=head[u];i!=-1;i=mp[i].next)
    {
        int v=mp[i].to;
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(instack[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        num++;
        do{
            color[stck[top]] = num;
            sum[num]=min(sum[num],stck[top]);
            instack[stck[top]] = false;
        } while (stck[top--] != u);
    }
}
set<int> st;

int main()
{
    init();
    int a[MAXN],b[MAXN];
    scanf("%d%d",&n,&m);
    for(int i=1;i<=MAXN;i++)
        sum[i]=inf;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&a[i],&b[i]);
        add(a[i],b[i]);
    }
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);

    memset(in,0,sizeof(in));

    for(int i=1;i<=m;i++)           //把图缩成一个有向无环图
        if(color[a[i]]!=color[b[i]])
            in[color[b[i]]]++;      

    for(int i=1;i<=num;i++)
        if(in[i]==0)          //取度为0的点
            st.insert(sum[i]);

    set<int>::iterator it;
    printf("%d\n",st.size());
    for(it=st.begin();it!=st.end();it++)
    {
        if(it!=st.begin())
            printf(" %d",*it);
        else
            printf("%d",*it);
    }
    printf("\n");
    return 0;
}

割点+点双:

链接:https://ac.nowcoder.com/acm/problem/20099
来源:牛客网
 

时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld

题目描述

煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。

于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。

请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

输入描述:

输入文件有若干组数据,每组数据的第一行是一个正整数 N(N ≤ 500),表示工地的隧道数,
接下来的N行每行是用空格隔开的两个整数S和T,表示挖S与挖煤点T由隧道直接连接。
输入数据以 0 结尾。

输出描述:

输入文件中有多少组数据,输出文件 output.txt 中就有多少行。
每行对应一组输入数据的结果。
其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与:之间无空格,: 之后有空格),其后是用空格隔开的两个正整数,
第一个正整数表示对于第 i 组输入数据至少需 要设置几个救援出口,
第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总数。
输入数据保证答案小于 2^64。输出格式参照以下输入输出样例。

示例1

输入

9                       
1  3                     
4  1
3  5
1  2
2  6
1  5
6  3
1  6
3  2
6 
1  2
1  3
2  4
2  5
3  6
3  7
0

输出

Case 1: 2 4
Case 2: 4 1

解析:

先得求割点,然后判断各连通块的情况

如果连通块连没有连接割点,呢么就必须要有两个出口

如果只连接一个割点,呢么还要开个出口

如果连接两个割点,呢么一个割点塌了,还可以走另外一条路,不需要建出口

ac:

#include<bits/stdc++.h>
#define ll long long
#define MAXN 1005
using namespace std;

struct node
{
    int to,val,next;
}mp[MAXN<<2];
int head[MAXN<<2],tot;
int u[MAXN],v[MAXN];

int vis[MAXN]={0};
int dfn[MAXN],low[MAXN],dfs_clock;
bool cut[MAXN];

void init()
{
    dfs_clock=tot=0;
    memset(head,-1,sizeof(head));
    memset(vis,0,sizeof(vis));
    memset(mp,0,sizeof(mp));
    memset(cut,0,sizeof(cut));
    memset(dfn,0,sizeof(dfn));
    memset(low,0,sizeof(low));
    memset(u,0,sizeof(u));
    memset(v,0,sizeof(v));
}

void add(int u,int v)
{
    mp[tot]={v,0,head[u]};
    head[u]=tot++;
}

void tarjan(int u,int root)
{
	dfn[u]=low[u]=++dfs_clock;
	int son=0;
	for(int i=head[u];i!=-1;i=mp[i].next)
    {
        int v=mp[i].to;
		if(!dfn[v])
		{
			tarjan(v,root);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]&&u!=root)
                cut[u]=true;
			if(u==root)++son;
		}
		low[u]=min(low[u],dfn[v]);
	}
	if(son>=2&&u==root)
        cut[u]=true;
}
int f=0;
set<int> st;

void dfs(int u)
{
    f++;
    vis[u]=1;
    for(int i=head[u];i!=-1;i=mp[i].next)
    {
        int v=mp[i].to;
        if(cut[v]==1)
            st.insert(v);
        if(cut[v]==1||vis[v]==1)
            continue;
        dfs(v);
    }
}

int main()
{
    int n,cas=1;
    while(scanf("%d",&n)!=EOF)
    {
        init();
        if(n==0)
            break;
        int maxs=0;
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&u[i],&v[i]);
            add(u[i],v[i]);
            add(v[i],u[i]);
            maxs=max(u[i],max(v[i],maxs));
        }
        int cc=0;
        for(int i=1;i<=maxs;i++)//点
        {
            if(!dfn[i])
                tarjan(i,i);
        }
        ll ans=1,sum=0;
        for(int i=1;i<=maxs;i++)
        {
            if(cut[i]==1||vis[i]==1)
                continue;
            f=0;
            st.clear();
            dfs(i);
            if(st.size()==0)//没有割点
            {
                ans=ans*(f*(f-1)/2);
                sum+=2;
            }
            else if(st.size()==1)//仅一个割点
            {
                ans=ans*f;
                sum+=1;
            }
        }
        printf("Case %d: %lld %lld\n",cas++,sum,ans);
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值