【做题笔记】虚树 (LuoguP2495 - [SDOI2011] 消耗战)

虚树

在给定的树上问题中,树的大小过大,导致总时间复杂度 O ( n q ) O(nq) O(nq)不能被接受,但是有很多点在查询的过程中根本用不到,所以对于每一个查询,建立一个小的查询树,在这个小树上求解问题。这个小树就是虚树。

LuoguP2495 - [SDOI2011] 消耗战

题目链接

如果只要一组数据,那么可以通过 d p dp dp进行求解。

从树的叶子结点向根节点 d p dp dp

如果当前这个节点是关键点,那么这个点必然要被删掉,那么一定是将这个点连带着以这个节点为根节点的子树一并删除,即一定是一刀切。但是不一定只删除这一个子树,也可以连带着上面的点一起删。为了让代价最小,就选择从整棵树的根节点到这个点的路径上代价最小的边删即可。

如果这个点不是关键点,那么可以选择删除这个点,此时代价和上面的情况一样;也可以选择不删除这个点,而是把其子节点删除。子节点是已经 d p dp dp算好的最优结果,所以遍历每一个子节点,计算删除代价即可。(如果这个子节点的子树下没有关键点,那么 d p dp dp后的结果自然是 0 0 0)。

这样从下往上跑一次 d p dp dp,根节点的答案就是这道题目的答案。

但是这道题目 O ( n q ) O(nq) O(nq)不能接受,我们要考虑优化。优化的思路是,我们发现在有些查询当中,有些点是根本不必要的,比如说有几个连续的非关键点,那么这个点的贡献就是传递答案,把这些点删除显然不影响答案。那么我们可以在每次查询中,将关键点单独拎出来做一棵子树,然后跑上述 d p dp dp即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 500005;
const LL INF = 1e18;

LL en1, en, n, k, m, si, tot;
//si:栈的当前元素个数
LL front[N], front1[N], a[N], dep[N], fa[N][22], lg[N];
LL stk[N], id[N], mp[N], dp[N], vis[N];
//dep[N]:树上节点的深度(求LCA用)
//fa[N][22]:距离为2的幂数的祖先(求LCA用)
//lg[N]:以2为底的对数
//stk[N]:构建虚树时要用的栈
//id[N]:所有节点的dfs序
//mp[N]:从根节点到这个节点的路径中最小的边权
//dp[N]:存储从下至上删除当前节点的子树内所有关键点的最小代价
//vis[N]:用来存储这个点是不是关键点

struct Edge {
	LL v, w, next;
}e1[N * 2], e[N * 2];

//对每一次查询建的虚树
void addEdge(int u, int v) {
	e[++en] = {v, 0, front[u]};
	front[u] = en;
}

//总体的树
void addEdge1(int u, int v, int w) {
	e1[++en1] = {v, w, front1[u]};
	front1[u] = en1;
}

void dfs(int u, int f) {
	//获取所有节点的dfs序
	id[u] = ++tot;
	//获取深度
	dep[u] = dep[f] + 1;
	fa[u][0] = f;
	//倍增获取祖先信息
	for (int i = 1; (1 << i) <= dep[u]; ++i) {
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	}
	for (int i = front1[u]; i; i = e1[i].next) {
		LL v = e1[i].v, w = e1[i].w;
		if (v != f) {
			//更新根节点到u路径上的最小边权
			mp[v] = min(mp[u], w);
			dfs(v, u);
		}
	}
}

//构建虚树时使用,求最近公共祖先(LCA)
int lca(int x, int y) {
	if (dep[x] < dep[y]) swap(x, y);
	while (dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]]];
	if (x == y) return x;
	for (int k = lg[dep[x]]; k >= 0; --k) {
		if (fa[x][k] != fa[y][k]) {
			x = fa[x][k]; y = fa[y][k];
		}	
	}
	return fa[x][0];
}

//建立虚树
void build_virtual_tree() {
	//将关键点按照原题中的树的dfs序进行排列
	sort(a + 1, a + m + 1, [](const int &A, const int &B) {
		return id[A] < id[B];
	});
	//将根节点推入栈中
	stk[++si] = 1;
	front[1] = 0;
	for (int i = 1; i <= m; ++i) {
		if (a[i] == 1) continue;
		//g为a[i]和当前栈顶节点的LCA
		int g = lca(a[i], stk[si]);
		//如果LCA不是栈顶节点,说明当前节点进入了其中一个祖先的新的子树
		//需要将当前栈中原来子树的节点处理掉
		if (g != stk[si]) {
			while (id[g] < id[stk[si - 1]]) {
				//依次建边,并弹出栈
				addEdge(stk[si - 1], stk[si]);
				--si;
			}
			//如果LCA的dfs序大于栈顶第二个节点的dfs序
			//说明LCA点不是当前栈顶节点的祖先
			//不是的需要先推入栈,然后再连边
			if (id[g] > id[stk[si - 1]]) {
				front[g] = 0;
				addEdge(g, stk[si]);
				stk[si] = g;
			}
			else {
				addEdge(g, stk[si--]);
			}
		}
		front[a[i]] = 0;
		stk[++si] = a[i];
	}
	//栈中剩余的节点都是一条链上的
	for (int i = 1; i < si; ++i) {
		addEdge(stk[i], stk[i + 1]);
	}
}

void dfs1(int u) {
	//如果当前节点是叶子节点
	if (front[u] == 0) {
		if (vis[u]) dp[u] = mp[u];
		else dp[u] = 0;
		front[u] = vis[u] = 0;
		return;
	} 
	//tmp:如果u不是关键点,则统计删除自己所有子树的最小贡献,记为tmp
	LL tmp = 0;
	for (int i = front[u]; i; i = e[i].next) {
		LL v = e[i].v;
		dfs1(v);
		tmp += dp[v];
	}
	//如果u是关键节点,则只能一刀切,将以u为根节点的子树一并摘除
	//删除从根节点到u的路径上最短的一条
	//如果u不是关键点,则可以选择一刀切,也可以选择用tmp,取其中的最小
	if (vis[u]) dp[u] = mp[u];
	else dp[u] = min(mp[u], tmp);
	//求dp对每个节点只访问一次,所以可以直接把状态清空
	//现在清空可以避免对于每组数据统一清空,时间复杂度变回O(nq)
	front[u] = 0;
	vis[u] = 0;
}

void main2() {
	cin >> n;
	lg[1] = 0; lg[2] = 1;
	for (int i = 3; i <= n; ++i) {
		lg[i] = lg[i / 2] + 1;
	}
	en1 = 0;
	for (int i = 1; i <= n; ++i) {
		front1[i] = id[i] = 0;
		mp[i] = INF;
	}
	for (int i = 1; i < n; ++i) {
		LL u, v, w;
		cin >> u >> v >> w;
		addEdge1(u, v, w);
		addEdge1(v, u, w); 
	}
	tot = 0;
	mp[1] = INF; 
	dfs(1, 0);
	cin >> k;
	for (int i = 1; i <= k; ++i) {
		cin >> m;
		for (int j = 1; j <= m; ++j) {
			cin >> a[j];
			vis[a[j]] = 1;
		}
		si = en = 0;
		build_virtual_tree();
		dfs1(1);
		cout << dp[1] << '\n';
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	LL _ = 1;
//	cin >> _
	while (_--) main2();
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值