【説明する】グラフ理論

数组模拟邻接表存储

详细请见: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\n");
74     dfs(1);
75     printf("\n");
76     memset(que,0,sizeof(que));
77     printf("bfs\n");
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\n",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\n",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("\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;
}
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\n",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\n",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为图中的边数

 

posted @ 2017-06-20 16:02  夜雨声不烦  阅读(519)  评论(0编辑  收藏  举报