[最短路] 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. 昂贵的聘礼(单源最短路建图+超级源点+知识理解+好题)的主要内容,如果未能解决你的问题,请参考以下文章
[最短路] aw1129. 热浪(单源最短路建图+spfa循环队列+模板题)
[最短路] aw1127. 香甜的黄油(单源最短路建图+模板题)