在给定的树上问题中,树的大小过大,导致总时间复杂度 O ( n q ) O(nq) O(nq)不能被接受,但是有很多点在查询的过程中根本用不到,所以对于每一个查询,建立一个小的查询树,在这个小树上求解问题。这个小树就是虚树。
题目链接
如果只要一组数据,那么可以通过 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
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;
}