首页 > 其他分享 >虚树

虚树

时间:2024-04-14 11:56:06浏览次数:12  
标签:head int top edge Maxn 虚树 dp

1 引入

首先看这样一道题:[SDOI2011] 消耗战

有一棵树,边上有边权。每次询问给出 \(k\) 个点,找到一些边,使得删去这些边后从 \(1\) 号节点无法达到这 \(k\) 个点中任意一个,同时使边权最小。

显然这是一道树形 dp。如果说只给我们一次询问,可以很简单的 \(O(n)\) 求出。

但是现在有了 \(m\) 次询问,如果再使用这样的方式复杂度就是 \(O(nm)\),显然无法实现。

我们会发现,\(k\) 的数量并不大,如果复杂度达到 \(O(\sum k)\) 级别,也许可以通过。

那么如何实现呢?

2 虚树概念与建立

2.1 概念

假如我们有这样的一张图,其中红色节点就是 \(k\) 个点(称其为关键点):

我们会发现,此时 dp \(1\) 号节点的右子树完全是没有必要的。换句话说,就是这样无谓的 dp 浪费了时间。

我们能不能将 dp 只保留在关键点中呢?这是就要引入虚树。

先看几张图片:

oi-wiki.org/graph/images/vtree-vtree3.svg

oi-wiki.org/graph/images/vtree-vtree4.svg

这些节点和边就构成了虚树。

我们发现,虚树并没有改变祖孙关系,也没有遗漏信息(因为保留了关键点的 LCA),只是将原先较大的一棵树变为较小的一棵树。

由此得到虚树定义:保存关键点及所有关键点的 LCA 的树。

现在的问题是:如何建树?

2.2 建树

我们考虑使用单调栈建树。

首先我们考虑之前所学单调栈建树,不难发现单调栈总是在维护树上的一条链。因此考虑维护一个单调递增栈(注意指的是 DFS 序单调递增)。在这个栈中,每个节点在虚树上的父亲就是它栈下面的节点。

首先将所有关键点按照 DFS 序排序,然后将第一个关键点直接加入。

接下来顺次加入每一个关键点。设当前加入的是 \(x\),栈顶元素为 \(t\),同时令 \(l=\text{Lca}(x,t)\)。

接下来我们需要往里面插入这个节点,显然要将不属于这条链上的先取出,取出的同时建边。

什么时候停止呢?设此时栈中第二个元素为 \(p\),当 \(dfn_p\le dfn_l\) 的时候就表明此时已经来到了这条链上。

接下来分类讨论:

  • 当 \(l=t\) 时,此时根本不用进行弹栈操作,直接将 \(x\) 加入即可。
  • 当 \(l=y\) 时,表明 \(l\) 就在栈内,我们让栈顶连边后弹出(因为它不属于这条链),然后加入 \(x\) 即可。
  • 当 \(l\ne y\) 时,表明 \(l\) 此时没有进栈,也就没有和此时的栈顶连边。将栈顶与 \(l\) 连边后弹出,最后加入 \(l\) 和 \(x\) 即可。

这就表明,只要出单调栈就要建边。

最后放三种情况分别表示的图:

52299.png (496×496) (luogu.com.cn) 52301.png (496×496) (luogu.com.cn) 52300.png (496×496) (luogu.com.cn)

至此你就学会了虚树。

建好虚树之后考的就是树形 dp 了。

2.3 完整代码

//SDOI2011 消耗战
#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef long long LL;
const int Maxn = 5e5 + 5;

int n, q, m;
int imp[Maxn];
bool is[Maxn];

int dfn[Maxn], son[Maxn], siz[Maxn], dep[Maxn], fa[Maxn], ind, top[Maxn], minn[Maxn];
struct Tree {
	int head[Maxn], edgenum;
	struct node {
		int nxt, to, w;
	}edge[Maxn];
	void add(int from, int to, int w) {
		edge[++edgenum] = {head[from], to, w};
		head[from] = edgenum;
	}
	void dfs1(int x) {
		son[x] = -1;
		siz[x] = 1;
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to;
			if(to == fa[x]) continue;
			fa[to] = x;
			dep[to] = dep[x] + 1;
			minn[to] = min(minn[x], edge[i].w);
			dfs1(to);
			siz[x] += siz[to];
			if(son[x] == -1 || siz[to] > siz[son[x]]) {
				son[x] = to;
			}
		}
	}
	void dfs2(int x, int rt) {
		top[x] = rt;
		dfn[x] = ++ind;
		if(son[x] == -1) return;
		dfs2(son[x], rt);
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to;
			if(to == fa[x] || to == son[x]) continue;
			dfs2(to, to);
		}
	}
	int lca(int x, int y) {
		while(top[x] != top[y]) {
			if(dep[top[x]] < dep[top[y]]) {
				swap(x, y);
			}
			x = fa[top[x]];
		}
		return dep[x] < dep[y] ? x : y;
	}
}T;

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

struct Virtual_Tree {
	int head[Maxn], edgenum;
	struct node {
		int nxt, to;
	}edge[Maxn];
	void add(int from, int to) {
		edge[++edgenum] = {head[from], to};
		head[from] = edgenum;
	}
	int s[Maxn], top;
	void build() {//建树操作
		edgenum = top = 0;
		s[++top] = imp[1];
		head[imp[1]] = 0;
		for(int i = 2; i <= m; i++) {
			int x = imp[i], t = s[top];
			int l = T.lca(x, t);
			if(l == t) {
				head[x] = 0, s[++top] = x;
				continue;
			}
			while(dfn[s[top - 1]] > dfn[l]) {
				add(s[top - 1], s[top]);
				top--;
			}
			if(s[top - 1] != l) {
				head[l] = 0, add(l, s[top]);
				s[top] = l;
			}
			else {
				add(l, s[top--]);
			}
			head[x] = 0;
			s[++top] = x;
		}
		for(int i = 1; i < top; i++) {
			add(s[i], s[i + 1]);
		}
	}
	int dp[Maxn];
	void dfs(int x) {
		int cnt = 0;
		for(int i = head[x]; i; i = edge[i].nxt) {
			int to = edge[i].to;
			dfs(to);
			cnt += dp[to];
		}
		if(is[x]) {
			dp[x] = minn[x];
		}
		else {
			dp[x] = min(cnt, minn[x]);
		}
		is[x] = 0;
	}
	void solve() {
		build();
		dfs(s[1]);
		cout << dp[s[1]] << '\n';
	}
}VT;

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n;
	for(int i = 1; i < n; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		T.add(u, v, w);
		T.add(v, u, w);
	}
	minn[1] = 1e18;
	T.dfs1(1);
	T.dfs2(1, 1);
	cin >> q;
	while(q--) {
		cin >> m;
		for(int i = 1; i <= m; i++) {
			cin >> imp[i];
			is[imp[i]] = 1;
		}
		sort(imp + 1, imp + m + 1, cmp);
		VT.solve();
	}
	return 0;
}

标签:head,int,top,edge,Maxn,虚树,dp
From: https://www.cnblogs.com/dzbblog/p/18133937

相关文章

  • [DS 小计] 虚树
    概念什么是虚树?通俗的来说,虚树是原树的一些点集组成的树,这些点是一些关键点。在树形dp遍历中,如果每次都遍历整棵树会很浪费时间,这时候虚树就派上用场了。简介虚树的节点有哪些?在dp中,我们建立虚树包含着关键节点和关键节点的任意二者的\(\text{lca}\)。到这里,你已经会......
  • 虚树学习笔记
    关于虚树对于一些在树上进行某些询问的查询,且每个询问实际用到的点并不多的时候,可以考虑建虚树来查询。虚树的建立复杂度是\(O(m\logn)\)的,\(m\)是虚树节点数量,\(n\)是原树节点数量。也有方法可以做到\(O(m\logm)\)。例题题目链接。分析注意到范围:\(\sumk_i\le5......
  • 虚树学习笔记
    1.简介虚树,顾名思义1,就是不真实的树,常用于动态规划,所以可以说,虚树就是为了解决一类动态规划问题而诞生的当一次询问中仅涉及一颗树中的少量节点时,在整棵树上dp时间复杂度显然难以接受所以可以建立一颗只包含关键节点的树,将非关键的链简化或省略,在新树上进行dp一颗虚树包含所......
  • 虚树
    虚树用途一棵树上进行m次不同的操作,每次用到k个节点($\sumk$$O(n)级别$)用于于树上DP原理将原树里的一部分用到的节点抠出来,建一棵新树(虚树),在上面进行DP优点:降低每次操作的复杂度构建将要用到的节点(设为s)按照dfn序排序dfn序相近的在原树上......
  • 虚树学习笔记
    虚树学习笔记定义虚树指的是不同于原树(我称之为实树)的重构树,使得在同样能够求解的情况下,整棵树的节点数更少,从而使得在存在多次询问时,一些复杂度关于树的节点数的树上算法能够在时限内求解。常用场景一般来说,虚树的使用场景比较单一,常见于在实树上存在一些特殊节点,并且答案与......
  • Educational Codeforces Round 162 E 点分治 虚树 树形dp
    传送门给出\(n\le2\cdot10^5\)的一棵树,每个节点有一个颜色。求出路径长度为\(2\)路径首端和尾端拥有相同颜色,且路径上其他点不存在相同颜色的点的路径条数。当时看错题了,把颜色抽出来后没法做了。后来感觉能点分治,然后把题看对了,遂写了一个极其抽象的点分治。除此之外,把某......
  • 虚树
    虚树主要是针对这一类问题:答案只跟选定的某些点(及它们的lca)有关,且选定点的总量可以接受而每次询问都搜索一遍整棵树很浪费因此建出虚树,在虚树上进行各种操作构建有两种方法:二次排序求lca,单调栈单调栈单调栈上维护的是虚树的一条链第一步肯定是将点按照dfn序排序为了......
  • 虚树学习笔记
    虚树学习笔记虚树,顾名思义,不是真实的树。在关于树的问题中,虚树起到缩小题目规模,优化算法的作用。算法思路引入P2495SDOI2011消耗战设\(dp[i]\)为\(i\)与所有该子树内资源丰富节点不联通的代价。如果\(u\)的儿子\(v\),不是资源丰富节点。\[dp[u]+=\min(w(u,v),dp[......
  • 虚树
    一种很有用的东西。体现了关键点的思想。应用1树上每个节点有颜色,问一个子树内有几种颜色。对每种颜色的点集按DFS序排序,点所在位置权值+1,相邻两个的LCA处权值-1。子树求和即可。正经的应用建虚树:q[hh=0]=1;for(i){ intl=lca(a[i],q[hh]); while(hh&&dep[q[hh]]>d......
  • [学习笔记]虚树
    虚树虚树可以应用于树形\(DP\)的加速。当题目规定查询点集的大小和\(\le10^5\)时可以用虚树解决。虚树的原理是在原树上重新建一棵树,使得树上只包含要询问的点和它们的\(lca\)。普通树形\(DP\)的时间复杂度为\(O(n^2)\)。最坏形成一棵二叉树,点集大小为\(n\),总点数为......