[Question Notes] Virtual Tree (LuoguP2495 - [SDOI2011] War of Attrition)

virtual tree

In the given tree problem, the size of the tree is too large, resulting in a total time complexity of O (nq) O(nq)O ( n q ) cannot be accepted, but there are many points that are not used at all in the query process, so for each query, a small query tree is built and the problem is solved on this small tree. This small tree is the virtual tree.

LuoguP2495 - [SDOI2011] War of Attrition

Question link

If you only need one set of data, you can use dp dpd p ​​to solve.

From the leaf node of the tree to the root node dp dpdp

If the current node is a key point, then this point must be deleted, then this point must be deleted together with the subtree with this node as the root node, that is, it must be one-size-fits-all. But it is not necessary to delete only this subtree, you can also delete the above points together. In order to minimize the cost, just select the edge with the minimum cost on the path from the root node of the entire tree to this point to delete.

If this point is not a key point, you can choose to delete this point, and the cost is the same as above; you can also choose not to delete this point, but delete its child nodes. The child node is already dp dpd p ​​has calculated the optimal result, so it can traverse each child node and calculate the deletion cost. (If there are no key points under the subtree of this child node, thendp dpThe result after d p is naturally 0 00)。

Run dp dp from bottom to top like thisd p ​​, the answer of the root node is the answer to this question.

But this question O ( nq ) O(nq)O ( n q ) is not acceptable, we need to consider optimization. The idea of ​​optimization is that we find that in some queries, some points are simply unnecessary. For example, if there are several consecutive non-key points, then the contribution of this point is to deliver the answer. Deleting these points will obviously not affect the answer. Then we can separate the key points in each query to make a subtree, and then run the abovedp dpd p ​​is enough.

#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;
}

Guess you like

Origin blog.csdn.net/xhyu61/article/details/127444077