图论 强(双)连通分量tarjan算法

强(双)连通分量tarjan算法

这里挂两个题,第一个题求强联通分量,第二个题求割点
先说一下tarjan的读法:ta ran(ta ren)( j 不发音)

hdu5934(tarjan算法+缩点)bomb

There are N bombs needing exploding.

Each bomb has three attributes: exploding radius ri, position (xi,yi) and lighting-cost ci which means you need to pay ci cost making it explode.

If a un-lighting bomb is in or on the border the exploding area of another exploding one, the un-lighting bomb also will explode.

Now you know the attributes of all bombs, please use the minimum cost to explode all bombs.

Input
First line contains an integer T, which indicates the number of test cases.

Every test case begins with an integers N, which indicates the numbers of bombs.

In the following N lines, the ith line contains four intergers xi, yi, ri and ci, indicating the coordinate of ith bomb is (xi,yi), exploding radius is ri and lighting-cost is ci.

Limits

  • 1≤T≤20
  • 1≤N≤1000
  • −108≤xi,yi,ri≤108
  • 1≤ci≤104
    Output
    For every test case, you should output ‘Case #x: y’, where x indicates the case number and counts from 1 and y is the minimum cost.
    Sample Input
    1
    5
    0 0 1 5
    1 1 1 6
    0 1 1 7
    3 0 2 10
    5 0 1 4
    Sample Output
    Case #1: 15

题目出处:
HDU - 5934

#include
using namespace std; 
const int maxx=1010; 
typedef long long ll;
ll m,n,top,q,number,minn;//number  dfs序计数器 
int in[maxx],dfn[maxx],low[maxx],cost[maxx],stack[maxx],root[maxx];//in入度, cost点燃一个强连通分量的代价 
int head[maxx];//maxx: 最大点数 
int d[maxx];//是否在栈中 
struct  edge{    
    int to;  
    int next; 
}e[maxx*maxx]; //边为点的平方大 
struct vertex{
	ll x;
	ll y;
	ll r;
	ll w;
}ver[maxx];
void add(int u,int v){ //连图 
    e[++q].next=head[u];  
    e[q].to=v;  
    head[u]=q;
}
int tarjan (int u){
	int i;
	dfn[u]=low[u]=++number; //设置dfn与low数组的初值
	root[u]=u;
	stack[++top]=u; //将当前点压栈 
	d[u]=1;//表示 当前点已入栈; 
	for (i=head[u];i;i=e[i].next){
		int v=e[i].to ;
		if (!dfn[v]){//未遍历 
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else{
			if (d[v]==1)
	        	low[u]=min(low[u],low[v]); 
		}
		
	}
	if (dfn[u]==low[u]){
		minn=ver[u].w;
		d[u]=0;
		while (stack[top]!=u){
			d[stack[top]]=0;
			int v=stack[top--];
	    		root[v]=u;
			if(ver[v].w<minn)
	    		minn=ver[v].w;
		}
		cost[u]=minn;//找出此强连通分量的最大值,并且将值赋值给此强连通分量的根所对应的cost;
		top--;
	}
	return 0;
}
int main()  
{  
 
    int t,ans=1;
    scanf("%d",&t);
    while (t--){
    int sum=0;
    q=number=top=0;
    memset(in,0,sizeof(in));  
    memset(cost,0,sizeof(cost));
	memset(dfn,0,sizeof(dfn));  
    memset(head,0,sizeof(head)); 
    memset(low,0,sizeof(low));
    scanf("%d",&n);
    for (int i=1;i<=n;i++) //边 
    scanf("%lld%lld%lld%lld",&ver[i].x,&ver[i].y,&ver[i].r,&ver[i].w);
    for (int i=1;i<=n;i++){
    	for (int j=1;j<=n;j++){
    		if (i!=j){
    		    ll disr=(ver[i].x-ver[j].x)*(ver[i].x-ver[j].x)+(ver[i].y-ver[j].y)*(ver[i].y-ver[j].y);
    		    ll range=ver[i].r*ver[i].r;
    			if (disr<=range)
    			add(i,j);//注意这个题是单向边
			}
		}
	}
    for(int i=1;i<=n;i++)
        if (!dfn[i])//整个图不一定是全联通的,可能有多个根节点 
		tarjan(i);
    for(int i=1;i<=n;i++){
         	for (int j=head[i];j;j=e[j].next){
         	    int v=e[j].to;
	    		if(root[v]!=root[i])//如果两个相邻点的根不是一个,即在两个强连通分量中,直接将被指向的那个点的主人的入度++;
	    		    in[root[v]]++; 
    		}
    	}
	for (int i=1;i<=n;i++){
	 	if (root[i]==i&&!in[i])//如果入度为0并且是一个强连通分量的根,统计最小值;
    	 	sum+=cost[i];
    	}
	printf("Case #%d: %d\n",ans++,sum);
    }
	 return 0;
}

注意一下这道题不能直接统计所有点入度然后加和入度0的点,因为可能有单独的环

poj1144-Network(求割点)

A Telephone Line Company (TLC) is establishing a new telephone cable network. They are connecting several places numbered by integers from 1 to N . No two places have the same number. The lines are bidirectional and always connect together two places and in each place the lines end in a telephone exchange. There is one telephone exchange in each place. From each place it is
possible to reach through lines every other place, however it need not be a direct connection, it can go through several exchanges. From time to time the power supply fails at a place and then the exchange does not operate. The officials from TLC realized that in such a case it can happen that besides the fact that the place with the failure is unreachable, this can also cause that some other places cannot connect to each other. In such a case we will say the place (where the failure
occured) is critical. Now the officials are trying to write a program for finding the number of all such critical places. Help them.

Input
The input file consists of several blocks of lines. Each block describes one network. In the first line of each block there is the number of places N < 100. Each of the next at most N lines contains the number of a place followed by the numbers of some places to which there is a direct line from this place. These at most N lines completely describe the network, i.e., each direct connection of two places in the network is contained at least in one row. All numbers in one line are separated
by one space. Each block ends with a line containing just 0. The last block has only one line with N = 0;
Output
The output contains for each block except the last in the input file one line containing the number of critical places.

Sample Input

5
5 1 2 3 4
0
6
2 1 3
5 4 6 2
0
0
Sample Output
1
2
Hint
You need to determine the end of one line.In order to make it’s easy to determine,there are no extra blank before the end of each line.

题目出处:

poj1144

这里贴一段大佬的分析
割点怎么求?
与之前强连通分量中的tarjan差不多。但要加一个特判,
首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。
对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。(注意这里的概念,称之为子树,表示了子树互不相连,而且不连向祖先)
对于非根节点,判断是不是割点就有些麻烦了。我们维护两个数组dfn[]和low[](就是上面用过的),对于边(u, v),如果low[v]>=dfn[u],此时u就是割点。这是我认为最难以理解的一部分,可以这样想:low[v]>=dfn[u]也就意味着,v不能回到u的前面,这是一种什么情况呢?
显然如果节点U的所有孩子节点可以不通过父节点U而访问到U的祖先节点,那么说明此时去掉节点U不影响图的连通性,U就不是割点。
相反,如果节点U至少存在一个孩子顶点,必须通过父节点U才能访问到U的祖先节点,也就是如果存在一个孩子low[v]>=dfn[u],那么去掉节点U后,顶点U的祖先节点和孩子节点就不连通了,说明U是一个割点。
ps:无向图一定要注意回边的存在
争议点:
关于tarjan算法,一直有一个很大的争议,就是low[u]=min(low[u],dfn[v]);(你可以发现这和上面求强连通分量是不一样的)
这句话,如果改成low[u]=min(low[u],low[v])就会wa掉,但是在求强连通分量时却没有问题
根据许多大佬的观点,我想提出自己的一点看法,在求强连通分量时,如果v已经在栈中,那么说明u,v一定在同一个强连通分量中,所以到最后low[u]=low[v]是必然的,提前更新也不会有问题,但是在求割点时,low的定义有了小小的变化,不再是最早能追溯到的祖先,(因为是个无向图)没有意义,应该是最早能绕到的割点,为什么用绕到,是因为是无向边,所以有另一条路可以走,如果把dfn[v]改掉就会上翻过头,可能翻进另一个环中,所以wa掉,仅是本人的一些个人看法,不知道讲的对不对,请各位指教。
然后其实这个dfn[v]这个东西其实也不一定是翻到另一个环里面,有可能 是已经遍历过导致louv很小而不符合,例子可以看大佬原文

大佬原文
另一个大佬的关于tarjan的讲解

好了下面是题解代码

#include
#include
struct node
{
    int to;
    int next;
} gra[10005];
int dfn[105],low[105];//dfn:dfs序
//low有向图:当前结点能指向的dfs序最小的节点或者 无向图:当前结点能绕到的dfs序最小的节点 
int first[105],vis[105];//链式前向星的节点的始边,是否被访问过的标记 
int cut[105];//割点标记 
int m,e,root,cnt;// 总共m个数,e边的总个数,cnt dfs序 
int min(int a,int b)
{
    return a<b?a:b;
}
void add(int u,int v)//连边 
{
	
    gra[++e].to=v;
    gra[e].next=first[u];
    first[u]=e;
}
void tarjan(int u)
{
    int son=0;
    vis[u]=1;
    dfn[u]=low[u]=++cnt;//求割点的时候不用栈操作,求强连通分量的时候才要栈操作 
    for(int i=first[u];i;i=gra[i].next)
    {
        int v=gra[i].to;
		if(!dfn[v])//该点没有被访问过 
        {
            tarjan(v);//先遍历 
            son++;//统计儿子其实只需要统计根节电一个节点的儿子其他节点不用统计 
            low[u]=min(low[u],low[v]);
            if((u!=root&&low[v]>=dfn[u]))//如果该点不是根节点,并且这个的儿子不能连向u的祖先节点那么就是割点 
                cut[u]=1;
        }
		if(dfn[v])
            low[u]=min(low[u],dfn[v]);//注意这里是dfn v不是low v否则会回溯过头 
        
    }
    if(u==root&&son>1)//如果是搜索树的根节点,那么如果儿子数大于等于2,那么就是割点,这是个特判 
        cut[u]=1;
}
int main()
{
    while(scanf("%d",&m)&&m!=0)
    {
        memset(dfn,0,sizeof(dfn));//注意这里这些数组和e,cnt都是全局变量
		//初始化默认是0,但是因为是多组样例每组样例运行前记得再初始化一下 
        memset(low,0,sizeof(low));
        memset(first,0,sizeof(first));
        memset(vis,0,sizeof(vis));
        memset(cut,0,sizeof(cut));
        int n;
        e=0,cnt=0;
        while(scanf("%d",&n)&&n!=0)
        {
            while(getchar()!='\n')
            {
                int a;
                scanf("%d",&a);
                add(n,a);
                add(a,n);
            }
        }
        root=1;
        int sum=0;
        tarjan(1);
        for(int i=1; i<=m; i++)
            if(cut[i])
                sum++;
        printf("%d\n",sum);
    }
    return 0;
}

然后就是文中的两个链接讲tarjan算法很透彻感谢大佬,模板在连接里也有

还有一个关于牛的题也放进来吧

poj2186 Popular Cows(tarjan+缩点)

告诉你有n头牛,m个崇拜关系,并且崇拜具有传递性,如果a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被所有牛崇拜。
Sample Input
3 3
1 2
2 1
2 3
Sample Output
1

其实缩点就是给一个联通的分量染成一种颜色,然后统计的时候把同一种颜色看成一个节点,思路在两个大佬的博客里都有
下面是大佬的原话我粘贴一下
显然一个强联通分量内的所有点都是满足条件的,我们可以对整张图进行缩点,然后就简单了。

剩下的所有点都不是强连通的,现在整张图就是一个DAG(有向无环图)

那么就变成一道水题了,因为这是一个有向无环图,不存在所有点的出度都不为零的情况。

所以必然有1个及以上的点出度为零,如果有两个点出度为零,那么这两个点肯定是不相连的,即这两圈牛不是互相崇拜的,于是此时答案为零,如果有1个点出度为0,那么这个点就是被全体牛崇拜的,

这个点可能是一个强联通分量缩成的超级点,所以应该输出整个强联通分量
贴大佬的代码

#include
#include
#include
#include
#include
#include
using namespace std;

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

void tarjan(int u)
{
    dfn[u]=low[u]=++deep;
    vis[u]=1;
    stack[++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],low[v]);
            }
        }
    }
    if(dfn[u]==low[u])
    {
        color[u]=++sum;
        vis[u]=0;
        while(stack[top]!=u)
        {
            color[stack[top]]=sum;
            vis[stack[top--]]=0;
        }
        top--;
    }
}


int main()
{
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        memset(vis,0,sizeof(du));
        memset(vis,0,sizeof(low));
        memset(dfn,0,sizeof(dfn));
        memset(vis,0,sizeof(vis));
        memset(vis,0,sizeof(cnt));
        memset(vis,0,sizeof(color));
        memset(vis,0,sizeof(stack));
        for(int i=1; i<=n; i++)
        {
            g[i].clear();
        }
        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++)
        {
            int sz=g[i].size();
            for(int j=0; j<sz; j++)
            {
                int v=g[i][j];
                if(color[v]!=color[i])
                {
                    du[color[i]]++;
                }
            }
            cnt[color[i]]++;
        }
        for(int i=1; i<=sum; i++)
        {
            if(du[i]==0)
            {
                tmp++;
                ans=cnt[i];
            }
        }
        if(tmp==0)
        {
            printf("0\n");

        }
        else
        {
            if(tmp>1)
            {
                printf("0\n");
            }
            else
            {
                printf("%d\n",ans);
            }
        }
    }
}


2021.7.21

你可能感兴趣的:(ACM,图论,图论,算法,c++)