[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\),这样可以愉快地用启发式合并秒了啊!但是练习一道左偏树懒惰标记的题目,感觉也颇有收获呢。但是但是,线段树无敌爱敲

posted @ 2024-03-19 16:28  XuYueming  阅读(31)  评论(0编辑  收藏  举报