首页 > 其他分享 >虚树#1

虚树#1

时间:2024-04-14 17:47:47浏览次数:34  
标签:int tt 虚树 dep stac lca lcafa

基环树那块闲了再写。

本文针对虚树板题作原理解释和介绍写法。

消耗战

如果不考虑多测那么这是一道裸的树形dp。

令 \(dp_u\) 表示切断以 \(u\) 为根的子树里所有关键点的最小花费。

\[ans=dp_{root}=dp_1 \]

\[dp_u=min(minv_u,\sum_{v\in son_u}dp_v) \]

其中 \(minv_u\) 表示切断路径 \(1->u\) 的最小花费。可以容易地实现 \(O(n)\) 预处理

也就是:要么把 \(u\) 切了,要么把 \(u\) 的儿子中的路都切了。

时间复杂度 \(O(n)\),加了多测是 \(O(nm)\)。会导致TLE。

考虑优化,注意到 \(\sum k\) 与 \(n\) 同阶,也就是在 \(m\) 足够大时,树中的大部分节点是无用的,我们不需要管没有关键点的节点,而且 \(root\) 到关键点的路径上也有大多数点是无用的,因为 \(minv_i\) 已经帮我们实现了断开位置的选取。

也就是说,唯一有用的点是查询的关键点和他们的 \(lca\)。

用这些点捏一棵新的树,则 \(m\) 颗新树的大小之和与 \(n\) 同阶,新树上的转移方程是适用的。

这里借用题解的图方便理解。

从原树中提取的新树长这样。

可以发现,答案没有变化。

这个思想就叫做虚树。

现在考虑如何构建虚树。

构建思想和笛卡尔树很像,都使用单调栈维护右链的方式。

首先把关键点按 dfs序 排序,将第一个点入栈。

令 \(lca\) 表示待添加节点 \(u\) 与 \(stac_{top}\) 的最近公共祖先。然后讨论如何加入。

  • \(lca=stac_{top}\)

说明节点 \(u\) 就在栈顶节点的子树中,直接加入右链。

  • lca 在 \(stac_{top}\) 到 \(stac_{top-1}\) 的路径上。

此时弹出 \(stac_{top}\),把 \(lca\) 和 \(u\) 压入栈中。

  • \(lca=stac_{top-1}\)

这个时候只弹栈顶,把 \(u\) 入栈。

  • \(dep_{lca}<dep_{stac_{top-1}}\)

此时 \(stac_{top}\) 和 \(stac_{top-1}\) 没法界定 \(stac\) 了,那就一直弹栈顶连边直到满足上面三种情况之一,然后把 \(lca\) 和 \(u\) 入栈。

于是可以跑出本题的虚树的建树过程。

for(int i=2;i<=k;i++){
	int u=tar[i],anc=lca(u,stac[tt]);//跑出lca
	while(1){
		if(dep[anc]>=dep[stac[tt-1]]){//第四种情况以外的情况,此时stactop和stactop-1能够界定lca的位置。
			if(anc!=stac[tt]){//只要lca不是栈顶那就得踢掉(2,3种情况)
				readd(anc,stac[tt]);
				if(anc!=stac[tt-1])stac[tt]=anc;//第二种情况。
				else --tt;//第三种情况。
			}
			break;
		}
		else{
			readd(stac[tt-1],stac[tt]);
			--tt;
		}
	}
	stac[++tt]=u;//无论如何 u 都入栈。
}

最后右链很可能还是有东西的,把右链逐个连边。

while(--tt)readd(stac[tt],stac[tt+1]);

然后在新图上跑 \(dp\) 就可以了,不过要注意跑的时候还要顺带把图给拆了,这时向量的拆图效率就不如前向星。

提供完整代码。

#include<bits/stdc++.h>
#define MAXN 500005
#define int long long
using namespace std;
int n,m,k;
const int inf=1e18;
struct node{
	int v,w,nxt;
}edge[MAXN];
int h[MAXN],tmp;
inline void add(int u,int v,int w){
	edge[++tmp].v=v;edge[tmp].w=w;
	edge[tmp].nxt=h[u];
	h[u]=tmp;
}
int lcafa[25][MAXN],dfn[MAXN],tim,dep[MAXN],minv[MAXN];
inline void dfs1(int u){
	for(int i=0;lcafa[i][u];i++){
		lcafa[i+1][u]=lcafa[i][lcafa[i][u]];
	}
	dfn[u]=++tim;
	for(int i=h[u];i;i=edge[i].nxt){
		int v=edge[i].v,w=edge[i].w;
		if(!dfn[v]){
			dep[v]=dep[u]+1;
			minv[v]=min(minv[u],w);
			lcafa[0][v]=u;
			dfs1(v);
		}
	}
}
inline int lca(int x,int y){
	if(dep[x]<dep[y])swap(x,y);
	for(int i=20;i>=0;i--){
		if(dep[lcafa[i][x]]>=dep[y])x=lcafa[i][x];
	}
	if(x==y)return x;
	for(int i=20;i>=0;i--){
		if(lcafa[i][x]!=lcafa[i][y]){
			x=lcafa[i][x];
			y=lcafa[i][y];
		}
	}
	return lcafa[0][x];
}
inline void INIT(){
	scanf("%lld",&n);
	for(int i=1,u,v,w;i<n;i++){
		scanf("%lld%lld%lld",&u,&v,&w);
		add(u,v,w);
		add(v,u,w);
	}
	minv[1]=inf;
	dfs1(1);
}
bool cmp(int x,int y){
	return dfn[x]<dfn[y];
}
int tar[MAXN];
bool que[MAXN];
int stac[MAXN],tt;
node nedge[MAXN];
int nh[MAXN],ntmp;
inline void readd(int u,int v){
	nedge[++ntmp].v=v;
	nedge[ntmp].nxt=nh[u];
	nh[u]=ntmp;
}
inline int dfs(int u){
	int sum=0,res=inf;
	for(int i=nh[u];i;i=nedge[i].nxt){
		int v=nedge[i].v;
		sum+=dfs(v);
	}
	if(que[u])res=minv[u];
	else res=min(minv[u],sum);
	que[u]=0;
	nh[u]=0;
	return res;
}
inline void Work(){
	scanf("%lld",&m);
	while(m--){
		scanf("%lld",&k);
		for(int i=1;i<=k;i++)scanf("%lld",&tar[i]),que[tar[i]]=1;
		sort(tar+1,tar+1+k,cmp);
		tt=1,stac[tt]=tar[1];
		for(int i=2;i<=k;i++){
			int u=tar[i],anc=lca(u,stac[tt]);
			while(1){
				if(dep[anc]>=dep[stac[tt-1]]){
					if(anc!=stac[tt]){
						readd(anc,stac[tt]);
						if(anc!=stac[tt-1])stac[tt]=anc;
						else --tt;
					}
					break;
				}
				else{
					readd(stac[tt-1],stac[tt]);
					--tt;
				}
			}
			stac[++tt]=u;
		}
		while(--tt)readd(stac[tt],stac[tt+1]);
		printf("%lld\n",dfs(stac[1]));
		ntmp=0;
	}
}
signed main(){
	INIT();
	Work();
	return 0;
}

虚树的 \(dfn\) 似乎与重链剖分的 \(dfn\) 不共用,第一遍用树剖打就打炸了,也可能是我自己的原因。

标签:int,tt,虚树,dep,stac,lca,lcafa
From: https://www.cnblogs.com/Claimo0611/p/18134414

相关文章

  • 虚树
    1引入首先看这样一道题:[SDOI2011]消耗战有一棵树,边上有边权。每次询问给出\(k\)个点,找到一些边,使得删去这些边后从\(1\)号节点无法达到这\(k\)个点中任意一个,同时使边权最小。显然这是一道树形dp。如果说只给我们一次询问,可以很简单的\(O(n)\)求出。但是现在有了......
  • [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......