您的位置:首页 > 其它

acm,基本图论算法及其解释

2014-06-29 16:09 141 查看
图论基本算法其实就6个,两个生成树,4个最短路径。

之前提到过的BFS算法,优点在于不用显式构建整个图,每次找到相邻即可,发现要到达的或者访问完毕即可退出。一般用于只求最短路。

技巧:设置数组,记录每个点的前驱点。

但是对于DFS来说需要显式构建整个图,因为回退的时候还会到之前的点,或者通过另一种方式到达。用于求可行路径总数。

技巧:强大的剪枝........

--------------------------------------------------------------------------------------------------------------------------------------------------

prim

算法核心思想:用集合论的观点来看,则是不断将离这棵树最近的点归约进来。

weight
记录此集合中到各个点(i)的最短距离

adjex
记录此集合中哪个点到各个点(i)的最短距离

visit
记录哪些点属于那个集合 1为此树中,0为未归约进来的点

#include <stdio.h>
#include <string.h>
#define N 401
int state

;
int maps

;

int solve(int n)
{
int weight
;
int adjex
;
int visit
;
int i,j,k;
int minnum,ans;
ans=0;
for(i=0;i<n;i++)
weight[i]=maps[0][i],adjex[i]=visit[i]=0;
visit[0]=1;
for(i=1;i<n;i++)
{
minnum=0xffffff;
for(j=1;j<n;j++)
if(minnum>weight[j] && visit[j] == 0)
minnum=weight[j],k=j;
visit[k]=1;
ans+=minnum;
for(j=1;j<n;j++)
if(maps[k][j]<weight[j] && visit[j]==0 )
weight[j]=maps[k][j],adjex[j]=k;

}
return ans;

}

int main()
{
int i,j,k,n;

while(	scanf("%d",&n)!=EOF)
{
for(i=0;i<n;i++)
for(j=0;j<n;j++)
scanf("%d",&maps[i][j]);

printf("%d\n",solve(n));
}

return 0;
}


Kruskal

核心思想:不断用最短的边构建生成树,且避免回路。

使用并查集来快速查询一条边的两点是否在同一集合中,避免回路

/*
课堂上,归并的时候以边中最小结点编号作为连通子图的编号
此处使用归并集
*/
void kruskal (edgeset ge, int n, int e)
// ge为权按从小到大排序的边集数组
{
int set[MAXE], v1, v2, i, j;
for (i=1;i<=n;i++)
set[i]=0;   // 给set中每个元素赋初值
i=1; // i表示获取的生成树中的边数,初值为1
j=1; // j表示ge中的下标,初始值为1
while (j<n && i<=e)
// 检查该边是否加入到生成树中
{
v1=seeks(set,ge[i].bv);
v2=seeks(set,ge[i].tv);
if (v1!=v2) // 当v1,v2不在同一集合,该边加入生成树
{
printf(“(%d,%d)”,ge[i].bv,ge[i].tv);
set[v1]=v2;
j++;//j是为了判断是否已经使n个点加入生成树中,是则结束
}
i++;
}
}
int seeks( int *set,int i)
{
while(set[i]!=i)
i=set[i];
return i;
}


注:并查集可以使用路径压缩,每次查询的时候递归返回,使查询时间到O(1)。

Dijkstra

每次用图中节点更新所有点路径,以达到最短路径的目的
1.循环n次
2.在所以d[]节点中找到最小点x
3.标记该点x
4.对于从该点出发的边更新d[y]=min{d[y],d[x]+w[x][y]}
void Dijkstra(){
int k;
for(int i=1;i<=n;i++)
dis[i] = map[1][i];
for(int i=1;i<n;i++){
int tmin = maxint;
for(int j=1;j<=n;j++)
if( !used[j] && tmin > dis[j] ){
tmin = dis[j];
k = j;
}
used[k] = 1;
for(int j=1;j<=n;j++)
if( !used[j] &&dis[k] + map[k][j] < dis[j] )  //跟佛洛依德算法相似,看中间是否有个中间点
dis[j] = dis[k] + map[k][j];
}
printf("%d",dis
);
} /* 求1到N的最短路,dis[i] 表示第i个点到第一个点的最短路 By Ping*/
//找未用过的最短邻接点,以此为中转修正其余点,直到全部完成


Floyd

注意使用更新的中间结点k在最外层。(之前写了个在最内层,违背了方法。用一个节点更新所有路径)

可以有负权边的图进行计算

对于任意两点 (i,j) 之间,每次使用一个结点更新所有路径。同时我们知道,任意两点之间最短距离最多经过n-1个结点

这样下来,对于特定两点来说加入的点的次序变得不重要

1.加的点不再所求两点的路上,无所谓

2.从与两点直接相连到间接相连,符合我们的思维

3.最重要,若是无序,如果从中间一个开始怎么办?

若是中间点,则会更新与之相连点间的最短路径,这样对于下一次次中间点有帮助,同时也相当于先修了"中间路".

//d[i][j]用于记录从i到j的最短路径的长度
// path[i][j]用于记录从i到j的最短路径上j点的前一个节点
//d[i][j]用于记录从i到j的最短路径的长度
void Floyd(int num,int **path,int**d,int **a)

{
int i,j,k;
for(i=0;i<num;i++)
{
for(j=0;j<num;j++)//初始化
{
if(a[i][j]<max) path[i][j]=j;//从i到j有路径
else path[i][j]=-1;
d[i][j]=a[i][j];
}
}
for(k=0;k<num;k++)
for(i=0;i<num;i++)
for(j=0;j<num;j++)
if(d[i][j]>d[i][k]+d[k][j])//从i到j的一条更短的路径
{
d[i][j]=d[i][k]+d[k][j];
path[i][j]=path[i][k];
}
}


Bellman-Ford

之前的求最短路径要求图中无负向边,该算法可以求有负向边的图最短路径。

对每条边进行松弛,每次松弛至少可以增加一个抵达点,所以外循环至多v-1次,

与迪杰斯特拉不同之处在于:迪杰斯特拉每次使用一个点更新后不再使用,而贝尔曼的每条边每次都使用。

迪杰斯特拉每次都从已找到的最短路的集合中寻找一条连接到没找到的集合的一条路,不会再次计算集合内部的点的最短路。

解释:

dijkstra由于是贪心的,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径(d[i]<--dmin);但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了。
比如n=3,邻接矩阵:
0,3,4
3,0,-2
4,-2,0
用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。


摘自维基:

贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。 然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共|V | − 1次,其中 |V |是图的边的数量。在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。这样的策略使得贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。

贝尔曼-福特算法的最多运行O(|V|·|E|)次,|V|和|E|分别是节点和边的数量)。

算法实现:

#include <iostream>
using namespace std;
const int maxnum = 100;
const int maxint = 99999;

// 边,
typedef struct Edge{
int u, v;    // 起点,重点
int weight;  // 边的权值
}Edge;

Edge edge[maxnum];     // 保存边的值
int  dist[maxnum];     // 结点到源点最小距离

int nodenum, edgenum, source;    // 结点数,边数,源点

// 初始化图
void init()
{
// 输入结点数,边数,源点
cin >> nodenum >> edgenum >> source;
for(int i=1; i<=nodenum; ++i)
dist[i] = maxint;
dist[source] = 0;
for(int i=1; i<=edgenum; ++i)
{
cin >> edge[i].u >> edge[i].v >> edge[i].weight;
if(edge[i].u == source)          //注意这里设置初始情况
dist[edge[i].v] = edge[i].weight;
}
}

// 松弛计算
void relax(int u, int v, int weight)
{
if(dist[v] > dist[u] + weight)
dist[v] = dist[u] + weight;
}

bool Bellman_Ford()
{
for(int i=1; i<=nodenum-1; ++i)
for(int j=1; j<=edgenum; ++j)
relax(edge[j].u, edge[j].v, edge[j].weight);
bool flag = 1;
// 判断是否有负环路
for(int i=1; i<=edgenum; ++i)
if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight)
{
flag = 0;
break;
}
return flag;
}
int main()
{
//freopen("input3.txt", "r", stdin);
init();
if(Bellman_Ford())
for(int i = 1 ;i <= nodenum; i++)
cout << dist[i] << endl;
return 0;
}


SPFA

初始化的时候只添加出边,不添加入边不然顶住

使用队列优化 Bellman-Ford 算法其原理是利用:

不一定每次都要把所有的边都松弛一遍,只有在上次更新过的点所连的边才会对下次松弛有作用。而SPFA则记录了这些刚刚更新过点

若一个点进队N次则判断该图有环,退出。或者队空,退出

初始化时仅起始点进队.

/*
一般使用前向星的数据结构(一般用于点太多或者一条边多个权值)
因此先对其按(起点第一关键字,终点第二关键字)排序得到point数组
在数组point中,其元素个数比图的节点数多1(即共有v+1个节点),且一定有point[1]=1,point[v+1]=E+1.
对于节点i,其对应的边存放在边信息数组的位置区间为[point[i],point[i+1]-1]
如果point[i]=point[i+1],则节点没有出边。
*/
int SPFA(int source)
{
int i,j,flag,node;
int tag[nodenum]={0};
for(i=0;i<nodenum;i++)
dist[i]=0xffff;
dist[source]=0;
tag[source]=1;
heap[tail++]=source;
//此处先排序再得出point数组
while(front<tail)
{
node=heap[front++];
for(i=point[node];i<point[node+1];i++)
if(dist[ v[i]]>dist[u[i]]+w[i])
{
dist[v[i]]=dist[u[i]]+w[i],heap[tail++]=v[i];
tag[v[i]]++;
if(tag[v[i]]>=nodenum)
return 1;

}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: