[USACO09JAN] Safe Travel G 题解
UPDATE on 2024.5.10
删去左偏树代码中令人误解的 fa 数组。
前话
题目链接:洛谷。
貌似别人都是使用并查集维护的方法,然而由于排序、最短路等算法瓶颈,以下令 \(n\) 和 \(m\) 同阶,总的时间复杂度依然是 \(\Theta(n \log n)\) 的,那么并查集是否有点大材小用了。事实上,在建完最短路径树后,我给出了两种带 \(\log\) 的数据结构完成此题。
题目分析
翻译里已经把问题抽象出来了,这里不过多赘述。考虑到从 \(1\) 到任意结点的最短路是唯一的,说明将所有最短路径保留,删去其它边,一定能形成一棵树,这棵树就是最短路径树。我们称保留下来的边为树边,删去的边为非树边。
题目要求不经过原来最短路上最后一条边的最短路,也就是删去 \(u\) 和 \(fa[u]\) 之间的树边后,通过剩余的树边非树边到达 \(u\) 的最短路。很容易想到,必然要经过一条非树边,而且最多只经过一条非树边。前者考虑连通性证明显然,后者有如下证明:
证明:
假设删去 \(u\) 和 \(fa[u]\) 之间的树边后,经过了两条非树边得到最短路径是 \(1 \stackrel{\color{blue}{\texttt{树边}}}{\color{blue}{\longrightarrow}} xym \stackrel{\color{red}{\texttt{非树边}}}{\color{red}{\longrightarrow}} xym' \stackrel{\color{blue}{\texttt{树边}}}{\color{blue}{\longrightarrow}} yzh \stackrel{\color{red}{\texttt{非树边}}}{\color{red}{\longrightarrow}} yzh' \stackrel{\color{blue}{\texttt{树边}}}{\color{blue}{\longrightarrow}} u\),那么根据最短路径树的相关性质,有路径 \(1 \stackrel{\color{blue}{\texttt{树边}}}{\color{blue}{\longrightarrow}} yzh \stackrel{\color{red}{\texttt{非树边}}}{\color{red}{\longrightarrow}} yzh' \stackrel{\color{blue}{\texttt{树边}}}{\color{blue}{\longrightarrow}} u\) 比以上路径更短,原假设不成立,经过多条非树边情况同理。证毕。
考虑走的过程,容易知道 \(yzh\) 不能是 \(u\) 子树里的节点,因为如果是的话,走到 \(yzh\) 的时候就经过了 \(fa[u] \rightarrow u\) 这条边,与题意不符。另外,\(yzh'\) 是 \(u\) 子树里的结点(当然 \(u\) 也属于 \(u\) 的子树里),这样才能一路向上走走到 \(u\)。放张图吧。

记 \(d_u\) 为 \(u\) 的最短路长度,即 \(1 \rightarrow u\) 这条路径的距离,记 \(\operatorname{dist}(u, v)\) 为 \(u\) 经过一条非树边走到 \(v\) 的长度。
所以先跑一遍最短路建出最短路径树。考虑第一遍深搜,枚举 \(yzh\) 连出的所有非树边 \(yzh \rightarrow yzh'\),在 \(yzh'\) 上打上一个标记 \(d_{yzh} + \operatorname{dist}(yzh, yzh')\),这样在第二遍深搜的时候,就可以把子树的标记不断向上传递、合并,把子树的所有标记加上到父节点的距离,再和父节点合并,就是模拟 \(yzh'\) 到 \(u\) 的过程。然后得到答案的时候就是取出标记里最小的那个就行了。但是发现这样存在一个问题,就是不能保证不会出现上文提到的 \(yzh\) 不能是 \(u\) 子树里的节点这个要求,也就是在不断向上传递的时候,有可能传递到了 \(yzh\) 甚至 \(yzh\) 的祖先,这显然会造成问题。解决方法就是将标记增加一个信息变为 \((d_{yzh} + \operatorname{dist}(yzh, yzh'), yzh)\),这样每次取出当前最小值时,发现 \(yzh\) 不满足要求,就将这个标记删除。判断合法可以在第一遍深搜时处理出 dfs 序判断。这个过程套路化地有以下两种方法解决。
1. 左偏树 + 懒惰标记
合并、整体加、删除最小值、获取最小值,明显是可合并堆的范畴啊,这里使用左偏树解决。顺带一提,由于有整体加这个操作,不好使用启发式合并,所以老老实实写左偏树吧。
2. 线段树
什么?你竟然不会使用左偏树,那就快乐地打线段树吧!
为什么要合并?我们只用知道整个子树的信息啊,所以可以在另外记一个信息的 dfs 序,然后树上问题有变成序列上的问题了,区间加、求最小值,使用线段树维护,删除的时候将其赋成 \(\infty\) 就能不会对之后产生影响。注意区分两个 dfs 序的区别。
代码(已略去快读快写,码风清新,注释详尽)
1. 左偏树 + 懒惰标记
//#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <iostream>
#include <cstdio>
#define debug(a) cerr << "Line: " << __LINE__ << " " << #a << endl
#define print(a) cerr << #a << "=" << (a) << endl
#define file(a) freopen(#a".in", "r", stdin), freopen(#a".out", "w", stdout)
#define main Main(); signed main(){ return ios::sync_with_stdio(0), cin.tie(0), Main(); } signed Main
using namespace std;
#include <cstring>
#include <queue>
int n, m;
int ans[100010];
struct Graph{
struct node{
int to, len, nxt;
} edge[200010 << 1];
int eid, head[100010];
inline void add(int u, int v, int w){
edge[++eid] = {v, w, head[u]};
head[u] = eid;
}
node & operator [] (const int x){
return edge[x];
}
} xym, yzh;
// 一个是原图,一个是最短路径树
// 最短路,父亲,和父亲的距离
int dist[100010], fr[100010], lenfa[100010];
template <class T> using MinHeap = priority_queue<T, vector<T>, greater<T> >;
// dfs 序判断子树
int L[100010], R[100010], timer;
int root[100010]; // u 在左偏树里的结点
int dis[400010], lson[400010], rson[400010]; // 左偏树
int lazy[400010]; // 左偏树懒惰标记
pair<int, int> val[400010]; // 信息
int pcnt;
int NewNode(pair<int, int> v){
return val[++pcnt] = v, pcnt;
}
void pushtag(int u, int tag){
val[u].first += tag;
lazy[u] += tag;
}
void pushdown(int u){
if (lazy[u] == 0) return;
if (lson[u]) pushtag(lson[u], lazy[u]);
if (rson[u]) pushtag(rson[u], lazy[u]);
lazy[u] = 0;
}
int combine(int a, int b){
if (!a || !b) return a | b;
if (val[a] > val[b]) swap(a, b);
pushdown(a), rson[a] = combine(rson[a], b);
if (dis[lson[a]] < dis[rson[a]]) swap(lson[a], rson[a]);
dis[a] = dis[rson[a]] + 1;
return a;
}
// 第一遍 dfs
void dfs(int now){
L[now] = ++timer; // 记录 dfs 序
for (int i = xym.head[now]; i; i = xym[i].nxt){ // 枚举 yzh -> yzh' 这条非树边
int to = xym[i].to, len = xym[i].len;
if (fr[to] == now && len == lenfa[to]) continue; // 这里要很小心地判断树边
int node = NewNode({dist[now] + len, now});
if (!root[to]) root[to] = node;
else root[to] = combine(root[to], node);
}
for (int i = yzh.head[now]; i; i = yzh[i].nxt) dfs(yzh[i].to);
R[now] = timer;
}
// 第二遍 dfs
void redfs(int now){
for (int i = yzh.head[now]; i; i = yzh[i].nxt){ // 把子树标记合并过来
int to = yzh[i].to, len = yzh[i].len;
redfs(to);
if (!root[to]) continue;
pushtag(root[to], len);
if (!root[now]) root[now] = root[to];
else root[now] = combine(root[now], root[to]);
}
// 弹出不符合的标记
while (root[now] && L[now] <= L[val[root[now]].second] && L[val[root[now]].second] <= R[now]){
if (lson[root[now]] == 0 && rson[root[now]] == 0){
root[now] = 0;
} else {
root[now] = combine(lson[root[now]], rson[root[now]]);
}
}
// 统计答案
if (root[now]) ans[now] = val[root[now]].first;
else ans[now] = -1;
}
// 迪迦哥斯拉算法(逃
// 迪杰斯特拉求最短路,并建出最短路径树
void build(){
memset(dist, 0x3f, sizeof dist);
MinHeap<pair<int, int> > Q; Q.push({dist[1] = 0, 1});
while (!Q.empty()){
auto [ndis, now] = Q.top(); Q.pop();
if (dist[now] < ndis) continue;
for (int i = xym.head[now]; i; i = xym[i].nxt){
int to = xym[i].to, len = xym[i].len;
if (dist[to] > dist[now] + len){
Q.push({dist[to] = dist[now] + len, to});
fr[to] = now, lenfa[to] = len;
}
}
}
for (int i = 2; i <= n; ++i) yzh.add(fr[i], i, lenfa[i]);
}
signed main(){
read(n, m);
for (int i = 1, u, v, w; i <= m; ++i) read(u, v, w), xym.add(u, v, w), xym.add(v, u, w);
build(), dfs(1), redfs(1);
for (int i = 2; i <= n; ++i) write(ans[i], '\n');
return 0;
}
2. 线段树
//#pragma GCC optimize(3)
//#pragma GCC optimize("Ofast", "inline", "-ffast-math")
//#pragma GCC target("avx", "sse2", "sse3", "sse4", "mmx")
#include <iostream>
#include <cstdio>
#define debug(a) cerr << "Line: " << __LINE__ << " " << #a << endl
#define print(a) cerr << #a << "=" << (a) << endl
#define file(a) freopen(#a".in", "r", stdin), freopen(#a".out", "w", stdout)
#define main Main(); signed main(){ return ios::sync_with_stdio(0), cin.tie(0), Main(); } signed Main
using namespace std;
#include <cstring>
#include <queue>
#include <vector>
const int inf = 0x3f3f3f3f;
int n, m;
int ans[100010];
struct Graph{
struct node{
int to, len, nxt;
} edge[200010 << 1];
int eid, head[100010];
inline void add(int u, int v, int w){
edge[++eid] = {v, w, head[u]};
head[u] = eid;
}
node & operator [] (const int x){
return edge[x];
}
} xym, yzh;
// 一个是原图,一个是最短路径树
// 最短路,父亲,和父亲的距离
int dist[100010], fr[100010], lenfa[100010];
template <class T> using MinHeap = priority_queue<T, vector<T>, greater<T> >;
// dfs 序判断子树
int L[100010], R[100010], timer;
// 信息的 dfs 序
int infoL[100010], infoR[100010], infot;
vector<pair<int, int> > info[100010];
pair<int, int> val[400010]; // 注意这里要开大一些
struct Segment_Tree{
#define lson (idx << 1 )
#define rson (idx << 1 | 1)
struct Info{
pair<int, int> val;
int pos;
Info operator + (const Info & o) const {
if (val.first == inf) return o;
if (o.val.first == inf) return *this;
if (val.first < o.val.first) return *this;
return o;
}
Info operator + (const int o) const {
if (val.first == inf) return *this;
return {{val.first + o, val.second}, pos};
}
};
// 信息
struct node{
int l, r;
Info info;
int tag;
} tree[400010 << 2];
void pushup(int idx){
tree[idx].info = tree[lson].info + tree[rson].info;
}
void build(int idx, int l, int r){
tree[idx] = {l, r, {{inf, -1}, -1}, 0};
if (l == r) return tree[idx].info = {val[l], l}, void();
int mid = (l + r) >> 1;
build(lson, l, mid), build(rson, mid + 1, r), pushup(idx);
}
void pushtag(int idx, const int t){
tree[idx].info = tree[idx].info + t;
tree[idx].tag = tree[idx].tag + t;
}
void pushdown(int idx){
if (!tree[idx].tag) return;
pushtag(lson, tree[idx].tag), pushtag(rson, tree[idx].tag);
tree[idx].tag = 0;
}
Info query(int idx, int l, int r){
if (tree[idx].l > r || tree[idx].r < l) return {{inf, -1}, -1};
if (l <= tree[idx].l && tree[idx].r <= r) return tree[idx].info;
return pushdown(idx), query(lson, l, r) + query(rson, l, r);
}
void modify(int idx, int l, int r, const int t){
if (tree[idx].l > r || tree[idx].r < l) return;
if (l <= tree[idx].l && tree[idx].r <= r) return pushtag(idx, t);
pushdown(idx), modify(lson, l, r, t), modify(rson, l, r, t), pushup(idx);
}
void erase(int idx, int p){
if (tree[idx].l > p || tree[idx].r < p) return;
if (tree[idx].l == tree[idx].r) return tree[idx].info = {{inf, -1}, -1}, void();
pushdown(idx), erase(lson, p), erase(rson, p), pushup(idx);
}
#undef lson
#undef rson
} tree;
// 第一遍 dfs
void dfs(int now){
L[now] = ++timer; // 记录 dfs 序
for (int i = xym.head[now]; i; i = xym[i].nxt){ // 枚举 yzh -> yzh' 这条非树边
int to = xym[i].to, len = xym[i].len;
if (fr[to] == now && len == lenfa[to]) continue; // 这里要很小心地判断树边
info[to].push_back({dist[now] + len, now});
}
for (int i = yzh.head[now]; i; i = yzh[i].nxt) dfs(yzh[i].to);
R[now] = timer;
}
// 其实也算一次深搜,记录标记的 dfs 序
void prework(int now){
infoL[now] = infot + 1;
for (auto x: info[now]) val[++infot] = x;
for (int i = yzh.head[now]; i; i = yzh[i].nxt) prework(yzh[i].to);
infoR[now] = infot;
}
// 第二遍 dfs
void redfs(int now){
if (infoL[now] > infoR[now]) return;
for (int i = yzh.head[now]; i; i = yzh[i].nxt){ // 把子树标记合并过来
int to = yzh[i].to, len = yzh[i].len;
redfs(to), tree.modify(1, infoL[to], infoR[to], len);
}
// 弹出不符合的标记
while (true){
Segment_Tree::Info res = tree.query(1, infoL[now], infoR[now]);
if (res.val.first == inf || res.pos == -1) break;
if (L[now] <= L[res.val.second] && L[res.val.second] <= R[now]){
tree.erase(1, res.pos);
continue;
}
// 统计答案
ans[now] = res.val.first;
break;
}
}
// 迪迦哥斯拉算法(逃
// 迪杰斯特拉求最短路,并建出最短路径树
void build(){
memset(dist, 0x3f, sizeof dist);
MinHeap<pair<int, int> > Q; Q.push({dist[1] = 0, 1});
while (!Q.empty()){
auto [ndis, now] = Q.top(); Q.pop();
if (dist[now] < ndis) continue;
for (int i = xym.head[now]; i; i = xym[i].nxt){
int to = xym[i].to, len = xym[i].len;
if (dist[to] > dist[now] + len){
Q.push({dist[to] = dist[now] + len, to});
fr[to] = now, lenfa[to] = len;
}
}
}
for (int i = 2; i <= n; ++i) yzh.add(fr[i], i, lenfa[i]);
}
signed main(){
read(n, m);
for (int i = 1, u, v, w; i <= m; ++i) read(u, v, w), xym.add(u, v, w), xym.add(v, u, w);
for (int i = 2; i <= n; ++i) ans[i] = -1;
build(), dfs(1), prework(1), tree.build(1, 1, infot), redfs(1);
for (int i = 2; i <= n; ++i) write(ans[i], '\n');
return 0;
}
总结 & 后话
考场上没想到可以将长度拆开来算,也就是计算答案的时候不用一步一步加与父亲的长度加上去,而是用 \(d_{yzh'} - d_u\) 计算,这样对于 \(u\) 来说,\(d_u\) 是确定的,要求的就是 \(\min \lbrace d_{yzh} + \operatorname{dist}(yzh, yzh') + d_{yzh'} \rbrace\),这样可以愉快地用启发式合并秒了啊!但是练习一道左偏树懒惰标记的题目,感觉也颇有收获呢。但是但是,线段树无敌爱敲。
[USACO09JAN] Safe Travel G 题解的更多相关文章
- luogu P2934 [USACO09JAN]安全出行Safe Travel
题目链接 luogu P2934 [USACO09JAN]安全出行Safe Travel 题解 对于不在最短路树上的边(x, y) 1 | | t / \ / \ x-----y 考虑这样一种形态的图 ...
- P2934 [USACO09JAN]安全出行Safe Travel
P2934 [USACO09JAN]安全出行Safe Travel https://www.luogu.org/problemnew/show/P2934 分析: 建出最短路树,然后考虑一条非树边u, ...
- 数据结构(左偏树,可并堆):BNUOJ 3943 Safe Travel
Safe Travel Time Limit: 3000ms Memory Limit: 65536KB 64-bit integer IO format: %lld Java class ...
- 洛谷P3104 Counting Friends G 题解
题目 [USACO14MAR]Counting Friends G 题解 这道题我们可以将 \((n+1)\) 个边依次去掉,然后分别判断去掉后是否能满足.注意到一点, \(n\) 个奶牛的朋友之和必 ...
- 洛谷P2115 Sabotage G 题解
题目 [USACO14MAR]Sabotage G 题解 本蒟蒻又来了,这道题可以用二分答案来解决.我们可以设答案最小平均产奶量为 \(x \ (x \in[1,10000])\) .然后二分搜索 \ ...
- 「BZOJ1576」[Usaco2009 Jan] 安全路经Travel------------------------P2934 [USACO09JAN]安全出行Safe Travel
原题地址 题目描述 Gremlins have infested the farm. These nasty, ugly fairy-like creatures thwart the cows as ...
- ●洛谷P2934 [USACO09JAN]安全出行Safe Travel
题链: https://www.luogu.org/problemnew/show/P2934 题解: 最短路(树),可并堆(左偏堆),并查集. 个人感觉很好的一个题. 由于题目已经明确说明:从1点到 ...
- 洛谷—— P2934 [USACO09JAN]安全出行Safe Travel || COGS ——279|| BZOJ——1576
https://www.luogu.org/problem/show?pid=2934 题目描述 Gremlins have infested the farm. These nasty, ugly ...
- [USACO09JAN]安全出行Safe Travel 最短路,并查集
题目描述 Gremlins have infested the farm. These nasty, ugly fairy-like creatures thwart the cows as each ...
- [USACO09JAN]安全出行Safe Travel
题目 什么神仙题啊,我怎么只会\(dsu\)啊 我们考虑一个非常暴力的操作,我们利用\(dsu\ on \ tree\)把一棵子树内部的非树边都搞出来,用一个堆来存储 我们从堆顶开始暴力所有的边,如果 ...
随机推荐
- SpringBoot+Selenium模拟用户操作浏览器
Selenium Selenium是一个用于Web应用程序自动化测试的开源工具套件.它主要用于以下目的: 浏览器自动化:Selenium能够模拟真实用户在不同浏览器(如Chrome.Firefox.I ...
- 迭代器模式(Interator Pattern)
一.模式动机 迭代器模式(Iterator Pattern)是一种使用频率非常高的行为型设计模式,迭代器用于对一个聚合对象进行遍历.通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负 ...
- [翻译].NET 8 的原生AOT及高性能Web开发中的应用[附性能测试结果]
原文: [A Dive into .Net 8 Native AOT and Efficient Web Development] 作者: [sharmila subbiah] 引言 随着 .NET ...
- Java面试知识点(五)hashmap、hashtable和hashset
1. 关于 HashMap 的一些说法: a) HashMap 实际上是一个 "链表散列" 的数据结构,即数组和链表的结合体.HashMap 的底层结构是一个数组,数组中的每一项是 ...
- FEDORA 显卡驱动安装
FEDORA 显卡驱动安装 在fedora中akmod-nvidia包可以自动的处理开源驱动屏蔽等各种问题, 强烈推荐用这个安显卡驱动. -1. 在 BIOS 中关闭安全启动 0. 切换桌面环境至 X ...
- python基础-入门必备知识
1 标识符 标识符是编程时使用的名字,用于给变量.函数.语句块等命名,Python 中标识符由字母.数字.下划线组成,不能以数字开头,区分大小写. 以下划线开头的标识符有特殊含义,单下划线开头的标识符 ...
- 开发一个微信小程序流程及需要多少费用?
流程如下: 小程序是一种新的开放能力,开发者可以快速地开发一个小程序.小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验. 开放注册范围:个人 企业 政府 媒体 其他组织 1.注册 在微信公 ...
- rem适配布局
没有一张图解决不了的事 https://www.processon.com/mindmap/5e3a589be4b021dc2899f511 <link rel="stylesheet ...
- Apache基于IP和端口
Apache基于IP 步骤1:添加并配置虚拟网卡 添加虚拟网卡:通常在虚拟机环境中,可以通过虚拟机软件(如VMware或VirtualBox)的网络设置来添加额外的网络适配器. 配置IP地址:编辑/e ...
- webpack4.15.1 学习笔记(八) — 缓存(Caching)
目录 输出文件名(Output Filenames) 缓存第三方库 将 js 文件放到一个文件夹中 webpack 打包模块化后的应用程序,会生成一个可部署的 /dist目录,只要 /dist 目录中 ...