@loj - 2553@「CTSC2018」暴力写挂


@description@

temporaryDO 是一个很菜的 OIer 。在 4 月,他在省队选拔赛的考场上见到了《林克卡特树》一题,其中 k = 0 的部分分是求树 T 上的最长链。可怜的 temporaryDO 并不会做这道题,他在考场上抓猫耳挠猫腮都想不出一点思路。
这时,善良的板板出现在了空中,他的身上发出璀璨却柔和的光芒,荡漾在考场上。‘‘题目并不难。’’ 板板说。那充满磁性的声音,让 temporaryDO 全身充满了力量。 他决定:写一个枚举点对求 LCA 算距离的 k = 0 O(n^2\log\ n) 的部分分程序!于是, temporaryDO 选择以 1 为根,建立了求 LCA 的树链剖分结构,然后写了二重 for 循环枚举点对。
然而,菜菜的 temporaryDO 不小心开小了数组,于是数组越界到了一片神秘的内存区域。但恰好的是,那片内存区域存储的区域恰好是另一棵树 T′ 。这样一来,程序并没有 RE ,但他求 x y 的距离的时候,计算的是

depth(x) + depth(y) - (depth(LCA(x , y)) + depth′ (LCA′ (x, y)))

最后程序会输出每一对点对 i, j (i \le j) 的如上定义的‘‘距离’’ 的最大值。
temporaryDO 的程序在评测时光荣地爆零了。但他并不服气,他决定花好几天把自己的程序跑出来。请你根据 T T′ 帮帮可怜的 temporaryDO 求出他程序的输出。

input
第一行包含一个整数 n,表示树上的节点个数;
第 2 到第 n 行,每行三个整数 u, v, w,表示 T 中存在一条从 u 到 v 的边,其长度为 w;
第 n+1 到第 2n - 1 行,每行三个整数 u, v, w,表示 T' 中存在一条从 u 到 v 的边,其长度为 w。

output
输出一行一个整数,表示 temporaryDO 的程序的输出。

sample input
6
1 2 2
1 3 0
2 4 1
2 5 -7
3 6 0
1 2 -1
2 3 -1
2 5 3
2 6 -2
3 4 8
sample output
5

样例解释
点对 (3, 4) 的距离计算为 3 + 0 - (0 + (-2)) = 5。

数据范围与提示
对于所有数据, n <= 366666, |v| <= 2017011328。
depth(p) 和 depth'(p) 分别表示树 T、T' 中点 1 到点 p 的距离,这里规定,距离指的是经过的边的边权总和,其中 depth(1) = 0。
LCA(x, y) 和 LCA'(x, y) 分别表示树 T、T' 中点 x 与点 y 的最近公共祖先,即在从 x 到 y 的最短路径上的距离根经过边数最少的点。

@solution@

先考虑对式子 depth(x) + depth(y) - depth(LCA(x, y)) - depth'(LCA'(x, y)) 进行一定程度的变形。
可以联想到我们求两点间的距离公式 dist(x, y) = depth(x) + depth(y) - 2*depth(LCA(x, y)),反解出 depth(LCA(x, y)) = 1/2*(depth(x) + depth(y) - dist(x, y))。
于是可以消掉 LCA(x, y) 变成 depth(x) + depth(y) - 1/2*(depth(x) + depth(y) - dist(x, y)) - depth'(LCA'(x, y)) = 1/2*(depth(x) + depth(y) + dist(x, y)) - depth'(LCA'(x, y))。

因为出现了 dist(x, y),我们可以类比 WC2018 的通道一题的边分治做法,将 T 进行重构边分治。
考虑边分治的某一层求出 f[u] 表示 u 离当前连通块的中心边的距离,则我们相当于最大化 1/2*(depth(x) + f[x] + depth(y) + f[y] - 2*depth'(LCA'(x, y))) + 1/2*val 的值,其中 val 是中心边的长度。
然后我们再参考 WC2018 的做法,对 T' 建构虚树,再树上简单 dfs 一下就可以了。

边分治 O(nlogn) 套上虚树 O(nlogn),然而出题人很明显就是要卡这种 O(nlog^2n) 的做法。
但是我们可以将建构虚树的复杂度降低至 O(n) 。考虑虚树的瓶颈复杂度在于排序,一种常规方法是离线计数排序;另外一种是我自己BB出来的所以不知道正确性就可以利用边分治的子问题,进行线性的有序数列归并操作。

不过还有一种更有意思的做法。考虑类比存储点分治的分治过程而建出的点分树,我们存储边分治中的分治过程信息建出 T 的边分树。
这个边分树的结构有些类似于 kruskal 重构树:它的叶子对应原图中的结点,非叶结点对应原图中的边;且两个叶子 u, v 的 LCA 点 e 表示 u, v 这条路径在以 e 为中心边的地方被处理。
同时,类比点分树,边分树的最大深度是 O(log n) 的级别的。于是就可以存储非叶结点的左右儿子以及到达叶结点经过的路径向左记 0 向右记 1 得到的 01 串。

然后,我们这一次从 T' 出发,尝试枚举 LCA'(x, y),令其为 p。考虑将 p 的子树内所有结点以此塞入边分树,并在保证是以 p 为 LCA 的前提下求解答案。
我们在边分树的每个非叶结点处维护它的左/右子树中分别 depth[x] + f[x] 的最大值,于是每一个非叶结点都会贡献一个可能的答案。
不难发现我们不能将 p 的子树内所有结点暴力一个个塞,但是我们可以通过合并儿子的信息得到 p 的信息。启发式合并是 O(nlog^2n) 的,如果将边分树类比为线段树进行合并时间复杂度就成功降为 O(nlogn) 了。

@accepted code@

#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const ll INF = (1LL<<61);
const int MAXN = 2*366666;
struct Graph{
	struct edge{
		edge *nxt, *rev;
		int to, dis;
		bool tag;
	}edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt;
	Graph() {ecnt = &edges[0];}
	void addedge(int u, int v, int w) {
		edge *p = (++ecnt), *q = (++ecnt);
		p->to = v, p->dis = w, p->tag = false, p->nxt = adj[u], adj[u] = p;
		q->to = u, q->dis = w, q->tag = false, q->nxt = adj[v], adj[v] = q;
		p->rev = q, q->rev = p;
	}
}G1, G2, G3;
ll dep[MAXN + 5];
int m, n;
void rebuild(int x, int f, ll d) {
	int lst = -1; dep[x] = d;
	for(Graph::edge *p=G1.adj[x];p;p=p->nxt) {
		if( p->to == f ) continue;
		rebuild(p->to, x, d + p->dis);
		if( lst == -1 ) {
			G3.addedge(x, p->to, p->dis);
			lst = x;
		}
		else {
			int s = (++m);
			G3.addedge(lst, s, 0);
			G3.addedge(s, p->to, p->dis);
			lst = s;
		}
	}
}
bool a[35][MAXN + 5]; ll dis[35][MAXN + 5];
int ch[2][MAXN + 5], cnt = 0;
Graph::edge *e[MAXN + 5];
int siz[MAXN + 5];
void update(Graph::edge *&a, Graph::edge *b, int tot) {
	if( a == NULL ) a = b;
	else if( b && max(siz[b->to], tot-siz[b->to]) <= max(siz[a->to], tot-siz[a->to]) )
		a = b;
}
Graph::edge *get_mid_edge(int x, int f, int tot) {
	Graph::edge *ret = NULL; siz[x] = 1;
	for(Graph::edge *p=G3.adj[x];p;p=p->nxt) {
		if( p->to == f || p->tag ) continue;
		update(ret, get_mid_edge(p->to, x, tot), tot);
		siz[x] += siz[p->to];
		update(ret, p, tot);
	}
	return ret;
}
void dfs(int x, int f, int t, ll d, const int &dep) {
	a[dep][x] = t, dis[dep][x] = d;
	for(Graph::edge *p=G3.adj[x];p;p=p->nxt) {
		if( p->to == f || p->tag ) continue;
		dfs(p->to, x, t, d + p->dis, dep);
	}
}
int divide(int x, int tot, int dep) {
	Graph::edge *m = get_mid_edge(x, 0, tot);
	if( m == NULL ) return -1;
	int tmp = (++cnt);
	e[tmp] = m, m->tag = m->rev->tag = true;
	dfs(m->to, 0, 0, 0, dep), dfs(m->rev->to, 0, 1, 0, dep);
	ch[0][tmp] = divide(m->to, siz[m->to], dep + 1);
	ch[1][tmp] = divide(m->rev->to, tot-siz[m->to], dep + 1);
	return tmp;
}
struct node{
	node *ch[2]; ll mx[2];
}pl[35*MAXN + 5], *ncnt=&pl[0], *NIL=&pl[0];
node *new_tree(const int &x, int nw, int d) {
	if( nw == -1 ) return NIL;
	node *p = (++ncnt); p->ch[0] = p->ch[1] = NIL;
	p->ch[a[d][x]] = new_tree(x, ch[a[d][x]][nw], d + 1);
	p->mx[a[d][x]] = dep[x] + dis[d][x], p->mx[!a[d][x]] = -INF;
	return p;
}
ll ans = -INF;
node *merge(node *rt1, node *rt2, const ll &x, int rt) {
	if( rt1 == NIL ) return rt2;
	if( rt2 == NIL ) return rt1;
	ans = max(ans, rt1->mx[0] + rt2->mx[1] + e[rt]->dis - 2*x);
	ans = max(ans, rt1->mx[1] + rt2->mx[0] + e[rt]->dis - 2*x);
	rt1->ch[0] = merge(rt1->ch[0], rt2->ch[0], x, ch[0][rt]);
	rt1->ch[1] = merge(rt1->ch[1], rt2->ch[1], x, ch[1][rt]);
	rt1->mx[0] = max(rt1->mx[0], rt2->mx[0]);
	rt1->mx[1] = max(rt1->mx[1], rt2->mx[1]);
	return rt1;
}
node *get_ans(int x, int f, ll d) {
	node *rt = new_tree(x, 1, 0);
	for(Graph::edge *p=G2.adj[x];p;p=p->nxt)
		if( p->to != f )
			merge(rt, get_ans(p->to, x, d + p->dis), d, 1);
	ans = max(ans, 2*(dep[x] - d));
	return rt;
}
int main() {
	scanf("%d", &n);
	for(int i=1;i<n;i++) {
		int x, y, v; scanf("%d%d%d", &x, &y, &v);
		G1.addedge(x, y, v);
	}
	for(int i=1;i<n;i++) {
		int x, y, v; scanf("%d%d%d", &x, &y, &v);
		G2.addedge(x, y, v);
	}
	m = n, rebuild(1, 0, 0);
	divide(1, m, 0); get_ans(1, 0, 0);
	printf("%lld\n", ans / 2);
}

@details@

处理上有一个小细节:为了避免小数,可以先将所有权值 * 2,再在最后输出时 / 2。

代码量还是比 WC2018 那道题小些,也不是很难调就调出来了。

事实证明 trie 树可以线段树合并,边分树也可以线段树合并万物皆可线段树合并

posted @ 2019-08-04 19:21  Tiw_Air_OAO  阅读(167)  评论(0编辑  收藏  举报