虚树 学习笔记
2023/10/6 发现找不到题做了,决定学习新算法。经过在一些题单中的翻找,决定学习虚树。
Part1. 引入
以一道例题来引入虚树吧。
[HEOI2014] 大工程
给定一棵有
个点的树,边权均为 。
现在有次询问。每次询问取 个点出来建立完全图。定义连接两个点的代价为在树上 的最短路径的长度。求:
- 建立完全图的代价和。
- 代价最小的边的代价。
- 代价最大的边的代价。
对于的数据, 。
以第一问为例。考虑树形 dp。设
将子树合并,考虑每条边的贡献即可,两个子树合并时的贡献和状态转移方程为:
但是对于每次询问都要遍历整棵树,单次询问是
那么有什么办法解决呢?我们发现有
显然是有的,虚树可以方便的解决「多次询问,每次询问给定一个特殊点集,求在这一点集上某一问题的答案」这样的问题。
Part2. 虚树的概念
我们发现在上面的树形 dp 中,有很多点其实没什么作用。具体而言,我们可以只保留点集中的点和点集中的点的 lca,而其它的点和边可以忽视,为新树分配边权为原树两点的距离。
借一下 OI-Wiki 的图捏。
如图所示,点集为
而另一边,为了保留
虚树大概就是这个样子,虚树中 保存着信息的重要节点 都被保留了,而虚树的点数被压缩到了
在本题中,在虚树中以新的边权树形 dp,我们发现仍然能算出正确的结果,因为我们仅仅是压缩掉了原树中对于特殊点而言无用的节点而已。
Part3. 虚树的建立
虚树是非常有用的,但是我们要先把它复杂度正确的建起来。总不能直接
一般的构建方法可以使用单调栈构建,但是不够直观。而在 这篇博客 中介绍了一种直观简洁的虚树构建方法,代码也非常好写,即通过「二次排序 + LCA 连边」构建虚树。步骤如下:
-
首先我们需要找出虚树中所有的点。可以先将点集中的点按 dfs 序排序,然后再将相邻的点求下 lca。根据 dfs 序的性质,这样我们就得到了所有的点的可能的 lca。
-
然后对这些点进行一个去重,就得到了虚树中的所有节点。
-
再次按 dfs 序排序,每个点在虚树上的父亲节点即为它和它的前驱的 lca。
为什么这样是正确的呢?考虑下 dfs 序的性质。按 dfs 序从小到大枚举点,设相邻两点为
-
点是 点的祖先。 -
点先向上走一些,然后拐下去走到 。在这种情况其实也表示考虑完了 的子树,因为 下面的子树如果有东西也被第一种情况考虑完了。
一棵一棵子树合并,自然可以从低到高,dfs 序从小到大的找到所有的 lca。
首先发现一个性质,
因为我们知道从 lca 节点到
如果 LCA 和
所以
那么会不会有遗漏呢?我们发现按照这个构造流程,除了 dfs 序处于第一个的节点,其他都有连向它的边,所以正好构造一棵虚树。
因此这样的流程成功在
代码实现大概是这样子的。
/* ... */ cin >> tot; for(int i = 1; i <= tot; i ++) cin >> a[i], vis[a[i]] = 1; sort(a + 1, a + tot + 1, cmp); num = tot; for(int i = 2; i <= num; i ++){ int lca = getlca(a[i], a[i - 1]); if(lca != a[i] && lca != a[i - 1]) a[++ tot] = lca; } sort(a + 1, a + tot + 1); num = tot; tot = unique(a + 1, a + tot + 1) - (a + 1); sort(a + 1, a + tot + 1, cmp); for(int i = 2; i <= tot; i ++){ int lca = getlca(a[i], a[i - 1]); edg[lca].push_back(a[i]); } /* ...*/
Part4. 虚树上树形 dp / 其它复杂的算法
事实上建完虚树后就是要思考的部分了。只要在虚树上,再怎么乱搞你的时间复杂度都是只与
当然要考虑下合并完边后的边权。
状态转移方程如下:
同样,第二小问和第三小问也是简单的树形 dp,这边就不给出方程了。
Part5. 例题代码
#include<iostream> #include<fstream> #include<algorithm> #include<cstring> //#define int long long using namespace std; const int inf = 0x3f3f3f3f; int n, T; vector<int> edge[1000005]; int fa[1000005], siz[1000005], son[1000005], dep[1000005]; int dfs1(int u, int ft){ fa[u] = ft, siz[u] = 1, dep[u] = dep[ft] + 1; for(int i = 0, v; i < edge[u].size(); i ++){ v = edge[u][i]; if(v == ft) continue; dfs1(v, u); siz[u] += siz[v]; if(siz[v] > siz[son[u]]) son[u] = v; } return 0; } int dfn[1000005], top[1000005], rnk[1000005], tim; int dfs2(int u, int ft, int tp){ dfn[u] = ++ tim, rnk[tim] = u, top[u] = tp; if(son[u] != 0) dfs2(son[u], u, tp); for(int v : edge[u]){ if(v == ft || v == son[u]) continue; dfs2(v, u, v); } return 0; } int getlca(int u, int v){ if(dep[top[u]] <= dep[top[v]]) swap(u, v); if(top[u] == top[v]){ if(dep[u] > dep[v]) return v; return u; } return getlca(fa[top[u]], v); } int a[1000005], tot, num; bool cmp(int u, int v){ return dfn[u] < dfn[v]; } bool vis[1000005]; vector<int> edg[1000005]; vector<int> vl[1000005]; long long g[1000005]; int sz[1000005]; int mx[1000005], mn[1000005]; long long ret; int ret1, ret2; int dfs(int u){ bool sta = 0; if(vis[u]) sz[u] ++, mx[u] = mn[u] = 0, sta = 1; for(int i = 0, v, w; i < edg[u].size(); i ++){ v = edg[u][i], w = vl[u][i]; dfs(v); ret += 1ll * (g[u] + 1ll * sz[u] * w) * sz[v] + 1ll * g[v] * sz[u]; // if(vis[u]) if(sz[v] != 0 && sta) ret1 = min(ret1, mn[u] + mn[v] + w), ret2 = max(ret2, mx[u] + mx[v] + w); if(sz[v] != 0){ mn[u] = min(mn[v] + w, mn[u]); mx[u] = max(mx[v] + w, mx[u]); if(!sta) sta = 1; } g[u] += g[v] + 1ll * sz[v] * w; sz[u] += sz[v]; } // cout << u << " " << mx[u] << " " << mn[u] << " " << ret1 << " " << ret2 << "\n"; return 0; } signed main(){ ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n; memset(mn, 0x3f, sizeof(mn)); for(int i = 1, u, v; i < n; i ++) cin >> u >> v, edge[u].push_back(v), edge[v].push_back(u); dfs1(1, 0), dfs2(1, 0, 1); cin >> T; while(T --){ cin >> tot; for(int i = 1; i <= tot; i ++) cin >> a[i], vis[a[i]] = 1; sort(a + 1, a + tot + 1, cmp); num = tot; for(int i = 2; i <= num; i ++){ int lca = getlca(a[i], a[i - 1]); if(lca != a[i] && lca != a[i - 1]) a[++ tot] = lca; } sort(a + 1, a + tot + 1); num = tot; tot = unique(a + 1, a + tot + 1) - (a + 1); sort(a + 1, a + tot + 1, cmp); for(int i = 2; i <= tot; i ++){ int lca = getlca(a[i], a[i - 1]); edg[lca].push_back(a[i]); vl[lca].push_back(dep[a[i]] - dep[lca]); } ret = 0; ret1 = inf; ret2 = 0; dfs(a[1]); cout << ret << " " << ret1 << " " << ret2 << "\n"; for(int i = 1; i <= tot; i ++) vis[a[i]] = 0, edg[a[i]].clear(), vl[a[i]].clear(), g[a[i]] = sz[a[i]] = mx[a[i]] = 0, mn[a[i]] = inf; } return 0; }
Part6. 总结
其实虚树毕竟只是工具,毕竟看到题目是怎么问的其实可以一眼虚树()。真正困难的还是建完虚树的部分,这个时候就很考验树上算法的功底了。
本文作者:AzusidNya の 部屋
本文链接:https://www.cnblogs.com/AzusidNya/p/17745184.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步