[最短路] aw1126. 最小花费(单源最短路建图+知识理解+代码细节+好题)

1. 题目来源

链接:1126. 最小花费

相关链接:

2. 题目解析

很不错的一道题。

可以将从起点 A 到终点 B 的最优转账路径花费进行如下表示:
100 = A ∗ w 1 ∗ w 2 ∗ w 3 + . . . 100=A*w_1*w_2*w_3+... 100=Aw1w2w3+...
则问题转化为,求 A 最小,则等价于求 w 1 ∗ w 2 ∗ w 3 + . . . w_1*w_2*w_3+... w1w2w3+... 最大。即,求乘积的最大值。

在以往最短路问题中,运用几大最短路算法,可以求和的最小值。在本题,需要做简单转化,也可使用最短路模型求解乘积的最大值:

  • w i w_i wi 是在 [ 0 , 1 ] [0, 1] [0,1] 之间的数,对 w 1 ∗ w 2 ∗ w 3 + . . . w_1*w_2*w_3+... w1w2w3+... 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(w1w2w3+...)=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 logwi0求负数的最大值等价于求正数的最小值。则可以对其取反,然后每个数都为正,求正权边的最小值即可。
  • 故,本题有 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 wi1,等价于无负权,取 l o g log log 后边权为正。dijkstra()spfa
    • w i ≥ 0 w_i\ge0 wi0,等价于有负权,spfa
  • 乘法最大值:
    • 0 ≤ w i ≤ 1 0 \le w_i\le1 0wi1,取 l o g log log 后边权为负,求负数最大等于求正数最小,正权图。dijkstra()spfa
    • w i ≥ 0 w_i\ge 0 wi0,存在负权,spfa
  • 在此, 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 倒推到起点,乘法变除法,就不再赘述了。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值