首页 > 其他分享 >虚树

虚树

时间:2024-08-11 16:18:06浏览次数:9  
标签:int fat 节点 leq LCA 虚树 dp

引入

$ \color{skyblue} P2495 [SDOI2011] 消耗战 $

题意

给定一颗 $ n $ 个节点的树,每条边有边权。有 $ m $ 次询问,每次询问钦定 $ k $ 个特殊节点,问使得节点 $ 1 $ 不与任何特殊节点相连通所需要删除的最小总边权是多少。

数据范围:

  • 对于 \(100\%\) 的数据,\(2\leq n \leq 2.5\times 10^5, 1\leq m\leq 5\times 10^5, \sum k_i \leq 5\times 10^5, 1\leq k_i< n, h_i\neq 1, 1\leq u,v\leq n, 1\leq w\leq 10^5\) 。

思路

首先考虑数据范围较小时怎么做。

显然存在 $ O(n) $ 的线性 $ dp $:

设 $ dp_i $ 表示在以 $ i $ 为根的子树内使得 $ i $ 不与特殊节点联通所需删除最小总边权。

设当前考虑到 $ i $ 的子节点 $ v $

  • 若 $ v $ 是特殊节点,$ dp_i = dp_i + w(i, v) $

  • 若 $ v $ 不是特殊节点,$ dp_i = dp_i + min(dp_v, w(i, v) $

可以发现,在 $ \sum k_i \leq 5\times 10^5 $ 这样的数据范围下,每次跑一遍线性 $ dp $ 会有很多冗余计算。

所以考虑如何将 $ dp $ 的复杂度降为与 $ k $ 相关。

这时就引入了虚树的概念。

在虚树中,特殊节点以及其两两之间的LCA都被保留,而其他节点则被省略。

如:

(图片来自OI Wiki)

因为特殊节点两两之间的LCA被保留,所以虚树的节点个数最多是 $ 2 \times k $ 个 $ ( k $ 为特殊节点个数 $ ) $ 。

显然,虚树中两点之间的祖先关系并不会改变,所以我们可以在虚树上跑 $ dp $, 而这时 $ dp $ 的时间复杂度就是 $ O(k) $ 的了。

接下来的问题就是如何构造虚树。

目前有两种方法,一是两次排序+LCA连边,二是单调栈建树。

这里只介绍第一种(个人认为代码简单且好理解)

步骤:

  1. 将特殊节点按 $ dfs $ 序排序。

  2. 将特殊节点及相邻两项间的 $ LCA $ 放入一个新的数组并去重。

  3. 对于新数组中的相邻两项 $ x $ 和 $ y $ ,把 $ LCA(x, y) $ 和 $ y $ 连边。

证明

如果 $ x $ 是 $ y $ 的祖先,那么 $ x $ 直接到 $ y $ 连边。因为 $ DFS $ 序保证了 $ x $ 和 $ y $ 的 $ DFS $ 序是相邻的,所以 $ x $ 到 $ y $ 的路径上面没有关键点。
如果 $ x $ 不是 $ y $ 的祖先,那么就把 $ LCA(x, y) $ 当作 $ y $ 的的祖先,根据上一种情况也可以证明 $ LCA(x, y) $ 到 $ y $ 点的路径上不会有关键点。
所以连接 $ LCA(x, y) $ 和 $ y $ 不会遗漏,也不会重复。
另外第一个点没有被一个节点连接会不会有影响呢?因为第一个点一定是这棵树的根,所以不会有影响,所以总边数就是 $ m-1 $ 条。

引自OI-Wiki

模板:

struct Edge {
	int nxt, to, w;
};

struct Tree {
	int cnt, head[N];
	Edge e[N << 1];
	void addedge(int u, int v, int w) {
		cnt++;
		e[cnt].nxt = head[u];
		e[cnt].to = v;
		e[cnt].w = w;
		head[u] = cnt;
	}
}v;

void build() {
	v.cnt = -1;
	k = read();
	for(int i = 1; i <= k; i++) h[i] = read(), q[h[i]] = 1;
	sort(h + 1, h + 1 + k, cmp);
	len = 0;
	for(int i = 1; i < k; i++) {
		a[++len] = h[i];
		a[++len] = lca(h[i], h[i + 1]);
	}
	a[++len] = h[k];
	a[++len] = 1;
	sort(a + 1, a + 1 + len, cmp);
	len = unique(a + 1, a + 1 + len) - a - 1;
	for(int i = 1; i < len; i++) {
		int p = lca(a[i], a[i + 1]);
		v.addedge(p, a[i + 1], dis(p, a[i + 1]));
	}
}

回到题目。

既然建树已经解决了,那这题就可以做了。

注意,对于每组询问都清空全部数组的话时间复杂度仍然会炸,所以就对用过的位置恢复即可。

代码

#include<bits/stdc++.h>
#define int long long
#define Debug puts("Oops!")
using namespace std;

const int N = 6e5 + 5, M = 5e5 + 5;

int n, m, k, len;
int h[N], a[N], q[N];

struct Edge {
	int nxt, to, w;
};

struct Tree {
	int cnt, head[N];
	Edge e[N << 1];
	void addedge(int u, int v, int w) {
		cnt++;
		e[cnt].nxt = head[u];
		e[cnt].to = v;
		e[cnt].w = w;
		head[u] = cnt;
	}
}g, v;

int dep[N], dfn[N], tot;
int fat[N][25], d[N][25];
void dfs(int x, int fa) {
	dfn[x] = ++tot;
	dep[x] = dep[fa] + 1;
	for(int i = g.head[x]; ~i; i = g.e[i].nxt) {
		int y = g.e[i].to;
		if(y == fa) continue ;
		fat[y][0] = x;
		d[y][0] = g.e[i].w;
		dfs(y, x);
	}
}

void init() {
	for(int i = 1; i <= 19; i++) {
		for(int j = 1; j <= n; j++) {
			fat[j][i] = fat[fat[j][i - 1]][i - 1],
			d[j][i] = min(d[j][i - 1], d[fat[j][i - 1]][i - 1]);
		}
	}
}

int lca(int x, int y) {
	if(dep[x] < dep[y]) swap(x, y);
	for(int i = 19; i >= 0; i--)
		if(dep[fat[x][i]] >= dep[y])
			x = fat[x][i];
	if(x == y) return x;
	for(int i = 19; i >= 0; i--)
		if(fat[x][i] != fat[y][i]) x = fat[x][i], y = fat[y][i];
	return fat[x][0];
}

int dis(int x, int y) {//求x到y的路径上的边权最小值 
	int res = LLONG_MAX;
	for(int i = 19; i >= 0; i--)
		if(dep[fat[y][i]] >= dep[x])
			res = min(res, d[y][i]), y = fat[y][i];
	return res;
}

bool cmp(int x, int y) {
	return dfn[x] < dfn[y];
}

int f[N];
void dp(int x) {
	f[x] = 0;
	for(int i = v.head[x]; ~i; i = v.e[i].nxt) {
		int y = v.e[i].to;
		dp(y);
		if(q[y]) f[x] += v.e[i].w;
		else f[x] += min(f[y], v.e[i].w);
	}
}
 
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
	while(isdigit(c)) x = x * 10 + c - '0', c = getchar();
	return x * f;
}

void solve() {
	v.cnt = -1;
	k = read();
	for(int i = 1; i <= k; i++) h[i] = read(), q[h[i]] = 1;
	sort(h + 1, h + 1 + k, cmp);
	len = 0;
	for(int i = 1; i < k; i++) {
		a[++len] = h[i];
		a[++len] = lca(h[i], h[i + 1]);
	}
	a[++len] = h[k];
	a[++len] = 1;
	sort(a + 1, a + 1 + len, cmp);
	len = unique(a + 1, a + 1 + len) - a - 1;
	for(int i = 1; i < len; i++) {
		int p = lca(a[i], a[i + 1]);
		v.addedge(p, a[i + 1], dis(p, a[i + 1]));
		//删除虚树上的这条边只要删除原树上p到a[i + 1]的路径中的最小边即可 
	}
	dp(1);
	cout << f[1] << endl;
	for(int i = 1; i < len; i++) v.head[a[i + 1]] = -1, v.head[lca(a[i], a[i + 1])] = -1;
	for(int i = 1; i <= k; i++) q[h[i]] = 0;
}

signed main() {
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	memset(g.head, -1, sizeof g.head);
	memset(v.head, -1, sizeof v.head);
	g.cnt = -1;
	n = read();
	for(int i = 1, u, v, w; i < n; i++) {
		u = read(), v = read(), w = read();
		g.addedge(u, v, w);
		g.addedge(v, u, w);
	}
	memset(d, 0x7f, sizeof d);
	dfs(1, 0);
	init();
	m = read();
	while(m--) solve();
	return 0;
}

标签:int,fat,节点,leq,LCA,虚树,dp
From: https://www.cnblogs.com/zeta-y/p/18353538

相关文章

  • 虚树
    虚树VirtualTree浓缩信息,把一整颗大树浓缩成一颗小树。下图中,红色结点是我们选择的关键点。红色和黑色结点都是虚树中的点。黑色的边是虚树中的边。OIWIKI两种建树方式1.第一种构造过程:二次排序+LCA连边(容易理解,常数略大)boolcmp(intx,inty){returndfn[x......
  • 关于虚树
    关于虚树瞎扯某些树上问题,给了巨多节点,而实际上它们之中只有小部分能做出贡献,其余都是些水军,为杀尽OIers的脑细胞做出努力考虑重新种一棵树,浓缩信息,简化节点个数,于是产生了虚树。大概是长这个样子:红色结点是我们选择的关键点,即能够做出贡献的点。红色和黑色结点都是虚树中......
  • 20240806:点分治,虚树选做
    POJ-1741Tree题意:给定一棵树,求多少无序对\((u,v)\)满足\(\text{dist}(u,v)\lek\)。对于树上的路径进行分类:经过根;不经过根。第二类路径点可以通过递归子树求出。对于经过根的路径,可以一遍dfs求出每个点到根的距离\(\text{dis}(u)\)。问题转化为求\(\text{dis}(u)......
  • 虚树【学习笔记】
    为什么要用虚树?例题在某些树上问题中,对于某次询问,我们并不需要用到全部的树上的点:例如,例题中:总点数\(n\le2.5\times10^5\)询问次数\(m\le5\times10^5\)询问的点数\(\sumk_i\le5\times10^5\)我们可以发现其实每次询问均摊下来的询问点数k并不多,但如果每次询问都......
  • 虚树复习 & O(1) 查询 LCA
    放假是不可能做题的。那就写总结把。虚树问题的情境涉及多次树上询问,每次指定一些点,让你计算。此类问题需要我们在线地找到尽可能少的【关键点】进行计算,最好和给的点级别一样。虚树的思想就是这个过程。二次排序一个关键直觉:【指定点】两两的LCA一定是【关键点】。并且......
  • 从 dfs 序求 lca 到虚树到树分块 学习笔记
    前言想象我在口胡三样我都不熟悉的东西并尝试称之为“学习笔记”。其实不过是我自己对于它的一点小理解,甚至可能是错误的!无所谓,口胡!口胡!口胡!口胡!口胡!一些备注\(dfn_u\)为点\(u\)的dfn序,\(nfd_i\)表示第\(i\)个dfs到的点是啥(前者的反数组)dfs序求lca这个很简单,想......
  • 虚树初步学习笔记
    虚树给定一棵树,树上有一些关键点,你要建另一棵树,保留关键点,以及任意一对关键点的\(\text{LCA}\)。当你发现对于一棵树,你只有一些关键点有用的时候,就可以尝试建虚树。两次排序思路先把所有点按\(\text{dfn}\)序排序,然后把\(\text{dfn}\)相邻的两个点取出来,再把它们的\(\t......
  • 虚树
    虚树什么是虚树虚树常常被用在树形\(dp\)中。当一次询问仅仅涉及到整棵树中少量节点时为每次询问都对整棵树进行\(dp\)在时间上是不可接受的。此时,我们建立一棵仅仅包含部分关键节点的虚树,将非关键节点构成的链简化成边或是剪去,在虚树上进行\(dp\)。虚树包含所有的询问点及它们......
  • 虚树
    虚树简介虚树一般用于树形DP中,可以有效减少冗余的计算量。其原理是将对DP无影响,或者在影响可快速运算范围内的点缩在一起,从而使得整棵树大小极大的减小。因此,可以使用虚树的题目一般有特殊点之类的设定,多测并限定特殊点的总量。P2495[SDOI2011]消耗战一道经典的虚......
  • trick:动态维护虚树大小
    对dfn序开数据结构(如线段树),每个结点\(p\)维护对应dfn序区间内所有存在的点所构成的虚树大小\(sz_p\),需要维护区间内所有存在的点中dfn序最大点\(rv_p\)和最小点\(lv_p\)、区间内所有存在点的LCA\(lca_p\).考虑合并左右儿子\(ls,rs\),按两棵虚树是否相交分讨,先考虑......