算法笔记——最短路径问题:Dijkstra算法/BF和SPFA/Floyd

11 篇文章 0 订阅

 1. Dijkstra算法:解决单源最短路径无负边

//=============算法思路====================
//1:邻接矩阵:一般不超过1000,无向边和有向边的区别
//2:邻接表(用链表),无向边和有向边的区别
//2.1:为了简便,vector(变长数组)
//3:Dijkstra算法
//3.1:Di伪代码
//G为图,一般设为全局变量;数据d为源点到各点的最短路径长度,s为起点
//D(G,D[],S)
//{
//初始化
//for(循环n次)
//{
//    u为D[u]中最小的还未被访问的顶点的标号;
//    设u已访问
//        for(以u为出发能到达的所有顶点V)
//    {
//        if(v未被访问&&以u为中介点使s到顶点v的最短路径更优)
//           优化d[v];
//             pre[v]=u;//记录v的前驱顶点是u//老是思考的一个问题:边界情况呢?
//4:怎么记录最短路径?上面可以进行优化的隐含信息:使d[v]变小的情况为让u作为s到v最短路径的前一个节点。
//4.1:记录v的前驱节点,pre[],第一个节点是没有前驱节点,初始化为自己
//4.2:然后递归找最短路径
//5:有多条最短路径:找第二标尺
//5.1:每一条边再加一个边权,最短路径的情况下求最小花费
//5.1.1:要判断最短路径相等的情况下,进行最小花费的判断
//5.2:每一个点增加一个点权(例如每个城市收集的物资),在前面的情况下求点权最大
//5.1.1:要判断最短路径相等的情况下,进行最大权值的判断
//5.3:有多少条最短路径
//5.3.1:更新的情况下:用num[u]代替num[v],表示最短路径(s->v)与(s->u)相同
//5.3.2:相等的情况:那么为num[v]+num[u],表示到达该点有多条路径
//}
//=============算法改进====================
//Dj+DFS
//1:先用DJ记录下最短路径;
//1.1:有可能有多个前驱节点:所以设置为vector<int>pre[MAXV]
//2:遍历最短路径(递归树),找到第二标尺最优的路径
//2.1:怎么写DFS函数:
//2.1.1:作为全局变量的第二标尺optValue
//2.1.2:记录最有路径(使用vector存储)
//2.1.3:临时记录DFS遍历叶子节点时的路径tempPath(使用vector存储)
//2.2:思路:
//2.2.1:递归边界(到达起点,也就是递归树的叶子节点)
//2.2.2:此时tempPath中存放了路径,再把起点加入,进行第二标尺的计算,与optValue进行比较
//2.3:递归式,遍历pre[v]的所有节点进行递归
//2.4:怎么生成tempPath
//2.4.1:访问当前节点v时把v加入
//2.4.2:遍历pre[v]进行递归
//2.4.3:所有节点全部遍历完之后删除v
//=============算法改进====================
//=============算法思路====================
#include <iostream>
#include <algorithm>
#include<vector>
#include<cstdio>
using namespace std;
//定义MAXV为最大顶点数,INF为一个很大的数
const int MAXV = 1000;
const int INF = 1000000000;//设为很大的数
//邻接矩阵版
int n,m,s, G[MAXV][MAXV];//n为顶点数,m为边数,s为起点,MAXV为最大顶点数
int d[MAXV],pre[MAXV];//起点到各点的距离
bool visit[MAXV] = { false };//点是否被访问
//DijkstraDFS新增的变量声明
//1:pre不进行初始化,那么起点的前驱节点设置为自己?
vector<int>pre[MAXV];//存放节点的前驱节点
int optValue;//第二标尺的最优值
vector<int>tempPath, path;//存放最佳路径和临时路径
void DijkstraDFS(int s) {
    //初始化
    fill(d, d + MAXV, INF);//需要加上头文件#include <algorithm>using namespace std;
    d[s] = 0;//自己到自己的距离为0
    /*visit[s] = true;这点在后面的循环中会进行执行*/
    for (int i = 0; i < n; i++)
    {
        int u = -1, min = INF;//u用记录最小值的下标,min用来记录最下值
        //利用一个for循环查找出当前最小的下标
        for (int j = 0; j < n; j++)
        {
            if (visit[j] == false && d[j] < min)
            {
                u = j;
                min = d[j];
            }
        }
        //防止出现不连通图,进行判断
        if (u == -1) { return; }
        /* else 有点多余,如果前面条件满足,就return了*/
        visit[u] = true;
        //以u出发到达的其他顶点v是否可以进行更新
        for (int v = 0; v < n; v++)
        {
            //还加了一个条件:if(G[u][v]!=INF//说明有边
            if (visit[v] == false && G[u][v] != INF  )
            {
                .if (d[u] + G[u][v] < d[v])
                {
                    d[v] = d[u] + G[u][v];
                    //进行清空
                    pre[v].clear();
                    pre[v].push_back(u);//把u加入前驱节点
                }
                else if (d[u] + G[u][v] = d[v])
                {
                    pre[v].push_back(u);
                }
                
               
            }
        }
    }
}
void DFS(int v)
{
    if (v == s) {
        tempPath.push_back(v);
        int value=0;//用来记录tempPath中的值,判断是否要对optValue进行更新
        //计算value值
        //边权之和;点权之和

        if (value优于optValue) {
            optValue = value; path = tempPath;
        }
        tempPath.pop_back(v);
        //要进行返回,递归边界
        return;
    }
    tempPath.push_back(v);
    for (int i = 0; i < pre[v].size(); i++)
    {
        DFS(pre[v][i]);
    }
    tempPath.pop_back(v);
}
void Dijkstra(int s)//s为起点
{
    //初始化
    fill(d, d + MAXV, INF);//需要加上头文件#include <algorithm>using namespace std;
    d[s] = 0;//自己到自己的距离为0
    /*visit[s] = true;这点在后面的循环中会进行执行*/
    for (int i = 0; i < n; i++)
    {
        int u = -1, min = INF;//u用记录最小值的下标,min用来记录最下值
        //利用一个for循环查找出当前最小的下标
        for (int j = 0; j < n; j++)
        {
            if (visit[j] == false && d[j] < min)
            {
                u = j;
                min = d[j];
            }
        }
        //防止出现不连通图,进行判断
        if (u == -1) { return; }
        /* else 有点多余,如果前面条件满足,就return了*/
        visit[u] = true;
        //以u出发到达的其他顶点v是否可以进行更新
        for (int v = 0; v < n; v++)
        {
            //还加了一个条件:if(G[u][v]!=INF//说明有边
            if (visit[v] == false &&G[u][v]!=INF &&d[u] + G[u][v] < d[v])
            {
                d[v] = d[u] + G[u][v];
                pre[v] = u;//记录v的前驱节点是u
            }
        }
    }
    
}
void DFS(int s, int v)//输出起点到终点的最短路径
{
    //递归边界
    if (s == v) { printf("%d\n", s); return; }
    DFS(s, pre[v]);//往前递归
    //回来时的处理
    printf("%d\n", v);//每一层回来输出每一层的顶点号
}
//邻接表法
//结构构造函数初始化:与c++和java中构造函数类似
struct node {
    int v;//变得终点号
    int w;//边权
    //定义无参构造函数
    node(){}
    //构造函数简化成一行
    node(int _v, int _w) :v(_v), w(_w) {}
};
//算法初始化和找u的过程是一样的,只是查找以u的中介的其他顶点更新不一样
vector<node>adj[MAXV];//图G,adj[u]存放从顶点u出发可以到达的所有顶点
void Dijkstra1(int s)//s为起点
{
    //初始化
    fill(d, d + MAXV, INF);//需要加上头文件#include <algorithm>using namespace std;
    d[s] = 0;//自己到自己的距离为0
    /*visit[s] = true;这点在后面的循环中会进行执行*/
    for (int i = 0; i < n; i++)
    {
        int u = -1, min = INF;//u用记录最小值的下标,min用来记录最下值
        //利用一个for循环查找出当前最小的下标
        for (int j = 0; j < n; j++)
        {
            if (visit[j] == false && d[j] < min)
            {
                u = j;
                min = d[j];
            }
        }
        //防止出现不连通图,进行判断
        if (u == -1) { return; }
        /* else 有点多余,如果前面条件满足,就return了*/
        visit[u] = true;
        //以u出发到达的其他顶点v是否可以进行更新
        //adj[u].size()获取u所连的边的个数
        for (int j = 0; j < adj[u].size(); j++)
        {
            //通过邻接表直接获得u能到达的顶点v
            int v = adj[u][j].v;//adj中储存了各条边的情况
            //还加了一个条件:if(G[u][v]!=INF//说明有边
            if (visit[v] == false && d[u]+ adj[u][j].w< d[v])
            {
                d[v] = d[u] + adj[u][j].w;
            }
        }
    }

}
//对应DJ+DFS:1.1:找最短路径,有可能有多个前驱节点:所以设置为vector<int>pre[MAXV]
void Dj(int v)
{
	fill(d, d + MAXV, INF);
	d[v] = 0;
	for (int i = 0; i < n; i++)
	{
		int u = -1, min = INF;
		for (int j = 0; j < n; j++)
		{
			if (vis[j] == false && d[j] < min)
			{
				u = j;
				min = d[j];
			}
		}
		if (u == -1)return;
		vis[u] = true;
		for (int k = 0; k < n; k++)
		{
			if (vis[k] == false && d[u] + G[u][k] < d[k])
			{
				//d[u]要进行更新
				d[k] = d[u] + G[u][k];
				pre[k].clear();
				pre[k].push_back(u);
			}
			else if (vis[k] == false && d[u] + G[u][k] == d[k])\
			{
				pre[k].push_back(u);

			}
		}
	}
}
int main()
{
    int u, v, w;
    scanf("%d%d%d", &n, &m, &s);//顶点个数、边数、起点编号
    fill(G[0], G[0] + MAXV * MAXV, INF);//初始化图
    for (int i = 0; i < m; i++)
    {
        scanf("%d%d%d", &u, &v, &w);//输入u、v以及u->v的边权
        G[u][v] = w;
    }
    Dijkstra(s);
    for (int i = 0; i < n; i++)
    {
        printf("%d ", d[i]);//输出所有顶点的最短距离
    }
    //for (int i = 0; i < n; i++)
    //{
    //    printf("%d", pre[i]);//输出所有顶点的最短距离
    //}

    return 0;
}



  1. BF:可以解决负边问题

2.1:[PAT1003]

//BF算法:解决单源路径最短问题(可处理负边权的问题)
//思路:
//1.1对图中的边进行V-1次遍历(用最小生成树理解松弛操作不会超过V-1:最小生成树最多不会超过V个,并且起点已经为d[s]=0,不能再进行优化),对每一个边,看是否可以进行优化
//1.2最后再检查一下是否有负环
//2:解决实际问题:有多条路径最短的问题:记录num值的时候,为了防止重复的值发生,则需要重新记录(并且用set进行存储)
//出现的问题:
#include <iostream>
#include<vector>
#include<set>
#include<algorithm>
using namespace std;
const int maxv = 500;
const int INF = 1000000000;
int n, m;//定义点和边
struct node{
    int v;//边的终点编号
    int w;//边权
    node(int v_, int w_) :v(v_), w(w_) {};
};
vector<node>adj[maxv];//图G的邻接表
int weight[maxv],num[maxv],w1[maxv];//用来存放个点的权值和数量,w1是用来存放最大的点权
set<int>pre[maxv];//记录前驱的数组
int d[maxv];

void kus(int s) {
    //初始化
    fill(num, num + maxv, 0);
    fill(d, d + maxv, INF);
    d[0] = 0;
    num[s] = 1;
    w1[s] = weight[s];//这一步忘记加入
    for (int i = 0; i < n - 1; i++)//遍历n-1次
    {
        //每次遍历每一条边
        for(int u=0;u<n;u++)
            for (int j = 0; j < adj[u].size(); j++)//看是否可以以u为中介可以进行优化
            {
                int v = adj[u][j].v;//邻接边的顶点
                int dis = adj[u][j].w;//邻接边的边权
                if (d[u] + dis < d[v])//可以进行优化:是对顶点进行优化
                {
                    d[v] = d[u] + adj[u][j].w;//覆盖d
                    w1[v] = w1[u] + weight[v];//优化边的权值
                    num[v] = num[u];//直接覆盖
                    pre[v].clear();
                    pre[v].insert(u);
                }
                else if (d[u] + dis == d[v])//找到路径相等
                {
                    if(w1[u] + weight[v]>w1[v])w1[v] = w1[u] + weight[v];//优化边的权值
                    pre[v].insert(u);//一步步进行优化
                    
                                                                         
              //此时有可能会有重复的节点,所以需要重新计算
                    num[v] = 0;
                set<int>::iterator it;
                for (it = pre[v].begin(); it != pre[v].end(); it++)
                {
                    num[v] += num[*it];
                }
                }
            }
    }

}
int main()
{
    int s, e;
    scanf_s("%d%d%d%d", &n, &m, &s, &e);
    for (int i = 0; i < n; i++)scanf_s("%d", &weight[i]);
    int s1, s2, s3;
    for (int i = 0; i < m; i++)
    {
        scanf_s("%d%d%d", &s1, &s2, &s3);
        adj[s1].push_back(node(s2,s3));
        adj[s2].push_back(node(s1, s3));
    }
    kus(s);//传入起点进去
    printf("%d %d", num[e], w1[e]);
    return 0;
}
  1. SPFA
    3.1:[PAT1003]

//思路: SPFA(shortest path faster algorithm)对BF进行改进,BF每次都要进行全部边的判断
//1:只有当某个顶点的d[u]值改变时,从它出发的邻接点的v的d[v]的值才有可能改变
//1.1:设置一个队列,每次将队首元素取出,然后对从u出发的所有边u-》v进行松弛操作,判断是否可以,如果可行,判断v是否在队列中,不在则加入队列,直到队列为空
//错误总结:1:邻接矩阵储存图的意思,怎么获取顶点值和边权
//2:SPFA是由BF改进来的,所以对num数组进行优化时的情况也要考虑是否会进行重复计算
//3:当路径相等时相当于也进行了优化
#include<cstring>//memset在这个头文件中
#include <iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<algorithm>
#include<set>
using namespace std;
int n, m;
const int maxv=510;
const int INF = 1000000000;
int weight[maxv];
struct node {//熟悉邻接表储存图的意思,什么时候获取顶点值,什么时候
    int s;//s为顶点
    int v;//表示边权:单源最短路径问题
    node(int s1, int v1) :s(s1), v(v1) {};
};
vector<node>adj[maxv];
int num[maxv], w[maxv],d[maxv];
bool inq[maxv];
set<int>pre[maxv];//记录前驱的数组
void SPFA(int s)
{
    memset(num, 0, sizeof(num));
    memset(w, 0, sizeof(w));
    memset(inq, false, sizeof(inq));
    fill(d, d + maxv, INF);
    //源点入队
    queue<int>q;
    q.push(s);
    d[s] = 0;
    w[s] = weight[s];//权值初始化
    num[s]++;
    inq[s] = true;
    while (!q.empty())
    {
        int v = q.front();
        q.pop();
        inq[v] = false;
        for (int j = 0; j < adj[v].size(); j++)
        {
            int q1 = adj[v][j].s;//获取点值
            int w2 = adj[v][j].v;//获取边权
            //进行松弛操作
            if (d[v] + w2 < d[q1])
            {
                d[q1] = d[v] + w2;
                num[q1] = num[v];//数量覆盖
                w[q1] = w[v] + weight[q1];
                pre[q1].clear();
                pre[q1].insert(v);
                //要进行判断是否在队列中
                if (!inq[q1]);
                {
                    q.push(adj[v][j].s);
                     inq[q1] = true;
                }
                
                
            }
            else if (d[v] + w2 == d[q1])
            {
                if (w[q1] < w[v] + weight[q1])
                {
                    w[q1] = w[v] + weight[q1];
                }
                pre[q1].insert(v);
                /*num[q1] += num[v];*/
                num[q1] = 0;
                //防止反复累计的值
                set<int>::iterator it;
                for (it = pre[q1].begin(); it != pre[q1].end(); it++)
                {
                    num[q1] += num[*it];
                }
                //这段也是至关重要的:此时也相当于进行了优化
                if (!inq[q1]);
                {
                q.push(adj[v][j].s);
                inq[q1] = true;
                }
            }
        }
    }

}
int main()
{
    int s, e;
    cin >> n >> m>>s>>e;
    for (int i = 0; i < n; i++)
    {
        cin >> weight[i];
    }
    int s1, e1, w1;
    for (int i = 0; i < m; i++)
    {
        cin >> s1>>e1>>w1;
        //这里是个双向图:用邻接表储存的方法
        adj[s1].push_back(node(e1, w1));
        adj[e1].push_back(node(s1, w1));
    }
    SPFA(s);
    cout << num[e] <<" "<< w[e];
    return 0;
}

// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单

// 入门使用技巧: 
//   1. 使用解决方案资源管理器窗口添加/管理文件
//   2. 使用团队资源管理器窗口连接到源代码管理
//   3. 使用输出窗口查看生成输出和其他消息
//   4. 使用错误列表窗口查看错误
//   5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
//   6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件

4:Floyd:解决全源路径最短问题

// Floyd.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//Floyd算法目的:解决全源最短路径问题:时间复杂度为O(n^3)
//Floyd算法思路:
//1:枚举每个顶点
//2:以每个顶点为中介,判断是否可以进行优化
#include <iostream>
#include<algorithm>
using namespace std;
//定义全局变量
int n, m;//定义点数和边数
const int  MAXV= 200;//把顶点数限制在200以内
const int INF = 100000000;
int dis[MAXV][MAXV];//距离数组:表示顶点i到j的距离
//不需要定义是否访问
//F算法
void Floyd()
{
    for (int k = 0; k < n; k++)//枚举顶点:注意不要放到内层循环
    {
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                //经过以k为中介,i到j的距离能不能缩短
                if (dis[i][k] != INF && dis[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j])
                    dis[i][j] = dis[i][k] + dis[k][j];
            }
        }
    }
}
int main()
{
    //输入顶点、边数
    
    scanf_s("%d%d", &n, &m);
    //初始化
    //1:数组进行全部初始化
    //初始化的格式写错了:应该为dis[0]
    fill(dis[0], dis[0] + MAXV * MAXV, INF);
    //2:初始距离设为0;
    //:2:自己到自己的距离初始化为0
    for (int i = 0; i < n; i++)dis[i][i] = 0;
    /*for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        { dis[i][j] = 0;}         
    }*/
    //输入边的信息
    int u, v, w;//输入顶点和边的权值
    for (int i = 0; i < m; i++)
    {
        scanf_s("%d%d%d", &u, &v,&w);
        //为什么还要定义距离矩阵呢?不能直接在邻接矩阵上进行操作吗?
        //没必要定义两个数组,直接在矩阵上进行操作
        dis[u][v] = w;
    }
    Floyd();//入口
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
            printf("%d ", dis[i][j]);
        } 
        printf("\n");

    }
    return 0;


}

// 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
// 调试程序: F5 或调试 >“开始调试”菜单

// 入门使用技巧: 
//   1. 使用解决方案资源管理器窗口添加/管理文件
//   2. 使用团队资源管理器窗口连接到源代码管理
//   3. 使用输出窗口查看生成输出和其他消息
//   4. 使用错误列表窗口查看错误
//   5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
//   6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值