「SOL」GRE Words Once More!(HDU)

很久沒寫博客了
此次準備在 cnblogs 和 個人博客 上同步更新~ios


# 題面

一個 \(n\) 個點 \(m\) 條邊的 DAG,有若干個特殊點,邊有權值。數據結構

一個單詞定義爲從點 \(1\) 出發到達一個特殊點的路徑上,邊權按訪問順序構成的一個序列。spa

\(Q\) 次詢問,每次給出 \(k\),求全部單詞中字典序從小到大第 \(k\) 個單詞的長度,或回答「不存在」。code

\(2\le n\le10^5\)\(0\le m\le10^5\)\(1\le k\le10^8\);保證 \(1\) 不是特殊點且 \(1\) 沒有入度;保證一個點的全部出邊的權值兩兩不一樣。blog


# 解析

思路挺讓人震驚的……原來不只樹能夠剖分遞歸

惋惜的是,但凡 \(k\) 的範圍開大到 \(10^9\) 就放不過那種直接跑出 \(10^8\) 的答案的暴力 awaci

首先不難想到DP,先求出 \(f_u\) 表示從 \(u\) 出發可以獲得多少個單詞。轉移式很簡單,pdo

\[f_u=\sum_{(u,v)\in E}f_v+[u\text{ is special}] \]

不過考慮到單詞數會很是大,遠遠大於 \(10^8\),會爆 int,因此當 \(f_u>2\times10^8\) 時就把 \(f_u\) 賦值爲 \(2\times10^8\)(不和 \(10^8\) 取 min,仍是留一點超出的空間)。字符串

而後先想暴力,每次詢問從 \(1\) 出發,按權值從小到大查看出邊,若是構成的單詞數量足夠 \(k\),就從那條出邊遞歸到子問題。複雜度是 \(O(Qn)\) 的。get

具體看一下怎麼遞歸子問題,設點 \(u\) 的出邊按權值從大到小爲 \(w_1,w_2,\dots,w_t\),若是咱們發現:

\[\sum_{j=1}^{i-1}w_j< k\le\sum_{j=1}^iw_j \]

說明應從邊 \(i\) 轉移,因而遞歸子問題 k-=w[1]+w[2]+...+w[i-1]

注意到每次 \(k\) 都會減少一些,因而就有了一個很妙的想法——對於一個點 \(u\),定義其重兒子dp 值最大的一個出點,其餘出點就是輕兒子。

實際上直接這樣定義,理論複雜度會有小問題(可是實測可過),理論複雜度正確的定義應是「dp 值小於 \(2\times10^8\) 的 dp 值最大的出點」

也就是說忽略 dp 值太大的點,如下均假設 \(f_u\le10^8\)

這樣有什麼性質?設 \(u\) 的重兒子是 \(p\),一個輕兒子是 \(q\),則 \(f_p\ge f_q\)。若是咱們要轉移到 \(q\),轉移後的子問題爲 \(k'\),則 \(k'\le f_q\le \frac{f_p+f_q}2\le\tfrac12f_u\)

這樣的話,\(k\) 的上界每次減半,只能減 \(O(\log f)\) 次,因而只會通過 \(O(\log f)\) 次輕邊。

與「樹」鏈剖分的聯繫

二者都是利用的這種「上界減半」的思想。

樹鏈剖分中若是走輕兒子,則子樹大小必定減半,那麼只會走 $O(\log n)$ 次輕兒子,也就是「一條鏈只會被剖成 $O(\log n)$ 條重鏈」這一結論。而走重兒子這種狀況,重兒子構成重鏈,鏈是一種更簡單的結構,所以能夠用一些處理序列的數據結構維護。

而這道題將 DAG 剖成「重鏈」——實際上不是鏈,而是一棵樹,一樣簡化了圖結構。接下來就能夠用一些樹的技巧解題了。

那若是從重兒子轉移呢?這一就不必定知足「上界減半」。可是不難發現重兒子構成了內向樹森林 的結構,能不能用什麼樹的技巧來加速走重兒子的過程?

考慮倍增,維護 \(u\) 的第 \(2^i\) 級祖先 jump[u][i],以及 \(\mathbf u\) 轉移到該祖先之間有多少個字符串 jumpcst[u][i]。畫個圖可能會好理解一些:

\[jumpcst_{u,i}=tot+tag_u+tag_a+tag_b \]

\(tag_u=1\) 表示 \(u\) 是特殊點)好比從 \(u\)\(a\)\(a\) 左邊的全部兒子表明的單詞都會被跳過,並且若是 \(u\) 自己是特殊點,在 \(u\) 結尾的單詞也會被跳過。

那麼能夠從 \(u\) 直接倍增到 jump[u][i] 的條件就是

\[jumpcst_{u,i}< k\le jumpcst_{u,i}+f_{jump_{u,i}} \]

別忘了還有上界。

顯然能夠倍增,複雜度 \(O(\log n)\)。而走輕兒子就能夠直接二分,預處理每一個點的出邊權值從小到大的 dp 值前綴和便可。

總的複雜度,每次查詢會走 \(O(\log f)\) 次輕兒子,每次 \(O(\log n)\) 二分;會走 \(O(\log f)\) 次重鏈,每次 \(O(\log n)\) 倍增,總的複雜度加上預處理爲 \(O(Q\log n\log f+n\log n)\)


# 源代碼

/*Lucky_Glass*/
#include<vector>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

inline int Rint(int &r){
    int b=1,c=getchar();r=0;
    while(c<'0' || '9'<c) b=c=='-'? -1:b,c=getchar();
    while('0'<=c && c<='9') r=(r<<1)+(r<<3)+(c^'0'),c=getchar();
    return r*=b;
}
const int N=1e5+10;
#define ci const int &
typedef pair<int,int> pii;

int n,m,Q,cas;
int sptag[N],dp[N],son[N],dpcst[N],jump[N][20],jumpcst[N][20];
bool dpdone[N];
vector<pii> lnk[N];
vector<int> key[N];
//dpcst[u]: 從u到son[u]須要跳過多少字符串
//key[u][i]: 從u到lnk[u][i]須要跳過多少字符串

int DP(ci u){
    if(dpdone[u]) return dp[u];
    dpdone[u]=true,dpcst[u]=dp[u]=sptag[u];
    for(int it=0,iit=lnk[u].size();it<iit;it++){
        int v=lnk[u][it].second;
        DP(v);
		//實際上不加 dp[u]<=1e8 也能過……
        if(dp[son[u]]<dp[v] && dp[u]<=1e8) son[u]=v,dpcst[u]=dp[u];
        dp[u]+=dp[v];
        if(dp[u]>2e8) dp[u]=2e8;
        key[u].push_back(dp[u]);
    }
    jump[u][0]=son[u],jumpcst[u][0]=dpcst[u];
    for(int i=1;i<20;i++){
        jump[u][i]=jump[jump[u][i-1]][i-1];
        jumpcst[u][i]=jumpcst[u][i-1]+jumpcst[jump[u][i-1]][i-1];
        if(jumpcst[u][i]>2e8) jumpcst[u][i]=2e8;
    }
    return dp[u];
}
int Jump(int rnk){
    int u=1,ret=0;
    while(true){
        if(rnk>dp[u]) exit(0);
        for(int i=19;~i;i--)
            if(jump[u][i] && rnk>jumpcst[u][i]){
                int v=jump[u][i];
                if(rnk>jumpcst[u][i]+dp[v]) continue;
                rnk-=jumpcst[u][i];
                u=v,ret+=(1<<i);
            }
        if(rnk==1 && sptag[u]) return ret;
        int tmp=int(lower_bound(key[u].begin(),key[u].end(),rnk)-key[u].begin());
        if(tmp) rnk-=key[u][tmp-1];
        else rnk-=sptag[u];
        u=lnk[u][tmp].second,ret++;
    }
}
void Solve(){
    Rint(n),Rint(m),Rint(Q);
    for(int i=1;i<=n;i++){
        if(i>1) Rint(sptag[i]);
        dpdone[i]=false;
        lnk[i].clear(),key[i].clear();
        son[i]=dpcst[i]=0;
        for(int j=0;j<20;j++) jump[i][j]=jumpcst[i][j]=0;
    }
    for(int i=1,u,v,varc;i<=m;i++){
        Rint(u),Rint(v),Rint(varc);
        lnk[u].push_back(make_pair(varc,v));
    }
    for(int i=1;i<=n;i++) sort(lnk[i].begin(),lnk[i].end());
    DP(1);
    while(Q--){
        int rnk;Rint(rnk);
        if(rnk>dp[1]) printf("-1\n");
        else printf("%d\n",Jump(rnk));
    }
}
int main(){
    // freopen("input.in","r",stdin);
    Rint(cas);
    for(int i=1;i<=cas;i++){
        printf("Case #%d:\n",i);
        Solve();
    }
    return 0;
}

THE END

Thanks for reading!

\[\begin{split} 「\ &這故事,被誰轉陳得極盡周詳\\ &流傳在雲垂的多少\ 大街小巷\\ &擦肩而過\ 聽聞\ 莞然卻悵惘\\ &若是我回望,那刻你會不會,也朝我回望\ 」\\ ——&\text{《今日重到蘇瀾橋(Cover)》 By 封茗囧菌} \end{split} \]

> Linked 今日重到蘇瀾橋-網易雲

相關文章
相關標籤/搜索