DijKstra算法核心在于一个数组d[Max],d[i]表示的是i这个节点距离中央节点的距离,算法想要实现得就是一步一步的更新d[Max],知道所有的点都被访问连接过才可退出
准备阶段
int n,m,s,e;
int mp[210][210];
int d[210];
int vis[210];
n,m是有n个点,m条边,s是开始点(中央节点与d[Max]数组有关),所求最短路的点是e
mp[i][j]二维数组存放的是i点到j点的距离
输入与清空阶段
while(scanf("%d %d",&n,&m) == 2)
{
getchar();
memset(vis,0,sizeof(vis));
memset(d,0,sizeof(d));
for(int i = 0;i < 210;i++)
{
for(int j = 0;j < 210;j++)
{
if(i == j)mp[i][j] = 0;
else mp[i][j] = mp[j][i] = inf;
}
}
for(int i = 0;i < m;i++)
{
scanf("%d %d %d",&a,&b,&x);
getchar();
if(mp[a][b] > x)mp[a][b] = mp[b][a] = x;//防止两个点之间出现多条路径的问题,只记录最短的路径
}
scanf("%d %d",&s,&e);
getchar();
Dijkstra(s,e);
}
更新mp数组,除非为mp[i][i]赋值为零,其余均赋值为inf(较大的值),这是为了表示未知4距离,方便后续的输入更新mp
输入更新时要小心两点多路的情况,最后只要记录最短的一条路就好
寻找最短路dijkscar算法
int next;
//输入顶点s
for(int i = 0;i < 210;i++)
{
d[i] = mp[s][i];
}
vis[s] = 1;
for(int i = 0;i < 210;i++)
{
//循环找点
int minnum = inf;
for(int j = 0;j < 210;j++)
{
if(!vis[j] && d[j] < minnum)
{
minnum = d[j];
next = j;
}
}
if(minnum == inf)
{
break;
}
vis[next] = 1;
//找到点相连
for(int j = 0;j < 210;j++)
{
if(!vis[j] && d[j] > mp[next][j] + d[next])//可以松弛
{
d[j] = mp[next][j] + d[next];
}
}
}
if(d[e] != inf)
printf("%d\n",d[e]);
else
printf("-1\n");
先根据中央节点s所连接的边,更新一波d[i]数组,这样你有了一个点一组线,接着去寻找这组线中最短的然后走过去(更新vis数组)这样你有了两个点一组线,其中一个线还已经走过了(强烈建议画一下)根据你刚刚找到的那个点所连接的边,更新一波d[i]数组(这里专业一点叫松弛),对于每一个线,需要判断的是——它所下接的点有没有访问过,这条线的开头(s中央节点)到结尾(第一个下接的点)的距离,是不是比当前d[
]数组里存的小,只有小才有更新的必要嘛(这也是为什么一开始mp二维数组初始化为inf的原因)
就这样直到循环到没点可寻~~
但是对于dijkscar算法有一个致命性的弱点,就是如果有负权回路的情况出现呢,由于你的vis数组导致你求出的最短路错误,因为负权回路的出现,会导致没有最短路
在这里要特别注意一下,应为平常我们考虑的都是双向边,所以只要你输入的边值为负就已经是一条负权回路了。
这里以hdu2544经典模板题为例
首先进行准备阶段
int N,M;
struct edge
{
int from;
int to;
int cost;
}e[10010];
int d[110];
void init()
{
memset(e,0,sizeof(e));
}
相信这些的含义你都能看懂的,和bijkscar算法的差不多
接下来是输入与清空阶段
while(scanf("%d %d",&N,&M) == 2)
{
getchar();
if(!N && !M)break;
init();
for(int i = 1;i <= M * 2;i ++)
{
scanf("%d%d%d",&e[i].from,&e[i].to,&e[i].cost);
i++;
e[i].from = e[i - 1].to;//双向边
e[i].to = e[i - 1].from;
e[i].cost = e[i - 1].cost;
getchar();
}
if(Bellman(1,N))
{
printf("%d\n",d[N]);
}
else
{
cout<<"no way,there is a negetive circle"<<endl;
}
}
注意一下双向边的输入问题就好了
接下来是Bellman-Ford算法
for(int i = 1;i <= N;i++)
{
if(i != s)d[i] = inf;
else d[i] = 0;
}
开头一个独立的for循环仅有d[s]会被更新为0
for(int i = 2;i <= N;i++)
{
for(int j = 1; j <= M * 2;j++)
{
if(d[e[j].to] > d[e[j].from] + e[j].cost)
{
d[e[j].to] = d[e[j].from] + e[j].cost;
}
}
}
然后第二个独立的for循环
i所控制的是你的更新次数,因为每一次更新你肯定都会确定一个点的最短路,n个点我们需要n次更新
j所控制的就是遍历所有的边
就下来的松弛(优化)判断思想和dijkscar算类似
int flag = 1;
for(int j = 1; j <= M * 2;j++)
{
if( d[e[j].to] > d[e[j].from] + e[j].cost )
{
flag = 0;
break;
}
}
return flag;
接下来是负权回路的判断
你再进行更新一次,如果还能更新,那就肯定存在负权回路了
其实你可以发现,这一个for循环明显就是从上一个for循环了拆出来的一个小分之嘛,把上面for循环的i初始值设为1,内部加一个判断,这里就可以省去了,方便理解~~
仍然是2544这个题,接下来是spfa算法,最常用的算法
spfa是针对点来进行操作的(针对点有没有让你联想到prim算法——针对点求最小生成树)
对于针对点的算法,我们就会分为两种思路——第一种是链式前向星,第二种是二维结构体动态数组
先来在回顾一遍链式前向星的应用
准备阶段
using namespace std;
struct edge{
//int from;
int to;
int cost;
int reid;
}e[20010];
int N,M,cnt;
int vis[110];
int id[110];
int time[110];
int d[110];
reid是用来将这个点所包含的边前后链接起来的变量
time数组是用来判断有没有负权回路的情况
输入与清空
while(scanf("%d %d",&N,&M) != EOF)
{
getchar();
if(!N && !M)break;
init();
for(int i = 1;i <= M;i++)
{
int a,b,x;
scanf("%d%d%d",&a,&b,&x);
getchar();
add(a,b,x);//我这里默认为双向边,所以一旦有一个负数边的出现就是负环的出现
add(b,a,x);
void add(int from,int to,int cost)
{
e[cnt].to = to;
e[cnt].cost = cost;
e[cnt].reid = id[from];
id[from] = cnt;
cnt++;
}
void init()
{
for(int i = 0;i < 110;i++)
{
d[i] = inf;
id[i] = -1;
}
cnt = 0;
memset(e,0,sizeof(e));
memset(vis,0,sizeof(vis));
memset(time,0,sizeof(time));
}
先来看看init()函数的初始化,d[i]数组仍然初始化为最大值,id[i]数组初始化为-1——代表的含义是i点所连接边的下标是-1就是还未知所连接的下标,cnt初始化为0,它是所有边得下标变量
然后spfa算法
queue<int> q;
q.push(S);
vis[S] = 1;
d[S] = 0;
int flag = 0;
先把中央节点push进去,判断找过该点,flag为有无负权回路得情况
while(q.size())
{
int now = q.front();//q是整数队列,怎么用的结构体类型去接受呢:?
q.pop();
for(int i = id[now];~i;i = e[i].reid)
{
if(d[e[i].to] > d[now] + e[i].cost)
{
d[e[i].to] = d[now] + e[i].cost;
if(!vis[e[i].to])
{
vis[e[i].to] = 1;
time[e[i].to]++;
if(time[e[i].to] > N)
{
flag = 1;
break;
}
q.push(e[i].to);
}
}
}
if(flag)break;
vis[now] = 0;//来检查环的情况,如果没有负数环是不会回去的,判断有没有负环才会这么回溯!
}
就受弹出来的第一个点,遍历第一个点所连接得所有的边,更新d[i]数组,然后观察边所连接得点,更新拜访状态,拜访次数,
如果一个点的拜访次数大于N了得话,那就代表存在负权回路,原理和Bellman-Ford算法差不多,最后要注意得一点是vis[i]
要重新更新其状态为0,也是为了判断负权回路。
queue不能有优先级,因为按照给定得顺序,就是从中央节点bfs往外拜访,一旦打乱那么d[i]还没有更新好,你的下一步
更新就进行了。
接下来是队列的实现 看看准备工作
int dis[maxn];
int v,e;//点,边
queue<int> q;
int flag[maxn];//表示所有的点都不在队列中
int times[maxn];//表示每一个点的入队次数
//边集
struct graph
{
int s,e,w;
}es[maxn];
输入的就略过了 接下来进入spfa
for(i=1;i<=v;i++)
{
dis[i]=info;
flag[i]=0;
times[i]=0;
}
dis[s]=0;
q.push(s);
times[s]++;
还是先初始化一波,然后针对中央节点初始化一波
while(!q.empty())
{
int t = q.front();
q.pop();
flag[t]=0;
for(i=1;i<=e;i++)
{
if(dis[es[i].e]>dis[es[i].s]+es[i].w)
{
dis[es[i].e]=dis[es[i].s]+es[i].w;
if(flag[es[i].e]==0)
{
q.push(es[i].e);
flag[es[i].e]=1;
times[es[i].e]++;
if(times[es[i].e]==v)
return false;//存在负环//双向就已经是负环了!!
}
}
}
}
取出队列中的第一个点,这里的flag就是vis
然后优化所有的边,差不多的原理,在这里就不缀述了