首页 > 其他分享 >虚树

虚树

时间:2024-08-09 14:41:02浏览次数:5  
标签:sta int top dfn ls LCA 虚树

虚树 Virtual Tree

浓缩信息,把一整颗大树浓缩成一颗小树。
下图中,红色结点是我们选择的关键点。红色和黑色结点都是虚树中的点。黑色的边是虚树中的边。
OIWIKI
image
image
image
image

两种建树方式
1.第一种构造过程:二次排序 + LCA 连边(容易理解,常数略大)

bool cmp(int x, int y) {
  return dfn[x] < dfn[y];  // 按照 dfn 序排序
}
void build()
{
	sort(h+1,h+1+m,cmp);//按dfn序从小到大
	for(int i=1;i<m;i++)
	{
		a[++len]=h[i];
		a[++len]=lca(h[i],h[i+1]);
	}
	a[++len]=h[m];
	sort(a+1,a+1+len,cmp);
	len=unique(a+1,a+1+len)-a-1;
	for(int i=1,lc;i<len;i++)
	{
		lc=lca(a[i],a[i+1]);
		connect(lc,a[i+1]);
	}
}

2.第二种:单调栈
链式前向星魅力时刻:其中有很多细节,比如用邻接表存图的方式存虚树的话,需要清空邻接表。但是直接清空整个邻接表是很慢的,所以我们在 有一个从未入栈的元素入栈的时候清空该元素对应的邻接表 即可。

void build(int M)
{
	sort(ls+1,ls+1+M,cmp);//按dfn序从小到大
	sta[top=1]=1;head[1]=0;cnt=0;// 1 号节点入栈,清空 1 号节点对应的邻接表,设置邻接表边数为 1
	for(int i=1;i<=M;i++)
	{
		if(ls[i]!=1)
		{
			int l=lca(ls[i],sta[top]);
			if(l!=sta[top])
			{
				while(dfn[l]<dfn[sta[top-1]])
					add(sta[top-1],sta[top]),top--;
				if(dfn[l]>dfn[sta[top-1]])  // 如果 LCA 不等于次大节点(这里的大于其实和不等于没有区别)
					head[l]=0,add(l,sta[top]),sta[top]=l;        // 说明 LCA 是第一次入栈,清空其邻接表,连边后弹出栈顶元素,并将 LCA// 入栈
				else add(l,sta[top--]); // 说明 LCA 就是次大节点,直接弹出栈顶元素	
			}
			head[ls[i]]=0;sta[++top]=ls[i];

		}
	}
	for(int i=1;i<top;i++)add(sta[i],sta[i+1]);
}

P2495 [SDOI2011] 消耗战

我们发现如果每一次都跑一遍\(O(n)\)\(DP\)是不值当的,所以可以建虚树

点击查看代码
#include <bits/stdc++.h>
#define speed() ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define ll long long
#define lid (rt<<1)
#define rid (rt<<1|1)
// #define endl '\n'
#define pb push_back
using namespace std;
const int N = 2.5e5+5;
vector <pair<int,int>> e[N];
struct eg
{
	int u,v,w,next;
}edge[N<<1];int cnt,head[N];
void add(int u,int v)
{
	edge[++cnt].u=u;
	edge[cnt].v=v;
	edge[cnt].next=head[u];head[u]=cnt;
}
int n,m,f[N][25],dfn[N],rk[N],dfstot,dep[N];ll minv[N];
void dfs(int u)
{
	dfn[u]=++dfstot;rk[dfstot]=u;
	dep[u]=dep[f[u][0]]+1;
	for(int i=1;i<=20;i++)
		f[u][i]=f[f[u][i-1]][i-1];
	for(auto [to,w]:e[u])
	{
		if(to==f[u][0])continue;
		minv[to]=min<ll>(minv[u],w);
		f[to][0]=u;
		dfs(to);
	}
}
int lca(int u,int v)
{
	if(dep[u]<dep[v])swap(u,v);
	for(int i=20;i>=0;i--)
		if(dep[f[u][i]]>=dep[v])u=f[u][i];
	if(u==v)return u;
	for(int i=20;i>=0;i--)
		if(f[u][i]!=f[v][i])
			u=f[u][i],v=f[v][i];
	return f[u][0];
}
int ls[N],query[N],sta[N],top;
bool cmp(int a,int b){return dfn[a]<dfn[b];};
void build(int M)
{
	sort(ls+1,ls+1+M,cmp);//按dfn序从小到大
	sta[top=1]=1;head[1]=0;cnt=0;// 1 号节点入栈,清空 1 号节点对应的邻接表,设置邻接表边数为 1
	for(int i=1;i<=M;i++)
	{
		if(ls[i]!=1)
		{
			int l=lca(ls[i],sta[top]);
			if(l!=sta[top])
			{
				while(dfn[l]<dfn[sta[top-1]])
					add(sta[top-1],sta[top]),top--;
				if(dfn[l]>dfn[sta[top-1]])  // 如果 LCA 不等于次大节点(这里的大于其实和不等于没有区别)
					head[l]=0,add(l,sta[top]),sta[top]=l;        // 说明 LCA 是第一次入栈,清空其邻接表,连边后弹出栈顶元素,并将 LCA// 入栈
				else add(l,sta[top--]); // 说明 LCA 就是次大节点,直接弹出栈顶元素	
			}
			head[ls[i]]=0;sta[++top]=ls[i];
		}
	}
	for(int i=1;i<top;i++)add(sta[i],sta[i+1]);
}
ll dfs1(int u)
{
	ll sum=0;ll tem;
	for(int i=head[u];i;i=edge[i].next)
	{
		int to=edge[i].v;
		sum+=dfs1(to);
	}
	if(query[u])
		tem=minv[u];
	else 
		tem=min(minv[u],sum);
	query[u]=0;
	head[u]=0;
	return tem;
}
int main()
{
	speed();
	// freopen("in.in","r",stdin);
	// freopen("out.out","w",stdout);
	cin>>n;int u,v,w;
	for(int i=1;i<=n-1;i++)
	{
		cin>>u>>v>>w;
		e[u].pb({v,w});e[v].pb({u,w});
	}
	minv[1]=1e18;
	dfs(1);
	cin>>m;
	int num;
	while(m--)
	{
		cin>>num;
		for(int i=1;i<=num;i++)
		{
			cin>>ls[i];query[ls[i]]=1;
		}
		// num++
		// sort(ls+1,ls+1+num,cmp);
		// cout<<m<<endl;
		build(num);
		cout<<dfs1(1)<<endl;
	}
	return 0;
}

标签:sta,int,top,dfn,ls,LCA,虚树
From: https://www.cnblogs.com/wlesq/p/18350666

相关文章

  • 关于虚树
    关于虚树瞎扯某些树上问题,给了巨多节点,而实际上它们之中只有小部分能做出贡献,其余都是些水军,为杀尽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\),按两棵虚树是否相交分讨,先考虑......
  • 虚树#1
    基环树那块闲了再写。本文针对虚树板题作原理解释和介绍写法。消耗战如果不考虑多测那么这是一道裸的树形dp。令\(dp_u\)表示切断以\(u\)为根的子树里所有关键点的最小花费。\[ans=dp_{root}=dp_1\]\[dp_u=min(minv_u,\sum_{v\inson_u}dp_v)\]其中\(minv_u\)表示切......