• 【説明する】グラフ理論


    数组模拟邻接表存储

    详细请见:http://www.cnblogs.com/zxqxwnngztxx/p/6682624.html


    图的遍历

    遍历是很多图论算法的基础,所谓图的遍历( graph traversal),也称为搜索( search),就是从图中某个顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次。

            遍历可以采取两种方法进行:
            深度优先搜索( DFS depth first search
            广度优先搜索( BFS breadth first search)。
     
    对图进行存储与遍历:
    输入:
    第一行:顶点数n
    第二行:边数m
    以下m行,每行两个顶点编号uv

    1DFS 遍历

    深度优先搜索是一个递归过程,有回退过程。
    对一个无向连通图,在访问图中某一起始顶点后,由出发,访问它的某一邻接顶点v1;再从v1 出发,访问与v1 邻接但还没有访问过的顶点v2;然后再从v2 出发,进行类似的访问;;如此进行下去,直至到达所有邻接顶点都被访问过的某个顶点为止;接着,回退一步,回退到前一次刚访问过的顶点,看是否还有其它没有被访问过的邻接顶点,如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再回退一步进行类似的访问。
    重复上述过程,直到该连通图中所有顶点都被访问过为止。
    模板:(伪代码)
    DFS( 顶点 u ) { //从顶点 i 进行深度优先搜索
        vis[ u ] = 1; //将顶点 i 的访问标志置为 1
        for( j=1; j<=n; j++ ) { //对其他所有顶点 j
    //j 是 i 的邻接顶点,且顶点 j 没有访问过
            if( map[u][ j ]==1 && !vis[ j ] ) {
    //递归搜索前的准备工作需要在这里写代码
                DFS( j ) //从顶点 j 出发进行 DFS 搜索
    //以下是 DFS 的回退位置
    //在很多应用中需要在这里写代码
            }
        }
    }

    2BFS 遍历

    广度优先搜索( BFS Breadth First Search)是一个分层的搜索过程,没有回退过程,是非递归的。
     
    BFS 算法思想:
    对一个连通图,在访问图中某一起始顶点后,由出发,依次访问的所有未访问过的邻接顶点v1, v2, v3, vt;然后再顺序访问v1, v2, v3, vt 的所有还未访问过的邻接顶点;再从这些访问过的顶点出发,再访问它们的所有还未访问过的邻接顶点,……,如此直到图中所有顶点都被访问到为止。
     
    BFS 算法的实现:
          与深度优先搜索过程一样,为避免重复访问,也需要一个状态数组 visited[n],用来存储各顶点的访问状态。如果visited[i] = 1,则表示顶点 i 已经访问过;如果 visited[i] = 0,则表示顶点还未访问过。初始时,各顶点的访问状态均为 0
          为了实现逐层访问, BFS 算法在实现时需要使用一个队列,来记忆正在访问的这一层和上一层的顶点,以便于向下一层访问(约定在队列中,取出元素的一端为队列头,插入元素的一端为队列尾,初始时,队列为空)
     
    真模板:
     1 #include<iostream>
     2 #include<cstdio>
     3 #include<string>
     4 #include<cstring>
     5 
     6 using namespace std;
     7 const int Maxn=1010;
     8 
     9 /*
    10 样例 
    11 9 10
    12 A B
    13 A D
    14 A E
    15 B C
    16 B E
    17 C G
    18 D F
    19 E F 
    20 F H
    21 H I
    22 */
    23 
    24 int m,n;
    25 int g[Maxn][Maxn],que[Maxn];
    26 bool d[Maxn];
    27 bool b[Maxn];
    28 
    29 void dfs(int i)//递归 
    30 {
    31     if(i<n-1) cout<<char(i+64)<<"-->";
    32     else cout<<char(i+64);//防止最后多输出一个箭头 
    33     d[i]=1;   //进行标记已经被搜过 
    34     for(int k=1;k<=n;k++)
    35     {
    36         if(g[i][k]==1&&!d[k])//如果k为是 i 的邻接顶点并且k没有被搜过 
    37         {
    38             dfs(k);//继续搜索k 
    39         }
    40     }
    41 }
    42 
    43 
    44 void bfs(int u)//非递归 
    45 {
    46     b[u]=1;//将第一个元素进行标记 
    47     cout<<(char)(u+64);//并输出 
    48     int head=0,tail=1;//制定队头与队尾 
    49     que[1]=u;//将第一个元素进队列,作为头号元素 
    50     while(head<tail)//当队头队尾重合之前 
    51     {
    52         head++;//删除第一个元素,将head指针指向下一个 
    53         for(int i=1;i<=n;i++)
    54         {
    55             if(g[que[head]][i]==1&&!b[i])//如果为此时搜索的邻接顶点并且为被标记 
    56             {
    57                 b[i]=1;
    58                 que[++tail]=i;//入队 
    59             }
    60         }
    61     }
    62 }
    63 
    64 int main()
    65 {
    66     char a,b;
    67     scanf("%d %d",&m,&n);
    68     for(int i=1;i<=n;i++)
    69     {
    70         cin>>a>>b;//需要用cin输入 
    71         g[a-64][b-64]=g[b-64][a-64]=1;//进行标记 
    72     }
    73     printf("dfs
    ");
    74     dfs(1);
    75     printf("
    ");
    76     memset(que,0,sizeof(que));
    77     printf("bfs
    ");
    78     bfs(1);
    79     for(int i=2;i<n;i++)
    80     {
    81         cout<<"-->"<<(char)(que[i]+64);//输出队列 
    82     }
    83     return 0;
    84 }
    DFS+BFS(混合)

    求最短路的3种算法

    1)Floyed算法 O(N3)

      简称Floyed(弗洛伊德)算法,是最简单的最短路径算法,可以计算图中任意两点间的最短路径。
      Floyed的时间复杂度是O (N3),适用于出现负边权的情况,但是不能解决带有“负权回路”(或者叫“负权环”)的图,因为带有“负权回路”的图没有最短路。
      例如下面这个图就不存在1号顶点到3号顶点的最短路径。
          因为1->2->3->1->2->3->…->1->2->3这样路径中,每绕一次1->-2>3这样的环,最短路就会减少1,永远找不到最短路。
          其实如果一个图中带有“负权回路”那么这个图则没有最短路。

                                  081030elthvel6et6k886y.png

    算法分析&思想讲解:
      三层循环,第一层循环中间点k,第二第三层循环起点终点i、j,算法的思想很容易理解:
    如果点i到点k的距离加上点k到点j的距离小于原先点i到点j的距离,那么就用这个更短的路径长度来更新原先点i到点j的距离。
    我们在初始化时,把不相连的点之间的距离设为一个很大的数,不妨可以看作这两点相隔很远很远,如果两者之间有最短路径的话,就会更新成最短路径的长度。
    Floyed算法的时间复杂度是O(N3)。
    引例小讲解:
     

    暑假,小哼准备去一些城市旅游。有些城市之间有公路,有些城市之间则没有,如下图。为了节省经费以及方便计划旅程,小哼希望在出发之前知道任意两个城市之前的最短路程。

    081028xjgvimgz7882qdu7.png

    上图中有4个城市8条公路,公路上的数字表示这条公路的长短。

    请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。

    详细过程请见:直通

    模板:
     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 #define Maxn 1001
     5 
     6 using namespace std;
     7 
     8 int maps[Maxn][Maxn];
     9 int ans;
    10 
    11 int main()
    12 {
    13     memset(maps,999999,sizeof(maps));
    14     int n,m;
    15     cin>>n>>m;
    16     int he,ta,len;
    17     for(int i=1; i<=m; i++)
    18     {
    19         cin>>he>>ta>>len;
    20         maps[ta][he]=maps[he][ta]=len;
    21     }
    22     int x,y;
    23     cin>>x>>y;
    24     for(int k = 1; k <= n; k++)
    25         for(int i = 1; i <= n; i++)
    26             for(int j = 1; j <= n; j++)
    27             {
    28                 if(maps[i][j]>maps[i][k]+maps[k][j])
    29                     maps[i][j]=maps[i][k]+maps[k][j];  //进行更新 
    30             }
    31 
    32     printf("%d",maps[x][y]);
    33     return 0;
    34 }
    Floyed

    2)Dijkstra算法O (N2)

    用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况
    Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况
    基本原理:
    每次新扩展一个距离最短的点,更新与其相邻的点的距离。当所有边权都为正时,由于不会存在一个距离更短的没扩展过的点,所以这个点的距离永远不会再被改变,因而保证了算法的正确性。不过根据这个原理,用Dijkstra求最短路的图不能有负权边,因为扩展到负权边的时候会产生更短的距离,有可能就破坏了已经更新的点距离不会改变的性质。
    模板:
     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 
     5 using namespace std;
     6 
     7 const int Maxn=0x7f,mm=1001;
     8 int map[mm][mm];   //输入关系
     9 int minn;   //the最小值
    10 int n,m,s1,ss,k;   //度and起止点
    11 int dis[mm];  //统计关系
    12 int p[mm];  //进行输出关系
    13 bool b[mm];
    14 
    15 void print(int u,int v)
    16 {//如果需要输出如何到达 
    17     int que[mm];
    18     int tot=1;
    19     que[tot]=v;
    20     tot++;
    21     int temp=p[v];
    22     while(temp!=u)
    23     {
    24         que[tot]=temp;
    25         tot++;
    26         temp=p[temp];
    27     }
    28     que[tot]=u;
    29     for(int i=tot;i>=1;i--)
    30         if(i!=1)
    31             cout<<que[i]<<"-->";
    32         else
    33             cout<<que[i]<<endl;
    34 }
    35 
    36 void Dijkstra(int s)
    37 {
    38     for(int i=1; i<=n; i++)
    39     {
    40         dis[i]=map[s][i];
    41         if(dis[i]!=Maxn)
    42             p[i]=s;   //指向s
    43         else
    44             p[i]=0;   //不进行指向
    45     }
    46     b[s]=1;
    47     dis[s]=0;
    48     for(int i=1; i<=n-1; i++)
    49     {
    50         minn=Maxn;  //便于寻找
    51         k=s;  //起点
    52         for(int j=1; j<=n; j++)
    53             if(!b[j]&&dis[j]<minn)
    54             {
    55                 minn=dis[j];  //寻找最小值
    56                 k=j;  //用k进行记录最小值
    57             }
    58         b[k]=1;  //进行标记
    59         for(int q=1; q<=n; q++)
    60             if(!b[q]&&dis[q]>dis[k]+map[k][q]&&map[k][q]<Maxn) {
    61                 dis[q]=dis[k]+map[k][q];
    62                 p[q]=k;
    63             }
    64     }
    65 }
    66 
    67 void gett()
    68 {
    69     int x,y,w;
    70     for(int i=1; i<=m; i++)
    71     {
    72         cin>>x>>y>>w;
    73         map[x][y]=map[y][x]=w;
    74     }
    75 }
    76 
    77 int main()
    78 {
    79     scanf("%d %d",&n,&m);
    80     memset(map,Maxn,sizeof(map));
    81     gett();
    82     memset(dis,Maxn,sizeof(dis));
    83     scanf("%d %d",&s1,&ss);
    84     Dijkstra(s1);
    85     printf("%d
    ",dis[ss]);//仅输出最短路线的长度即可
    86     print(s1,ss);
    87     return 0;
    88 }
    Dijkstra

     3)SPFA算法O(kE)

    主要思想是:
        初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束。
        这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。
    SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去。
    算法时间复杂度:O(kE),E是边数。K是常数,平均值为2。
     
    接下来答题思想为:
    算法实现:
        dis[i]记录从起点si的最短路径,w[i][j]记录连接ij的边的长度。pre[v]记录前趋。
        team[1..n]为队列,头指针head,尾指针tail
        布尔数组exist[1..n]记录一个点是否现在存在在队列中。
        初始化:d[s]=0,d[v]=∞(vs),memset(exist,false,sizeof(exist));
        起点入队team[1]=s; head=0; tail=1;exist[s]=true;
        do {
        1、头指针向下移一位,取出指向的点u
        2、exist[u]=false;已被取出了队列
        3、foru相连的所有点v  //注意不要去枚举所有点,用数组模拟邻接表存储
        i(d[v]>d[u]+w[u][v]) {
          d[v]=d[u]+w[u][v];
                   pre[v]=u;
                   i(!exist[v]) //队列中不存在v点,v入队。
                          exist[v]=true; //尾指针下移一位,v入队;
        }
      while (head < tail);
    循环队列:
      采用循环队列能够降低队列大小,队列长度只需开到2*n+5即可。例题中的参考程序使用了循环队列。
     

    样例:

    7 12
    1 2 24
    1 3 8
    1 4 15
    2 5 6
    3 5 7
    3 6 3
    4 7 4
    5 7 9
    6 7 3
    6 4 5
    6 5 2
    7 2 3
     模板:
     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 
     5 using namespace std;
     6 const int Maxn=1001,Maxx=999999;
     7 
     8 int que[Maxn],map[Maxn][Maxn],dis[Maxn];
     9 bool cun[Maxn];
    10 int n,m;
    11 int qianqu[Maxn],q[Maxn];
    12 
    13 void SPFA(int s)
    14 {
    15     int head=0,tail=1,v;
    16     que[1]=s;   //将s入队 
    17     dis[s]=0;    //s to s 的距离为0
    18     qianqu[s]=s;  //记录下s的前驱 
    19     cun[s]=1;  //进行标记,已经入队 
    20     do
    21     {
    22         v=que[++head];  //取出队头元素 
    23         cun[v]=0; //将标记撤销,说明已经出队 
    24         for(int i=1;i<=n;i++)
    25         {
    26             if(dis[i]>dis[v]+map[v][i])  //进行松弛 
    27             {
    28                 dis[i]=dis[v]+map[v][i];
    29                 qianqu[i]=v;   //记录前驱 
    30                 if(!cun[i])   //如果队中没有i元素 
    31                 {
    32                     que[++tail]=i;  //入队 
    33                     cun[i]=1;   //进行标记,已经入队 
    34                 }
    35             }
    36         }
    37         
    38     }while(head<tail);   //进行循环的条件 
    39 }
    40 
    41 void print(int s,int e)
    42 {
    43     int tot=1;
    44     q[tot]=e;
    45     tot++;
    46     int temp=qianqu[e];
    47     while(temp!=s)
    48     {
    49         q[tot]=temp;
    50         tot++;
    51         temp=qianqu[temp];
    52     }
    53     q[tot]=s;
    54     for(int i=tot;i>=1;i--)
    55     {
    56         if(i!=1)
    57           cout<<q[i]<<"-->";
    58         else
    59           cout<<q[i]<<endl;
    60     }
    61 }
    62 
    63 int main()
    64 {
    65     memset(dis,Maxx,sizeof(dis));   //dis与map必须!!!进行初始化 
    66     memset(map,Maxx,sizeof(map));   //这样才能够松弛 
    67     scanf("%d %d",&n,&m);
    68     int q,h,w,s,e;
    69     for(int i=1;i<=m;i++)
    70     {
    71         scanf("%d %d %d",&q,&h,&w);
    72         map[q][h]=w;
    73     }
    74     //memset(cun,0,sizeof(cun));
    75     //memset(que,0,sizeof(que));
    76     //这两个可以不用进行初始化 
    77     scanf("%d %d",&s,&e);
    78     SPFA(s);
    79     printf("%d
    ",dis[e]);
    80     print(s,e);
    81     return 0;
    82 }
    Spfa

    超详细讲解——

    orz   http://www.cnblogs.com/mjtcn/p/7599217.html

          tarjan算法

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

    了解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[]作为每个点在这颗树中的,最小的子树的根,每次保证最小,喜欢它的父亲结点的时间戳这种感觉。如果它自己的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)
    }

    来一发裸代码!

    输入:
    一个图有向图。

    输出:
    它每个强连通分量。

    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

    代码酱=u=

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    
    using namespace std;
    const int M = 1001;
    
    struct node
    {
        int v,next;
    } edge[M];
    
    int DFN[M],LOW[M];
    int stack[M],heads[M],visit[M];
    int 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(i);///当这个点没有访问过,就从此点开始。防止图没走完
        return 0;
    }
    tarjan

    最小生成树

    (注:给出2种代码均可以ACluoguP3366题题解

    最小生成树的简单定义

      给定一股无向联通带权图G(V,E).E 中的每一条边(v,w)权值位C(v,w)。

      如果G的子图G'是一个包含G中所有定点的子图,那么G'称为G的生成树,如果G'的边的权值最小那么G'称为G的最小生成树。

    1)Prim(普里姆)算法(采用贪心)

     from   http://www.cnblogs.com/biyeymyhjob/archive/2012/07/30/2615542.html

     (可能外加自己的理解?)

    1.引入

      图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小

                  我大致的理解为:最短的一定会存在与最小生成树上

    2.算法简单描述

      1).输入:一个加权连通图,其中顶点集合为V,边集合为E;

      2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;

      3).重复下列操作,直到Vnew = V:

        a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);

        b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;

      4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。

    下面对算法的图例描述

    图例说明不可选可选已选(Vnew
     

    此为原始的加权连通图。每条边一侧的数字代表其权值。 - - -

    顶点D被任意选为起始点。顶点ABEF通过单条边与D相连。A是距离D最近的顶点,因此将A及对应边AD以高亮表示。 C, G A, B, E, F D
     

    下一个顶点为距离DA最近的顶点。BD为9,距A为7,E为15,F为6。因此,FDA最近,因此将顶点F与相应边DF以高亮表示。 C, G B, E, F A, D
    算法继续重复上面的步骤。距离A为7的顶点B被高亮表示。 C B, E, G A, D, F
     

    在当前情况下,可以在CEG间进行选择。CB为8,EB为7,GF为11。E最近,因此将顶点E与相应边BE高亮表示。 C, E, G A, D, F, B
     

    这里,可供选择的顶点只有CGCE为5,GE为9,故选取C,并与边EC一同高亮表示。 C, G A, D, F, B, E

    顶点G是唯一剩下的顶点,它距F为11,距E为9,E最近,故高亮表示G及相应边EG G A, D, F, B, E, C

    现在,所有顶点均已被选取,图中绿色部分即为连通图的最小生成树。在此例中,最小生成树的权值之和为39。 A, D, F, B, E, C, G

    3.简单证明prim算法

    反证法:假设prim生成的不是最小生成树

      1).设prim生成的树为G0

      2).假设存在Gmin使得cost(Gmin)<cost(G0)   则在Gmin中存在<u,v>不属于G0

      3).将<u,v>加入G0中可得一个环,且<u,v>不是该环的最长边(这是因为<u,v>∈Gmin)

      4).这与prim每次生成最短边矛盾

      5).故假设不成立,命题得证.

    4.c++代码实现

    #include <iostream>
    #include <cstdio>
    #define Maxx 0x7fffffff
    
    using namespace std;
    
    const int M = 5050;
    int n,m;                                                         //n=顶点的个数,m=边的个数
    int edge[M][M]={ /*输入的邻接矩阵*/ };
    int lowcost[M];                                                  //记录Vnew中每个点到V中邻接点的最短边
    bool visited[M];                                                 //标记某点是否加入Vnew
    int pre[M];                                                      //记录V中与Vnew最邻近的点
    
    void prim(int start)
    {
         int sumweight=0,i,j,k=0;
         visited[start]=true;
         for(i=1;i<=n;i++)
         {
             lowcost[i]=edge[start][i];
             pre[i]=start;
         }
         int minn=Maxx;                                              //最小权值 
         int v=-1;                                                   //所对应的下标 
         for(i=1;i<n;i++)                                            //进行n-1次,因为此时已经知道当前start点到另一点距离最短                                       
         {
             minn=Maxx;
            for(j=1;j<=n;j++)                                      
            {
                if(visited[j]==false && lowcost[j]<minn)             //在Vnew之外寻找最短路径
                {
                    minn=lowcost[j];                                 //最短路径 
                    v=j;
                }
            }
    //      printf("%d %d %d
    ",pre[v],v,lowcost[v]);
            if(v==-1)
            {
                cout<<"orz"<<endl;
                return;
            }
            visited[v]=true;                                         //将v加Vnew中
            sumweight+=lowcost[v];                                   //计算路径长度之和
            for(j=1;j<=n;j++)
            {
                if(visited[j]==false && edge[v][j]<lowcost[j])      
                {
                    lowcost[j]=edge[v][j];                           //此时v点加入Vnew 需要更新lowcost
                    pre[j]=v;                             
                }
            }
        }
    //  printf("the minmum weight is %d",sumweight);                 //进行输出 
        printf("%d",sumweight);
    }
    
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1; i<=n; i++)
        {
    //        lowcost[i]=Maxx;
            for(int j=1; j<=n; j++)
                edge[i][j]=Maxx;                                     //初始化图
        }
        int x,y,w,s,Max=Maxx;
        for(int k=1; k<=m; k++)
        {
            cin>>x>>y>>w;
            if(w<edge[x][y])
                edge[x][y]=edge[y][x]=w;                             //构建图 
            if(w<Max)
            {
                Max=w;
                s=x;                                                 //寻找最初最"实惠"的点 
            }
        }
        prim(s);                                                     //进行求解最小生成树 
        return 0;
    }
    prim算法

    5.时间复杂度

      这里记顶点数v,边数e

      邻接矩阵:O(v2)                 邻接表:O(elog2v)


    2)Kruskal(克鲁斯卡尔)算法(采用并查集)

    用来解决同样问题的还有Prim算法和Boruvka算法等。三种算法都是贪心算法的应用。和Boruvka算法不同的地方是,Kruskal算法在图中存在相同权值的边时也有效。

        (所有的边从短到长进行排序,每次都选取最短的边)

    1.kruskal算法的基本思想:

      1.首先将G的n个顶点看成n个孤立的连通分支(n个孤立点)并将所有的边按权从小大排序。

      2.按照边权值递增顺序,如果加入边后存在圈则这条边不加,直到形成连通图

        对2的解释:如果加入边的两个端点位于不同的连通支,那么这条边可以顺利加入而不会形成圈

    2.算法简单描述

      1).记Graph中有v个顶点,e个边

      2).新建图Graphnew,Graphnew中拥有原图中相同的e个顶点,但没有边

      3).将原图Graph中所有e个边按权值从小到大排序

      4).循环:从权值最小的边开始遍历每条边 直至图Graph中所有的节点都在同一个连通分量中

                    if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中

                                             添加这条边到图Graphnew

      图例描述:

        首先第一步,我们有一张图Graph,有若干点和边,如右图: 

        

        然后将所有的边的长度排序,用排序的结果作为我们选择边的依据,这里再次体现了贪心算法的思想。

        资源排序,对局部最优的资源进行选择,排序完成后,我们率先选择了边AD。这样我们的图就变成了右图:

        在剩下的边中寻找,我们找到了CE,这里边的权重也是5,如右:

        依次类推我们找到了6,7,7,即DF,AB,BE,如右:

        下面继续选择, BC或者EF尽管现在长度为8的边是最小的未选择的边。

        但是现在他们已经连通了(对于BC可以通过CE,EB来连接,类似的EF可以通过EB,BA,AD,DF来接连),所以不需要选择他们。

        类似的BD也已经连通了(这里上图的连通线用红色表示了)。

        最后就剩下EG和FG了,当然我们选择了EG。所以最后成功的图就是右图所示:

    3.简单证明Kruskal算法

      对图的顶点数n做归纳,证明Kruskal算法对任意n阶图适用。

    归纳基础:

      n=1,显然能够找到最小生成树。

    归纳过程:

      假设Kruskal算法对n≤k阶图适用,那么,在k+1阶图G中,我们把最短边的两个端点a和b做一个合并操作,

      即把u与v合为一个点v',把原来接在u和v的边都接到v'上去,这样就能够得到一个k阶图G'(u,v的合并是k+1少一条边),G'最小生成树T'可以用Kruskal算法得到。

      我们证明T'+{<u,v>}是G的最小生成树。

      用反证法,如果T'+{<u,v>}不是最小生成树,最小生成树是T,即W(T)<W(T'+{<u,v>})。

      显然T应该包含<u,v>,否则,可以用<u,v>加入到T中,形成一个环,删除环上原有的任意一条边,形成一棵更小权值的生成树。

      而T-{<u,v>},是G'的生成树。所以W(T-{<u,v>})<=W(T'),也就是W(T)<=W(T')+W(<u,v>)=W(T'+{<u,v>}),产生了矛盾。

      于是假设不成立,T'+{<u,v>}是G的最小生成树,Kruskal算法对k+1阶图也适用。

        由数学归纳法,Kruskal算法得证。

    4.c++代码实现

    #include <algorithm>
    #include <iostream>
    #include <cstdio>
    using namespace std;
    
    const int N = 5010;
    const int M = 200020;
    int n,m,ans;
    int dad[N];
    
    struct A {
        int u,v,w;
        bool operator < (const A &qwq)const
        {
            return w < qwq.w;
        }
    }t[M];
    
    int getdad(int x)
    { return dad[x] == x ? x : dad[x] = getdad( dad[x] ); }
    
    void kruskal()
    {
        sort(t+1,t+1+m);
        for(int i=1;i<=m;i++)
        {
            int f1=getdad(t[i].u),f2=getdad(t[i].v);
            if(f1!=f2)
            {
                dad[f1]=f2;
                ans+=t[i].w;
            }
        }
        int tmp=getdad(1);
        for(int i=2;i<=n;i++)
        {
            if(getdad(i)!=tmp)
            {
                printf("orz");
                return;
            }
        }
        printf("%d
    ",ans);
    }
    
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=m;i++)
            scanf("%d%d%d",&t[i].u,&t[i].v,&t[i].w);
        for(int i=1;i<=n;i++)
            dad[i]=i;
        kruskal();
        return 0;
    }
    Kruskal算法

    5.时间复杂度:

      elog2e  e为图中的边数

    如果运气好也是错,那我倒愿意错上加错!

    ❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀❀

  • 相关阅读:
    Linux 系统使用WordPress开启“固定链接设置”之后部分页面打不开(404)的解决办法
    WordPress安全配置
    去掉Windows2003的自动锁定
    用IP访问wordpress的css和js为何不能加载
    元素伸缩
    Cookie之获设删
    输入0开头的数字,自动纠正
    PHP入门(一)
    node.js(一)
    vue-demo-tab切换
  • 原文地址:https://www.cnblogs.com/zxqxwnngztxx/p/6690052.html
Copyright © 2020-2023  润新知