[最短路] aw903. 昂贵的聘礼(单源最短路建图+超级源点+知识理解+好题)

Posted Ypuyu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[最短路] aw903. 昂贵的聘礼(单源最短路建图+超级源点+知识理解+好题)相关的知识,希望对你有一定的参考价值。

1. 题目来源

链接:903. 昂贵的聘礼

关于 spfa 循环队列初始化问题:

2. 题目解析

典型阅读理解题目,模拟样例理解的更快。

简单理解下题意n 个人,每个人有自己的物品和价格和等级,如果要得到这个人的物品有两种方式:

  • 第一种:直接在他手里买他的物品
  • 第二种:每个人有自己喜欢的其他人的物品,如果自己手里恰好有这个物品,那么可以将这个物品给他,并补偿一定的差价即可获得他的物品。

注意,每个人都有等级。我们的目的是花最少的钱得到 1 号点的物品,并且在整个交易过程中,交易过程中的每个人的等级与 1 号人的等级相差均不超过 m。


建图:

  • 建图需要抓住起点、终点。
    • 终点:终点就是 1 号点。
    • 起点:直接单独买1号点,也可以通过其他点来到1号点。所以可以用一个虚拟的超级源点,因为可以单独买一号点,也可以单独买某个点,再到 1 号点,所以超级源点和任何一个点 v 连一条边,边的权值是点v的单独价值,意味着可以直接买它然后入图。
  • 超级源点的使用在本题中很巧妙,大大减少了建图的复杂度。
  • 得先直接买一个点的物品,然后才能成为起点,入图,到终点 1 号点。所以每个点都能成为起点,故为多源问题,再将其用超级源点将这些多源起点相连,就很巧妙的完成了用超级源点作为多个起点的建图。

等级限制:

  • 记 1 号点的等级为 level,等级差限制为 M
  • [level-M,level][level-M+1,level+1],…,[level,level+M] 这些区间都是满足的等级差区间。
  • 故可以枚举这 M 个有效的合法区间。
  • 在状态转移时,如果某点的等级不在这个区间中的话就不能对其进行状态更新。即他的物品,不能替换不能购买,不可将其加入到贸易序列中。

时间复杂度分析:

  • n , m , x = 100 n,m,x=100 n,m,x=100,本题是稠密图,数据范围小,使用哪个最短路算法都是可以的。
  • dijkstra 算法, O ( n 2 m ) = 100 × 100 × 100 = 1 0 6 O(n^2m)=100\\times100\\times100=10^6 O(n2m)=100×100×100=106 的时间复杂度。

本题注意点:

  • 超级源点将多起点统筹起来使用,注意学习。
  • 等级限制采用多个枚举的方式,非常精妙的暴力枚举,注意学习。

个人笔记:

在这里插入图片描述在这里插入图片描述


时间复杂度: O ( n 2 m ) O(n^2m) O(n2m)

空间复杂度: O ( n 2 ) O(n^2) O(n2)


dijkstra:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 105;

int m, n;
int w[N][N], level[N];
int dist[N];            // 0 号点为 S 源点
bool st[N];

int dijkstra(int l, int r) {                            // 合法的等级区间
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    
    dist[0] = 0;
    for (int i = 1; i <= n + 1; i ++ ) {                // 本题有 0 号点超级源点存在,需要枚举 n+1 个点
        int t = -1;
        for (int j = 0; j <= n; j ++ )                  // 本题有 0 号点超级源点存在,需要枚举 n+1 个点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;                     
        
        // t 点一开始是源点,无等级,自然成立,后面更新到的 j 点有判断存在,一定是在等级区间中的点,即合法点
        // 故这个更新加与不加都是可以的,相当于做了剪枝
        //
        // 剪枝1:当距离 1 号点的最近点 t 距离都是 INF 时,该点及以后不会在更新一号点了,则可以直接 return;
        // if (dist[t] == 0x3f3f3f3f) return dist[1];
        // 这个属实是个废物剪枝,t 点选出来一定是在等级范围内的点,不需要再次判断
        // if (t && (level[t] < l || level[t] > r)) return dist[1];
        
        st[t] = true;
        
        for (int j = 1; j <= n; j ++ ) 
            if (level[j] >= l && level[j] <= r)         // 枚举点 j 不在合法的等级区间内,不可更新        
                dist[j] = min(dist[j], dist[t] + w[t][j]);
    }
    
    return dist[1];
}

int main() {
    scanf("%d%d", &m, &n);
    memset(w, 0x3f, sizeof w);
    for (int i = 0; i <= n; i ++ ) w[i][i] = 0;
    
    for (int i = 1; i <= n; i ++ ) {
        int p, cnt;
        scanf("%d%d%d", &p, &level[i], &cnt);
        w[0][i] = min(p, w[0][i]);                  // 初始化源点,起点为直接购买自己的价格
        while (cnt -- ) {
            int id, cost;
            scanf("%d%d", &id, &cost);
            w[id][i] = min(cost, w[id][i]);         // id-->i 路线,id 是 i 的前置物品编号,补差价 cost
        }
    }
    
    // 等级差距只与 level[1] 有关,等级差在 [level[1]-m, level[1]+m] 之间的任一个长度为 m 的区间均可
    // 故可以暴力枚举这 m 个区间,每次与答案取最小即可
    int res = 1e9;
    for (int i = level[1] - m; i <= level[1]; i ++ ) res = min(res, dijkstra(i, i + m));
    
    printf("%d\\n", res);
    
    return 0;
}

spfa+邻接矩阵

虽然 spfa 已死,但是还是很香啊。

spfa 很少在邻接矩阵中使用,但是最短路算法要抓住算法本质,跟图的存储没有任何关系。

邻接矩阵的 spfa 依旧很香。


spfa 循环队列坑点:

注意 spfa 使用循环队列时,使用后置 ++ 进行入队,若初始入队不使用 q[0]=x; 的话,统一使用 q[tt ++ ]=x; 则这个 tt 要初始化成 0,而不是 1。道理同朴素队列一样,0 位置是要使用的。

一般来讲,是 hh=0,tt=1 进行初始化,多个点初始加入时,需要 q[tt++]; 时,将 tt 初始化为 0,再使用 q[tt++]; 即可。

之前写的时候,会直接在 tt=1 时,初始化队列为 q[tt++]; 这个操作,等于 中间会实质的空 1 个位置不会使用到。且在取队头元素时会 q[hh++]; 取到一个 0 值,若将静态数组定义为局部变量还未初始化的话,则将取到一个随机值…那就很糟了…

貌似还没出错过。因为如果第一次取到 t=q[hh++]=0 那么由于 0 这个点不会使用,在spfa中,h[0]=-1 等价于没更新…所以没出错。但实际的想法却不是这样的,很危险!

自己手误已经写错多次了…以为是统一写法,其实属实伞兵。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 105;

int m, n;
int w[N][N], level[N];
int dist[N];
bool st[N];
int q[N];

int spfa(int l, int r) {
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    
    int hh = 0, tt = 0;				// 注意写法,tt=0,才可使用 q[tt++]; 初始化
    q[tt ++ ] = 0, dist[0] = 0, st[0] = true;
    
    while (hh != tt) {
    	// 否则,0 位置存 0 是无意义的,进来错误更新了一次...
    	// 但由于下个有效值也是 0,所以貌似无所谓,答案也正确
    	// 其余题的话,0 就是无效点,根本就不会进行更新,因为 h[0]=-1 不会更新
        int t = q[hh ++ ];		
        if (hh == N) hh = 0;
        
        st[t] = false;
        
        // 邻接矩阵的 spfa 写法
        for (int i = 1; i <= n; i ++ ) 
            if (level[i] >= l && level[i] <= r && dist[i] > dist[t] + w[t][i]) {
                dist[i] = dist[t] + w[t][i];
                if (!st[i]) {
                    st[i] = true;
                    q[tt ++ ] = i;
                    if (tt == N) tt = 0;
                }
            }
    }
    
    return dist[1];
}

int main() {
    cin >> m >> n;
    memset(w, 0x3f, sizeof w);
    for (int i = 0; i <= n; i ++ ) w[i][i] = 0;
    
    for (int i = 1; i <= n; i ++ ) {
        int p, cnt;
        cin >> p >> level[i] >> cnt;
        w[0][i] = min(w[0][i], p);
        while (cnt -- ) {
            int id, cost;
            cin >> id >> cost;
            w[id][i] = min(w[id][i], cost);
        }
    }
    
    int res = 1e9;
    for (int i = level[1] - m; i <= level[1]; i ++ ) res = min(res, spfa(i, i + m));
    
    cout << res << endl;
    
    return 0;
}

以上是关于[最短路] aw903. 昂贵的聘礼(单源最短路建图+超级源点+知识理解+好题)的主要内容,如果未能解决你的问题,请参考以下文章

第三章 图论未完成

AcWing903 昂贵的聘礼(最短路)

[最短路] aw1129. 热浪(单源最短路建图+spfa循环队列+模板题)

[最短路] aw1127. 香甜的黄油(单源最短路建图+模板题)

[最短路] aw1128. 信使(单源最短路建图+Floyd算法+最短路理解+模板题)

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