「筆記」虛樹
寫在前面
以前寫的太簡略了,重新來總結一下。
如果您是初學者建議配合閱讀 虛樹 - OI Wiki 上的圖示閱讀。
概念
對於樹 \(T=(V,E)\),給定關鍵點集 \(S\subseteq V\),則可定義虛樹 \(T'=(V',E')\)。
對於點集 \(V'\subseteq V\),使得 \(u\in V'\) 當且僅當 \(u\in S\),或 \(\exist x,y\in S,\operatorname{lca}(x,y)=u\)。
對於邊集,\((u,v)\in E'\),當且僅當 \(u,v\in V'\),且 \(u\) 為 \(v\) 在 \(V'\) 中深度最深的祖先。
個人理解:
僅保留關鍵點及其 \(\operatorname{lca}\),縮子樹成邊,僅保留分叉點,可能刪去一些不包含關鍵點的子樹。
壓縮了樹的資訊,同時也丟失了部分樹的資訊。
一個分叉點會合並至少兩個關鍵點,虛樹節點數最多為 \(2k-1\) 個。節點數變為了 \(O(k)\) 級別。
關鍵點集 \(S = \{2, 6, 8, 9\}\) 的虛樹如圖中紅色部分所示。
演算法
建樹考慮增量法,每次向虛樹中新增一個關鍵點。考慮先求得 關鍵節點 的 dfs 序,按照 dfs 序新增關鍵節點。這樣可以保證相鄰兩個關鍵點的 \(\operatorname{lca}\) 深度不小於不相鄰關鍵點的深度。
考慮單調棧維護虛樹最右側的鏈(上一個關鍵點與根的鏈),單調棧中節點深度遞增,棧頂一定為上一個關鍵點。欽定 1 號節點為根,先將其壓入棧中。
每加入一個關鍵點 \(a_i\),令 \(\operatorname{lca}(a_{i-1},a_i)=w\)。將棧頂 \(\operatorname{dep}_x > \operatorname{dep}_w\) 的彈棧,加入 \(w,a_i\),即為新的右鏈。特別地,若棧頂存在 \(\operatorname{dep}_x=\operatorname{dep}_w\),不加入 \(w\) 節點。
在此過程中維護每個節點的父節點,在彈棧時進行連邊並維護資訊,即得虛樹。單次建虛樹複雜度 \(O(kw)\)
程式碼
其中 \(\operatorname{Cut}\) 為封裝後的樹鏈剖分。
namespace VT { //Virtual Tree
#define dep Cut::dep
const int kMaxNode = kN;
int top, node[kMaxNode], st[kMaxNode]; //棧
int tag[kMaxNode]; //標記是否為關鍵點
std::vector <int> newv[kMaxNode]; //虛樹
bool CMP(int fir_, int sec_) { //按 dfs 序比較
return Cut::dfn[fir_] < Cut::dfn[sec_];
}
void Push(int u_) { //向虛樹中加入 u_
int lca = Cut::Lca(u_, st[top]);
for (; dep[st[top - 1]] > dep[lca]; -- top) {
newv[st[top - 1]].push_back(st[top]);
}
if (lca != st[top]) {
newv[lca].push_back(st[top]); -- top;
if (lca != st[top]) st[++ top] = lca;
}
if (st[top] != u_) st[++ top] = u_;
}
void Build(int siz_) {
for (int i = 1; i <= siz_; ++ i) {
node[i] = read();
tag[node[i]] = 1;
}
std::sort(node + 1, node + siz_ + 1, CMP);
st[top = 0] = 1;
for (int i = 1; i <= siz_; ++ i) Push(node[i]);
for (; top; -- top) newv[st[top - 1]].push_back(st[top]);
}
}
例題
「SDOI2011」消耗戰
給定一棵 \(n\) 個節點的樹,邊有邊權。
給定 \(m\) 次詢問,每次給定 \(k\) 個關鍵點,要求切除一些邊,使得 \(k\) 個關鍵點與編號為 \(1\) 的點不連通。
最小化切除的邊的權值之和。
\(2\le n\le 2.5\times 10^5\),\(1\le m\le 5\times 10^5\),\(\sum k \le 5\times 10^5\),\(1\le k\le n\),邊權值 \(w\le 10^5\)。
2S,512MB。
首先想到一個簡單的 DP。對於單次查詢,設 \(f_u\) 為令以 \(u\) 為根的子樹中的所有關鍵點 與 \(u\) 不連通的最小代價。
轉移時列舉 \(u\) 的子節點,有狀態轉移方程:
單次查詢複雜度 \(O(n)\),總複雜度 \(O(nm)\),無法通過本題。
發現關鍵點集較小,不含任何關鍵點的子樹顯然無用,考慮建立虛樹。
發現使得一個關鍵點 \(u\) 與根不相連的最小代價為根到關鍵點路徑上最短的邊長,設其為 \(\operatorname{val}_u\),在 dfs 時順便維護。對於建立的虛樹,有新的狀態轉移方程:
總複雜度 \(O(\sum k)\) 級別,可以通過本題。
對於本題,還可以刪除以關鍵點作為祖先的關鍵點 進行進一步的優化。正確性顯然,因為一定要使得其祖先與根不相連。
//知識點:虛樹
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kMaxn = 2e5 + 5e4 + 10;
const int kMaxm = 5e5 + 10;
const LL kInf = 1e15 + 2077;
//=============================================================
int n, m, edge_num, head[kMaxn], v[kMaxm << 1], w[kMaxm << 1], ne[kMaxm << 1];
std :: vector <int> newv[kMaxn];
int top, node[kMaxn], st[kMaxn];
bool tag[kMaxn];
LL minw[kMaxn];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void GetMin(LL &fir, LL sec) {
if (sec < fir) fir = sec;
}
void AddEdge(int u_, int v_, int w_) {
v[++ edge_num] = v_, w[edge_num] = w_;
ne[edge_num] = head[u_], head[u_] = edge_num;
}
namespace TCC { //TreeChainCut
int fa[kMaxn], dep[kMaxn], size[kMaxn], son[kMaxn], top[kMaxn];
int dfn_num, dfn[kMaxn];
void Dfs1(int u_, int fa_) {
fa[u_] = fa_;
size[u_] = 1;
dep[u_] = dep[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i], w_ = w[i];
if (v_ == fa_) continue;
minw[v_] = std :: min(minw[u_], (LL) w_);
Dfs1(v_, u_);
size[u_] += size[v_];
if (size[v_] > size[son[u_]]) son[u_] = v_;
}
}
void Dfs2(int u_, int top_) {
top[u_] = top_;
dfn[u_] = ++ dfn_num;
if (son[u_]) Dfs2(son[u_], top_);
for (int i = head[u_]; i; i = ne[i]) {
if (v[i] == son[u_] || v[i] == fa[u_]) continue;
Dfs2(v[i], v[i]);
}
}
int Lca(int u_, int v_) {
for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
if (dep[top[u_]] < dep[top[v_]]) std :: swap(u_, v_);
}
return (dep[u_] < dep[v_]) ? u_ : v_;
}
}
bool CMP(int fir, int sec) {
return TCC::dfn[fir] < TCC::dfn[sec];
}
LL Dfs(int u_) {
LL sum = 0;
for (int i = 0, size = newv[u_].size(); i < size; ++ i) {
sum += Dfs(newv[u_][i]);
}
newv[u_].clear();
if (tag[u_]) {
tag[u_] = false;
return minw[u_];
}
return std::min(minw[u_], sum);
}
#define dep (TCC::dep)
void Push(int u_) {
int lca = TCC::Lca(u_, st[top]);
for (; dep[st[top - 1]] > dep[lca]; -- top) {
newv[st[top - 1]].push_back(st[top]);
}
if (lca != st[top]) {
newv[lca].push_back(st[top]); -- top;
if (lca != st[top]) st[++ top] = lca;
}
st[++ top] = u_;
}
//=============================================================
int main() {
n = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read(), w_ = read();
AddEdge(u_, v_, w_), AddEdge(v_, u_, w_);
}
minw[1] = kInf;
TCC::Dfs1(1, 0), TCC::Dfs2(1, 1);
m = read();
for (int i = 1; i <= m; ++ i) {
int k = read();
for (int j = 1; j <= k; ++ j) {
node[j] = read();
tag[node[j]] = true;
}
std :: sort(node + 1, node + k + 1, CMP);
st[top = 0] = 1;
for (int j = 1; j <= k; ++ j) Push(node[j]);
for (; top; -- top) newv[st[top - 1]].push_back(st[top]);
printf("%lld\n", Dfs(1));
}
return 0;
}
「HEOI2014」大工程
我個人十分痛恨這種多合一的題目。
*這簡直野蠻至極*
給定一棵 \(n\) 個節點的樹,邊權均為 1。
給定 \(m\) 次詢問,每次給定 \(k\) 個關鍵點,求 \(k\) 個點對之間的路徑長度和、最短路徑長度、最長路徑長度。
\(1\le n\le 10^6\),\(1\le m\le 5\times 10^4\),\(\sum k \le 2\times n\)。
2S,256MB。
先建立虛樹,維護各點的深度,之後簡單 DP。
第 2、3 問簡單維護子樹內關鍵點到根的最長鏈/最短鏈即可,考慮如何做第 1 問。
設 \(f_u\) 表示以 \(u\) 為根的子樹內關鍵點對的路徑長度之和,\(g_u\) 表示以 \(u\) 為根的子樹內關鍵節點到 \(u\) 的距離之和,\(\operatorname{size}_u\) 表示以 \(u\) 為根的子樹內關鍵節點的個數。
轉移時分路徑在子樹內/跨越根節點討論,則有顯然的轉移方程:
其中 \(\operatorname{dis}(u,v) = \operatorname{dep}_v - \operatorname{dep}_u\)。
程式碼實現中使用了樹鏈剖分,總複雜度 \(O(\sum k\log n)\) 級別。
細節比較多。
//知識點:虛樹
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <vector>
#define LL long long
const int kN = 1e6 + 10;
const LL kInf = 1e15 + 2077;
//=============================================================
int n, q, k;
int e_num, head[kN], v[kN << 1], ne[kN << 1];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(LL &fir, LL sec) {
if (sec > fir) fir = sec;
}
void Chkmin(LL &fir, LL sec) {
if (sec < fir) fir = sec;
}
void Add(int u_, int v_) {
v[++ e_num] = v_, ne[e_num] = head[u_], head[u_] = e_num;
}
namespace Cut {
const int kMaxNode = kN;
int fa[kMaxNode], dep[kMaxNode], siz[kMaxNode];
int dfn_num, dfn[kN], son[kMaxNode], top[kMaxNode];
void Dfs1(int u_, int fa_) {
fa[u_] = fa_, dfn[u_] = ++ dfn_num, siz[u_] = 1, dep[u_] = dep[fa_] + 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ == fa_) continue;
Dfs1(v_, u_);
if (siz[v_] > siz[son[u_]]) son[u_] = v_;
siz[u_] += siz[v_];
}
}
void Dfs2(int u_, int top_) {
top[u_] = top_;
if (son[u_]) Dfs2(son[u_], top_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (v_ != son[u_] && v_ != fa[u_]) Dfs2(v_, v_);
}
}
int Lca(int u_, int v_) {
for (; top[u_] != top[v_]; u_ = fa[top[u_]]) {
if (dep[top[u_]] < dep[top[v_]]) std::swap(u_, v_);
}
return dep[u_] < dep[v_] ? u_ : v_;
}
}
namespace VT { //Virtual Tree
#define dep Cut::dep
const int kMaxNode = kN;
int top, node[kMaxNode], st[kMaxNode], tag[kMaxNode];
LL f1[kMaxNode], f2[kMaxNode], f3[kMaxNode];
LL sumdis[kMaxNode], maxdis[kMaxNode], mindis[kMaxNode], siz[kMaxNode];
std::vector <int> newv[kMaxNode];
bool CMP(int fir_, int sec_) {
return Cut::dfn[fir_] < Cut::dfn[sec_];
}
void Push(int u_) {
int lca = Cut::Lca(u_, st[top]);
for (; dep[st[top - 1]] > dep[lca]; -- top) {
newv[st[top - 1]].push_back(st[top]);
}
if (lca != st[top]) {
newv[lca].push_back(st[top]); -- top;
if (lca != st[top]) st[++ top] = lca;
}
if (st[top] != u_) st[++ top] = u_;
}
void Build(int siz_) {
for (int i = 1; i <= siz_; ++ i) {
node[i] = read();
tag[node[i]] = 1;
}
std::sort(node + 1, node + siz_ + 1, CMP);
st[top = 0] = 1;
for (int i = 1; i <= siz_; ++ i) Push(node[i]);
for (; top; -- top) newv[st[top - 1]].push_back(st[top]);
}
void Dfs(int u_) {
f1[u_] = f3[u_] = 0, f2[u_] = kInf;
sumdis[u_] = 0, maxdis[u_] = tag[u_] ? 0 : -kInf, mindis[u_] = tag[u_] ? 0 : kInf;
siz[u_] = tag[u_];
for (int i = 0, lim = newv[u_].size(); i < lim; ++ i) {
int v_ = newv[u_][i];
LL dis = dep[v_] - dep[u_];
Dfs(v_);
siz[u_] += siz[v_];
sumdis[u_] += sumdis[v_] + siz[v_] * dis;
Chkmin(mindis[u_], mindis[v_] + dis);
Chkmax(maxdis[u_], maxdis[v_] + dis);
Chkmin(f2[u_], f2[v_]);
Chkmax(f3[u_], f3[v_]);
}
LL maxv = -1, maxvv = -1, minv = kInf, minvv = kInf;
if (tag[u_]) maxv = minv = 0;
for (int i = 0, lim = newv[u_].size(); i < lim; ++ i) {
int v_ = newv[u_][i];
LL dis = dep[v_] - dep[u_];
f1[u_] += f1[v_] + (sumdis[v_] + siz[v_] * dis) * (siz[u_] - siz[v_]);
if (maxdis[v_] + dis >= maxv) maxvv = maxv, maxv = maxdis[v_] + dis;
else if (maxdis[v_] + dis > maxvv) maxvv = maxdis[v_] + dis;
if (mindis[v_] + dis <= minv) minvv = minv, minv = mindis[v_] + dis;
else if (mindis[v_] + dis < minvv) minvv = mindis[v_] + dis;
}
if (minv != kInf && minvv != kInf) Chkmin(f2[u_], minv + minvv);
if (maxv != -1 && maxvv != -1) Chkmax(f3[u_], maxv + maxvv);
tag[u_] = 0;
newv[u_].clear();
}
void Solve(int siz_) {
Build(siz_);
Dfs(1);
printf("%lld %lld %lld\n", f1[1], f2[1], f3[1]);
}
}
//=============================================================
int main() {
n = read();
for (int i = 1; i < n; ++ i) {
int u_ = read(), v_ = read();
Add(u_, v_), Add(v_, u_);
}
Cut::Dfs1(1, 0), Cut::Dfs2(1, 1);
int q = read();
while (q --) {
k = read();
VT::Solve(k);
}
return 0;
}
寫在最後
鳴謝: