初學的時候整個人是懵的對於一類樹上的問題,如果僅有一部分點對答案「有用」(也就是說另一部分「可以不要」),那麼我們考慮只儲存那些有用的點。這就是虛樹的思想。所以為什麼它叫虛樹?我也不知道……不過總算是弄懂了
通常情況下我們把一些點的 lca 也算作「有用」的點。
虛樹可做的題目一般來說(以我對虛樹短淺的認知)會限制有用的點的總個數,並且會用多組詢問的方式來使一般的方法tle……
仿照著其他部落格的思路,我覺得用一道例題來講會更加容易理解——
〔bzoj 2286 消耗戰〕這道題最基礎的做法是對於每一組資料跑一遍dp,顯然是 \(o(nm)\) 的做法,會 tle。「題意」
給定一棵n個點的樹,每個邊有權值表示將它割斷的花費,已知其中的k個點是有價值的,現在需要把這些點與點1斷開(不連通),求最小花費。
輸入時先給定一棵樹。
m組資料,每次詢問給出k個點,表示它們是有價值的,對於每組資料輸出最小花費。
[反正\(o(nm)\)會超時就對了,保證所有資料的k之和不超過500000]
那麼這道題也算是一道比較特殊的虛樹題(某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 序從小到大排好序 ...