1. 题目来源
链接:1126. 最小花费
相关链接:
2. 题目解析
很不错的一道题。
可以将从起点 A
到终点 B
的最优转账路径花费进行如下表示:
100
=
A
∗
w
1
∗
w
2
∗
w
3
+
.
.
.
100=A*w_1*w_2*w_3+...
100=A∗w1∗w2∗w3+...
则问题转化为,求 A
最小,则等价于求
w
1
∗
w
2
∗
w
3
+
.
.
.
w_1*w_2*w_3+...
w1∗w2∗w3+... 最大。即,求乘积的最大值。
在以往最短路问题中,运用几大最短路算法,可以求和的最小值。在本题,需要做简单转化,也可使用最短路模型求解乘积的最大值:
- w i w_i wi 是在 [ 0 , 1 ] [0, 1] [0,1] 之间的数,对 w 1 ∗ w 2 ∗ w 3 + . . . w_1*w_2*w_3+... w1∗w2∗w3+... 取 l o g log log,则 l o g ( w 1 ∗ w 2 ∗ w 3 + . . . ) = l o g w 1 + l o g w 2 + l o g w 3 + . . . log(w_1*w_2*w_3+...)=logw_1+logw_2+logw_3+... log(w1∗w2∗w3+...)=logw1+logw2+logw3+... 此时,由于 l o g log log 函数单调递增,等价于求 l o g w 1 + l o g w 2 + l o g w 3 + . . . logw_1+logw_2+logw_3+... logw1+logw2+logw3+... 的最大值。
- 每个 l o g w i ≤ 0 logw_i \le 0 logwi≤0,求负数的最大值等价于求正数的最小值。则可以对其取反,然后每个数都为正,求正权边的最小值即可。
- 故,本题有
w
i
w_i
wi 是在
[
0
,
1
]
[0, 1]
[0,1] 之间的数(左区间一般不能取到 0),这个范围限制,才导致全为正权边,可以使用
dijkstra()
算法进行求解。否则,只能使用spfa()
。
实现细节:
- 虽然分析是要将边权取 l o g log log、取反,再进行最短路求解。
- 但实际上不需要这样做,现在求乘积最大值,边权也是设到了
[
0
,
1
]
[0, 1]
[0,1] 之间。就将最短路算法里的加法替代成乘法即可。 这也是算法中的常见操作,并且需要将
dist[S] = 1
,然后将更新操作的dist[j]=dist[t]+w
改变为dist[j]=dist[t]*w
就行了。 - 这样操作就相当的方便了。以往的选取一条边,距离需要加和。现在就将这个加和改成了乘积。 以前
0x3f3f3f3f
是不可达的点,现在 0 是不可达的点。所以dist
数组也不必初始化了。最终dist[T]
放的其实就是起点到终点所有路径中乘积的最大值。 - 并且在重边处理上,需要取权值最大的一条边,这条边的手续费就少。在更新时也是,取
max
而不是取min
,保证乘积最大!
考虑清楚,细节实现!
简单总结:
- 加法最小值:
- 无负权:
dijkstra()
、spfa
- 有负权:
spfa
- 无负权:
- 加法最大值:
- 不会严格证明,不知道
spfa
能否搞定
- 不会严格证明,不知道
- 乘法最小值(关于乘法,边权只能是全为正,不能为负。一旦为负,最大值、最小值成一个负数就立马颠倒过来了,十分难求,需要维护更多的信息。一般来讲,乘法求最值,边权都是正数):
-
w
i
≥
1
w_i\ge1
wi≥1,等价于无负权,取
l
o
g
log
log 后边权为正。
dijkstra()
、spfa
-
w
i
≥
0
w_i\ge0
wi≥0,等价于有负权,
spfa
-
w
i
≥
1
w_i\ge1
wi≥1,等价于无负权,取
l
o
g
log
log 后边权为正。
- 乘法最大值:
-
0
≤
w
i
≤
1
0 \le w_i\le1
0≤wi≤1,取
l
o
g
log
log 后边权为负,求负数最大等于求正数最小,正权图。
dijkstra()
、spfa
-
w
i
≥
0
w_i\ge 0
wi≥0,存在负权,
spfa
-
0
≤
w
i
≤
1
0 \le w_i\le1
0≤wi≤1,取
l
o
g
log
log 后边权为负,求负数最大等于求正数最小,正权图。
- 在此, w i w_i wi 是否能够取 0 值得商榷。
小知识点:
- 如何对double型变量进行memset获得极大值或极小值
- 这个是重要的知识点,不要以为初始化
memset(dist, 0x3f, sizeof dist)
是将double
类型的dist
初始化为极大值。实际上它和 0 差不多。 - 一般来讲可以循环初始化。或者采用链接中的方法。
- 极大值的时候,可以选择0x7f,如果觉得这个数字过于夸张,可以选择0x42或者0x43。同样,想清最小值的时候,可以选择0xfe或0xc2。
时间复杂度: O ( n 2 ) O(n^2) O(n2),由算法决定
空间复杂度: O ( n ) O(n) O(n)
朴素版 dijkstra
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2005;
int n, m, S, T;
double g[N][N];
double dist[N];
bool st[N];
void dijkstra() {
dist[S] = 1;
for (int i = 0; i < n; i ++ ) {
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] < dist[j])) // 这个是最大值
t = j;
st[t] = true;
// 取最大值
for (int j = 1; j <= n; j ++ ) dist[j] = max(dist[j], dist[t] * g[t][j]);
}
}
int main() {
scanf("%d%d", &n, &m);
while (m -- ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
double t = (100.0 - c) / 100; // 变为 0~1 的小数
g[a][b] = g[b][a] = max(g[a][b], t); // 建图,乘积最大值,取重边较大的一个
}
scanf("%d%d", &S, &T);
dijkstra();
printf("%.8lf\n", 100.0 / dist[T]);
return 0;
}
spfa+循环队列:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2005, M = 1e5*2;
int n, m, S, T;
int h[N], e[M], w[M], ne[M], idx;
double dist[N];
bool st[N];
int q[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void spfa() {
memset(dist, 0, sizeof dist);
int hh = 0, tt = 1;
q[0] = S, dist[S] = 1;
while (hh != tt) {
auto t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
double cost = (100.0 - w[i]) / 100;
if (dist[j] < dist[t] * cost) { // 注意这里的符号,松弛条件改变
dist[j] = dist[t] * cost;
if (!st[j]) {
st[j] = true;
q[tt ++] = j;
if (tt == N) tt = 0;
}
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
scanf("%d%d", &S, &T);
spfa();
printf("%.8lf\n", 100.0 / dist[T]);
return 0;
}
spfa+转换为 log 后直接求最短路:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 2005, M = 2e5+5;
int n, m, S, T;
int h[N], e[M], ne[M], idx;
double w[M];
double dist[N];
bool st[N];
int q[N];
void add(int a, int b, double c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void spfa() {
// 不要对 double 类型使用 memset 0x3f 来初始化极大值,这样做只会是一个和 0 差不多的小数
// 详解查看:https://blog.csdn.net/PoPoQQQ/article/details/38926889
//memset(dist, 0x3f, sizeof dist);
for (int i = 0; i < N; i ++ ) dist[i] = 1000;
int hh = 0, tt = 1;
q[tt ++ ] = S;
dist[S] = 0;
st[S] = true;
while (hh != tt) {
auto t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
if (!st[j]) {
st[j] = true;
q[tt ++ ] = j;
if (tt == N) tt = 0;
}
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- ) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
double t = (100.0 - c) / 100;
add(a, b, -log(t)), add(b, a, -log(t));
}
scanf("%d%d", &S, &T);
spfa();
printf("%.8lf\n", exp(dist[T]) * 100);
return 0;
}
堆优化 dijkstra
处理,注意谁大选谁…大根堆,初始化最小值:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<double, int> PDI;
const int N = 2005, M = 2e5+5;
int n, m, S, T;
int h[N], e[M], ne[M], idx;
double dist[N], w[M];
bool st[N];
void add(int a, int b, double c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ;
}
void dijkstra() {
for (int i = 0; i < N; i ++ ) dist[i] = 0; // 由于堆中谁大选谁,所以需要初始化 dist 最小值...
priority_queue<PDI, vector<PDI>> heap; // 大顶堆,需要保证选出堆中 dist 最大值
dist[S] = 1;
heap.push({dist[S], S});
while (heap.size()) {
auto t = heap.top(); heap.pop();
double d = t.first;
int idx = t.second;
if (st[idx]) continue;
st[idx] = true;
for (int i = h[idx]; ~i; i = ne[i]) {
int j = e[i];
if (dist[j] < dist[idx] * w[i]) {
dist[j] = dist[idx] * w[i];
heap.push({dist[j], j});
}
}
}
}
int main() {
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
while (m -- ) {
int a, b;
double c;
scanf("%d%d%lf", &a, &b, &c);
c = (100.0 - c) / 100;
add(a, b, c), add(b, a, c);
}
scanf("%d%d", &S, &T);
dijkstra();
printf("%.8lf\n", 100.0 / dist[T]);
return 0;
}
本题还可以从终点 100
倒推到起点,乘法变除法,就不再赘述了。