现在的位置: 首页 > 综合 > 正文

20131111:图的应用:最小生成树;拓扑排序;最短路径;最小树形图

2014年02月12日 ⁄ 综合 ⁄ 共 17019字 ⁄ 字号 评论关闭

今天又是寝室三个人一起睡到12点才起,晕~~

好吧,不过这两天做完的事还是不少的, 主要是算法方面的,那么现在将昨天和今天的一起做个总结, 当然遗留的问题也真不少!~

数算现在已经基本结束了图论的讲解, 关于图简单地说下, 就是在森林的基础上加了个回路而已, 当然我们现在涉及的一般都是最基本的图, 也就是简单图, 简单图的意思

就是不存在平行边(即两顶点间不存在多条边,若是有向图,则指不存在多条方向相同的边, 百度百科中的定义(加了不能有环)是错误的)。

只要模拟森林对图进行学习就基本能掌握个大概, 需要认真关注的是图中特有的那些操作:最小生成树、最短路径、拓扑排序、关键路径、最小树形图等等, 其中牵涉到的算法真心不少,而且并不是都能一眼看出来的~

值得注意的是, 我们在处理图的时候, 存储结构有多种, 邻接矩阵和邻接表等等, 但邻接矩阵毫无疑问是最好有用的,因为它和矩阵这样一种便于操作的结构联系起来, 代数学里面的知识都可以直接作用在上面, 虽说某些情况下可能特别耗空间(稀疏矩阵), 对于无向图,有边为1, 无边为0, 对角线均是1; 而对于有向图, 如果只需要利用其无向图的属性另当别论, 一般都是无边为正无穷, 有边为权值(默认是1), 对角线上全是0

这里关于使用的表示正无穷的数要提下, 首先INT_MAX和INT_MIN和UINT_MAX都在limits.h中有宏定义, 最小的无符号整数就是0了, 而DBL_MAX和DBL_MIN都在float.h

中定义; 然后, 如果命名为无穷的数不牵涉到运算(相加等等), 即不会产生溢出情况, 那么不妨直接使用那些宏定义的常量, 否则, 应该根据需要和实际情况选择更小的数作为正无穷, 如果题目中有提及权值的范围则根据范围选取, 总之就是绝对不允许出现溢出的情况, 因为这样的话作比较时会混乱而出错!!!

 

首先是最小生成树问题, 注意最小生成树是对于无向图而言的, 有向图情况会有变化,这个以后再讨论, 我采取的是Prim算法,因为这个算法比较省空间也不难实现, 只要能够理解原理记住模板,没有什么值得特别注意的地方, 但有个问题是我虽然会用这个算法却无法真正证明它的正确性, 这样的话自己构思算法时就会受到制约, 等我把它想出来吧!!!

程序对应在下方:

/*

 Name: 最小生成树的Prim算法实现

 Copyright: poj  宝昌县长要修路

 Author: xiaoli

 Date: 10/11/13 23:51

 Description:

 1. 无向图的最小生成树   将必需的边一条条加入!保证最小的连通, 加入的边数必是n-1

 2. 边集分为两块,一个是已经加入生成树的,一个是从当前生成树到其他各顶点的最短边,

     将两块放在一个数组中既节省空间又便于加入操作(交换位置)

 3. 注意待加入边集的初始化(先选定一个顶点)

*/

#include<iostream>

#define MAX 100

using namespace std;

struct dd                    //边结构

{

        intfrom;                //头顶点(此题中不需要用到)

        intto;                               //尾顶点

        intweight;                           //边的权值         

}road[80];                   //表示待加入的边集

int edge[26][26];            //注意邻接矩阵的初始化:对角线0,无边为正无穷(防止越界),有边为权值

 

int main()

{

        intn,i,j,k;

        charc1,c2;

        for(i=0;i<26;++i)

               for(j=0;j<26;++j)

               {

                       if(i==j)

                               edge[i][j]=0;

                       else

                               edge[i][j]=MAX;

               }

        cin>>n;

        for(i=1;i<n;++i)

        {

               cin>>c1>>j;

               intpos=c1-'A';

               while(j--)

               {

                       cin>>c2>>k;

                       intp=c2-'A';

                       edge[pos][p]=edge[p][pos]=k;

               }

        }

        //初始化待加入的边集

        for(i=0;i<n-1;++i)

        {

               road[i].from=0;

               road[i].to=i+1;

               road[i].weight=edge[0][i+1];

        }

        //逐个加入

        for(k=1;k<n;++k)                //0~k-2是已加入的, k-1~n-2是未加入的

        {

               intmin=MAX,mini=-1;

               for(i=k-1;i<n-1;++i)

                       if(road[i].weight<min)

                       {

                               min=road[i].weight;

                               mini=i;

                       }

               ddt;

               t=road[k-1];                 //交换位置后即将其加入!

               road[k-1]=road[mini];

               road[mini]=t;

               j=road[k-1].to;              //新的中间点,下面根据它对剩下的边作更新

               for(i=k;i<n-1;++i)

               {

                       inttmp=road[i].to;

                       if(edge[tmp][j]<road[i].weight)

                       {

                               road[i].weight=edge[tmp][j];

                               road[i].from=j;

                       }

               }

        }

 

        //下面计算最小生成树的总权值

        intsum=0;

        for(i=0;i<n-1;++i)

               sum+=road[i].weight;

        cout<<sum<<endl;

        return0;

}

 

 

下面是拓扑排序的问题, 这个算法极其简单,就算不看书应该也能想出来, 只要用一个队列,不断将入度为0的顶点加入即可, 作业题中甚至都是无环图,更加简化了, 

不过也增加了一个新要求, 同等条件下标号小的先输出, 这就需要用堆而不是队列来维护了(当然也可以用优先队列),这里稍微提下, 我到今天才真正学习了堆的实现啊啊啊, 以前牵涉到堆的情况比较少, 我一直都是用STL中的堆算法,包含在algorithm头文件中, 但是这个堆算法使用时需要注意的东西太多, 远不如自己实现来得灵活!~

程序如下:

/*

 Name: 拓扑排序的实现 , 要求在同等条件下,编号小的顶点在前

 Copyright: poj

 Author: xiaoli

 Date: 10/11/13 23:34

 Description:

 1. 拓扑排序可以用队列实现,但为了能保证同等条件下标号小的先输出,应该使用堆结构

 2. 可以用STL中的优先队列或者堆算法(包含在algorithm中,是对已有的一个容器建堆并维护)

     但自己写会更灵活可靠,需要记忆的东西也更少

 3. 这道题不需要判断是否有回路, 判断是否有回路的方法:应该设置一个标记记录是否访问, 在最后枚举检验

     是否有标志为未访问的元素, 如果有说明原图存在回路

*/

#include<iostream>

#include<cstring>

#define MAX 1000

using namespace std;

int edge[MAX][MAX]={0};            //这里只需要判断是否有边相连,所以可以这样初始化

int in[MAX]={0};                    //统计入度

class heap

{

public:

        inta[MAX];

        intlen;

        voidpush(int t)

        {

               inti=len,j=0;                    

               while(i!=0)                        //不断和父节点做比较

               {

                       j=(i-1)/2;                                       //将堆按照层遍历进行顺序存储时的下标计算

                       if(a[j]<=t)break;

                       a[i]=a[j];                     //父节点下移

                       i=j;                           //移动到父节点位置

               }

               a[i]=t;                            //将新元素加入到最终位置

               len++;

        }

        voidpop()     //删除堆顶:换上堆尾元素,这样可以避免数组的大幅移动

        {

               inti=0,j=1,k=0,t=a[len-1];  //i 父节点    j 左子结点   右子标号=左子+1

               len--;

               while(j<len)

               {

                       if(j<len-1&&a[j]>a[j+1])

                               j++;                  //只需考虑较小的一个子节点

                       if(a[j]>=t)break;        //此条件跳转或一直到叶子节点

                       a[i]=a[j];

                       i=j;

                       j=2*j+1;

               }

               a[i]=t;

        }

}my;

 

int main()

{

        intn,m,i,j,k;

        cin>>n>>m;

       

        intt1,t2;

        while(m--)

        {

               cin>>t1>>t2;

               edge[t1][t2]=1;

                in[t2]++;

        }

        my.len=0;

        memset(my.a,-1, sizeof(my.a));

        for(i=1;i<=n;++i)              //标号从1开始

               if(in[i]==0)

                       my.push(i);

        while(my.len)

        {

               intt=my.a[0];

               cout<<'v'<<t<<'';

               my.pop();

               for(i=1;i<=n;++i)

                       if(edge[t][i])          //题目保证不会出现环,应该可以不用做标记

                       {

                               in[i]--;

                               if(in[i]==0)        //入度减为0则加入

                                      my.push(i);

                       }

        }

        cout<<endl;

        return0;

}

 

 

 

 

关于最短路径问题, 算法太多了, 戴爷爷的用于处理单源最短路径十分有效, 但是作业题牵涉到一堆顶点间的最短路径, 所以最直接的是先把所有的都求出来, 即求出任意一对顶点间的最短路径, 这样就不如使用Floyed算法了,因为这个算法实在是太好实现了, 但是由于要记录路径, 所以稍微有些麻烦, 对于求一对顶点间的最短路径并记录用戴爷爷的算法也好实现,弄一个父亲-儿子链接即可, 在Floyed算法中,因为是一次性求所有最短路径, 所以路径也必须全部对应记录才行, 我是定义了一个三维数组来记录每对顶点间的最短路径,
也可以用链表的形式记录路径,, 这样会省些空间, 但肯定都牵涉到不断地拷贝路径的操作, 总觉得应该有更简单的方法来实现路径的记录呢!~

程序如下:

/*

 Name: 最短路径的Floyed算法实现(需要记录路径)

 Copyright: poj 我爱北大

 Author: xiaoli

 Date: 10/11/13 23:43

 Description:

 1. 题意是无向图,但对于每一对顶点,路径是2种, 长度相同

 2. 在Floyed的算法上增加一个结构用于记录路径

 3. Floyed算法的推导公式中满足的性质允许我们使用类似滚动数组的结构,

     即不断对一个数组更新即可,不需要用两个交换操作!

 */

#include<iostream>

#include<map>

#include<string>

#define MAX 0x3fffffff                 //防止正向溢出

using namespace std;

map<string,int> match;

string s[30];

int d[30][30]={0};                       //图的邻接矩阵

int a[30][30]={0};                       //用来求最短路的矩阵

int son[30][30][30]={0};              //用来记录每两点间的最短路径

int main()

{

        intp,q,r,i,j,t,k;

        strings1,s2;

        cin>>p;

        for(i=0;i<p;++i)

        {

               cin>>s[i];

               match.insert(make_pair(s[i],i));

        }

        cin>>q;

        for(i=0;i<30;++i)

               for(j=0;j<30;++j)

               {

                       if(i==j)

                               d[i][j]=0;

                       else

                               d[i][j]=MAX;

               }

               while(q--)

               {

                       cin>>s1>>s2>>t;

                       i=match[s1];

                       j=match[s2];

                       d[i][j]=d[j][i]=t;

               }

               for(i=0;i<p;++i)

                       for(j=0;j<p;++j)

                       {

                               a[i][j]=a[j][i]=d[i][j];

                               son[i][j][i]=j;

                               son[i][j][j]=j;

                       }

 

               //确定每对顶点间的最短路径,Floyed算法是一次性求出所有路径

               for(k=0;k<p;++k)

                       for(i=0;i<p;++i)

                               for(j=0;j<p;++j)

                               {

                                      if(i==j||i==k||j==k)                   //情况不变,不需要操作

                                              continue;

                                      t=a[i][k]+a[k][j];

                                      if(t<a[i][j])                          //是否加入一个新顶点作为中间点

                                      {

                                              a[i][j]=t;

                                              t=i;

                                              while(1)                           //拷贝路径

                                              {

                                                     son[i][j][t]=son[i][k][t];

                                                     t=son[i][k][t];

                                                     if(t==k)

                                                             break;

                                              }

                                              while(1)

                                              {

                                                     son[i][j][t]=son[k][j][t];

                                                     t=son[k][j][t];

                                                     if(t==j)

                                                             break;

                                              }

                                      }

                               }

 

               cin>>r;

               while(r--)

               {

                       cin>>s1>>s2;

                       i=match[s1];

                       j=match[s2];

                       t=i;

                       intpre=i;

                       cout<<s[i];

                       while(1)

                       {

                               t=son[i][j][t];

                               cout<<"->("<<d[pre][t]<<")->"<<s[t];

                               pre=t;

                               if(t==j)

                                      break;

                       }

                       cout<<endl;

               }

              

               return0;

}

 

 

最后一道, 很难, 怎么说呢, 是个失败的作品了, 因为我自己开始用最小生成树去做(感觉单向图应该和无向图差别不大。。。),于是WA了, 然后迫于时间压力不得已搜了个代码用于提交, 待考完ICS后得好好攻这道题,到那时再真正作总结吧!

poj  地震之后                               树形图的使用

//最小树形图(针对有向图),而非最小生成树(针对无向图)

//但这道题对应的是单向图,要先弄明白为何必须用最小树形树

//还未解决, 待重做!

 

#include <cstdio>

#include <cstdlib>

#include <cstring>

#include <string>

#include <cmath>

 

#define MAXN 120

#define inf 1000000000

 

double len[MAXN * 2][MAXN * 2] ={0};

 

class poi

{

public:

        intx;

        inty;

};

poi po[MAXN];

 

 

typedef double elem_t;

 

//以下为模板

//多源最小树形图,edmonds算法,邻接阵形式,复杂度O(n^3)

//返回最小生成树的长度,构造失败返回负值

//传入图的大小n和邻接阵mat,不相邻点边权inf

//可更改边权的类型,pre[]返回树的构造,用父结点表示

//传入时pre[]数组清零,用-1标出源点

elem_t edmonds (int n, elem_tmat[][MAXN * 2], int *pre)

{

        elem_tret = 0;

        intc[MAXN * 2][MAXN * 2], l[MAXN * 2], p[MAXN * 2], m = n, t, i, j, k;

 

        for(i = 0; i < n; l[i] = i, i++);

        do

        {

               memset(c, 0, sizeof (c)), memset (p, 0xff, sizeof (p));

               for(t = m, i = 0; i < m; c[i][i] = 1, i++);

               for(i = 0; i < t; i++)

                       if (l[i] == i && pre[i]!= -1)

                       {

                               for(j = 0; j < m; j++)

                                      if(l[j] == j && i != j && mat[j][i] < inf

                                              &&(p[i] == -1 || mat[j][i] < mat[p[i]][i]))

                                              p[i]= j;

                               if((pre[i] = p[i]) == -1)

                                      return-1;

                               if(c[i][p[i]])

                               {

                                      for (j = 0; j <= m;mat[j][m] = mat[m][j] = inf, j++);

                                      for(k = i; l[k] != m; l[k] = m, k = p[k])

                                              for(j = 0; j < m; j++)

                                                     if(l[j] == j)

                                                     {

                                                             if(mat[j][k] - mat[p[k]][k] < mat[j][m])

                                                                     mat[j][m]= mat[j][k] - mat[p[k]][k];

                                                             if(mat[k][j] < mat[m][j])

                                                                     mat[m][j]= mat[k][j];

                                                     }

                                      c[m][m]= 1, l[m] = m, m++;

                               }

                               for(j = 0; j < m; j++)

                                      if(c[i][j])

                                              for(k = p[i]; k != -1 && l[k] == k;

                                                      c[k][j] = 1, k = p[k]);

                       }

        }

        while(t < m);

        for(; m-- > n; pre[k] = pre[m])

               for(i = 0; i < m; i++)

                       if(l[i] == m)

                       {

                               for(j = 0; j < m; j++)

                                      if(pre[j] == m && mat[i][j] == mat[m][j])

                                              pre[j]= i;

                               if(mat[pre[m]][m] == mat[pre[m]][i] - mat[pre[i]][i])

                                      k= i;

                       }

        for(i = 0; i < n; i++)

               if(pre[i] != -1)

                       ret+= mat[pre[i]][i];

        returnret;

}

 

 

int main()

{

        intpoin = 0, linen = 0;

        intpar[MAXN] = {0};

        while(scanf("%d%d", &poin, &linen) != EOF)

        {

               //init

               memset(par,0, sizeof(par));

               memset(len,0, sizeof(len));

               par[0]= -1;

               for(int i = 0; i < MAXN; ++i)

               for(int j = 0; j < MAXN; ++j)

               {

                       if(i == j) len[i][j] = 0;

                       elselen[i][j] = inf;

               }

               intix = 0, iy = 0;

               for(int i = 0; i < poin; ++i)

               {

                       scanf("%d%d",&ix, &iy);

                       po[i].x= ix;

                       po[i].y= iy;

               }

               intsrc = 0, dest = 0;

               for(int i = 0; i < linen; ++i)

               {

                       scanf("%d%d",&src, &dest);

                       --src;

                       --dest;

                       doubledx = po[src].x - po[dest].x;

                       doubledy = po[src].y - po[dest].y;

                       doublelength = sqrt(dx*dx + dy*dy);

                       if(len[src][dest] > length)

                       {

                               len[src][dest]= length;

                       }

               }

               /*test

               for(int i = 0 ; i < poin; ++i)

                       {

                               for(int j = 0 ; j < poin; ++j)

                                      printf("%.1lf", len[i][j]);

                               printf("\n");

                       }

               */

               doubleans = edmonds (poin, len, par);

               if(ans >= 0) printf("%.2lf\n", ans);

               elseprintf("NO\n");

        }

        return0;

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

/*

//有向图的最小生成树问题

#include<iostream>

#include<cmath>

#include<iomanip>

#include<cstring>

#include<cfloat>                //包含浮点数的最大值的宏定义

using namespace std;

 

struct EDGE

{                               //double(a+b)!=(double)a+(double)b  a,b   int

        doubleweight;               //距离为浮点数,注意运算时要采取浮点数运算!

        intto;                      //尾顶点

}a[100];

double edge[100][100];

struct point

{

        doublex;

        doubley;

}dot[100];

 

int main()

{

        intn,m,i,j,k;

        while(!cin.eof())

        {

               memset(a,0,sizeof(a));

               cin>>n>>m;

               for(i=0;i<n;++i)

                       for(j=0;j<n;++j)

                       {

                               if(i==j)edge[i][j]=0;

                               elseedge[i][j]=DBL_MAX;

                       }

               for(i=0;i<n;++i)

                       cin>>dot[i].x>>dot[i].y;

               while(m--)

               {

                       cin>>i>>j;

                       i--;j--;

                       edge[i][j]=sqrt((dot[i].x-dot[j].x)*(dot[i].x-dot[j].x)+(dot[i].y-dot[j].y)

                               *(dot[i].y-dot[j].y));

               }

               //初始化选边数组

               for(i=0;i<n-1;++i)

               {

                       a[i].weight=edge[0][i+1];

                       a[i].to=i+1;

               }

               //0~k-2      k-1~n-2

               boolflag=true;

               for(k=1;k<n;++k)

               {

                       doublemin=DBL_MAX;

                       intmini=-1;

                       for(i=k-1;i<n-1;++i)

                               if(a[i].weight<min)

                               {

                                      min=a[i].weight;

                                      mini=i;

                               }

                       if(mini==-1)

                       {

                               flag=false;

                               break;

                       }

                       EDGEt;

                       t=a[k-1];

                       a[k-1]=a[mini];

                       a[mini]=t;

                       j=a[k-1].to;

                       for(i=k;i<n-1;++i)

                               if(edge[j][a[i].to]<a[i].weight)

                                      a[i].weight=edge[j][a[i].to];

               }

               if(flag)                           //标志存在最小生成树

               {

                       doublesum=0;

                       for(i=0;i<n-1;++i)

                               sum+=a[i].weight;

                       cout<<setiosflags(ios::fixed)<<setprecision(2)<<sum;

               }

               else

                       cout<<"NO";

               cout<<endl;

        }

        return0;

}

*/

 

图论的应用就总结到这里了, 今天做了数算网上的期中考试, 有个感觉就是算法的进程有点快啊, 但是又不深入, 或许是我自己没有深入吧, 但是学完就忘确实是个问题啊,看来是思考的还不够没有抓住本质啊, 理想的境界应该是记忆之后自然融合, 当再次需要时能够在头脑中快速生成比原来更好的算法, 不得不说, 还差得远啊!!!~

 

接下来一天得复习下ICS, 毕竟是个大课,虽然我觉得没啥意义, 将浮点数看完后就直接刷一套试卷, 发现自己的薄弱点并巩固, 也就差不多了, 毕竟我已经决定把大多数的时间和精力花在算法上!~

 

这几天虽然起的晚但做的事还是不少的,嘿嘿~

Goodnight!~

 

抱歉!评论已关闭.