虛樹學習筆記

2022-09-20 08:30:16 字數 3352 閱讀 3537

在樹上 dp 的問題中,可能有多次詢問,每次詢問包括的總點數規模較小(例如 \(10^5\))。我們記節點數為 \(n\),詢問次數為 \(m\),詢問中總點數為 \(\sum k\),那麼直接在整棵樹上暴力 dp 的複雜度為 \(\mathcal(nm)\),不可接受。能不能發明一種 dp 的方法,不需要訪問所有節點,只訪問 \(\mathcal(k)\) 個節點呢?這樣時間複雜度就優化成了 \(\mathcal(m+\sum k)\)。

這種方法當然是有的,就是虛樹。

我們稱單次詢問中涉及到的節點為關鍵節點,它們兩兩的 lca 為關鍵 lca。

虛樹是我們構建的一棵外向樹,它只包含所有關鍵節點、關鍵 lca 和樹根,其它的不重要的節點都被壓沒了,因為它們對答案沒有貢獻,遞迴地計算它們只會浪費時間。

舉個例子,對於下面這棵樹:

如果關鍵節點為 \(\\),則虛樹(外向樹)長這個樣子:

容易發現虛樹的點數不超過 \(2k\)(證明考慮每次至少合併兩個點,直到合併到根)。

我們需要預處理出整棵樹的 dfs 序時間戳,節點 \(u\) 的時間戳記為 \(_u\)。

我們使用乙個棧來暫存樹鏈,棧底為根,棧頂為當前列舉到的樹鏈底端。由於需要訪問次頂端(也就是頂端下面的元素),stl 的std::stack不那麼方便,我們使用手寫棧。\(stk\) 為棧,從下標 \(1\) 開始存,\(top\) 為棧頂指標,指向棧頂元素(而不是棧頂元素的後乙個)。

我們先對所有關鍵點按照 \(dfn\) 公升序排序,然後把它們依次插入虛樹。下面考慮怎麼把乙個點插入虛樹:

如果棧中只有乙個元素即根節點,我們延長這條樹鏈,即stk[++top] = u;

令 \(lca\) 為 \(u\) 和 \(_\) 的 lca,如果 \(lca=_\),就意味著 \(u\) 是 \(_\) 的後代,我們延長這條樹鏈,即stk[++top] = u;

如果 \(lca\ne_\),就意味著 \(u\) 和 \(_\) 屬於它們 lca 的兩棵子樹,並且棧中這棵子樹已經構建完畢,我們需要把 lca 包含的棧中樹鏈退棧並完成虛樹建邊,為了虛樹結構的完整性,如果 lca 不在棧中則需要壓棧,然後把當前節點壓棧,延長樹鏈。

這部分**如下:

void insert(ll u) 

ll lca = lca(u, stk[top]);

if(lca == stk[top])

while(top > 1 && dfn[lca] <= dfn[stk[top-1]])

if(lca != stk[top])

stk[++top] = u;

}

給定一棵 \(n\) 點的有邊權樹,\(m\) 次詢問,每次給定 \(k\) 個點,查詢要使得這 \(k\) 個點均不與 \(1\) 連通需要切斷的邊的最小邊權和。

設 \(_u\) 表示節點 \(u\) 到根的路徑的最小邊權,即 \(_u=\min\limits_u\textrm1\}}w_i\)。

設 \(_u\) 表示 \(u\) 子樹內的關鍵點不與 \(u\) 連通需要切斷的最小邊權和,顯然答案為 \(_1\)。

容易得到轉移方程:

\[_u=\sum\limits_u\}}

\begin

_v,&v\textrm\\

\min(_v,_v)&\textrm\\

\end

\]發現轉移只與是否是關鍵節點有關,每次詢問建出虛樹然後 dp 即可。

注意虛樹清空時不能只清關鍵節點的出邊,因為還有關鍵 lca,我一開始就是沒清完結果造出了重複邊,可以再 dfs 一遍清空。

**如下:

//by: luogu@rui_er(122461)

#include #define rep(x,y,z) for(ll x=y;x<=z;x++)

#define per(x,y,z) for(ll x=y;x>=z;x--)

#define debug printf("running %s on line %d...\n",__function__,__line__)

#define fileio(s) dowhile(false)

using namespace std;

typedef long long ll;

const ll n = 2.5e5+5;

ll n, m, k, h[n], dfn[n], tms, fa[n][20], dis[n], mn[n], stk[n], top, tag[n];

vector> e[n];

vectorvg[n];

templatevoid chkmin(t& x, t y)

templatevoid chkmax(t& x, t y)

void dfs1(ll u, ll f)

}ll lca(ll u, ll v)

} if(u == v) return u;

per(i, 19, 0)

} return fa[u][0];

}void insert(ll u)

ll lca = lca(u, stk[top]);

if(lca == stk[top])

while(top > 1 && dfn[lca] <= dfn[stk[top-1]])

if(lca != stk[top])

stk[++top] = u;

}ll dfs2(ll u)

void dfsclear(ll u)

int main()

dfs1(1, 0);

for(scanf("%lld", &m);m;m--)

sort(h+1, h+1+k, [&](ll a, ll b) );

stk[top=1] = 1;

rep(i, 1, k) insert(h[i]);

while(top > 1)

// for(auto v : vg[1]) printf("1 -> %lld\n", v);

// rep(u, 1, k) for(auto v : vg[h[u]]) printf("%lld -> %lld\n", h[u], v);

printf("%lld\n", dfs2(1));

rep(i, 1, k) tag[h[i]] = 0;

dfsclear(1);

} return 0;

}

虛樹學習筆記

將關鍵點按dfs序排序後,所有關鍵點與相鄰關鍵點的lca合起來構成虛樹 通常還要加上整棵樹的根 虛樹至多有2k2k 個點。體現在實現中就是每次都pop若干點,並有機會push2個點。stk中存的是從根到當前點的遞迴棧中目前選入虛樹的點。stk中的點之間都未連邊 因為事實上關係還未確定 pop掉乙個點...

虛樹 學習筆記

水平不夠,學習來湊 又開了個天大的新坑 sdoi 2011 消耗戰 題目大意就是講 給出一棵樹,有邊權,然後給出k個查詢點,問從1號店不能到任何乙個查詢點的代價是多少.先考慮一下樹形動歸.dp i 表示從1不能到以i為根的子樹中的所有查詢點的最小代價 考慮維護乙個量,mins i 表示從1到i路徑最...

虛樹學習筆記

虛樹常常被使用在樹形 dp 中。有些時候,我們需要計算的節點僅僅是一棵樹中的某幾個節點 這個時候如果對整棵樹都進行一次計算開銷太大了 所以我們需要把這些節點從原樹中抽象出來 按照它們在原樹中的關係重新建一棵樹,這樣的樹就是虛樹 在構建之前,我們需要把所有需要加入的節點按照 dfn 序從小到大排好序 ...