zoukankan      html  css  js  c++  java
  • tarjan算法(学习笔记)

    首先声明一下,我写这篇博客,不是想讲tarjan算法的原理,我只是总结我自己对tarjan算法与强连通分量,缩点,割点的学习;如果想认真学习tarjan算法,推荐这篇博客;

    定义

    如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。---引自<<某度某科>>

    注意有向图这个前提条件;

    以tarjan算法求强连通分量为模板:(只有这份代码会是完整有注释的,下面都只是在这份代码上稍加修改)

    #include<bits/stdc++.h>
    using namespace std;
    #define gt getchar()
    inline int read(){
        int s=0,w=1;
        char ch=gt;
        while(ch<'0'||ch>'9'){
        	if(ch=='-') w=-1;ch=gt;
        }
        while(ch>='0'&&ch<='9'){
        	s=s*10+ch-'0';ch=gt;
        }
        return s*w;
    }
    int n,m,tot;
    int head[20005],nxt[200005],to[200005];
    int num,low[20005],dfn[20005];
    void add(int a,int b){
        nxt[++tot]=head[a];
        head[a]=tot;
        to[tot]=b;
    }//数组模拟邻接表存图
    void tarjan(int u){
        dfn[u]=low[u]=++timeclock;
    //timeclock时间戳
    //dfn[u]记录访问时间(即u是第几个访问的结点)
    //low[u]为u或u的子树能够回溯到的最早的栈中结点的DFN值
        st[++top]=u;
    //st[]模拟栈,top模拟栈指针
    //将第一个结点u压入栈
        for(int i=head[u];i;i=nxt[i]){
    //枚举与点u相连的每一条边
        	int v=to[i];
    //v是u连接的点
        	if(!dfn[v]){
    //如果v点没被访问过
            	tarjan(v);
    //从v点开始询问(有没有看到一点DFS遍历的影子)
            	low[u]=min(low[u],low[v]);
        	}
        	else if(!color[v])
    //如果v点访问过且color[v]==0即还在栈内
    //这个else不能省略,好好理解一下
            	low[u]=min(low[u],dfn[v]);
        }
    //如果满足强连通分量的条件
    //则说明u和栈中u之后的点属于同一个强连通分量,可退栈
        if(low[u]==dfn[u]){
        	color[u]=++cnt;
    //color[u]表示u属于第几个强连通分量
    //cnt统计强连通分量的个数
        	while(st[top]!=u){
            	color[st[top]]=cnt;
            	top--;
    //u之后的点与u属于同一个强连通分量
    //同时把u之后的点退栈
        	}
        	top--;
    //u本身也要退栈
        }
    }
    int main(){
        n=read();m=read();
        for(int i=1;i<=m;i++){
        	int a,b;
            a=read();b=read();
        	add(a,b);
        }
        for(int i=1;i<=n;i++)
        	if(!dfn[i]) tarjan(i);
    //至于最后输出什么,就看题目求什么了;
    //总之,我们已经得到了强连通分量的个数cnt
    //每个点分别属于哪个强连通分量(color数组记录)
        return 0;
    }
    

    P3387 【模板】缩点

    给定一个n个点m条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

    简单分析一下,我们先tarjan把图中所有强连通分量缩成一个点,这个点(强连通分量)记录了这个点包含的所有点的权值,然后我们在建图连边时,如果这条边所连接的两个点属于同一个强连通分量,我们就忽略掉,否则就加边;

    建好图后跑一遍记忆化搜索,找最大值就可以了;

    void tarjan(int u){
        dfn[u]=low[u]=++timeclock;
        st[++top]=u;
        for(int i=head[u];i;i=nxt[i]){
        	int v=to[i];
        	if(!dfn[v]){
            	tarjan(v);
            	low[u]=min(low[u],low[v]);
        	}
        	else if(!color[v])
            	low[u]=min(low[u],dfn[v]);
        	}
        if(low[u]==dfn[u]){
        	color[u]=++num;
        	sum[num]+=w[u];
    //额外开一个sum数组
    //sum[num]记录第num个强连通分量的所有点的权值之和
       		while(st[top]!=u){
          	 	color[st[top]]=num;
            	sum[num]+=w[st[top]];
    //记得把每个点的权值都加进去
            	top--;
        	}
        	top--;
        }
    }
    void search(int x){
        if(f[x])return;//记忆化搜索
        f[x]=sum[x];
        int maxsum=0;
        for(int i=head[x];i;i=nxt[i]){
        	int v=to[i];
            if(!f[v])search(v);
            maxsum=max(maxsum,f[v]);
        }
        f[x]+=maxsum;
    }
    
    for(int i=1;i<=m;i++){
        if(color[a[i]]!=color[b[i]])
            add(color[a[i]],color[b[i]]);
    }//跑完tarjan后,先判断再存图
    
    for(int i=1;i<=num;i++){
        if(!f[i]){
            search(i);
            ans=max(ans,f[i]);
    }
    

    P2341 [HAOI2006]受欢迎的牛

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

    分析一下这道题,不妨把A喜欢B,理解为A到B的一条有向边,建图后,跑一遍tarjan,找出强连通分量并缩点;

    对于缩点后的图(一定是一个有向无环图DAG);出度为0的点(一个强连通分量)一定是被所有的奶牛喜欢的(因为如果一个点它的出度不为0,则它连接的那个点所代表的奶牛一定不喜欢这个点,所以出度不为0的点不受所有奶牛的喜欢),但是如果有两个或两个以上出度为0的点,也不符合题意,因为这些出度不为0的点肯定互相不喜欢;

    P3388 【模板】割点

    给出一个n个点,m条边的无向图,求图的割点。

    分析:跟tarjan算法模板不同的是要额外考虑根节点首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。
    对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。

    void tarjan(int x){
        dfn[x]=low[x]=++num;
        int child=0;//记录孩子数量
        for(int i=head[x];i;i=nxt[i]){
        	int y=to[i];
        	if(!dfn[y]){
            	tarjan(y);
            	low[x]=min(low[x],low[y]);
            	if(low[y]>=dfn[x]){
            		child++;
            	if(x!=root||child>=2)
                	cut[x]=true;//标记x是割点
            	}
        	}
        	else low[x]=min(low[x],dfn[y]);
        }
    }
    
    for(int i=1;i<=n;i++)
        if(!dfn[i]) root=i,tarjan(i);
    for(int i=1;i<=n;i++)
        if(cut[i])ans++;
    printf("%d
    ",ans);
    for(int i=1;i<=n;i++){
        if(cut[i])printf("%d ",i);
    }
    

    想推荐一道很有趣的题

    我国的离婚率连续7年上升,今年的头两季,平均每天有近5000对夫妇离婚,大城市的离婚率上升最快,有研究婚姻问题的专家认为,是与简化离婚手续有关。

    25岁的姗姗和男友谈恋爱半年就结婚,结婚不到两个月就离婚,是典型的“闪婚闪离”例子,而离婚的导火线是两个人争玩电脑游戏,丈夫一气之下,把电脑炸烂。

    有社会工作者就表示,80后求助个案越来越多,有些是与父母过多干预有关。而根据民政部的统计,中国离婚五大城市首位是北京,其次是上海、深圳,广州和厦门,那么到底是什么原因导致我国成为离婚大国呢?有专家分析说,中国经济急速发展,加上女性越来越来越独立,另外,近年来简化离婚手续是其中一大原因。

    ——以上内容摘自第一视频门户

    现代生活给人们施加的压力越来越大,离婚率的不断升高已成为现代社会的一大问题。而其中有许许多多的个案是由婚姻中的“不安定因素”引起的。妻子与丈夫吵架后,心如绞痛,于是寻求前男友的安慰,进而夫妻矛盾激化,最终以离婚收场,类似上述的案例数不胜数。

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

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

    输入输出格式

    输入格式:

    第一行为一个正整数n,表示夫妻的对数;

    以下n行,每行包含两个字符串,表示这n对夫妻的姓名(先女后男),由一个空格隔开;

    第n+2行包含一个正整数m,表示曾经相互喜欢过的情侣对数;

    以下m行,每行包含两个字符串,表示这m对相互喜欢过的情侣姓名(先女后男),由一个空格隔开。

    输出格式:

    输出文件共包含n行,第i行为“Safe”(如果婚姻i是安全的)或“Unsafe”(如果婚姻i是不安全的)。

    之前的题都不会把输入输出转过来,但这道题的输入输出提示作用很大,留个小悬念;

    分析:这道题其实就是把每对关系(不管是夫妻还是曾经喜欢过的情侣)都看作一条边存图(存图很重要,下面会讲),然后跑一遍tarjan就行了,因为tarjan模板在本题没有任何修改,所以就不再放一遍了;

    存图没存好这题就挂掉了,因为输入说名字顺序先女后男,所以我们存夫妻关系时建有向边女指向男,存情侣关系时建有向边男指向女;

    最后判断的条件就是这对关系的夫妻两人是不是在同一个强连通分量里面,如果是则关系不安全;

    做这道题,顺便学了一下基础map的用法,挺好用的;

    map<string,int> h;
    int main(){
        n=read();
        char str1[10],str2[10];
        for(int i=1;i<=n;i++){
        	scanf("%s",str1);
        	h[str1]=++num;
        	scanf("%s",str2);
        	h[str2]=++num;
        	add(h[str1],h[str2]);
        }
        m=read();
        for(int i=1;i<=m;i++){
        	scanf("%s",str1);
        	scanf("%s",str2);
        	add(h[str2],h[str1]);
        }
        for(int i=1;i<=2*n;i++){
        	if(!dfn[i])tarjan(i);
        }
        for(int i=1;i<=n;i++){
        	if(color[(i-1)*2+1]!=0&&color[(i-1)*2+1]==color[(i-1)*2+2])
    //判断的时候记住color不能为0;
            	puts("Unsafe");
        	else puts("Safe");
        }
        return 0;
    }
    
  • 相关阅读:
    [Go] go build 和 go install 的区别
    [FAQ] Vmmem 内存占用高的问题 Win10 WLS2
    [FAQ] mogodb Robo3T 客户端全屏后 怎么退出全屏
    [FAQ] PHPStorm None project files detection
    [DApp] ethers.js VS Moralis
    [Pholcus] Go项目 Pholcus 编写静态规则文件, 0 到 1
    [Gse] 高效的Golang中文分析库推荐
    [FAQ] Edge/Chrome 网络请求的编辑并重发
    浏览器扩展开发Firefox临时载入附加组件(图)
    [FAQ] IDE: Goland 注释符后面添加空行
  • 原文地址:https://www.cnblogs.com/PPXppx/p/9897987.html
Copyright © 2011-2022 走看看