今天又是寝室三个人一起睡到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
#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 { 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 { road[i].from=0; road[i].to=i+1; road[i].weight=edge[0][i+1]; } //逐个加入 for(k=1;k { intmin=MAX,mini=-1; for(i=k-1;i if(road[i].weight { 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 { inttmp=road[i].to; if(edge[tmp][j] { road[i].weight=edge[tmp][j]; road[i].from=j; } } } //下面计算最小生成树的总权值 intsum=0; for(i=0;i sum+=road[i].weight; cout< return0; } 下面是拓扑排序的问题, 这个算法极其简单,就算不看书应该也能想出来, 只要用一个队列,不断将入度为0的顶点加入即可, 作业题中甚至都是无环图,更加简化了, 不过也增加了一个新要求, 同等条件下标号小的先输出, 这就需要用堆而不是队列来维护了(当然也可以用优先队列),这里稍微提下, 我到今天才真正学习了堆的实现啊啊啊, 以前牵涉到堆的情况比较少, 我一直都是用STL中的堆算法,包含在algorithm头文件中, 但是这个堆算法使用时需要注意的东西太多, 远不如自己实现来得灵活!~ 程序如下: /* Name: 拓扑排序的实现 , 要求在同等条件下,编号小的顶点在前 Copyright: poj Author: xiaoli Date: 10/11/13 23:34 Description: 1. 拓扑排序可以用队列实现,但为了能保证同等条件下标号小的先输出,应该使用堆结构 2. 可以用STL中的优先队列或者堆算法(包含在algorithm中,是对已有的一个容器建堆并维护) 但自己写会更灵活可靠,需要记忆的东西也更少 3. 这道题不需要判断是否有回路, 判断是否有回路的方法:应该设置一个标记记录是否访问, 在最后枚举检验 是否有标志为未访问的元素, 如果有说明原图存在回路 */ #include #include #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 { if(j 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'< my.pop(); for(i=1;i<=n;++i) if(edge[t][i]) //题目保证不会出现环,应该可以不用做标记 { in[i]--; if(in[i]==0) //入度减为0则加入 my.push(i); } } cout< return0; } 关于最短路径问题, 算法太多了, 戴爷爷的用于处理单源最短路径十分有效, 但是作业题牵涉到一堆顶点间的最短路径, 所以最直接的是先把所有的都求出来, 即求出任意一对顶点间的最短路径, 这样就不如使用Floyed算法了,因为这个算法实在是太好实现了, 但是由于要记录路径, 所以稍微有些麻烦, 对于求一对顶点间的最短路径并记录用戴爷爷的算法也好实现,弄一个父亲-儿子链接即可, 在Floyed算法中,因为是一次性求所有最短路径, 所以路径也必须全部对应记录才行, 我是定义了一个三维数组来记录每对顶点间的最短路径, 也可以用链表的形式记录路径,, 这样会省些空间, 但肯定都牵涉到不断地拷贝路径的操作, 总觉得应该有更简单的方法来实现路径的记录呢!~ 程序如下: /* Name: 最短路径的Floyed算法实现(需要记录路径) Copyright: poj 我爱北大 Author: xiaoli Date: 10/11/13 23:43 Description: 1. 题意是无向图,但对于每一对顶点,路径是2种, 长度相同 2. 在Floyed的算法上增加一个结构用于记录路径 3. Floyed算法的推导公式中满足的性质允许我们使用类似滚动数组的结构, 即不断对一个数组更新即可,不需要用两个交换操作! */ #include #include #include #define MAX 0x3fffffff //防止正向溢出 using namespace std; map 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
{ 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
for(j=0;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
for(i=0;i
for(j=0;j
{ if(i==j||i==k||j==k) //情况不变,不需要操作 continue; t=a[i][k]+a[k][j];