1. 程式人生 > >虛樹學習筆記(洛谷2495 消耗戰)

虛樹學習筆記(洛谷2495 消耗戰)

題目連結

因為辣雞csdn,導致之前快寫好的部落格沒了
QWQ悲傷逆流成河qwqqq
首先虛樹,這個東西,我感覺是一種思想,或者是方法,而並不是一個數據結構什麼的。
他主要是用來解決:給出一棵樹,每次詢問選擇一些關鍵點,求一些資訊。
這些資訊的特點是,許多未選擇的點可以通過某種方式剔除而不影響最終結果。
於是就有了建虛樹這個演算法。我們根據原樹的資訊重新建樹,這棵樹中要儘量少地包含非關鍵節點。 這棵樹就叫做虛樹。這棵虛樹包含任意兩個關鍵節點的LCA。
通常這類題 O (

n m ) O(nm) T T 飛,而 O (
O(\sum 詢問點個數)
是能跑得過的QWQ

那我們應該怎麼構造虛樹呢QWQ我們把所有關鍵點按照dfs序排好序。
然後,虛樹要有一個根。這裡一般直接把1號節點設為根。構建虛樹的主要過程就是使用一個棧,維護從根開始的一條鏈。這條鏈上的所有點的dfs序一定是遞增的。
我們把關鍵點掃描一遍,一邊維護這個棧一邊連邊構建虛樹。

具體來說:

1.把根節點放入棧中2.把所有關鍵點點掃一遍。設當前的節點為 x x ,棧頂(鏈末端)的節點為 y y 。求出 x x y y l c a lca

此時有2種情況。

1) l c a lca y y 。此時 x x y y 子樹內。我們就把 x x 壓入棧,相當於直接把 x x 新增到這條鏈的末尾。

2) x y x,y 分立在 l c a lca 的兩個子樹中。此時y這個子樹中的所有關鍵點一定都被遍歷過了。(原因:設有一關鍵點a在y子樹中,沒有被遍歷過。則 d f n [ y ] < d f n [ a ] < d f n [ x ] dfn[y]<dfn[a]<dfn[x] 。但是我們是把關鍵點按照dfs序來排的,a一定在x之前被掃)

因此y子樹內的所有關鍵點都已經被被加入虛樹。接下來我們要把 y > l c a y->lca 這一段的點加入虛樹。我們設棧頂的節點為 y y ,棧頂的第二個節點為 z z

重複以下操作:

1.若 d f n [ z ] > d f n [ l c a ] dfn[z]>dfn[lca] 直接連邊 z > y z->y ,然後把 y y 出棧。

2.若 d f n [ z ] = d f n [ l c a ] dfn[z]=dfn[lca] 這意味著 z z 就是 l c a lca 。直接連邊 l c a > y lca->y 。此時子樹已構建完畢。

3.若 d f n [ z ] > d f n [ l c a ] dfn[z]>dfn[lca] ,說明 l c a lca y y z z 夾在中間。此時我們必須要把 l c a lca 加入虛樹,所以連邊 l c a > y lca->y ,然後把 y y 彈出棧,把lca入棧。然後子樹構造完畢。

最後把棧裡面的元素搞一搞,每次 a d d e d g e ( s [ t o p 1 ] , s [ t o p ] ) addedge(s[top-1],s[top]) 即可感覺配合程式碼會比較好理解

void solve()
{
// memset(point,0,sizeof(point));
    cnt=0;
    sort(a+1,a+1+k,cmp);
    top=1;
    sta[top]=1;
    for (int i=1;i<=k;i++)
    {
        int l = lca(sta[top],a[i]);
        if (l!=sta[top])
        {
            while (top>1)
            {
                if (dfn[sta[top-1]]>dfn[l])
                {
                    addedge(sta[top-1],sta[top],0);
                    top--;
                }
                else
                {
                    if (dfn[sta[top-1]]==dfn[l])
                    {
                        addedge(sta[top-1],sta[top],0);
                        top--;
                        break;
                    }
                    else
                    {
                      addedge(l,sta[top],0);
                      sta[top]=l;
                      break;
                    }
                }
                
            }
        }
        if (sta[top]!=a[i]) sta[++top]=a[i];
    }
    while (top>1)
    {
        addedge(sta[top-1],sta[top],0);
        top--;
    }
}

那麼剩下的主要就是 d p dp 部分了

首先,我們定義 m n [ x ] mn[x] 表示在原樹上 1 x 1到x 的路徑上邊權的最小值, f [ x ] f[x] 表示切割完 x x 子樹內所有關鍵點的最小花費。

對於當前點 x x ,如果他是關鍵點,那麼必須切割這個點到1的路經上的一條邊,那麼就是 m n [ x ] mn[x] ,否則 f [ x ] = m i n ( m n [ x ] , f [ s o n ] f[x]=min(mn[x],\sum f[son]

上程式碼

int dp(int x,int flag)
{
    
    int sum=0;
    for (int &i=point[x];i;i=nxt[i])
    {
        int p = to[i];
        sum=sum+dp(p,flag);
    }
    if (tag[x]==flag)
    {
        return mn[x];
    }
    return min(sum,mn[x]);
}

其中有一個要注意的地方就是虛樹的時候,因為每次要重新建樹,所以要清空 p o i n t point 陣列,而由於時間原因,又不能直接 m e m s e t memset ,所以我們只能使用奇妙的手段!自殺式遍歷通過取地址,不斷修改 p o i n t point

for (int &i=point[x];i;i=nxt[i])

由於宕機了三次QWQ
所以暫時沒有辦法放整個題的程式碼