在樹上 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 序從小到大排好序 ...