最短路问题DijKstra算法Bellman-Ford和spfa算法

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"<
注意一下双向边的输入问题就好了

接下来是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 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 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
然后优化所有的边,差不多的原理,在这里就不缀述了

你可能感兴趣的:(算法入门)