最短路径问题

一、Bellman-Ford算法

Q:有一张有 n n n 个点、 m m m 条边的有向图,可能存在重边、负边和自环,但不存在负环,求起点 s s s 到每个点的最短路径。

1.1 算法简析

记图为 G G G G [ u ] G[u] G[u] 表示以 u u u 为起点的所有边的集合; e ( u , v ) e(u, v) e(u,v) 表示 u u u v v v 的某一条有向边( e ( u , v ) ∈ G [ u ] e(u, v)\in G[u] e(u,v)G[u]), e ( u , v ) . w e(u, v).w e(u,v).w 为权值;。
d [ u ] = d[u] = d[u]= 从起点 s s s u u u 的最短路径,则

d [ v ] = m i n ( { d [ u ] + e ( u , v ) . w ∣ e ( u , v ) ∈ G [ u ] } ) d[v] = min(\{d[u] + e(u, v).w | e(u, v)\in G[u]\}) d[v]=min({d[u]+e(u,v).we(u,v)G[u]})

注: e ( u , v ) e(u, v) e(u,v) 可能有重边,所以用集合形式表示。

1.2 代码

#define MAX 20                  // 最大容纳的点数
#define INF 1e8                 // 防止 d[e.from] + e.worth 溢出

// 定义边的数据类型
typedef struct
{
    int from, to, worth;        // from -- 起点; to -- 终点; worth -- 权值
} edge;

int n, m, s;                    // n -- 点数; m -- 边数; s -- 起点
vector<edge> E;                 // 存放边
int d[MAX];                     // d[i] -- 从 s 到 i 的最短路径
int pre[MAX];                   // 记录前驱点

// 寻找最短路径
void bellman_ford(int s)
{
    // 初始化
    fill(begin(d), end(d), INF);
    d[s] = 0;

    // 外层循环至多进行 n - 1 次
    for (int i = 0; i < n - 1; i++)
    {
        bool flag = false;                          // 初始状态为未松弛

        // 遍历每一条边
        for (int j = 0; j < m; j++)
        {
            edge e = E[j];

            // 松弛操作
            if (d[e.to] > d[e.from] + e.worth)      // 松弛条件
            {
                d[e.to] = d[e.from] + e.worth;		// 松弛
                pre[e.to] = e.from;                 // 更新前驱点
                flag = true;                        // 松弛过
            }
        }
        if (!flag)          // 若遍历完所有的边都未进行松弛,则已经求出最短路径
            break;
    }
}

// 打印 s 到 i 的最短路径
void print_path(int i)
{
	printf("%d\n", d[i]);						// s 到 i 的最短路径
	
	// 记录从 i 到 s 的路径
	vector<int> ans;
	int p = i;
	ans.push_back(p);
	while (p != s)
	{
		p = pre[p];
		ans.push_back(p);
	}

	reverse(ans.begin(), ans.end());			// 逆序路径
	printf("%d", ans[0]);
	for (int i = 1; i < ans.size(); i++)
		printf(" -> %d", ans[i]);
}

1.3 注意点

  • 1、定义 INF 时,要防止溢出。
    松弛的条件是 d[e.to] > d[e.from] + e.worthd[e.from] 的值可能仍是 INF。若 INF 定义得太大,如 #define INF __INT_MAX__,则 d[e.from] + e.worth 可能会溢出(因为 int d[MAX])。
    在数学上,一个有限数加上一个无限数仍为无限大。但在编程中,存在数据类型的制约,溢出后可能变成一个很小的数。
    为了防止溢出,可以采取以下三个方法:
// 法一:合理定义 INF
#define INF 1e8
int d[MAX];         // 以 int 为例。若结果很大,则采用 long long

/* ********************************************************************* */

// 法二:将 INF 定义为 __INT_MAX__,修改松弛条件
#define INF __INT_MAX__
int d[MAX];         // 以 int 为例。若结果很大,则采用 long long
...
   			if (d[e.from] != INF && d[e.to] > d[e.from] + e.worth)
...

/* ********************************************************************* */

// 法三:使 d[] 的类型最大值远大于 INF
#define INF __INT_MAX__
long long d[MAX];    // 适用于结果用 int 就能存储的情况

  • 2、外层循环 for (int i = 0; i < n - 1; i++),最多进行 n - 1 次(n 为点数)。
    这是 B e l l m a n − F o r d Bellman-Ford BellmanFord 的一个性质,即在不存在负环的条件下,该算法最多进行 n - 1 次外循环就可得到最短路径
    利用该性质,可以检测一张图是否存在负环。若存在负环,则不存在最短路径,因为该值可以一直变小。显然,若无限制,会进行第 n 次外循环。
// 若图中有负环,则返回 true; 否则,返回 false
bool bellman_ford(int s)
{
    // 初始化
    fill(begin(d), end(d), 0);

    // 若无负环,外层循环至多进行 n - 1 次;若存在负环,万层循环会进行第 n 次
    for (int i = 0; i < n; i++)
    {
        // 遍历每一条边
        for (int j = 0; j < m; j++)
        {
            edge e = E[j];

            // 松弛操作
            if (d[e.to] > d[e.from] + e.worth)      // 松弛条件
            {
                d[e.to] = d[e.from] + e.worth;		// 松弛

				// 若 i == n - 1,即进行第 n 次循环,则存在负环
				if (i == n - 1)
					return true;
            }
        }
    }
    return false;
}
  • 3、定义 flag 来优化循环。
    若不存在负环,可能不需要进行 n - 1 次外循环,提前求出最短路径。若在某一次循环中,遍历所有的边,都未进行松弛操作,说明已经求出了最短路径,无需再进行松弛。这时,没有必要再进行循环。
  • 4、利用 pre[MAX] 记录最短路径。
    我们只需要在每次进行松弛时,更新 i 的前驱点 pre[i]。若要打印 st 的最短路径,只要从 pre[t] 开始,从 ts 打印路径,最后逆序即可。

二、Dijkstra算法

Q:有一张有 n n n 个点、 m m m 条边的有向图,可能存在重边和自环,但不存在负边和负环,求起点 s s s 到每个点的最短路径。

2.1 算法简析

记图为 G G G G [ u ] G[u] G[u] 表示以 u u u 为起点的所有边的集合; e ( u , v ) e(u, v) e(u,v) 表示 u u u v v v 的某一条有向边( e ( u , v ) ∈ G [ u ] e(u, v)\in G[u] e(u,v)G[u]), e ( u , v ) . w e(u, v).w e(u,v).w 为权值;
B e l l m a n − F r o d Bellman-Frod BellmanFrod 中,只要满足松弛条件,我们就进行 d[e.to] = d[e.from] + e.worth。但是,在算法执行的过程中,如果 d [ e . f r o m ] d[e.from] d[e.from] 不是 e.from 的最短路径,那么 d[e.to] 肯定也不是正确结果。在这种情况下,进行松弛是没有必要的,除非 d[e.from] 是正确的最短路径。
因此,我们改变策略,只对已经找到最短路径的顶点,以该顶点为起点的边进行松弛。我们的重点是如何确定已经找到最短路径的点。一开始,我们只确定了起点的最短路径 d [ s ] = 0 d[s] = 0 d[s]=0。显然,与 s 最接近的点就是我们要找的顶点。

2.2 代码

#define MAX 20                      // 点的最大数目
#define INF 1e8                     // 比结果大的数

// 定义边
typedef struct
{
    int to, worth;                  // to -- 终点; worth -- 权值
} edge;

// 定义点
typedef struct
{
    int id, d;                      // id -- 编号为 id 的点; d -- s 到 id 的最短路径
} dis;

int n, m, s;                        // n -- 点的数目; m -- 边的数目; s -- 起点
vector<edge> G[MAX];                // G[i] -- 以 i 为起点的边的集合
int d[MAX];                         // d[i] -- s 到 i 的最短路径
int pre[MAX];                       // 记录前驱点

// 最小堆的cmp()
struct cmp
{
    bool operator()(const dis &a, const dis &b)
    {
        return a.d > b.d;
    }
};

// 求 s 到各点的最短路径
void dijkstra(int s)
{
    // 初始化
    fill(begin(d), end(d), INF);
    d[s] = 0;

    priority_queue<dis, vector<dis>, cmp> Q;        // Q为最小堆
    dis tmp;
    tmp = {s, 0};
    Q.push(tmp);

    while (!Q.empty())
    {
        dis q = Q.top();
        Q.pop();

        int u = q.id;
        if (d[u] < q.d)                             // q.d非最短路径,不必进行下一步
            continue;
        
        for (int i = 0 ; i < G[u].size(); i++)      // 此时d[u]已经为最短路径
        {
            edge e = G[u][i];
            if (d[e.to] > d[u] + e.worth)           // 以u为起点进行松弛
            {
                d[e.to] = d[u] + e.worth;
                pre[e.to] = u;                      // 记录前驱点
                tmp = {e.to, d[e.to]};
                Q.push(tmp);
            }
        }
    }
}

// 打印 s 到 i 的最短路径
void print_path(int i)
{
	printf("%d\n", d[i]);						// s 到 i 的最短路径
	
	// 记录从 i 到 s 的路径
	vector<int> ans;
	int p = i;
	ans.push_back(p);
	while (p != s)
	{
		p = pre[p];
		ans.push_back(p);
	}

	reverse(ans.begin(), ans.end());			// 逆序路径
	printf("%d", ans[0]);
	for (int i = 1; i < ans.size(); i++)
		printf(" -> %d", ans[i]);
}

int main()
{
    scanf("%d%d%d", &n, &m, &s);
    for (int i = 0; i < m; i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edge e = {b, c};
        G[a].push_back(e);
    }

    dijkstra(s);

    for (int i = 1; i <= n; i++)
        printf("%d ", d[i]);

    return 0;
}

三、Floyd-Warshall算法

Q:有一张有 n n n 个点、 m m m 条边的无向图,可能存在重边、负边和自环,但不存在负环,求 i i i j j j 的最短路径。

3.1 算法简析

d [ i ] [ j ] = d[i][j] = d[i][j]= i i i j j j 的最短路径,则
d [ i ] [ j ] = m i n ( d [ i ] [ j ] , d [ i ] [ k ] + d [ k ] [ j ] ) d[i][j] = min(d[i][j], d[i][k] + d[k][j]) d[i][j]=min(d[i][j],d[i][k]+d[k][j])

3.2 代码

#define MAX 20
#define INF 1e8

int n, m;
int d[MAX][MAX];

void floyd_warshall(void)
{
    // 初始化
    // 若i == j, 则d[i][j] = 0; 否则, d[i][j] = INF
    for (int i = 0; i < MAX; i++)
        for (int j = 0; j < MAX; j++)
            if (i == j)
                d[i][j] = 0;
            else
                d[i][j] = INF;

    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);

        // 处理无向重边,存最短的边
        d[a][b] = min(d[a][b], c);
        d[b][a] = min(d[b][a], c);
    }

    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

注:

  • 初始时, d [ i ] [ j ] d[i][j] d[i][j] e ( i , j ) e(i, j) e(i,j) 的权值。若不存在,则 d [ i ] [ j ] = I N F d[i][j] = INF d[i][j]=INF;若 i = = j i == j i==j,则 d [ i ] [ j ] = 0 d[i][j] = 0 d[i][j]=0
  • 外中内三层循环都是 n 次(顶点数)。初始值是 0 还是 1 取决于顶点编号是从 0 还是 1 开始。
  • 三重循环结束后, d [ i ] [ j ] d[i][j] d[i][j] 即为 i i i j j j 的最短路径;若 d [ i ] [ j ] = I N F d[i][j] = INF d[i][j]=INF,则不存在 i i i j j j 的路径。

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值