1. 程式人生 > >競賽題解 - NOIP2018 保衛王國

競賽題解 - NOIP2018 保衛王國

\(\mathcal{NOIP2018}\) 保衛王國 - 競賽題解

按某一個炒雞dalao名曰 taotao 的話說:
\(\ \ \ \ \ \ \ \ \ “一道sb倍增題”\)
順便提一下他的〔題解〕(因為按照這個思路寫的,所以程式碼看起來也差不多)
因為比較復(胡)雜(炸),可能需要理解久一點


『題目』

參見 〔洛谷 P5024〕


『解析』

一、初步思考

如果不考慮多次詢問的話,顯然可以進行一次簡單的樹形DP,特殊判斷一下當前的點(也就是城市)能不能選(放軍隊)就行了~ 但是顯然會\(TLE\)
由於\(m\)也就是詢問次數非常大,而dp狀態非常之多,不可能 \(O(1)\)

的時間處理每次詢問。那麼剩下兩種時間複雜度的可能性要麼是 \(O(log_2n)\) 要麼是 \(O(\sqrt{n})\)。根據我淺薄的對演算法的理解……好像沒有什麼用來優化dp的有關樹的演算法是 \(O(\sqrt n)\) 的,倒是有一個樹上倍增(\(LCA\))是 \(O(log_2n)\) 的。

(至於一般的樹形dp相信大家都會)

對這道題本身進行分析——假設現在被限制的點是 \(u\),\(v\) ,根節點是 \(1\),那麼:
\(lca\) 到 點\(1\) 的路徑上的 \(dp\) 值被 \(u\),\(v\) 共同影響;
\(u\)\(lca\) 的路徑上的 \(dp\)

值被 \(u\) 影響;
\(v\)\(lca\) 的路徑上的 \(dp\) 值被 \(v\) 影響;

恰好我們知道有一種倍增優化dp的演算法,所以正解順理成章就是樹上倍增優化樹形dp~(出題人的思維還是非常厲害的)

對於倍增求lca的運用不止可以求出lca,還經常用來統計兩點經過lca的簡單路徑上的一些特殊值,其轉移方法大多數是:

如果要求 u 到 u的第\(2^i\)個祖先 的路徑上的值,就綜合 u 到 u的第\(2^{i-1}\)個祖先 的路徑上的值,和 u的第\(2^{i-1}\)個祖先 到 u的第\(2^i\)個祖先 的路徑上的值;

二、倍增推導dp陣列

那麼對樹形dp的優化也是這樣的~因為一般的dp狀態定義是 “dp[u][0/1] 表示u選/不選時以u為根的子樹的最小花費”,那麼我們定義 \(fdp[u][k][0/1][0/1]\) 表示 “u選/不選(第三維)”並且 u的第\(2^i\)個祖先 選/不選(第四維)時,以 u的第\(2^i\)個祖先 為根的子樹的最小花費。另外幾個定義如下:

  • \(fa[u][i]\) 表示“點u的第\(2^i\)個祖先”,如果沒有則值為0;
  • \(dep[u]\) 表示“點u的深度”;

一次DFS就可以求出 dp,dep 的值(DFS1)。

因為我們考慮了2個點的取捨,必然就會有一些之前的(沒有任何限制時的最優值)是不合法的,所以我們需要對 fdp 進行一些修改。這就進入了第2次DFS:

根據 taotao 的說法,大概就是“合法的值=可能不合法的值-原有貢獻+合法的貢獻”

倍增求lca的基本操作——先把 u 到 u的父親 的值處理出來,然後依次處理出 fa和fdp 。也就是說我們先要處理 \(fdp[u][0][0/1][0/1]\) ,以下簡稱pre為u的父親:

  • fdp[u][0][0/1][0/1]=INF :因為 u 和 pre 至少有一個要選;
  • fdp[u][0][1][0]=dp[pre][0] :因為如果pre不選那麼u一定選
  • fdp[u][0][0][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][0]:因為選pre時,u的貢獻為min(dp[u][0],dp[u][1]),但是我們必須限制u不選,所以減去它原有貢獻再加上dp[u][0]也就是合法貢獻;
  • fdp[u][0][1][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][1]:其他的都和上面的那種情況是相同的,只不過這是限制了u必須選也就是必須產生dp[u][1]的貢獻。

重要的不是遞推式,是思路“合法的值=可能不合法的值-原有貢獻+合法的貢獻”——
假設我們現在要推導從u到u的第\(2^k\)個祖先的狀態,也就是fdp[u][k][a][b](a決定u選/不選,b決定u的第\(2^k\)個祖先選/不選)
那麼,我們可以根據已經得到的
\(\ \ \ \ \ \ \ ①\)u到u的第\(2^{k-1}\)個祖先(定義為anc)的狀態也就是fdp[u][k-1][a][0/1]
\(\ \ \ \ \ \ \ ②\)anc到anc的第\(2^{k-1}\)個祖先的狀態也就是fdp[anc][k-1][0/1][b]
可見anc有兩種狀態——選/不選,那麼我們定義這個狀態為c
① 那麼上面說的“可能不合法的值”是指fdp[anc][k-1][c][b],因為我們並不知道這個值所代表的狀態是否滿足u的狀態;
② “原有貢獻”也就是dp[anc][c],在這種狀態下,u的狀態是不受限制的;
③ “合法的貢獻”說的是fdp[u][k-1][a][c],這樣的話也就限制了u的狀態

綜上所述,我們可以得到轉移式:
\[ fdp[u][k][a][b]=\min\begin{cases} fdp[anc][k-1][0][b]-dp[anc][0]+fdp[u][k-1][a][0]\\ fdp[anc][k-1][1][b]-dp[anc][1]+fdp[u][k-1][a][1] \end{cases} \]
(感覺開始difficult了)
這樣的話我們就可以轉移了~這就是第二次DFS(DFS2)。

三、lca求解答案

接下來就是求LCA,順便求解答案的過程——類比之前的轉移,這裡的轉移要點也是“合法的值=可能不合法的值-原有貢獻+合法的貢獻”;只是說這裡的轉移可以大致分為3步(下面都假設u的深度大於v):

  1. 將u移動到與v深度相同的位置,移動的路徑就是u的狀態會影響的路徑;
  2. 將u,v同時移動到它們的lca,u,v各自移動的路徑分別受u,v的狀態的影響;
  3. 將lca移動到根節點,lca移動的路徑由u,v的狀態共同影響;

那麼我們設在限制下,當前的以u(這裡強調“當前”是因為u在不斷移動)為根的子樹的最小花費為 val[1](選擇u) 和 val[2](不選擇u);以及當前的以v為根的子樹的 val[3] 和 val[4]。然後在定義一個fval[1~4]分別對應val[1~4],方便轉移~

(1)將u移動到與v同層

這裡涉及到的變數有 u 對應的狀態 val[1],val[2],以及 u 將要移動到的位置對應的狀態 fval[1],fval[2]。
假設現在要將u移動到u的第\(2^i\)個祖先,顯然u的狀態可以選也可以不選,那麼定義a為u的狀態:

對於fval[1],這裡的“可能不合法的值”是fdp[u][i][a][1](因為不知道是否滿足限制),“原有貢獻”為dp[u][a],“合法貢獻”為val[a](u的狀態對應的val值);當然fval[2]也可以類比上面的方法。

(2)將u,v同時移動到lca

和(1)相同,只是多了v也要同時轉移~(也就是說還涉及到val[3],val[4])
但是有一種特殊情況——v是u的祖先,這樣的話根據v的限制,我們就需要捨去一個val值:
現在的val[1],val[2]也就是lca選/不選所對應的值,但是如果v就是lca,v就會對val產生限制,也就是說當v不能選時,表示選的val[1]就要被捨去,而當v必須選時,表示不選的val[2]就要被捨去。

(3)將lca移動到根節點

完全一樣了,這裡的lca就代替了u,val[1],val[2]分別指lca選/不選時以lca為根的子樹的花費。
最後得到答案就是min(val[1],val[2])

(感覺好難講懂啊 \(QwQ\),不如先看一看程式碼?)


『原始碼』

/*Lucky_Glass*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100000;
const ll INF=(1ll<<60);
struct GRAPH{
    struct NODE{
        int to,nxt;
        NODE(){}
        NODE(int _to,int _nxt):to(_to),nxt(_nxt){}
    }edg[N*2+5];
    int edgtot,adj[N+5];
    void Init(){
        edgtot=0;
        memset(adj,-1,sizeof adj);
    }
    void AddEdge(int u,int v){
        edg[++edgtot]=NODE(v,adj[u]);
        adj[u]=edgtot;
    }
}grp;
int n,m;
int cst[N+5],dep[N+5],fa[N+5][23];
ll dp[N+5][2],fdp[N+5][23][2][2];
void DFS1(int u,int pre){
    dep[u]=dep[pre]+1;
    dp[u][0]=0;dp[u][1]=cst[u];
    for(int it=grp.adj[u];it!=-1;it=grp.edg[it].nxt){
        int v=grp.edg[it].to;
        if(v==pre) continue;
        DFS1(v,u);
        dp[u][0]+=dp[v][1];
        dp[u][1]+=min(dp[v][0],dp[v][1]);
    }
}
void DFS2(int u,int pre){
    fa[u][0]=pre;
    fdp[u][0][0][0]=INF;
    fdp[u][0][1][0]=dp[pre][0];
    fdp[u][0][0][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][0];
    fdp[u][0][1][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][1];
    for(int i=1;i<20;i++){
        int anc=fa[u][i-1];
        fa[u][i]=fa[anc][i-1];
        fdp[u][i][0][0]=min(fdp[anc][i-1][0][0]-dp[anc][0]+fdp[u][i-1][0][0],fdp[anc][i-1][1][0]-dp[anc][1]+fdp[u][i-1][0][1]); //u -> anc -> fa[anc][i-1]
        fdp[u][i][1][0]=min(fdp[anc][i-1][0][0]-dp[anc][0]+fdp[u][i-1][1][0],fdp[anc][i-1][1][0]-dp[anc][1]+fdp[u][i-1][1][1]);
        fdp[u][i][0][1]=min(fdp[anc][i-1][0][1]-dp[anc][0]+fdp[u][i-1][0][0],fdp[anc][i-1][1][1]-dp[anc][1]+fdp[u][i-1][0][1]);
        fdp[u][i][1][1]=min(fdp[anc][i-1][0][1]-dp[anc][0]+fdp[u][i-1][1][0],fdp[anc][i-1][1][1]-dp[anc][1]+fdp[u][i-1][1][1]);
    }
    for(int it=grp.adj[u];it!=-1;it=grp.edg[it].nxt){
        int v=grp.edg[it].to;
        if(v==pre) continue;
        DFS2(v,u);
    }
}
ll LCA(int u,int v,int U,int V){
    if(dep[u]<dep[v]) swap(u,v),swap(U,V);
    ll val[5]={0,(U? dp[u][1]:INF),(U? INF:dp[u][0]),(V? dp[v][1]:INF),(V? INF:dp[v][0])};
    ll fval[5];
    for(int i=19;i>=0;i--)
        if(dep[fa[u][i]]>=dep[v]){
            fval[1]=min(fdp[u][i][0][1]-dp[u][0]+val[2],fdp[u][i][1][1]-dp[u][1]+val[1]);
            fval[2]=min(fdp[u][i][0][0]-dp[u][0]+val[2],fdp[u][i][1][0]-dp[u][1]+val[1]);
            val[1]=fval[1];val[2]=fval[2];
            u=fa[u][i];
        }
    int lca;
    if(u==v){
        if(V) val[2]=INF;
        else val[1]=INF;
        lca=v;
    }
    else{
        for(int i=19;i>=0;i--)
            if(fa[u][i]!=fa[v][i]){
                fval[1]=min(fdp[u][i][0][1]-dp[u][0]+val[2],fdp[u][i][1][1]-dp[u][1]+val[1]);
                fval[2]=min(fdp[u][i][0][0]-dp[u][0]+val[2],fdp[u][i][1][0]-dp[u][1]+val[1]);
                fval[3]=min(fdp[v][i][0][1]-dp[v][0]+val[4],fdp[v][i][1][1]-dp[v][1]+val[3]);
                fval[4]=min(fdp[v][i][0][0]-dp[v][0]+val[4],fdp[v][i][1][0]-dp[v][1]+val[3]);
                val[1]=fval[1];val[2]=fval[2];val[3]=fval[3];val[4]=fval[4];
                u=fa[u][i];v=fa[v][i];
            }
        lca=fa[u][0];
        fval[1]=dp[lca][1]-min(dp[u][1],dp[u][0])-min(dp[v][1],dp[v][0])+min(val[1],val[2])+min(val[3],val[4]);
        fval[2]=dp[lca][0]-dp[u][1]-dp[v][1]+val[1]+val[3];
        val[1]=fval[1];val[2]=fval[2];
    }
    for(int i=19;i>=0;i--)
        if(dep[fa[lca][i]]>=dep[1]){
            fval[1]=min(fdp[lca][i][0][1]-dp[lca][0]+val[2],fdp[lca][i][1][1]-dp[lca][1]+val[1]);
            fval[2]=min(fdp[lca][i][0][0]-dp[lca][0]+val[2],fdp[lca][i][1][0]-dp[lca][1]+val[1]);
            val[1]=fval[1];val[2]=fval[2];
            lca=fa[lca][i];
        }
    return min(val[1],val[2]);
}
int main(){
    grp.Init();
    scanf("%d%d%*s",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&cst[i]);
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        grp.AddEdge(u,v);grp.AddEdge(v,u);
    }
    DFS1(1,0);
    DFS2(1,0);
    while(m--){
        int u,v,U,V;scanf("%d%d%d%d",&u,&U,&v,&V);
        if((fa[u][0]==v || fa[v][0]==u) && (!U && !V)){
            printf("-1\n");
            continue;
        }
        printf("%lld\n",LCA(u,v,U,V));
    }
    return 0;
}

\(\mathcal{The\ End}\)

\(\mathcal{Thanks\ For\ Reading!}\)

有什麼沒講清楚的就在郵箱 \(lucky\[email protected]\) 裡面問嘛~