图论——最短路——算法(3.0)

最短路算法:

  1. floyd算法
  2. Dijkstra算法
  3. Dijkstra算法(堆优化)
  4. ford算法
  5. spfa算法(ford算法的队列优化)

一、只有5行代码的floyd算法:

1、 什么是floyd算法

弗洛伊德算法是解决多元最短路径的算法(什么是多源, 顾名思义就是起点有多个, 跑完floyd算法就算出以每个顶点做起点到各个点的最短路径)。

2、时间复杂度 O(n^3), 空间复杂度O(n^2)

3、适用性:

			1、多源最短路
			2、带负权值的
			优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
			缺点:时间复杂度比较高,不适合计算大量数据。

4、代码实现

flody算法是从中间结点的角度来考虑的

for(k = 0;k < n;k++)
	for(i = 0;i< n;i++)
		for(j = 0;j <n;j++)
			if(grap[i][k]!=inf&&grap[k][j]&&grap[i][j] > grap[i][k]+grap[k][j])
				 grap[i][j] = grap[i][k]+grap[k][j];

floyd算法用到的是动态规划算法。
动规公式: grap[i][j] = min(gtap[i][j], grap[i][k]+grap[k][j]).

对于floyd的证明:

floyd的证明

二、单源最短路径的Dijkstra算法(未用堆排优化):

1、什么是Dijkstra

2、时间复杂度O(n^2):

		为优化的时间复杂度:外循环n-1次, 内循环 2*n次 , 所以时间复杂度 O((n-1)(2*n))近似 O(n^2)

3、适用性:

		1、单源最短路径
		2、不带负权值的边

4、代码实现:

对于dijkstra我一般喜欢用邻接矩阵, 邻接表储存图

(1)实现代码1
/**********

原理

每次 通过 未使用的点中,距起点最近的那个点 去松弛其他点的距离  (其实就 用近点(从最近开始, 然后是第二近...) 松弛远的点, 本质就是贪心思想)

所以dijkstra 时间复杂度主要跟点的个数有关

***********/
#include
#define inf 0x3f3f3f
using namespace std;
int G[1111][1111];// 邻接矩阵 ,  也可以用邻接表
int n; //图中点的个数

void Dijkstra(int s, int d)// s 起点 , 终点
{
	int i;
	int dis[1111];// 存s到  其他各点的距离

	/**初始化距离**/
	for(i = 1;i <= n;i++) dis[i] = inf;// 到其他点的距离为无限大
	dis[s] = 0; //初始化到自己为1

	/***标记数组初始化****/
	int book[1111];//标记数组,标记该点是否使用 
	memset(book, 0 , sizeof(book));//初始化 0表示未使用


	for(i = 1;i <= n-1;i++)// 循环 n-1 次, 最远的那个点(就是距离 起点 第n近的点(这里第1近的点是起点自己)肯定不能松弛其他店)
	{
		
		/**********找点过程**********/
		int u = -1;//-1 u表示未使用的点中,距起点最近的那个点,初始化z最近点的编号为 -1
		int Min = inf;// 初始化 未使用的点中,距起点最近的那个点到起点的距离为 inf
		for(int j = 1;j <= n;j++)
		{
			if(book[j] == 0&& dis[j] < Min)//寻找未使用的点中,距起点最近的那个顶点
			{
				Min = dis[j];
				u = j;//储存该顶点
			}
		}
		if(u == -1)break;//没找到 可以提前结束
		book[u] = 1;//标记该点使用

		/***********通过 u点松弛其他点的过程*********/
		for(int j = 1;j <= n;j++)
		{
			if(G[u][j] < inf)//查找该点相连接的点(查看改点有哪些出边)
			{
				if(dis[j]> dis[u]+G[u][j])//判断源点是否能通过点u 与点j缩短距离(松弛操作)
				{
					dis[j] = dis[u]+G[u][j];
				}
			}
		}

	}

	printf("%d\n", dis[d]);

}
(2) 实现代码2
#include
#include
#define inf 0x3f3f3f
int G[1111][1111];
int n; //点 的个数

void Dijkstra(int s, int d)
{
    int i;

    /***初始化起点到其他点的距离***/
    int dis[1111];//表示 起点s  到其他点的距离
    for(i = 1; i <= n; i++) dis[i] = G[s][i]; //将邻接矩阵的值赋给 dis[]
    dis[s] = 0;

    /**初始化 标记数组***/
    int book[1111];
    memset(book, 0, sizeof(book)); //标记数组
    book[s] = 1; //标记自己


    for(i = 1; i <= n-2; i++) // 因为 在初始化距离是 起点s已经被使用了 所以循环 n-2次就行了
    {
        int u = -1;//-1 初始化 未被使用的到起点最近的点
        int min = inf;// 初始化距离

        /****找点过程****/
        for(int j = 1; j <= n; j++)
        {
            if(book[j] == 0&& dis[j] < min)//寻找离源点最近且未被标记的顶点
            {
                min = dis[j];
                u = j;//储存该顶点
            }
        }
        if(u == -1)break;
        book[u] = 1;//标记该点

        /********通过u点松弛过程*********/
        for(int j = 1; j <= n; j++)
        {
            if(G[u][j] < inf)//查找该点相连接的点(查看改点有哪些出边)
            {
                if(dis[j]> dis[u]+G[u][j])//判断源点是否能通过点u 与点j缩短距离(松弛操作)
                {
                    dis[j] = dis[u]+G[u][j];
                }
            }
        }

    }

    printf("%d\n", dis[d]);

}
(3)实现代码3:略

对于不理解为什么 最外层for 循环多少次的 可以改成while(1): 通过if(u == -1) break;结束循环。
注释:u == -1表示 找不到未被使用的 到起点最近的 点(就是当前未被使用点的集合中 找不到到起点距离最小的点)。

6、优化后的dijkstra:

时间复杂度O((m+n)logn);// n点的个数 m边的个数

/*********
  优化部分:通过小顶堆,优化了 对于 被使用的点中,距起点最近的点 的查找。
************/
#include
using namespace std;
const int INF = 0x3f3f3f;

struct Edge
{
    int v;
    int w;
};
int n, m;
vector <Edge> G[5000];//vector形式的邻接表
typedef pair<int, int> P;//第二个值表示点,  第一个值表示 从起点到该点的距离


void Dijkstra(int s, int e)
{
    priority_queue<P, vector<P>, greater<P> > que;//按照greater排序, 实现小顶堆
    while(!que.empty())que.pop(); //初始化堆

    /******初始化距离********/
    int dis[511111];
    fill(dis,dis+n+1, INF);//初始化
    dis[s] = 0; //到自己的距离 为 0;

    /*********先将自己(自己是距离起点最近的点)压入优先队列中***********/
    que.push(P(0,s));

    while(!que.empty())//直到队列为空才结束
    {

        P temp = que.top();//顶部赋给 temp
        que.pop();//出队

        int u = temp.second;// 表示堆中距离起点最近的点

        if(temp.first > dis[u])continue; // 该操作 相当于 标记点u 是否被重复使用, 可以更换为标记数组

        for(int i = 0; i < (int)G[u].size(); i++) //通过该顶点u(u的边都循环一遍) 进行松弛操作
        {
            Edge e = G[u][i];
            if(dis[e.v] > dis[u] + e.w)
            {
                dis[e.v] = dis[u]+e.w;
                que.push(P(dis[e.v], e.v));
            }


        }




    }

    cout<<dis[e]<<endl;


}

三、单源负权的ford算法:

时间复杂度:n*m; // n节点数 m 边数

/********
原理
ford是通过 路(准确的说是迹)的路长 进行松弛的。(第一次是 找路径为1的路 松弛对应终点 ,在找路径为2的路 松弛 )

过程 从路长从小到大 松弛 每个点。

所以 ford 算法 跟边有关
***********/
#include

using namespace std;
const INF = 0x3f3f3f;
const maxn = 1e5;

struct ENode
{
    int f;
    int to;
    int w;
} E[maxn];


int Ford(int s)
{
    int flag = 0; // 标记 有没有 负边
    int dis[maxn];
    memset(dis, INF, sizeof(dis));
    dis[s] = 0;
    
    // n是图中 点的个数, m是图中边的个数

	/********通过路长松弛******/
    for(int i = 1; i <= n-1; i++) // 循环 n-1次,原因 起点到每个点的最短路 都是迹  迹的长度最大为n-1, 所以循环n-1次
    {
        for(int j = 1; j <= m; j++) // 循环m个边
        {
            int u = E[j].f; int v = E[j].t;  int w = E[j].w;
            if(dis[v] > dis[u]+ w)
            {
                dis[v] = dis[u]+w;
            }
        }
    }
/**********判断是否有负环***********************/
    for(int j = 1; j <= m; j++) //再循环一次边
    {
        int u = E[j].f; int v = E[j].t; int w = E[j].w;
        if(dis[j] > dis[u]+w)//如果dis 松弛还能变小,就有负边
        {
            flag = 1;
            break;
        }
    }
    return flag;


}

四、单源负权的spfa算法:

复杂度: o(e); e表示边数

/***********
	ford算法 是通过最短路的路长  来松弛的路的终点, 每次循环所有的边 每次循环是 不是每个边都被用上
	所以spfa 就通过队列来优化 有点像 bfs的涟漪效应。
	
	通过队列将路径为1的压入队列, 然后在利用路径为1的路 的终点,利用该终点的边  ,来松弛路径为2的路对应的终点。依次类推。
*************/
#include
#include
#include
#include
#include
using namespace std;
const int INF = 0x3f3f3f;
const int maxn = 1e5;

struct ENode
{
    int f;
    int to;
    int w;
};

vector<ENode>E[maxn];//邻接表 , 或者叫 vector+ 前向星, 或者叫链式前向星。
 int dis[maxn];//记录距离的数组
bool spfa(int s, int n)
{
    int cnt[maxn];//统计入队次数, 判断 是否存在负环
    memset(cnt, 0, sizeof(cnt));
   
    memset(dis, INF, sizeof(dis));//初始化
    dis[s] = 0;

    bool vis[maxn];//标记数组
    memset(vis, false, sizeof(false));//初始化

    queue<int>que;//定义一个队列

    que.push(s);//将起点压入
    vis[s] = true;//标记
    cnt[s]++;

    while(!que.empty())
    {
        int u = que.front();
        que.pop();

        vis[u] = false;
        for(int i = 0;i < (int)E[u].size();i++)
        {
            int v = E[u][i].to;
            if(dis[v] > dis[u]+ E[u][i].w)
            {
                dis[v] = dis[u]+E[u][i].w;
                if(!vis[v])
                {
                    vis[v] = true;
                    que.push(v);
                    if(++cnt[v] == n)return false;// 通过每个点入栈的次数判断是否存在负环路; 本质就是图中的最短路的 路长不大于n-1 ,如果大于n-1就有负环 路导致。
                }
            }
        }

    }
    return true;

}

最后补充一点: 从某点 s 到另一点 的最短路 这条路 一定是通路(通路就是点都不同的一条路), 也一定是迹(迹就是边都不相同的一条路)

你可能感兴趣的:(算法,#,图论,(学习))