天天看点

图论(最短路,最小生成树,并查集)

本文目录:

tarjan算法(判断环)

最小生成树(Kruskal算法)

最小生成树(Prim算法)

优先队列实现dijkstra(最短路)

并查集(求环)

floyd(弗洛伊德) (最短路)

判断环:

tarjan算法

讲解:http://www.sohu.com/a/245954819_100201031

tarjan算法,一个关于图的联通性的神奇算法。基于DFS(迪法师)算法,深度优先搜索一张有向图。!注意!是有向图。根据树,堆栈,打标记等种种神(che)奇(dan)方法来完成剖析一个图的工作。而图的联通性,就是任督二脉通不通。。的问题。

了解tarjan算法之前你需要知道:

强连通,强连通图,强连通分量,解答树。

  • 强连通(strongly connected): 在一个有向图G里,设两个点 a b 发现,由a有一条路可以走到b,由b又有一条路可以走到a,我们就叫这两个顶点(a,b)强连通。
  • 强连通图: 如果 在一个有向图G中,每两个点都强连通,我们就叫这个图,强连通图。
  • 强连通分量strongly connected components):在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量 [分量:把一个向量分解成几个方向的向量的和,那些方向上的向量就叫做该向量(未分解前的向量)的分量]

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)

}
           

邻接表的建立建议看看这篇博客:

https://blog.csdn.net/Mr_S_Edward/article/details/82942847

代码实现:

#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("\n");
     }
     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(i);//当这个点没有访问过,就从此点开始。防止图没走完
    return 0;
 }
           

洛谷:https://www.luogu.org/problemnew/show/P2921

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

const int MAXN=100005;
int nexts[MAXN],ans[MAXN],head[MAXN],len[MAXN],color[MAXN];;
int cnt=0,col=0;
struct node{
    int to,next;
}edge[MAXN*2];

void add(int a,int b){
    cnt++;
    edge[cnt].to=b;
    edge[cnt].next=head[a];
    head[a]=cnt;
}

int tot=0,indexs=0,DFN[MAXN],LOW[MAXN],vis[MAXN]={0},stacks[MAXN];

void tarjan(int x){
    DFN[x]=LOW[x]=++tot;
    stacks[++indexs]=x;
    vis[x]=1;
    for(int i=head[x];i!=0;i=edge[i].next){
        if(DFN[edge[i].to]==0){
            tarjan(edge[i].to);
            LOW[x]=min(LOW[x],LOW[edge[i].to]);
        }
        else if(vis[edge[i].to]==1){
            LOW[x]=min(LOW[x],DFN[edge[i].to]);
        }
    }
    if(LOW[x]==DFN[x]){
        col++;
        do{
            color[stacks[indexs]]=col;
            vis[stacks[indexs]]=0;
            indexs--;
        }while(x!=stacks[indexs+1]);
    }
    return;
}

void searchs(int root,int x,int step){
    if(ans[x]!=0){
        ans[root]=ans[x]+step;
        return;
    }
    else searchs(root,nexts[x],step+1);
}

int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        scanf("%d",&nexts[i]);
        add(i,nexts[i]);
        if(nexts[i]==i) ans[i]=1;
    }
    for(int i=0;i<=n;i++)
        if(DFN[i]==0) tarjan(i);
    for(int i=1;i<=n;i++)
         len[color[i]]++;
    for(int i=1;i<=n;i++)
         if(len[color[i]]!=1) ans[i]=len[color[i]];
    for(int i=1;i<=n;i++)
        if(ans[i]==0) searchs(i,nexts[i],1);
    for(int i=1;i<=n;i++)
        printf("%d\n",ans[i]);
}
           

最小生成树

  • 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
  • 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
    图论(最短路,最小生成树,并查集)

Kruskal算法(相对最好)

此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

  1. 把图中的所有边按代价从小到大排序;
  2. 把图中的n个顶点看成独立的n棵树组成的森林;
  3. 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
  4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
    图论(最短路,最小生成树,并查集)

Kruskal 模板

#include <bits/stdc++.h>
using namespace std;
#define ll long long
int n,m,sum;
struct node{
 int start,ends,power;//start为起始点,end为终止点,power为权值
}edge[5050];
int pre[5050];

bool cmp(const node&a,const node&b){
  return a.power<b.power;//按照权值排序
}

int Find(int x){  //并查集找祖先
 if(x!=pre[x]){
    pre[x]=Find(pre[x]);
 }
 return pre[x];
}

void Merge(int x,int y,int n){//并查集合并函数,n是用来记录最短路中应该加入哪个点
  int fx=Find(x);
  int fy=Find(y);
  if(fx!=fy){
    pre[fx]=fy;
    sum+=edge[n].power;
  }
}
int main(){
  while(~scanf("%d %d",&n,&m),n+m){//n是点数,m是边数
    sum=0;
    int start,ends,power;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&start,&ends,&power);
        edge[i].start=start,edge[i].ends=ends,edge[i].power=power;
    }
    for(int i=1;i<=m;i++){
        pre[i]=i;
    }//并查集初始化
    sort(edge+1,edge+m+1,cmp);
    for(int i=1;i<=m;i++){
        Merge(edge[i].start,edge[i].ends,i);
    }
    printf("%d\n",sum);
  }
  return 0;
}
           

Prim

图来演示Prim算法的过程。

图论(最短路,最小生成树,并查集)
图论(最短路,最小生成树,并查集)

我们选择一个起点,然后在与起点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。假设起点是0节点,与0节点相连且未被选的节点是{1,2,3},分别对应的权值是{6,1,5},可见当前最小的权值1,权值最小的节点就是2节点,所以将2节点和0-2的边添加入生成树,如图b所示。

图论(最短路,最小生成树,并查集)

接着我们在与已选节点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。当前已选节点是0,2节点,与已选节点相连且未被选的节点有{1,3,4,5},分别对应的权值是{(6,5),(5,5),6,4,},可见当前最小的权值4,权值最小的节点就是5节点,所以将5节点和2-5的边添加入生成树,如图c所示。(其实在编程时,我们只需记录与更新当前较小的那个权值,如与{1,3,4,5}对应的权值我们只需记录{5,5,6,4},当然我们也需利用了另一个数组来加以区别当前权值对应的连接点,如当前权值{5,5,6,4}所对应的连接点就是{2,0,2,2})

图论(最短路,最小生成树,并查集)

接着我们继续在与已选节点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。当前已选节点是0,2,5节点,与已选节点相连且未被选的节点有{1,3,4},分别对应的权值是{(6,5),(2,5,5),(6,6),}(其实当前我们可只记录{5,2,6},同时记录其对应的连接点分别是{2,5,2}),可见当前最小的权值2,权值最小的节点就是3节点,所以将3节点和5-3的边添加入生成树,如图d所示。

图论(最短路,最小生成树,并查集)

接着我们依照上一次的步骤继续在与已选节点相连且未被选的节点中选择一个权值最小的节点,将该节点与其相连边添加入生成树。如图e,f所示。最终图f就是我们通过Prim算法得到的最小生成树了。

图论(最短路,最小生成树,并查集)
图论(最短路,最小生成树,并查集)

算法概念

现在我们给出Prim的严谨概念:Prim算法是一种构造性算法。假设G=(V,E)是一个具有n个顶点的带权连通无向图,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起始顶点v出发的最小生成树T的步骤如下:

(1)初始化U={v},以v到其他顶点的所有边为候选边;

(2)重复以下步骤(n-1)次,使得其他(n-1)个顶点被加入到U中:

1.从侯选边中挑选权值最小的边加入TE,设该边在V-U中的顶点是k,将k加入U中;

2.考察当前V-U中所有顶点j,修改侯选边,若边(k,j)的权值小于原来和顶点j关联的侯选边,则用边(k,j)取代后者作为侯选边

图论(最短路,最小生成树,并查集)
#include<stdio.h>
#include<string.h>
#include <iostream>
#include <bits/stdc++.h>
#define MAX 0x3f3f3f3f
using namespace std;
int logo[1010];//用来标记0和1  表示这个点是否被选择过
int map1[1010][1010];//邻接矩阵用来存储图的信息
int dis[1010];//记录任意一点到这个点的最近距离
int n;//点个数
int prim(){
    int i,j,now;
    int sum=0;
    /*初始化*/
    for(i=1; i<=n; i++){
        dis[i]=MAX;
        logo[i]=0;
    }
    /*选定1为起始点,初始化*/
    for(i=1; i<=n; i++){
        dis[i]=map1[1][i];
    }
    dis[1]=0;
    logo[1]=1;
    /*循环找最小边,循环n-1次*/
    for(i=1; i<n; i++){
        now=MAX;
        int min1=MAX;
        for(j=1; j<=n; j++){
            if(logo[j]==0&&dis[j]<min1){
                now=j;
                min1=dis[j];
            }
        }
        if(now==MAX)
            break;//防止不成图
        logo[now]=1;
        sum+=min1;
        for(j=1; j<=n; j++){//添入新点后更新最小距离
            if(logo[j]==0&&dis[j]>map1[now][j])
                dis[j]=map1[now][j];
        }
    }
    if(i<n)
        printf("?\n");
    else
        printf("%d\n",sum);
}
int main(){
    while(scanf("%d %d",&n,&m),n+m){//n是点数,m是边数
        memset(map1,0x3f3f3f3f,sizeof(map1));//map是邻接矩阵存储图的信息
        for(int i=0; i<m; i++){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            if(c<map1[a][b])//防止重边
                map1[a][b]=map1[b][a]=c;
        }
        prim();
    }
}
           

如果是想求最短路的话,只需要将被调中更新数组部分的两行代码改为:

if(logo[j]==0&&dis[j]>map1[now][j]+dis[now])
                dis[j]=map1[now][j]+dis[now];
           

且此时min1和sum就没有用了,可以删掉。另外还需注意:其中表示循环的变量,有好几个都是从1开始的,如果纯粹用来计数,从0开始不会有影响,但是其他的不能改,在其他题目中,要参考其题意再决定。

优先队列实现dijkstra

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define maxn 10005
#define inf 0x3f3f3f3f
using namespace std;
int n,m,cnt,s,e;
struct node{
    int to;
    int w;
    int next;
    bool operator<(const node&a)const{
        return a.w<w;
    }
}edge[maxn],Now,Next;
int head[maxn];

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

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

bool vis[maxn];
int dis[maxn];
void dijkstra(int ans){
   memset(dis,inf,sizeof(dis));
   memset(vis,false,sizeof(vis));
   priority_queue<node>Q;
   Now.to=ans;
   dis[ans]=0;
   Q.push(Now);
   while(!Q.empty()){
    Now=Q.top();
    Q.pop();
    int now=Now.to;
    if(vis[now]) continue;
    vis[now]=true;
    for(int i=head[now];i!=-1;i=edge[i].next){
        int u=edge[i].to;
        if(!vis[u]&&dis[u]>dis[now]+edge[i].w){
            dis[u]=dis[now]+edge[i].w;
            Next.to=u;
            Next.w=dis[u];
            Q.push(Next);
            }
        }
    }
    printf("%d\n",dis[n]);
}

int main(){
    while(~scanf("%d %d",&n,&m)){
    if(n==0&&m==0) break;
    init();
    for(int i=0;i<m;i++){
        int u,v,w;
        scanf("%d %d %d",&u,&v,&w);
        add(u,v,w);
        add(v,u,w);
        }
    dijkstra(1);
    }
    return 0;
}
           

题目:https://www.luogu.org/problemnew/show/P1339

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define maxn 20005
#define inf 0x3f3f3f3f
using namespace std;
int n,m,cnt,s,e;
struct node{
    int to;
    int w;
    int next;
    bool operator<(const node&a)const{
        return a.w<w;
    }
}edge[maxn],Now,Next;
int head[maxn];

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

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

bool vis[maxn];
int dis[maxn];
void dijkstra(int ans){
   memset(dis,inf,sizeof(dis));
   memset(vis,false,sizeof(vis));
   priority_queue<node>Q;
   Now.to=ans;
   dis[ans]=0;
   Q.push(Now);
   while(!Q.empty()){
    Now=Q.top();
    Q.pop();
    int now=Now.to;
    if(vis[now]) continue;
    vis[now]=true;
    for(int i=head[now];i!=-1;i=edge[i].next){
        int u=edge[i].to;
        if(!vis[u]&&dis[u]>dis[now]+edge[i].w){
            dis[u]=dis[now]+edge[i].w;
            Next.to=u;
            Next.w=dis[u];
            Q.push(Next);
            }
        }
    }
    printf("%d\n",dis[e]-dis[s]);
}

int main(){
    scanf("%d %d %d %d",&n,&m,&s,&e);
    init();
    for(int i=0;i<m;i++){
        int u,v,w;
        scanf("%d %d %d",&u,&v,&w);
        add(u,v,w);
        add(v,u,w);
        }
    dijkstra(s);
    return 0;
}
           

并查集:

https://www.luogu.org/problemnew/show/P2661

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

typedef long long ll;

int f[200005],d[2000005],n,minn,last;
int fa(int x){
    if(x!=f[x]){
        int last=f[x];
        f[x]=fa(f[x]);
        d[x]+=d[last];
    }
    return f[x];
}
void check(int a,int b){
    int fx=fa(a);
    int fy=fa(b);
    if(fx!=fy){
        f[fx]=fy;
        d[a]=d[b]+1;
    }
    else
    minn=min(minn,d[a]+d[b]+1);
}


int main(){
    int t;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) f[i]=i;
    minn=0x7777777;
    for(int i=1;i<=n;i++){
        scanf("%d",&t);
        check(i,t);
    }
    printf("%d\n",minn);
    return 0;
}
           

floyd(弗洛伊德) 最短路

整个算法一共只有五行,三重循环+一个判断就能求出图中任意两点之间的最短路径。

这个算法的主要思路,就是通过其他的点进行中转来求的两点之间的最短路。因为我们知道,两点之间有多条路,如果换一条路可以缩短距离的话,就更新最短距离。而它最本质的思想,就是用其他的点进行中转,从而达到求出最短路的目的。

核心代码:
for(k=1;k<=n;k++)
    for(i=1;i<=n;i++)
        for(j=1;j<=n;j++)
            if(e[i][j]>e[i][k]+e[k][j])
                 e[i][j]=e[i][k]+e[k][j];
           

例题:https://www.luogu.org/problemnew/show/P1119

题目背景

BB地区在地震过后,所有村庄都造成了一定的损毁,而这场地震却没对公路造成什么影响。但是在村庄重建好之前,所有与未重建完成的村庄的公路均无法通车。换句话说,只有连接着两个重建完成的村庄的公路才能通车,只能到达重建完成的村庄。

题目描述

给出BB地区的村庄数NN,村庄编号从00到N-1N−1,和所有MM条公路的长度,公路是双向的。并给出第ii个村庄重建完成的时间 ti ,你可以认为是同时开始重建并在第ti天重建完成,并且在当天即可通车。若ti 为0则说明地震未对此地区造成损坏,一开始就可以通车。之后有QQ个询问(x, y, t)(x,y,t),对于每个询问你要回答在第tt天,从村庄xx到村庄y的最短路径长度为多少。如果无法找到从xx村庄到yy村庄的路径,经过若干个已重建完成的村庄,或者村庄xx或村庄yy在第t天仍未重建完成 ,则需要返回-1−1。

#include<iostream>
#include<cstdio>
#define N 205
using namespace std;
int n,m;
int a[N];
int f[N][N];//邻接矩阵存边
inline void updata(int k){
    for(int i=0;i<n;i++)
    for(int j=0;j<n;j++)
    if(f[i][j]>f[i][k]+f[j][k])
    f[i][j]=f[j][i]=f[i][k]+f[j][k];//用这个新的更新所有前面的 
    return;
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++)
    scanf("%d",a+i);//依次输入每一个村庄建立完成时需要的时间
    for(int i=0;i<n;i++)
    for(int j=0;j<n;j++){
        f[i][j]=1e9;//初始化为保证它不爆炸范围内的最大值 
    }
    for(int i=0;i<n;i++)
    f[i][i]=0;
    int s1,s2,s3;
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&s1,&s2,&s3);
        f[s1][s2]=f[s2][s1]=s3;//初始化边长 
    }
    int q;
    cin>>q;
    int now=0;
    for(int i=1;i<=q;i++){//处理各询问 
        scanf("%d%d%d",&s1,&s2,&s3);
        while(a[now]<=s3&&now<n){
            updata(now);//依次更新点,使它可以被用来更新其他的点 
            now++;
        }
        if(a[s1]>s3||a[s2]>s3)cout<<-1<<endl;
        else {
            if(f[s1][s2]==1e9)cout<<-1<<endl;
            else cout<<f[s1][s2]<<endl;
        }
    }
    return 0;
}