學習筆記 虛樹

2022-04-30 23:18:29 字數 3470 閱讀 2915

初學的時候整個人是懵的

不過總算是弄懂了

對於一類樹上的問題,如果僅有一部分點對答案「有用」(也就是說另一部分「可以不要」),那麼我們考慮只儲存那些有用的點。這就是虛樹的思想。所以為什麼它叫虛樹?我也不知道……

通常情況下我們把一些點的 lca 也算作「有用」的點。

虛樹可做的題目一般來說(以我對虛樹短淺的認知)會限制有用的點的總個數,並且會用多組詢問的方式來使一般的方法tle……

仿照著其他部落格的思路,我覺得用一道例題來講會更加容易理解——

〔bzoj 2286 消耗戰〕

「題意」

給定一棵n個點的樹,每個邊有權值表示將它割斷的花費,已知其中的k個點是有價值的,現在需要把這些點與點1斷開(不連通),求最小花費。

輸入時先給定一棵樹。

m組資料,每次詢問給出k個點,表示它們是有價值的,對於每組資料輸出最小花費。

[反正\(o(nm)\)會超時就對了,保證所有資料的k之和不超過500000]

這道題最基礎的做法是對於每一組資料跑一遍dp,顯然是 \(o(nm)\) 的做法,會 tle。

那麼這道題也算是一道比較特殊的虛樹題(某taotao給我說的:這道題必須把根節點1放在虛樹里,就不能體現虛樹的一些性質)。

不難想到將點u與根節點割開無非就是割去根節點到u的路徑上的一條邊,根據這一點我們定義dp[u] 表示 根節點到u的路徑上的最小邊,也就是將 u 從根節點割開的最小花費。

那麼就可以得到簡單的轉移式,首先初始值是 \(dp[1]=inf\)(感性理解就是1不可能與它本身割開),然後轉移式:

\[u是v的父親節點;\ (u,v)表示u到v的邊權\\

dp[v]=\min\\]

也就是說要麼在 1 到 u 的路徑上割掉一條邊,要麼割斷 u 到 v 的邊。

為什麼說「一些點的lca有用」呢?等會在「求解部分」會解釋。

虛樹的構建方法大概是:

將有用點按 dfs 序排序;

建立棧並將第乙個點壓入;

列舉下乙個點 u (直到列舉完為止);

如果棧內只有乙個點,將 u 壓入棧,跳到步驟 3,否則進行下一步;

求 u 與 棧頂點 的lca;

如果lca就是棧頂點,跳轉到步驟3,否則進行下一步;

設棧內棧頂點的後面乙個點為w;

如果w的dfs序大於等於lca的dfs序,進行下一步,否則跳轉到步驟 10;

在 w 和 棧頂點 之間連邊,並彈出棧頂,跳轉到步驟7;

如果 lca 不是現在的棧頂點,在 lca 和現在棧頂點之間連邊;

彈出棧頂,並將 lca 壓入棧;

壓入u,跳轉到步驟3;

(希望reader們都看懂了,可以把樣例拿來自己推一推)

這樣一棵虛樹就構造好了。

注意重置虛樹的方法,不要 memset(我已經試過了)

這裡的dfs實際上就是樹形dp,而dp[u]表示的並不是dp陣列,只是u到根節點到路徑上的最小邊-既然有reader問到了我還是補一下)

從根節點1開始,在虛樹上dfs一遍,如果當前節點是葉子節點,就直接返回它的dp值,否則返回 min

就相當於如果要割斷 u 和 v ,要麼割斷它們的lca,要麼分別將它們割斷(花費加起來)。所以lca是有用的~

具體一點的話就是:

先判斷當前u是否是葉子節點,如果是則說明這一定是題目給出的「有價值」的點,就不得不將它與它的父親割開——返回 dp[u]

否則判斷兩種情況: ① 當前點到根節點的路徑已經被割斷了——也就是 dp[u];② 逐個考慮u的兒子,分別將它們與根節點割開——\(\sum_dfs(v)\)(雖然可能dp[v]表示的割去的邊是根節點到u到路徑上到邊,看似是重複的,但是這樣的情況會在①②兩種情況取較小值時被排除~)

根據dp時的決策,我們發現需要的點無非3種——①根節點,因為這是dp的起點(而且轉移時根節點也可能有一定貢獻);②「有價值的點」,這會作為虛樹的葉子節點,並且限制dp時的轉移;③lca,在dp轉移時會有兩種情況,要麼是把lca到根節點的路徑割斷,要麼是把「有價值的點」到lca到路徑割斷。

那麼我們只需要考慮這3種點就可以了。

但是其實這只是一種特例——有一些(大多數)題是不一定要把根節點放在虛樹裡面的,比如 codeforces 613d 。

總的來說虛樹題都會出很多個詢問,然後我們要對每個詢問都建立虛樹……於是我們就面臨乙個問題——怎麼重置虛樹?

在這裡我是用的手寫鍊錶儲存的鄰接表,所以我會在dfs(u)計算出答案之後將u的表頭清零(就相當於把與u相連的所有邊刪掉了),最後再把鍊錶的計數器清零。

切忌用memset(tle的親身經歷),但是也要注意是否完全清零~

結合**更容易理解~

/*lucky_glass*/ 

#includeusing namespace std;

const int n=250000;

typedef long long ll;

int qread()

struct graph

edge(int _to,int _nxt,int _cst):to(_to),nxt(_nxt),cst(_cst){}

}edg[n*2+7];

int adj[n+7],edgtot;

void rebuild()

void addedge(int u,int v,int cst,bool dir=false)

}grp,tre;

int dfncnt,n,q,m,statop;

int dfn[n+7],fa[n+7][20],dep[n+7],pnt[n+7],sta[n+7];

ll dp[n+7];

void dfs(int u,int pre)

}int lca(int u,int v)

void insert(int u)

int lca=lca(u,sta[statop]);

if(lca==sta[statop]) return;

while(statop>1 && dfn[sta[statop-1]]>=dfn[lca])

tre.addedge(sta[statop-1],sta[statop],0,true),

sta[statop--]=0;

if(lca!=sta[statop]) tre.addedge(lca,sta[statop],0,true),sta[statop]=lca;

sta[++statop]=u;

}ll dp(int u)

bool cmp(int a,int b)

return 0;

}

\(\mathcal\)

沒看懂的部分可以在我的郵箱(\(lucky\[email protected]\))裡詢問~

虛樹學習筆記

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

虛樹 學習筆記

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

虛樹學習筆記

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