【No.18】蓝桥杯图论下|Bellman-Ford算法|SPFA算法|最小生成树|Prim算法|Kruskal算法|随机数据下的最短路问题|路径|聪明的猴子(C++)

Bellman-Ford 算法

单源最短路径问题:给定一个起点s。求它到图中所有n个结点的最短路径

BFS 的扩散思想

每个人都去问自己的相邻节点到 S 点的距离最近是多少。
第一轮至少有一个点得到了到 S 的最短距离,即与 S 相邻的节点,标记为 T1
重复以上操作,那么必然至少又有一个节点找到了与 S 的最短距离,即与 T1 相邻的节点,标记为 T2

  • 图中每个点上站着一个警察
  • 每个警察问邻居:走你这条路能到s吗,有多远?
  • 反复问多次,最后所有警察都能得到最短路
  1. 第1轮,给所有n个人每人一次机会,问他的邻居,到s的最短距离是多少?
    1. 更新每人到s的最短距离,
    2. 特别地,在s的直连邻居中,有个t,得到了到s的最短距离。(注意,算法并没有查找是哪个t)
  2. 第2轮,重复第1轮的操作。
    1. 更新每人到s的最短距离
    2. 特别地,在s和t的直连邻居中,有个v,得到了到s的最短距离
  3. 第3轮,……

一共需要几轮操作?
每一轮操作,都至少有一个新的结点得到了到 S 的最短路径。所以,最多只需要 n 轮操作,就能完成 n 个结点。
在每一轮操作中,需要检查所有 m 个边,更新最短距离。
Bellman-Ford 算法的复杂度:O(nm)

Bellman-Ford 能判断负圈:
没有负圈时,只需要 n 轮就结束。
如果超过 n 轮,最短路径还有变化,那么肯定有负圈。

SPFA 算法

队列优化版的 Bellman-Ford
SPFA = 队列处理+Bellman-Ford。

Bellman-Ford 算法有很多低效或无效的操作。
其核心内容,是在每一轮操作中,更新所有结点到起点 S 的最短距离。
计算和调整一个结点 US 的最短距离后,如果紧接着调整 U 的邻居结点,这些邻居肯定有新的计算结果;
而如果漫无目的地计算不与 U 相邻的结点,很可能毫无变化,所以这些操作是低效的。

改进: 计算结点 U 之后,下一步只计算和调整它的邻居,能加快收敛的过程。 这些步骤用队列进行操作,这就是 SPFA。

  1. 起点 S 入队,计算它所有邻居到 S 的最短距离。把 S 出队,状态有更新的邻居入队,没更新的不入队。
  2. 现在队列的头部是 S 的一个邻居 U。弹出 U,更新它所有邻居的状态,把其中有状态变化的邻居入队列。
  3. 继续以上过程,直到队列空。这也意味着,所有结点的状态都不再更新。最后的状态就是到起点 S 的最短路径。

弹出 U 之后,在后面的计算中,U 可能会再次更新状态(后来发现,U 借道别的结点去 S,路更近)。所以,U 可能需要重新入队列。
有可能只有很少结点重新进入队列,也有可能很多。这取决于图的特征。
所以,SPFA 是不稳定的,所以根据题目的类型,我们要选择合适的算法。

随机数据下的最短路问题 lanqiaoOJ 题号 1366

题目描述
给定 N 个点和 M 条单向道路,每条道路都连接着两个点,每个点都有自己编号,分别为 1∼N。问你从 S 点出发,到达每个点的最短路径为多少。
输入描述
输入第一行包含三个正整数 N,M,S。
第 2 到 M+1 行每行包含三个正整数 u,v,w,表示 u→v 之间存在一条距离为 w 的路。
1≤N≤5×103,1≤M≤5×104,1≤ui​,vi​≤N,0≤wi​≤10^9。
输出描述
输出仅一行,共 N 个数,分别表示从编号 S 到编号为 1∼N 点的最短距离,两两之间用空格隔开。(如果无法到达则输出 −1)
解题思路:
本题为单源最短路的模板题,直接套模板即可,本题我们采用 SPFA。

#include<bits/stdc++.h>
using namespace std;
const long long INF = 0x3f3f3f3f3f3f3f3f;
const int N = 5e3+10;
struct edge{
    int to;    
    long long w;
    edge(int tt, long long ww) {to = tt; w = ww;}
};
long long dist[N];
int inq[N];
vector<edge> e[N];
void spfa(int s){
    memset(dist, 0x3f, sizeof(dist)); //距离标记为负无穷
    dist[s] = 0;      //起点到自己的距离是0
    queue<int> q;     //创建一个队列
    q.push(s);        //从s开始,s进队列
    inq[s] = 1;       //起点在队列中
    //当队列不为空时,在队列中进行更新
    while(!q.empty()) {
        int u = q.front();  //取队头
        q.pop();
        inq[u] = 0;   //标记u已经不在队列中
        if(dist[u] == INF)    //没有任何更新的价值 
	        continue;   //跳过
        for(int i = 0;i < e[u].size();i++) 
        {   
        //遍历u的邻居,用它们去更新
            int v = e[u][i].to;
            long long w = e[u][i].w;
            //如果存在更新的
            if(dist[v] > dist[u]+w) {         //u的第i个邻居v,它借道u,到s更近
                dist[v] = dist[u]+w;          //更新邻居v到s的距离
                if(!inq[v]) {      //邻居v更新状态了,但v不在队列中,放进队列
                    q.push(v);
                    inq[v] = 1;
                }
            }
        }
    }
}
int main()
{
    int n, m, s;
    cin >> n >> m >> s;
    for(int i = 1; i <= m; i ++)    
    {
        int u, v; 
        long long w;
        cin >> u >> v >> w;
        e[u].push_back(edge(v, w));
    }
    spfa(s);
    for(int i = 1;i <= n;i++) 
    {
        if(dist[i]==INF)  
	        cout << -1;
        else              
	        cout << dist[i];
        if(i != n)        
	        cout << " ";
        else              
	        cout << endl;
    }
    return 0;
}
路径

本题为填空题,只需要算出结果后,在代码中使用输出语句将所填结果输出即可。

小蓝学习了最短路径之后特别高兴,他定义了一个特别的图,希望找到图中的最短路径。
小蓝的图由 2021 个结点组成,依次编号 1至 2021。
对于两个不同的结点a,b,如果a和b的差的绝对值大于 21,则两个结点 之间没有边相连;如果a和b的差的绝对值小于等于 21,则两个点之间有一条 长度为 a和b的最小公倍数的无向边相连。
例如:结点1和结点 23 之间没有边相连;结点3和结点 24 之间有一条无向边,长度为 24;结点 15 和结点 25 之间有一条无向边,长度为 75。
请计算,结点1和结点 2021 之间的最短路径长度是多少。

由于值很小,只有2021个点,小于10^3,可以直接用floyd
关键:不能直接套floyd的模板,因为没有直接给边的长度
因为是个填空题,所以可以用floyd,不存在超时的问题
要一个 n 2 n^2 n2的for循环,i,j之间,如果绝对值只差大于21,就没边,如果小于等于21,就认为它有边,值是最小公倍数
用这个题目要求去建图
建完边之后,用SPFA或者dijkstra

floyd算法用的DP的思想,复杂度高达 O ( n 3 ) O(n^3) O(n3),优点是简单好写

#include <bits/stdc++.h>
typedef long long ll;
using namespace std;

const int N = 3000;
const ll INF = 1e18;
ll mp[N][N];

void floyd(int n){
    for(int k = 1; k <= n; k ++)
        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= n;j ++)
                mp[i][j] = min(mp[i][j], mp[i][k] + mp[k][j]);
}

int main(){
    int n = 2021;
    //建图
    for(int i = 1; i <= n; i ++){
        for(int j = 1; j <= n; j ++){
            if(i == j) 
	            mp[i][j] = 0;
            else if(abs(i - j) <= 21) 
	            mp[i][j] = mp[j][i] = i*j/__gcd(i, j);
            else 
	            mp[i][j] = mp[j][i] = INF;
        }
    }
    floyd(n);
    cout << mp[1][2021];
    return 0;
}

最小生成树

聪明的猴子

题目描述
在一个热带雨林中生存着一群猴子,它们以树上的果子为生。昨天下了一场大雨,现在雨过天晴,但整个雨林的地表还是被大水淹没着,部分植物的树冠露在水面上。猴子不会游泳,但跳跃能力比较强,它们仍然可以在露出水面的不同树冠上来回穿梭,以找到喜欢吃的果实。
现在,在这个地区露出水面的有 N 棵树,假设每棵树本身的直径都很小,可以忽略不计。我们在这块区域上建立直角坐标系,则每一棵树的位置由其所对应的坐标表示(任意两棵树的坐标都不相同)。
在这个地区住着的猴子有 M 个,下雨时,它们都躲到了茂密高大的树冠中,没有被大水冲走。由于各个猴子的年龄不同、身体素质不同,它们跳跃的能力不同。有的猴子跳跃的距离比较远(当然也可以跳到较近的树上),而有些猴子跳跃的距离就比较近。这些猴子非常聪明,它们通过目测就可以准确地判断出自己能否跳到对面的树上。
现已知猴子的数量及每一个猴子的最大跳跃距离,还知道露出水面的每一棵树的坐标,你的任务是统计有多少个猴子可以在这个地区露出水面的所有树冠上觅食。
输入描述
第1行为一个整数,表示猴子的个数 M( 2 ≤ M ≤ 500 2 \le M \le 500 2M500);
第 2 行为 M 个整数,依次表示猴子的最大跳跃距离(每个整数值在 1~ 1000之间)
第 3 行为一个整数表示树的总棵数 N( 2 ≤ N ≤ 1000 2 \le N \le 1000 2N1000)
第 4行至第 N +3 行为 N 棵树的坐标(横纵坐标均为整数,范围为:-1000~ 1000)
(同一行的整数间用空格分开)
输出描述
输出一个整数,表示可以在这个地区的所有树冠上觅食的猴子数。

最小生成树

在无向图中,连通而且不含有圈(环路)的图,称为树。
最小生成树MST:一个有n个结点的连通图的生成树是原图的极小连通子图,包含原图中的所有n个结点,并且边的权值之和最小。

基于贪心的两种算法

Prim算法
  1. Prim算法。对点进行贪心操作:“最近的邻居一定在MST上”
    1. 从任意一个点u开始,把距离它最近的点v加入到MST中;
    2. 把距离{u,v}最近的点w加入到MST中;
    3. 继续这个过程,直到所有点都在MST中,
      模板
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAXN = 1005;
vector<int> demo;
int closest[MAXN],lowcost[MAXN],m,n;//m为节点的个数,n为边的数量
int G[MAXN][MAXN];//邻接矩阵
int prim()
{
    for(int i=0;i<m;i++)
    {
        lowcost[i] = INF;
    }
    for(int i=0;i<m;i++)
    {
        closest[i] = 0;
    }
    closest[0] = -1;//加入第一个点,-1表示该点在集合U中,否则在集合V中
    int num = 0,ans = 0,e = 0;//e为最新加入集合的点
    while (num < m-1)//加入m-1条边
    {
        int micost = INF,miedge = -1;
        for(int i=0;i<m;i++)
        if(closest[i] != -1)
        {
            int temp = G[e][i];
            if(temp < lowcost[i])
            {
                lowcost[i] = temp;
                closest[i] = e;
            }
            if(lowcost[i] < micost)
            micost = lowcost[miedge=i];
        }
        ans += micost;
        demo.push_back(micost);
        closest[e = miedge] = -1;
        num++;
    }
    return ans;
}

int main()
{

    scanf("%d %d", &m, &n);
    memset(G,INF,sizeof(G));
    for(int i = 0; i < n; ++i)
    {
       int a,b,c;
       cin>>a>>b>>c;

       G[b][a]=G[a][b]=c;

    }

    cout<<prim()<<endl;
    for(int i=0;i<m-1;i++) cout<<demo[i]<<" ";
    return 0;
}
Kruskal算法
  1. kruskal算法。对边进行贪心操作:“最短的边一定在MST上“
    1. 从最短的边开始,把它加入到MST中,
    2. 在剩下的边中找最短的边,加入到MST中;
    3. 继续这个过程,直到所有点都在MST中。
      n个点,只需要n-1条边就可以连通了,所以只需做n-1次这个操作就可以了

kruskal算法的2个关键技术:

  1. 对边进行排序。
  2. 判断圈,即处理连通性问题。这个问题用并查集简单而高效,并查集是kruskal算法的绝配。

![[Pasted image 20240325201604.png]]

初始时最小生成树 MST 为空。令S是以结点i为元素的并查集,开始的时候,每个点属于独立的集。

S12345
i12345

按边长从小到大进行边的遍历操作:
尝试将最小边加入最小生成树:

  • 如果边的两个端点属于同一个集合,就说明这两个点已经被加入最小生成树。则不能将边加入,否则就会生成一个环。
  • 如果两个端点不属于同一个集合,就说明该点还未纳入最小生成树,此边可以加入。
    重复上述操作,直到加入 n-1 条边。

kruskal 算法的复杂度包括两部分:

  1. 对边的排序 O(ElogE)
  2. 并查集的操作 O(E)
    一共是 O(ElogE + E),约等于 O(ElogE),时间主要花在排序上。
  • 如果图的边很多,kruskal 的复杂度要差一些。
  • kruskal 适用于稀疏图,prim 适合稠密图。
    模板:
#include <bits/stdc++.h>
using namespace std;

int n,m;
int father[1100000];
struct node
{
    int x;
    int y;
    int k;
} Q[1100000];

int find(int x)
{
    if (father[x] == x)
        return x;
    return father[x] = find(father[x]);
}
bool cmp(node a, node b)
{
    return a.k < b.k;
}
int main()
{
    scanf("%d %d", &n, &m);

    int cont = 0,
        sum = 0, st = 0;
    for (int i = 0; i < m; i++)
    {
        scanf("%d %d %d", &Q[i].x, &Q[i].y, &Q[i].k);
        cont += Q[i].k;
    }
    sort(Q, Q + m, cmp);
    for (int i = 1; i <= n; i++)
        father[i] = i;
    for (int i = 0; i < m; i++)
    {
        int tx = find(Q[i].x);
        int ty = find(Q[i].y);
        if (tx != ty)
        {
            sum += Q[i].k;
            st++;
            father[tx] = ty;
            if (st == n - 1)
                break;
        }
    }
    printf("%d\n", sum);
    return 0;
}
代码
#include <bits/stdc++.h>
using namespace std;
int a[5005], x[5005], y[5005], f[5005];
struct Edge{ int x, y; double w;} edge[1000005];
int find(int x)
{
	if(x == f[x])
		return x;
	f[x]= find(f[x]);
	return f[x];
}
int cmp(Edge a, Edge b)
{ 
	return a.w < b.w;
}
void merge(int x, int y)
{
	int xx = find(x);
	int yy = find(y);
	if(xx != yy)
		f[yy]= xx;
}

int main()
{
	int cnt = 0;
	int n, m;
	cin >> m;
	for (int i = 1; i <= m; i ++)
		cin >> a[i];    //输入每个猴子的跳跃距离
	cin >> n;
	for (int i = 1; i <= n; i ++)
		cin >> x[i] >> y[i];
	for (int i = 1; i <= n; i ++)
		f[i] = i;       //n个点的坐标
	for (int i = 1; i <= n; i ++)
	{
		for (int j = i+1; j <= n; j ++)
		{
			//求每个坐标之间的距离
			double w = sqrt((x[i] - x[j])*(x[i] - x[j]) + 
(y[i] - y[j])*(y[i] - y[j]));
			edge[++cnt] = {i,j,w};
		}
	}
	sort(edge + 1, edge + cnt + 1, cmp);  //排序
	int num = 0;
	double max = 0.0;
	for (int i = 1; i <= cnt; i ++)
	{
		//并查集,判断两个在不在同一个集合里
		if(find(edge[i].x) != find(edge[i].y))  
		{
			merge(edge[i].x, edge[i].y);
			num++;
			max = max >= edge[i].w ? max : edge[i].w;
			//不在同一个集合里,就加进去
		}
		if(num == n-1)
			break;    //一共处理n-1次
	int ans = 0;
	for (int i = 1; i <= m; i ++)
		if(a[i] >= max)
			ans++;    //做的时候保存了最小生成树上的最大值
			//最大值进行判断,能大于最大值,asn++
	cout << ans << endl;
	return 0;
}

总结

Dijkstra:适用于权值为非负的图的单源最短路径,用小顶堆的复杂度 O(E+VlgV)
BellmanFord:适用于权值有负值的图的单源最短路径,并且能够检测负圈,复杂度 O(VE)
SPFA:适用于权值有负值,且没有负圈的图的单源最短路径。论文中的复杂度为 O(kE), 其中 k 为每个节点进入队列的次数,且 k 一般 <=2,但此处的复杂度证明是有问题的,其实 SPFA 的最坏情况应该是 O(VE)
Floyd:每对节点之间的最短路径。

所以:
单源最短路

  1. 当权值为非负时,用 Dijkstra
  2. 当权值有负值,且没有负圈,则用 SPFASPFA 能检测负圈,但是不能输出负圈。
  3. 当权值有负值,而且可能存在负圈需要输出,则用 BellmanFord。能够检测并输出负圈。
  4. 多源最短路使用 Floyd
  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值