虚树学习笔记

1. 简介

虚树,顾名思义,就是不真实的树,常用于动态规划,所以可以说,虚树就是为了解决一类动态规划问题而诞生的

当一次询问中仅涉及一颗树中的少量节点时,在整棵树上dp时间复杂度显然难以接受

所以可以建立一颗只包含关键节点的树,将非关键的链简化或省略,在新树上进行dp

一颗虚树包含所有询问点及它们的lca,所以所有询问点为叶子节点,所以,这棵树至多有2k-1个节点,大大降低了时间复杂度

2.例题

从一道例题入手

2.1. 题面

[SDOI2011] 消耗战

题目描述

在一场战争中,战场由 \(n\) 个岛屿和 \(n-1\) 个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为 \(1\) 的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他 \(k\) 个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。

侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到 \(1\) 号岛屿上)。不过侦查部门还发现了这台机器只能够使用 \(m\) 次,所以我们只需要把每次任务完成即可。

输入格式

第一行一个整数 \(n\),表示岛屿数量。

接下来 \(n-1\) 行,每行三个整数 \(u,v,w\) ,表示 \(u\) 号岛屿和 \(v\) 号岛屿由一条代价为 \(w\) 的桥梁直接相连。

\(n+1\) 行,一个整数 \(m\) ,代表敌方机器能使用的次数。

接下来 \(m\) 行,第 \(i\) 行一个整数 \(k_i\) ,代表第 \(i\) 次后,有 \(k_i\) 个岛屿资源丰富。接下来 \(k_i\) 个整数 \(h_1,h_2,..., h_{k_i}\) ,表示资源丰富岛屿的编号。

输出格式

输出共 \(m\) 行,表示每次任务的最小代价。

样例 #1

样例输入 #1

10
1 5 13
1 9 6
2 1 19
2 4 8
2 3 91
5 6 8
7 5 4
7 8 31
10 7 9
3
2 10 6
4 5 7 8 3
3 9 4 6

样例输出 #1

12
32
22
提示
数据规模与约定
  • 对于 \(10\%\) 的数据,\(n\leq 10, m\leq 5\)
  • 对于 \(20\%\) 的数据,\(n\leq 100, m\leq 100, 1\leq k_i\leq 10\)
  • 对于 \(40\%\) 的数据,\(n\leq 1000, 1\leq k_i\leq 15\)
  • 对于 \(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\)

2.2. 暴力

当只看一次查询时,记\(f_i\)为处理完i的最小代价

考虑转移,分两种情况:

  1. 断开与父亲的连接
  2. 断开子树内所有资源点与该点的连接,前提是该点没有资源

显然,时间复杂度为\(O(nm)\),会T

考虑优化,因为\(\sum k\)较小,所以可以从k入手

优化先放一下,我们先学虚树再做题

3. 虚树

3.1. 建树

首先对整棵树dfs,求出dfs序,再按dfs序将询问点排序

同时维护一个栈,表示根到栈顶元素这一条链,即最右链

假设当前加入的节点为p,栈顶元素为x,lca为它们的最近公共祖先

因为是按dfs序遍历,所以\(lca\neq p\)

分为两种情况

  1. lca=x,则p直接入栈
  2. x,p分别位于两棵不同的子树,必然x所在子树已经遍历完了(如果没有,则必然存在一个一个点y还为入栈,但因为\(dfn_y<dfn_p\),则应该先访问y,与假设矛盾),我们要对其进行构建

对于情况2又分为如下情况:

\(x=s_{top},y=s_{top-1}\)

  1. lca在x和y之间

显然此时最右链的末端由y->x,变为y->lca->x,所以,要先把lca-x这条边加入,然后x出栈,lca和p入栈

  1. lca=y

该情况与情况1基本相同,但lca不用入栈了

  1. 此时lca已经不在y的子树中,甚至不在\(s_{top-2},s_{top-3},\dots\)的子树中

以上图为例,最右链由\(s_{top-3}->s_{top-2}->y->x\)变为\(s_{top-3}->lca->p\),所以要循环将末枝剪下,知道不在是情况3

3.2. 回归例题

以询问2为例

建立虚树后是:

在建树的同时,可以记录1到p的路径上的最小边权minv,如果p是询问点,直接切断p及其子树上询问点的最小代价\(f_p=minv_p\),否则最小代价为\(f_p=\min(minv_p,\sum_{v\in son(p)} f_v)\)

但虽然p是询问点,其子树仍需遍历,因为需要清空

代码:

#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,head[250005],edgenum,q,k,h1[250005];
struct edge{
	int to,nxt,val;
}e[500005],e1[500005];
void add_edge(int u,int v,int w)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	e[edgenum].val=w;
	head[u]=edgenum;
}
void add_edge1(int u,int v)
{
	e1[++edgenum].nxt=h1[u];
	e1[edgenum].to=v;
	h1[u]=edgenum;
}
int f[250005][20],dep[250005],dfn[250005],idx;
ll minv[250005];
void dfs(int u,int fa)
{
	dfn[u]=++idx;
	f[u][0]=fa;
	dep[u]=dep[fa]+1;
	for(int i=1;i<20;i++)
	{
		f[u][i]=f[f[u][i-1]][i-1];
	}
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		minv[v]=min(minv[u],1ll*e[i].val);
		dfs(v,u);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=19;i>=0;i--)
	{
		if(dep[f[x][i]]>=dep[y])
		{
			x=f[x][i];
		}
		if(x==y) return x;
	}
	for(int i=19;i>=0;i--)
	{
		if(f[x][i]!=f[y][i])
		{
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}
int a[250005],st[250005],top;
bool vis[250005];
bool cmp(int x,int y)
{
	return dfn[x]<dfn[y];
}
ll dp(int u,int fa)
{
	ll sum=0,res;
	for(int i=h1[u];i;i=e1[i].nxt)
	{
		int v=e1[i].to;
		if(v==fa) continue;
		sum+=dp(v,u);
	}
	if(vis[u])
	{
		res=minv[u];
	}
	else
	{
		res=min(minv[u],sum);
	}
	vis[u]=0,h1[u]=0;
	return res;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<n;i++)
	{
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add_edge(u,v,w);
		add_edge(v,u,w);
		minv[i]=1e15;
	}
	minv[n]=1e15;
	dfs(1,0);
	scanf("%d",&q);
	while(q--)
	{
		scanf("%d",&k);
		for(int i=1;i<=k;i++)
		{
			scanf("%d",&a[i]);
			vis[a[i]]=1;
		}
		sort(a+1,a+k+1,cmp);
		top=1;
		st[top]=a[1];
		edgenum=0;
		for(int i=2;i<=k;i++)
		{
			int now=a[i];
			int lc=lca(now,st[top]);
			while(1)
			{
				if(dep[lc]>=dep[st[top-1]])
				{
					if(lc!=st[top])
					{
						add_edge1(lc,st[top]);
						add_edge1(st[top],lc);
						if(lc!=st[top-1])
						{
							st[top]=lc;
						}
						else top--;
					}
					break;
				}
				else
				{
					add_edge1(st[top-1],st[top]);
					add_edge1(st[top],st[top-1]);
					top--;
				}
			}
			st[++top]=now;
		}
		while(--top)
		{
			add_edge1(st[top],st[top+1]);
			add_edge1(st[top+1],st[top]);
		}
		printf("%lld\n",dp(st[1],0)); 
	}
	return 0;
}
posted @ 2024-04-06 10:04  wangsiqi2010916  阅读(16)  评论(2编辑  收藏  举报