1. 程式人生 > 其它 >可持久化資料結構維護路徑計數問題

可持久化資料結構維護路徑計數問題

其實就兩道比較相關的題目。

Graph Subpaths

給定一張 \(n\) 個節點 \(m\) 條的 \(DAG\),滿足每條有向邊 \(a\rightarrow b\)\(a<b\) 。另外給出 \(k\)\(DAG\) 上的路徑。對於 \(2\leq i\leq n\),求出 \(1\)\(i\) 的路徑條數,滿足路徑不包含給出的 \(k\) 條路徑中的任意一條。

\(2\leq n,m\leq 10^5\)\(k\leq 5\times 10^4\)\(k\) 條路徑總長度 \(\leq 10^5\)

來源:ByteDance/Moscow Workshops Programming Camp 2020 Online Contest, G

 

考慮將路徑當成字串來處理,對 \(DAG\) 建立類似 \(Trie\) 的資料結構,記 \(num(i)\) 為節點 \(i\) 答案,對於沒有路徑限制的情況,每個點將其入邊節點的路徑數量求和即可。

考慮給定的一條路徑帶來的影響,假設當前考慮到了路徑末端的節點 \(x\),那麼在路徑上的那個鄰居就不能對其產生貢獻,實際貢獻應來自路徑兩側的所有節點。

這裡直接做是 \(O(\sum \deg(i))\) 的,顯然不能接受。這裡的操作相當於路徑上點各自扣掉一部分,然後作貢獻,不過直接扣掉 \(num(son)\) 顯然是不對的,因為路徑上的節點可能是其它路徑的末尾,之前 \(son\)

對其的貢獻就不是 \(num(son)\),而是另一系列路徑兩側點。

於是考慮維護可持久化資料結構,我們用可持久化動態開點線段樹維護 \(Trie\) 的形態,需要支援的操作有:

  • 在特定位置插入一個節點:即 \(x\) 的所有入邊節點對其產生貢獻。
  • 刪除一個特定位置的節點:扣掉路徑上的兒子。
  • 複製一棵線段樹:路徑上的節點自身不需要這些容斥,容斥是作用於 \(x\) 處的資訊的。

於是每次先將 \(x\) 入邊節點的貢獻統計到 \(x\) 中,對於路徑自底向上對線段樹進行容斥即可。

時間複雜度 \(O(n\log n)\)

 

背單詞

有一個自動機,可以看做一張 \(n\) 個點 \(m\)

條邊的有向圖,節點編號為 \(1,2,..,n\),每條邊上都標有一個正整數,有一些節點是終止節點。對於一條從 \(1\) 到某個終止節點的路徑,把它經過的邊的標號依次連線起來,可以得到一個序列,這個序列就是一個單詞。

\(Q\) 個詢問,每次給出一個 \(k\),求所有單詞中字典序第 \(k\) 大的單詞長度是多少。

不存在第 \(k\) 大輸出 -1,長度是 \(\infty\) 時輸出 inf,保證一個節點的所有出邊標號不同。

\(2\leq n\leq 10^5,0\leq m\leq 10^5,1\leq q\leq 10^5,1\leq k\leq 10^{18}\)

來源:模擬賽,是 HDOJ5118 GRE Words Once More! 的加強版。

 

先看 \(DAG\) 上的情況,考慮樸素維護一個數組 \(len[x][k]\),表示從 \(x\) 點出發字典序第 \(k\) 大的單詞的長度。那麼轉移就是將 \(x\) 的所有出邊節點的 \(len\) 陣列按序合併,然後整體加 \(1\),若 \(x\) 是終止節點那麼再在序列頭部新增一個 \(0\)

雖然 \(len[x]\) 的長度可以達到 \(10^{18}\) 級別,但是其中有大量資訊重複來自同一個節點,本質上互異的資訊只有 \(O(n)\) 級別。於是考慮可持久化,由於要高效合併兩個序列,容易想到可持久化平衡樹。

於是每次直接做可持久化平衡樹合併即可,同時維護一個子樹加的標記,顯然當 \(x\) 的平衡樹大小超過 \(10^{18}\) 時再後面的資訊就不用合併了,平衡樹特性可以保證 \(x\) 的樹高是 \(O(\log k)\) 的。詢問時直接查詢對應位置答案即可。

對於圖上有環的情況,可以發現若對於某個 \(k\),其單詞處於一個環內並至少繞了一圈,那麼 \(k\) 往後的所有詢問便是一直在此環內繞圈了,於是只需要考慮第一個環。

先按照 \(dfs\) 的順序遍歷 照常合併平衡樹,然後目前走到了 \(x\) 點,發現 \(y\) 是其 \(dfs\) 樹上的祖先,那麼現在碰到了第一個環。於是現在 \(x\) 直接退出,因為剩下的路徑不可能被遍歷到了,\(dfs\) 的過程全部結束。那麼這樣 \(y\) 的平衡樹中儲存的就是繞環一圈以內的所有單詞資訊。

對於一個詢問 \(k\),若 \(k\leq\) 節點 \(1\) 的平衡樹大小,直接查詢答案。否則單詞一定繞環了若干圈,記環長為 \(len\)\(y\) 的平衡樹的大小為 \(mod\),即一個圈上有 \(mod\) 個單詞,那麼 \(k\) 對應單詞一定繞了 \(\lfloor\frac{k-pre}{mod}\rfloor\) 圈,\(pre\) 是不繞圈的單詞數量,即 \(1\) 的平衡樹大小,於是將 \(k\) 取模後在 \(y\) 的平衡樹上查詢答案即可,最後再加上圈數乘環長以及繞圈前的長度(\(1\)\(y\) 的距離)。而若 \(mod=0\),含義就是單詞會在環上無休止的繞下去,一旦退出環就會得到更劣的解,所以此時輸出 inf 即可。

維護可持久化平衡樹(FHQ Treap)有一個細節:合併平衡樹不能採用節點的隨機權值作為比較,否則在可持久化時,複製出來的樹和先前只有根節點權值是不同的,大量相同權值會導致時間複雜度退化到 \(O(nk)\),因此需要在合併時按照兩邊子樹大小隨機父子關係。

時間複雜度 \(O(n\log k)\)

 

另解:處理出每個節點可以到達的單詞數量,在環上且能到達終止節點即 \(\infty\),類似重鏈剖分找到重兒子,父親向重兒子連邊得到另一張有向圖,走輕邊時可達單詞數量至少減半,而在重鏈圖上維護 \(k\) 的上下界後可以倍增找到離開的地方。答案上界為 \(nk\),當前計算的值超過上界即對應 inf

值域實際為 \(O(2^n)\),但是可以對 \(k\)\(\min\),於是時間複雜度 \(O(n\log k(\log k+\log n))\)

 

Codes

Graph Subpaths

// ByteDance/Moscow Workshops Programming Camp 2020 Online Contest, G
#include<iostream>
#include<vector>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 101000
#define LOG 18
#define ll long long
#define mod 998244353
using namespace std;

struct Segment_Trie{
    struct node{
        int ls, rs, node; ll val;
    } t[10*N*LOG];
    int cnt, root[N];

    void insert(int &x, int l, int r, int pos){
        if(!x) x = ++cnt;
        if(l == r){ t[x].node = root[l], t[x].val = t[root[l]].val; return; }
        int mid = (l+r)>>1;
        if(mid >= pos) insert(t[x].ls, l, mid, pos);
        else insert(t[x].rs, mid+1, r, pos);
        t[x].val = (t[t[x].ls].val + t[t[x].rs].val) % mod;
    }

    void update(int &x, int y, int l, int r, int pos, int v){
        x = ++cnt;
        if(l == r){ t[x].node = v, t[x].val = t[v].val; return; }
        t[x].ls = t[y].ls, t[x].rs = t[y].rs;
        int mid = (l+r)>>1;
        if(mid >= pos) update(t[x].ls, t[y].ls, l, mid, pos, v);
        else update(t[x].rs, t[y].rs, mid+1, r, pos, v);
        t[x].val = (t[t[x].ls].val + t[t[x].rs].val) % mod;
    }

    int get(int x, int l, int r, int pos){
        if(l == r) return t[x].node;
        int mid = (l+r)>>1;
        if(mid >= pos) return get(t[x].ls, l, mid, pos);
        else return get(t[x].rs, mid+1, r, pos);
    }
} T;

int n, m, k;
vector<int> conn[N], path[N], relate[N];

int main(){
    ios::sync_with_stdio(false);
    cin>>n>>m;
    int u, v;
    rep(i,1,m) cin>>u>>v, conn[v].push_back(u);
    cin>>k;
    rep(i,1,k){
        int siz; cin>>siz, path[i].resize(siz);
        rep(j,0,siz-1) cin>> path[i][j];
        relate[path[i][siz-1]].push_back(i);
    }

    T.cnt = T.root[1] = 1, T.t[1].val = 1;
    rep(cur,2,n){
        for(int k : conn[cur]) T.insert(T.root[cur], 1, n, k);
        for(int k : relate[cur]){
            vector<int> chain = {T.root[cur]};
            int siz = path[k].size(), x = 0;
            per(i,siz-2,0){
                chain.push_back(T.get(chain.back(), 1, n, path[k][i]));
                if(chain.back() == 0) break;
            }
            if(chain.back() == 0) continue;
            per(i,siz-2,0) T.update(x, chain[i], 1, n, path[k][siz-i-2], x);
            T.root[cur] = x;
        }
    }

    rep(i,2,n) cout<< T.t[T.root[i]].val <<" \n"[i == n];
    return 0;
}

背單詞 \(O(n\log k)\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 101000
#define LOG 65
#define ll long long
#define PII pair<int, int>
#define fr first
#define sc second
using namespace std;

mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
inline ll rnd(ll l, ll r){ return uniform_int_distribution<ll>(l, r)(rng); }

inline int read(){
    int s = 0, w = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar(); }
    while(ch >= '0' && ch <= '9') s = (s<<3)+(s<<1)+(ch^48), ch = getchar();
    return s*w;
}

struct Fhq_Treap{
    struct node{
        int c[2];
        ll siz, val, tag;
    } t[3*N*LOG];
    int cnt;

    int New(ll val){
        t[++cnt].val = val, t[cnt].siz = 1, t[cnt].c[0] = t[cnt].c[1] = t[cnt].tag = 0;
        return cnt;
    }
    int copy(int x){
        if(x == 0) return 0;
        t[++cnt].val = t[x].val, t[cnt].siz = t[x].siz, t[cnt].tag = t[x].tag;
        rep(i,0,1) t[cnt].c[i] = t[x].c[i];
        return cnt;
    }
    void pushdown(int x){
        rep(i,0,1) if(t[x].c[i]){
            t[x].c[i] = copy(t[x].c[i]);
            t[t[x].c[i]].val += t[x].tag, t[t[x].c[i]].tag += t[x].tag;
        }
        t[x].tag = 0;
    }
    void pushup(int x){ t[x].siz = t[t[x].c[0]].siz + t[t[x].c[1]].siz + 1; }

    int merge(int x, int y){
        if(!x || !y) return x+y;
        pushdown(x), pushdown(y);
        int z;
        if(rnd(0, t[x].siz+t[y].siz) < t[x].siz){
            z = copy(x);
            t[z].c[1] = merge(t[z].c[1], y);
        } else{
            z = copy(y);
            t[z].c[0] = merge(x, t[z].c[0]);
        }
        pushup(z);
        return z;
    }

    ll query(int x, ll k){
        ll lft = t[t[x].c[0]].siz + 1;
        if(k == lft) return t[x].val;
        pushdown(x);
        return k < lft ? query(t[x].c[0], k) : query(t[x].c[1], k-lft);
    }
} T;

int n, m, q;
int s[N], root[N];

vector<PII> son[N]; vector<int> rev[N];
int dep[N], len, st;
bool legal[N], in[N], circ;

void flush(int x){
    legal[x] = true;
    for(int y : rev[x]) if(!legal[y]) flush(y);
}

void dfs(int x){
    in[x] = true;
    sort(son[x].begin(), son[x].end());
    if(s[x]) root[x] = T.New(0);
    for(PII p : son[x]) if(legal[p.sc]){
        int y = p.sc;
        if(!dep[y]) dep[y] = dep[x] + 1, dfs(y);
        if(in[y]){ circ = true, len = dep[x] - dep[y] + 1, st = y; break; }
        int z = T.copy(root[y]);
        T.t[z].val++, T.t[z].tag++;
        root[x] = T.merge(root[x], z);
        if(circ || T.t[root[x]].siz > 1e18) break;
    }
    in[x] = false;
}

int main(){
    freopen("c.in", "r", stdin);
    freopen("c.out", "w", stdout);
    n = read(), m = read(), q = read();
    rep(i,2,n) s[i] = read();
    int a, b, c;
    rep(i,1,m){
        a = read(), b = read(), c = read(), son[a].push_back({c, b});
        rev[b].push_back(a);
    }

    rep(i,1,n) if(s[i] && !legal[i]) flush(i);
    dep[1] = 1, dfs(1);

    ll k, lim = T.t[root[1]].siz, mod = T.t[root[st]].siz;
    while(q--){
        cin>>k;
        if(k <= lim) printf("%lld\n", T.query(root[1], k));
        else if(mod == 0) puts(st ? "inf" : "-1");
        else{
            k -= lim;
            __int128 ans = (__int128)len*((k-1)/mod+1) + T.query(root[st], k%mod ? k%mod : mod) + dep[st]-1;
            string s = "";
            while(ans) s += char(ans%10+'0'), ans /= 10;
            reverse(s.begin(), s.end());
            for(char c : s) putchar(c);
            putchar('\n');
        }
    }
    return 0;
}

背單詞 \(O(n\log k(\log k+\log n))\)

#include<iostream>
#include<fstream>
#include<vector>
#include<algorithm>
#include<cstring>
#include<stack>
#define mem(a,b) memset(a, b, sizeof(a))
#define rep(i,a,b) for(int i = (a); i <= (b); i++)
#define per(i,b,a) for(int i = (b); i >= (a); i--)
#define N 210000
#define LOG 80
#define ld long double
#define lll __int128
#define PII pair<int, int>
#define fr first
#define sc second
#define Inf ((ld)1e18)
using namespace std;

int n, m, q, s[N];
vector<PII> son[N];
vector<ld> choice[N];
ld num[N], lb[N][LOG+2], rb[N][LOG+2], k;
int num0, dfn[N], low[N], state[N], up[N][LOG+2];
bool in[N], vis[N];
lll ans;
stack<int> stk;

void dfs0(int x){
    low[x] = dfn[x] = ++num0;
    state[x] |= s[x], in[x] = true;
    stk.push(x);
    for(PII p : son[x]){
        int y = p.sc;
        if(!dfn[y]) dfs0(y), low[x] = min(low[x], low[y]);
        else if(in[y]) low[x] = min(low[x], dfn[y]);
        state[x] |= state[y];
    }
    if(dfn[x] == low[x]){
        int y;
        bool circ = stk.top() != x && state[x]&1;
        do{
            y = stk.top(); stk.pop();
            state[y] |= circ ? 2 | state[x] : 0, in[y] = false;
        } while(y != x);
    }
}

void dfs(int x){
    vis[x] = true;
    choice[x] = {num[x] = s[x]};
    int hson = 0;
    for(PII p : son[x]){
        int y = p.sc;
        if(!vis[y]) dfs(y);
        ld val = state[y] == 3 ? Inf : num[y];
        if(val > (state[hson] == 3 ? Inf : num[hson])) 
            hson = y, lb[x][0] = num[x]+1, rb[x][0] = min(num[x]+val, Inf);
        num[x] = min(num[x]+val, Inf), choice[x].push_back(num[x]);
    }
    if(state[x] == 3) num[x] = Inf;
    up[x][0] = hson;
}

void solve(int x, ld k){
    if(ans > (__int128)n*::k) return;
    if(num[x] < k) return;
    lll ret = 1;
    if(k >= lb[x][0] && k <= rb[x][0]){
        per(i,LOG,0) if(up[x][i] && k >= lb[x][i] && k <= rb[x][i])
            k -= lb[x][i]-1, x = up[x][i], ret += (__int128(1))<<(__int128(i));
    }
    if(s[x] && k == 1){ ans += ret; return; }
    int pos = lower_bound(choice[x].begin(), choice[x].end(), k) - choice[x].begin() - 1;
    k -= choice[x][pos];
    ans += ret;
    return solve(son[x][pos].sc, k);
}

int main(){
    freopen("c.in", "r", stdin);
    freopen("c.out", "w", stdout);
    ios::sync_with_stdio(false);
    cin>>n>>m>>q;
    rep(i,2,n) cin>>s[i];
    int a, b, c;  
    rep(i,1,m) cin>>a>>b>>c, son[a].push_back({c, b});

    dfs0(1);
    rep(i,1,n){
        vector<PII> vec;
        for(PII p : son[i]) if(state[p.sc]&1) vec.push_back(p);
        son[i] = vec;
        sort(son[i].begin(), son[i].end());
    }
    dfs(1);
    rep(i,1,LOG) rep(x,1,n) if(state[x]&1){
        if(up[x][i-1]){
            up[x][i] = up[up[x][i-1]][i-1];
            lb[x][i] = min(lb[x][i-1] - 1 + lb[up[x][i-1]][i-1], Inf);
            rb[x][i] = min(lb[x][i-1] - 1 + rb[up[x][i-1]][i-1], Inf);
        }
    }

    rep(i,1,q){
        cin>>k, ans = -1, solve(1, k);
        if(ans > (__int128)n*k) cout<<"inf\n";
        else if(ans == -1) cout<<"-1\n";
        else{
            string s = "";
            while(ans) s += char(ans%10+'0'), ans /= 10;
            reverse(s.begin(), s.end());
            cout<< s <<endl;
        }
    }
    return 0;
}