Floyd 算法

算法用途:

Floyd 算法是用于解决两点间最短路径的一种算法,可以处理有向图或负权的最短路问题

该算法时间复杂度为 O ( N 3 ) O(N^3) O(N3),空间复杂度为 O ( N 2 ) O(N^2) O(N2)

算法原理

Floyd 算法基于动态规划实现。

Floyd 算法一直在解决一个问题,寻找 i → j i \rightarrow j ij 的最短路径 (废话)

但是,既然是动态规划,那么我们就要为问题做一个全新的定义 (平生最烦动态规划就是因为这个)

从任意节点到另一个节点,不外乎就两种可能。

$
i \sim j 的最短路径
\begin{cases}
直接从 i 到 j \
从 i 开始,经过 x 个点,到达 j \
\end{cases}
$

设任意一个中途经过的节点为 k k k,我们检查 f i , k + f k , j < f i , j f_{i, k} + f_{k, j} < f_{i, j} fi,k+fk,j<fi,j 是否成立,如果成立即证明 i ∼ k ∼ j < i ∼ j i \sim k \sim j < i \sim j ikj<ij,则 f i , j = f i , k + f k , j f_{i, j} = f_{i, k} + f_{k, j} fi,j=fi,k+fk,j,遍历完所有节点后, f i , j f_{i, j} fi,j 记录的就是最短路径的长度。(咋感觉和背包有点像???)

算法思想

  1. 从任意一个边开始,将所有两点有直接连接的边的最短路径( f i , j f_{i, j} fi,j)设为边权,如果两点之间没有边则初始化为 inf ⁡ \inf inf

  2. 对于每一对顶点 [ i , j ] [i, j] [i,j],看是否有一个顶点 k k k 使得 i → k → j < i → j i \rightarrow k \rightarrow j < i \rightarrow j ikj<ij,如果有就更新路径长度。

核心代码 & 代码分析

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

    // print 部分
    for (int i = 1; i <= n; i++)
    {
        for (int j = i + 1; j <= n; j++)
            cout << i << " " << j << "的最路径为:" << f[i][j] << "\n";
    }

    return ;
}

(真的真的太太太 DP 了)

核心代码只有三层循环,一行更新,十分简洁,可是这四行代码为什么就能求出任意两点的最短路径呢?

从动态规划考虑,我们设 f k , i , j f_{k, i, j} fk,i,j 为经过前 k k k 个点的 i → j i \rightarrow j ij 最短路径,根据上述原理,我们有两种转移方法:

$
f_{k, i, j} = \begin{cases}
f_{k - 1, i, j} & 不经过 k 节点 \
f_{k - 1, i, k} + f_{k - 1, k, j} & 经过 k 节点 \
\end{cases}
$

众所周知,DP 的两个满足条件为 最优子结构无后效性

这里有一个显而易见的定理:

最短路径的子路径仍然是最短路径,这个定理显而易见,比如一条 a → e a \rightarrow e ae 的最短路 a → b → c → d → e a \rightarrow b \rightarrow c \rightarrow d \rightarrow e abcde 那么 a → b → c a \rightarrow b \rightarrow c abc 一定是 a → c a \rightarrow c ac 的最短路,反过来,如果说一条最短路必须要经过点 k k k,那么 i → k i \rightarrow k ik 的最短路加上 k → j k \rightarrow j kj 的最短路一定是 i → j i \rightarrow j ij 经过 k k k 的最短路。因此,最优子结构可以保证。

状态 f k , i , j f_{k, i, j} fk,i,j f k − 1 , i , j f_{k - 1, i, j} fk1,i,j f k − 1 , i , k + f k − 1 , k , j f_{k - 1, i, k} + f_{k - 1, k, j} fk1,i,k+fk1,k,j 转移过来,很容易可以看出, f k , x , x f_{k, x, x} fk,x,x 的状态完全由 f k − 1 , x , x f_{k - 1, x, x} fk1,x,x 转移过来。因此,只要我们把 k k k 放最外层循环中,就能保证无后效性。

参考资料

图最短路径算法之弗洛伊德算法(Floyd)

—(华丽的分界线)—

例题

注:本部分含有大量使用 Hexo-Theme-Redefine 渲染的内容,阅读感受不好请见谅。

或许更好的阅读体验

{% tabs Floyd Ques %}

算法:Floyd, 爆搜, 组合数学(全排列)

点我查看题目

看见这个题目,我们先来处理最最最无脑的 Floyd 预处理部分。

纯板子,见上。

{% folding white::Floyd 板子部分 %}

void floyd()
{
    for (int k = 1; k <= n; k++)
    {
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                if (i == j)
                {
                    f[i][j] = 0;
                    continue;
                }
                f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
            }
        }
    }
	return ;
}

{% endfolding %}

我们现在思考进行问题转化。

对于 Floyd 算法,最适合求导的问题就是最短路问题 (CNM,说了一堆废话)

我们现在思考,求 1 → n 1 \rightarrow n 1n 的最短路是简单的,那我们求 1 → n 1 \rightarrow n 1n 中间必须经过 x x x 个点的最短路,即为 1 → x 1 → x 2 → ⋯ → x k → n 1 \rightarrow x_1 \rightarrow x_2 \rightarrow \dots \rightarrow x_k \rightarrow n 1x1x2xkn,这也非常简单,我们可以用分治来考虑。

1 → 经过 n 个点 → n = { 1 → x 1 x 1 → x 2 … x k − 1 → x k x k → n 1 \rightarrow 经过 n 个点 \rightarrow n = \begin{cases} 1 \rightarrow x_1 \\ x_1 \rightarrow x_2 \\ \dots \\ x_{k - 1} \rightarrow x_k \\ x_k \rightarrow n \end{cases} 1经过n个点n= 1x1x1x2xk1xkxkn

即为: 1 → x 1 → x 2 → ⋯ → x k → n = f 1 , x 1 + f x 1 , x 2 + ⋯ + f x k − 1 , x k + f x k , n 1 \rightarrow x_1 \rightarrow x_2 \rightarrow \dots \rightarrow x_k \rightarrow n = f_{1, x_1} + f_{x_1, x_2} + \dots + f_{x_{k - 1}, x_k} + f_{x_k, n} 1x1x2xkn=f1,x1+fx1,x2++fxk1,xk+fxk,n

得到这个公式后,我们可以将问题转化。

既然要经过所有边,我们就转化为要经过所有指定边的点。

有两个问题:

  1. 怎么确定边的顺序?
    • 全排列,next_permutation
  1. 怎么确定点的顺序?
    • dfs 爆搜

{% folding white::爆搜部分 %}

注:每次全排列后,通过最短路到达目前执行边起点,在起点加上本边边权。

ll search(int now, int nowi, ll cnt)
{
	if (nowi == k + 1)
	{
		cnt += f[now][n];
		return cnt;
	}
	return min((search(a[need[nowi] - 1].v, nowi + 1, cnt + f[now][a[need[nowi] - 1].u] + a[need[nowi] - 1].w)), (search(a[need[nowi] - 1].u, nowi + 1, cnt + f[now][a[need[nowi] - 1].v] + a[need[nowi] - 1].w)));
}

{% endfolding %}

完整代码:

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

#define ll long long
int n, m;
struct road
{
	int id;
	int u, v;
	ll w;
};
vector<road> a;
ll f[505][505];
void floyd()
{
    for (int k = 1; k <= n; k++)
    {
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= n; j++)
            {
                if (i == j)
                {
                    f[i][j] = 0;
                    continue;
                }
                f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
            }
        }
    }
	return ;
}

int q;
int k, need[505];
ll search(int now, int nowi, ll cnt)
{
	if (nowi == k + 1)
	{
		cnt += f[now][n];
		return cnt;
	}
	return min((search(a[need[nowi] - 1].v, nowi + 1, cnt + f[now][a[need[nowi] - 1].u] + a[need[nowi] - 1].w)), (search(a[need[nowi] - 1].u, nowi + 1, cnt + f[now][a[need[nowi] - 1].v] + a[need[nowi] - 1].w)));
}
ll ans;

int main()
{
	cin >> n >> m;
	memset(f, 0x3f3f3f3f, sizeof(f));
	for (int i = 0; i < m; i++)
	{
		road now;
		now.id = i + 1;
		cin >> now.u >> now.v >> now.w;
		a.push_back(now);
		f[now.u][now.v] = min(f[now.u][now.v], now.w);
		f[now.v][now.u] = min(f[now.v][now.u], now.w);
	}
	floyd();
	
	cin >> q;
	while (q--)
	{
		cin >> k;
		for (int i = 1; i <= k; i++)
			cin >> need[i];
		ans = LONG_LONG_MAX;
        sort(need + 1, need + k + 1);
		do
		{
			ans = min(ans, search(1, 1, 0));
		}
		while (next_permutation(need + 1, need + k + 1));
		cout << ans << "\n";
	}
	return 0;
}

{% endtabs %}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值