今天又是寝室三个人一起睡到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!~