[UVA10816] Travel in Desert 题解
[UVA10816] Travel in Desert 题解
题目描述
沙漠中有 \(n\) 个绿洲(编号为 \(1\sim n\) )和 \(m\) 条连接绿洲的 双向道路。
每条道路都有一个长度 \(d\) 和一个温度值 \(r\) 。
给定起点绿洲编号 \(s\) 和终点绿洲编号 \(t\) ,求出一条 \(s\) 到 \(t\) 的路径,使得这条路径上经过的所有道路的 最高温度尽量小,如果有多条路径,选择总长度最短的那一条。
输出路径,长度,最高温度。
思路
这题有两种解法。
1. 二分答案
看到这有种 最大值最小 描述的题目基本上都可以使用 二分答案 求解。
观察题目的性质,发现如果二分最高温度的话,答案满足二分性:
即最高温度越高,能走的边越多,连通 \(S, T\) 的可能性越大。
由此可以考虑二分一个最高温度 \(mid\),剩下的就是判断只靠路径温度 \(\leq mid\) 的边是否能走到 \(T\),这个可以使用 \(\text{Dijkstra}\) 算法在 \(O(m\log n)\) 的时间内进行判断,最优答案可以迭代记录。
每次查询时间复杂度:\(O(m\log n\log \text{R} )\),其中 \(R\) 是温度最大的取值,这里取 \(10^9\)。
Show the Code
// Problem: Travel in Desert
// URL: https://www.luogu.com.cn/problem/UVA10816
// Author: Moyou
// Copyright (c) 2022 Moyou All rights reserved.
// Date: 2022-12-25 23:38:59
// 省略头文件
struct qwq
{
int x;
int y1, y2;
};
vector<qwq> g[N];
int dist[N], pre[N];
bool st[N];
int ansd, ansr, ans[N], cnt;
int n, m, S, T;
bool check(int x)
{
for (int i = 1; i <= n; i++)
dist[i] = INF;
memset(st, 0, sizeof st);
memset(pre, 0, sizeof pre);
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, S});
dist[S] = 0;
while (heap.size())
{
auto t = heap.top().y;
heap.pop();
if (st[t])
continue;
st[t] = 1;
for (auto i : g[t])
{
int j = i.x;
int r = i.y1, d = i.y2;
if (r > x) // 判断当前边是否能走
continue;
if (dist[j] > dist[t] + d)
{
dist[j] = dist[t] + d;
pre[j] = t;
heap.push({dist[j], j});
}
}
}
if (dist[T] == INF)
return false;
if (ansr > x || (ansr == x && dist[T] <= ansd))
{
ansd = dist[T];
ansr = x;
cnt = 1;
int i = T;
while (i != S)
{
ans[cnt++] = i;
i = pre[i];
}
ans[cnt] = S;
}
return true;
}
void init()
{
ansd = ansr = 1e9 + 7;
cnt = 0;
for (int i = 1; i <= n; i++)
g[i].clear();
}
int main()
{
while (~scanf("%d%d%d%d", &n, &m, &S, &T))
{
init();
for (int i = 1; i <= m; i++)
{
int a, b;
double c1, c2;
scanf("%d%d%lf%lf", &a, &b, &c1, &c2);
g[a].push_back({b, (int)10 * c1, (int)10 * c2}); // 边权x10省去浮点数二分
g[b].push_back({a, (int)10 * c1, (int)10 * c2});
}
int l = 0, r = 1e9;
while (l <= r)
{
int mid = (l + r) / 2;
if (check(mid))
r = mid - 1;
else
l = mid + 1;
}
for (int i = cnt; i >= 2; i--)
printf("%d ", ans[i]);
printf("%d\n", T); // UVA省略行末空格,输出算错Q^Q
printf("%.1lf %.1lf\n", 1.0 * ansd / 10, 1.0 * ansr / 10);
}
return 0;
}
2. 贪心(瓶颈生成树)
下面内容部分参考OI Wiki
引入两个概念。
- 无向图 \(G\) 的瓶颈生成树,它的最大的边权值在 \(G\) 的所有生成树中最小。
- 无向图 \(G\) 中 \(x\) 到 \(y\) 的最小瓶颈路是这样的一类简单路径,满足这条路径上的最大的边权在所有 \(x\) 到 \(y\) 的简单路径中是最小的。
两个性质:
- 最小生成树是瓶颈生成树的充分不必要条件。 即最小生成树一定是瓶颈生成树,而瓶颈生成树不一定是最小生成树。
关于最小生成树一定是瓶颈生成树这一命题,可以运用反证法证明:
我们设最小生成树中的最大边权为 ,如果最小生成树不是瓶颈生成树的话,则瓶颈生成树的所有边权都小于 ,我们只需删去原最小生成树中的最长边,用瓶颈生成树中的一条边来连接删去边后形成的两棵树,得到的新生成树一定比原最小生成树的权值和还要小,这样就产生了矛盾。
- \(x\) 到 \(y\) 的最小瓶颈路上的最大边权 \(w_1\) 等于最小生成树上 \(x\) 到 \(y\) 路径上的最大边权 \(w_2\)
反证法:
假设 \(w_1 \neq w_2\),那么有两种情况:
- \(w_1 > w_2\)。此时出现了一条新的最小瓶颈路,也就是最小生成树上 \(x\) 到 \(y\) 的路径 \(w_2\),所以原最小瓶颈路并非最小瓶颈路,矛盾。
- \(w_1 < w_2\)。此时若在原最小生成树的基础上删掉拥有 \(w_2\) 的边,并连上最小瓶颈路中拥有 \(w_1\) 的边,边权和一定比原最小生成树更小,矛盾。
综上,原命题得证。
答案求的是原图的最小瓶颈路,它一定在原图的最小生成树上,这里的最小生成树中的最小指的是温度,因此只需要求出原图的最小生成树即可。
这么说可能不太好理解,可以参考 \(\text{Kruskal}\) 算法的过程:
- 对所有的边从小到大排序,并从小到大枚举
- 如果当前边 \((a, b)\) 沟通的两个点 \(a, b\) 已经被沟通了,那么当前边一定不如之前的路径,因为之前的路径的 \(\max{r}\) 一定 \(\leq r_{(a, b)}\)(其中 \(r\) 指的是温度)。
- 如果当前边 \((a, b)\) 沟通的两个点 \(a, b\) 还没沟通了,那么直接连通这两个点,原因同上。
- 如果 \(S, T\) 被连通了,那么后面的边都没有贡献了,直接
break
。
这个过程能保证最后得到的图(最小生成树)是温度上的最优解,最后一步就是跑最短路即可,这类问题被称作:最小(大)瓶颈路。
每次查询时间复杂度 \(O(m\log m + n\log n)\)
// Problem: Travel in Desert
// URL: https://www.luogu.com.cn/problem/UVA10816
// Author: Moyou
// Copyright (c) 2022 Moyou All rights reserved.
// Date: 2022-12-26 08:04:13
int n, m, S, T;
struct qwq
{
int a, b, c, d;
};
vector<qwq> g;
int kruskal(int n) // Kruskal过程
{
int fa[N], cnt = 0, sum = 0, mx = -INF;
function<int(int)> find;
find = [&](int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); };
for (int i = 1; i <= n; i++)
fa[i] = i;
sort(g.begin(), g.end(), [](qwq a, qwq b) { return a.c < b.c; });
for (auto i : g)
{
int x = find(i.a), y = find(i.b);
if (x != y)
sum += i.c, cnt++, fa[x] = y;
mx = max(mx, i.c);
if (find(S) == find(T))
break;
}
return mx;
}
int h[N], ne[M], e[M], w[M], idx;
void add(int a, int b, int c)
{
e[++idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx;
}
int dist[N], pre[N];
bool st[N];
void dijkstra(int s)
{
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
memset(pre, 0, sizeof pre);
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, s});
dist[s] = 0;
while (heap.size())
{
auto t = heap.top().y;
heap.pop();
if (st[t])
continue;
st[t] = 1;
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];
pre[j] = t;
heap.push({dist[j], j});
}
}
}
}
void print(int p)
{
if (p == 0)
return;
print(pre[p]);
printf("%d ", p);
}
int main()
{
while (cin >> n >> m >> S >> T)
{
g.clear();
for (int i = 1; i <= m; i++)
{
int a, b;
double c, d;
cin >> a >> b >> c >> d;
c = (int)10 * c, d = (int)10 * d;
g.push_back({a, b, (int)c, (int)d});
}
int maxr = kruskal(n);
memset(h, -1, sizeof h);
idx = 0;
for (auto i : g)
if (i.c <= maxr)
add(i.a, i.b, i.d), add(i.b, i.a, i.d);
dijkstra(S);
print(pre[T]);
printf("%d\n", T);
printf("%.1lf %.1lf\n", 1.0 * dist[T] / 10, 1.0 * maxr / 10);
}
return 0;
}