線段樹

進階資結

\(225\)賴泓安

課程大綱

  • 題目
  • 線段樹優化建圖
  • 時間線段樹
  • Pattern
  • Segment tree beats

IOIC2023進階資結課程

去年資讀簡報

reference

暖身題目

給一個長為\(N\)的序列\(A\),有\(Q\)筆詢問

每次輸出\([l,r]\)之間的絕對眾數,不存在則輸出\(0\)

 

\(N,Q,a_i \leq 5\cdot10^5\)

絕對眾數:出現數目嚴格大於所有數字的一半

性質

假設只有一筆詢問

我們做個神奇操作

 

紀錄一開始的數字\(x = a_l\),並給它一個生命值\(hp = 1\)

然後往右掃

 

如果遇到的數字\(a_i = x\),\(hp\)++,否則\(hp\)--

若\(hp\)已經\(=0\),則設\(x = a_i\),\(hp\)重設為\(1\)

性質

只有做完後留下來的\(x\)有可能是答案!

可以線性 或 預處理+二分搜驗

為什麼?

注意到,我們把兩個相異元素\(x,y\)一起去除時

並不會影響絕對眾數的地位

 

如果\(x,y\)都不是絕對眾數,那沒有差。

如果其中一個是絕對眾數(假設有\(k\)個)

 

原本:\(k > \frac{N}{2}\)

刪除後的比例:\( (k-1):(N-2) \)

\((k-1) > \frac{N-2}{2} \)仍然成立

多筆詢問

用線段樹維護區間內的\(x和hp\)

對每個詢問\([l,r]\),各個樹上的子區間是可以合併的

 

所以能夠得到最終留下來的\(x\)

再二分搜確認是否為答案

code

#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define endl '\n'
#define pii pair<int,int>

int arr[500005];
vector <pair<int,int> > sg;

pii init(int l, int r, int node) {
    if (l == r) {
        sg[node].first = arr[l];
        sg[node].second = 1;
        return sg[node];
    }
    int mid = (l+r)>>1;
    pii lp = init(l,mid,node<<1);
    pii rp = init(mid+1,r,(node<<1)+1);
    if (lp.first == rp.first) {
        sg[node].first = lp.first;
        sg[node].second = lp.second+rp.second;
    } else {
        if (lp.second >= rp.second) {
            sg[node].first = lp.first;
            sg[node].second = lp.second-rp.second;
        } else {
            sg[node].first = rp.first;
            sg[node].second = rp.second-lp.second;
        }
    }
    return sg[node];
}

pii query(int ql, int qr, int l, int r, int node) {
    if (ql <= l && r <= qr) {
        return sg[node];
    }
    int mid = (l+r)>>1;
    if (qr <= mid) {
        return query(ql,qr,l,mid,node<<1);
    } 
    if (ql > mid) {
        return query(ql,qr,mid+1,r,(node<<1)+1);
    }
    pii lp = query(ql,qr,l,mid,node<<1);
    pii rp = query(ql,qr,mid+1,r,(node<<1)+1);
    pii ret;
    if (lp.first == rp.first) {
        ret.first = lp.first;
        ret.second = lp.second+rp.second;
    } else {
        if (lp.second >= rp.second) {
            ret.first = lp.first;
            ret.second = lp.second-rp.second;
        } else {
            ret.first = rp.first;
            ret.second = rp.second-lp.second;
        }
    }
    return ret;
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, q;
    cin>>n>>q;
    vector <vector<int> > pos(500005);
    sg.resize(n<<2);
    for (int i = 1; i <= n; ++i) {
        cin>>arr[i];
        pos[arr[i]].push_back(i);
    }
    init(1,n,1);
    while(q--) {
        int l, r;
        cin>>l>>r;
        pii pa = query(l,r,1,n,1);
        auto al = lower_bound(pos[pa.first].begin(),pos[pa.first].end(),l);
        auto ar = upper_bound(pos[pa.first].begin(),pos[pa.first].end(),r);
        int count = ar-al;
        if (count > ((r-l+1)>>1)) {
            cout<<pa.first<<endl;
        } else {
            cout<<0<<endl;
        }
    }
    return 0;
}

如果你要難一點的

線段樹優化建圖

先上題目

題敘

給定一張原本無邊的圖,有三種操作

 

\(1.\)連接一條\(u,v\)之間邊權\(w\)的邊

\(2.\)連接所有\([l,r]\)到\(v\)之間邊權\(w\)的邊

\(3.\)連接\(u\)到所有\([l,r]\)之間邊權\(w\)的邊

 

最後求單點源最短路

\(N,Q \leq 10^5, w \leq 10^9\)

問題是圖怎麼建?

最短路顯然是要Dijkstra

線段樹的本質?

每個線段樹上的節點

都代表某段連續區間的所有點

反過來說,針對給定的一段連續區間

我們可以將它分解成線段樹上的最多\(logN\)個節點

然後我們就會做了

先建兩棵線段樹,分別作為入度和出度使用

入度那棵往子節點連無權邊

出度那棵往父節點連無權邊

 

入度線段樹

1

2

3

4

5

6

7

8

出度線段樹

1

2

3

4

5

6

7

8

加邊操作

\(1.\)區間連到點\(v\):

從負責出度的那棵線段樹上,找到一些節點代表當前區間

從這些節點連邊到代表\(v\)的葉節點

\(2.\)點\(u\)連到區間:

從負責入度的那棵線段樹上,找到一些節點代表當前區間

由代表\(u\)的葉節點連邊回去

點連到區間 

1

2

3

4

5

6

7

8

2 -> [1,6]

8 -> [5,7]

區間連到點

1

2

3

4

5

6

7

8

[1,2] -> 3

[1,5] -> 7

複雜度

每次操作最多連\(log N\)條邊

操作複雜度\(O(Q log N)\)

總節點數會是\(O(N)\)的

最後跑一遍Dijsktra

code

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define pii pair<int,int>
#define p_q priority_queue
#define endl '\n'
#define pb push_back
#define mid ((l+r)>>1)
#define int ll

int mpk;
vector<vector< pii > > g;

struct SEG{
    int sz, t;
    void setedge(int l, int r, int p) {
        if (l == r) {
            if (t) g[p+mpk].pb({(p>>1)+mpk,0});
            else {
                g[p].pb({p+mpk,0});
                g[p+mpk].pb({p,0});
            }
            return;
        }
        setedge(l,mid,p<<1);
        setedge(mid+1,r,p<<1|1);
        if (!t) {
            g[p].pb({p<<1,0});
            g[p].pb({p<<1|1,0});
        } else if (p != 1) {
            g[p+mpk].pb({(p>>1)+mpk,0});
        }
    }
    void init(int _n,int _t) {
        sz = _n;
        t = _t;
        setedge(1,_n,1);
    }
    int find_node(int tar) {
        int l = 1, r = sz, p = 1;
        while(l != r) {
            p <<= 1;
            if (tar <= mid) r = mid;
            else {
                l = mid+1;
                p++;
            }
        }
        return t?(p+mpk):p;
    }
    void add(int l, int r, int p, int v, int ql, int qr, int w) {
        if (ql <= l && r <= qr) {
            if (!t) g[find_node(v)].pb({p,w});
            else g[p+mpk].pb({find_node(v),w});
            return;
        }
        if (qr <= mid) add(l,mid,p<<1,v,ql,qr,w);
        else if (ql > mid) add(mid+1,r,p<<1|1,v,ql,qr,w);
        else {
            add(l,mid,p<<1,v,ql,qr,w);
            add(mid+1,r,p<<1|1,v,ql,qr,w);
        }
    }
} seg[2];

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, q, s;
    cin>>n>>q>>s;
    mpk = 4*n;
    g.resize(2*mpk+5);
    seg[0].init(n,0), seg[1].init(n,1);
    while(q--) {
        int c, v, l, r, w;
        cin>>c>>v;
        if (c == 1) {
            cin>>l>>w;
            seg[0].add(1,n,1,v,l,l,w);
        } else if (c == 2) {
            cin>>l>>r>>w;
            seg[0].add(1,n,1,v,l,r,w);
        } else {
            cin>>l>>r>>w;
            seg[1].add(1,n,1,v,l,r,w);
        }
    }
    vector<int> dis(2*mpk+5,2e18), vis(2*mpk+5,0);
    dis[seg[0].find_node(s)] = 0;
    priority_queue< pii,vector< pii >,greater< pii > > pq;
    pq.push({0,seg[0].find_node(s)});
    while(!pq.empty()) {
        pii cur = pq.top();
        pq.pop();
        if (vis[cur.second]) continue;
        vis[cur.second] = 1;
        for (auto [u,w] : g[cur.second]) {
            if (dis[u] >= dis[cur.second]+w) {
                dis[u] = dis[cur.second]+w;
                pq.push({dis[u],u});
            }
        }
    }
    for (int i = 1; i <= n; ++i) {
        if (i != 1) cout<<' ';
        int ans = min(dis[seg[0].find_node(i)],dis[seg[1].find_node(i)]);
        if (ans >= 2e18) cout<<-1;
        else cout<<ans;
    }
    cout<<endl;
    return 0;
}

時間線段樹

顧名思義

跟值域線段樹差不多的概念

但這次我們以時間為Index開線段樹

一樣先看例題

題敘

給一張圖(\(N\)點\(M\)邊)

有一些加邊或刪邊操作

每次操作完問連通塊個數

\(N \leq 5\)x\(10^5\), \(M+Q \leq 5\)x\(10^5\)

問題就在

如何維護DSU的刪除操作?

DSU在幹嘛

說白一點

DSU的每次加邊操作

其實就是修改一個陣列上的某些值

進一步想,一使用啟發式合併時

每次加邊,陣列上最多只會有一個值被改變

Undo操作

在使用啟發式合併的狀況下

如果對DSU的操作只有回到上一步?

利用stack的概念

我們用一個stack紀錄每次加邊的時候誰被改到

每次的Undo操作

我們只需要把剛剛加邊時改到的值改回來

維護這個DSU的新增和刪除操作就很容易了吧

套用線段樹的結構

每條邊都會有一段存在的時間

我們可以把它拆成\(O(logN)\)個節點標在樹上

然後對線段樹DFS

 

看個圖

1

2

3

4

5

6

7

8

Time:

+

+

+

+

-

-

-

+

+

code

#pragma GCC optimize("O3")
#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define pii pair<int,int>
#define p_q priority_queue
#define endl '\n'
#define pb push_back
#define mid ((l+r)>>1)

struct DSU{
    vector<int> bs, sz;
    vector< pii > hsp;
    vector<int> num;
    int cnt;
    void init(int _n) {
        bs.assign(_n+5,0);
        sz.assign(_n+5,1);
        num.clear(), hsp.clear();
        cnt = _n;
        for (int i = 1; i <= _n; ++i) bs[i] = i;
    }
    int find(int p) {
        return (p == bs[p])?p:find(bs[p]);
    }
    void add(int u, int v) {
        int a = find(u), b = find(v);
        if (a == b) {
            hsp.pb({0,0});
            num.pb(0);
            return;
        }
        if (sz[a] < sz[b]) swap(a,b);
        bs[b] = a;
        num.pb(b);
        hsp.pb({a,sz[a]});
        sz[a] += sz[b];
        cnt--;
    }
    void undo() {
        if (num.back()) {
            sz[hsp.back().first] = hsp.back().second;
            bs[num.back()] = num.back();
            cnt++;
        }
        num.pop_back();
        hsp.pop_back();
    }
} dsu;

struct SEG{
    vector<vector< pii > > a;
    void init(int _n) {
        a.assign(_n<<2,vector< pii >());
    }
    void mdf(int l, int r, int p, int ql, int qr, int u, int v) {
        if (ql <= l && r <= qr) {
            a[p].pb({u,v});
            return;
        }
        if (ql > mid) mdf(mid+1,r,p<<1|1,ql,qr,u,v);
        else if (qr <= mid) mdf(l,mid,p<<1,ql,qr,u,v);
        else {
            mdf(mid+1,r,p<<1|1,ql,qr,u,v);
            mdf(l,mid,p<<1,ql,qr,u,v);
        }
    }
    void dfs(int l, int r, int p) {
        for (auto [u,v] : a[p]) dsu.add(u,v);
        if (l == r) {
            if (l) cout<<dsu.cnt<<endl;
            for (int i = 0; i < a[p].size(); ++i) dsu.undo();
            return;
        }
        dfs(l,mid,p<<1);
        dfs(mid+1,r,p<<1|1);
        for (int i = 0; i < a[p].size(); ++i) dsu.undo();
    }
} seg;

void slv() {
    int n, m, q;
    cin>>n>>m>>q;
    dsu.init(n);
    int a, b;
    map< pii,pii > mp;
    while(m--) {
        cin>>a>>b;
        a++, b++;
        if (a > b) swap(a,b);
        mp[{a,b}].second++;
    }
    char c;
    seg.init(q);
    for (int i = 1; i <= q; ++i) {
        cin>>c>>a>>b;
        a++, b++;
        if (a > b) swap(a,b);
        if (c == 'N') {
            if (!mp.count({a,b}) || !mp[{a,b}].second) mp[{a,b}].first = i;
            mp[{a,b}].second++;
        } else {
            if (--mp[{a,b}].second == 0) {
                seg.mdf(0,q,1,mp[{a,b}].first,i-1,a,b);
            }
        }
    }
    for (auto it : mp) {
        if (!it.second.second) continue;
        seg.mdf(0,q,1,it.second.first,q,it.first.first,it.first.second);
    }
    seg.dfs(0,q,1);
}

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t; cin>>t;
    while(t--) {
        slv();
    }
    return 0;
}

只要是可以支援Undo的

都能套上去喔

例如李超線段樹

Pattern

講師可能也不太會

Pattern?

就是要拆開出題者的包裝

看出題目真正要你處理的

題目

有\(N\)個學生(編號\(1\)~\(N\))由左而右排成一列

現在你要給每個學生一個介於\(1\)~\(N\)的成績

 

須符合下列條件:

1.由左而右,成績非嚴格遞增

2.學生編號遞增時,成績非嚴格遞增

 

有\(Q\)筆操作,每筆操作會交換兩位學生的位置

每次操作後輸出分派的成績最多可以有幾種數字

\(N \leq 10^6, Q \leq 3 \cdot10^5\)

例子

學生排列:\([2,3,1,5,4,7,6]\)

一組合法排列:\([1,1,1,2,2,3,3]\)

*有三個不同成績

好像有什麼類似環的結構耶

環的個數代表最多能放的數字種類

觀察相鄰兩人\(u,v\)(左與右)

若\(u > v\),則\([u,v]\)的分數必須一樣

這可以想像成

\([u,v)\)這些人都被\(v\)給控制住了

 

所以我們的任務變成

要找有多少自由的人

區間加值&詢問最小值個數!

 

對每組相鄰且\(u > v\)的\(u,v\)

我們對\([u,v)\)做區間加值

最後看有幾個\(0\)就是答案

(\(0\)一定是最小值,所以這等價詢問最小值個數)

可以用類似矩形覆蓋面積的線段樹維護

修改

就當成先刪除再新增就好了

 

總複雜度:\(O(NlogN+QlogN)\)

題敘

有\(HW\)個座位,安排成\(H\)個橫列和\(W\)個直行。

橫列編號為\(0\)到\(H-1\),直行編號為\(0\)到\(W-1\)。

你邀請了\(HW\)位選手,編號為\(0\)到\(HW-1\),

你有一張座位圖,把選手\(i\)原先都有獨特的座位
座位當中,一個面積為\(k\)的長方形區域為美麗的

表示這個長方形中的選手編號是\(0\)到\(k-1\)

一張座位圖的美麗程度即為座位圖中所能找到的美麗長方形個數

題敘

接著會有\(Q\)筆操作

每次將兩位選手交換位置

請在每次更新過後

輸出當下座位表的美麗程度

subtask:H = 1

我們可以先觀察看看座位為一直線的狀態

先假設所有座位一開始都沒人(記為白色)

然後讓選手一編號入座(該位置改為黑色)

 

所以我們的任務變成

找有多少時間點,黑色的位置是一段連續區間

符合要求:

不符合要求:

觀察:當一黑一白的1x2方格數恰為2時,即符合條件

我們可以先處理每個1x2方格會維持一黑一白的時間區間

假設該方格上的編號為\(L,R   (L < R)\)

則其為黑白狀態的時間為\([L,R)\)

 

我們可以想像這是一個區間加值

詢問的時候就是看有多少個時間點\(=2\)

另外,只要我們塗上第一個黑色之後

一黑一白的方格一定會\(\geq2\)

所以我們詢問的會是最小值的個數

 

修改的的話當作刪除再新增處理即可

二維情況

滿足黑色區域為一整塊長方形的條件為

  • 2x2且為三白一黑的方形恰有4個
  • 不存在三黑一白的格子

符合

不符合

複雜度

建樹:\(O(HW)\)

修改與詢問:\(O(QlogHW)\)

總共:\(O(HW+QlogHW)\)

SEGment tree beats

吉如一線段樹

區間開根號問題

有一個序列\(A\),長度為\(N\)

支援\(Q\)次下列兩種操作

 

1.詢問\([l,r]\)的區間和

2.把\([l,r]\)所有元素開根號

 

\(N,Q \leq 3\cdot10^5, a_i \leq 10^{12}\)

難點

我們沒有辦法如往常那樣

隨便打個懶標說

我要把這個區間開根號

觀察

題目沒有把值變大的操作

開根號對某個數字\(C\)來說

最多只要\(O(loglogC)\)次

它就會變成\(1\)了

那就暴力吧

如果一個要被開根號的區間都已經是\(1\)

那我們不用理他

 

否則就暴力再遞迴下去

這樣最多也只會多暴力\(O(loglogC)\)次

 

總複雜度:\(O(QlogN+NlogNloglogC)\)

區間取min問題

維護長度為\(N\)的序列\(A\),

支援\(Q\)次下列3種操作

 

 

 

 

 

 

\(N,Q \leq 10^6, a_i \leq 10^{18}\)

  1. 詢問\([l,r]\)的區間和
  2. 詢問\([l,r]\)中最大值
  3. 給定\(x\),對\([l,r]\)裡的\(a_i\),\(a_i = min(a_i,x)\)

\(a_i = min(a_i,x)\)做了什麼?

把區間內大於\(x\)的數字都變成\(x\)

其他的都不改變

如果區間內比\(x\)大的那些數字只有一種

那隨便就很好做

 

節點資訊

1.最大值  \(mx\)

2.最大值有幾個  \(cnt\)

3.嚴格次大值  \(snd\)

4.區間和  \(sum\)

5.懶人標記  \(tg\)

修改時  \(x \geq mx\)

很顯然我們什麼都不用做

修改時  \(snd < x < mx\)

修改\(mx = x\)

另外再打個懶標就好了

修改時  \(x \leq snd\)

我們好像在當前節點什麼也做不了?

那直接遞迴下去試試

看起來完全就是暴力?

但它其實是好的。

證明

詢問複雜度和一般線段樹都一樣

\(x > snd\)時也時間也不會爆炸

問題在暴力遞迴的部份

證明

假設\(D(p)\)代表節點\(p\)內數字的種類

如果我們需要暴力做

表示有兩種以上數字\(\geq x\)

然後修改完後他們都會變\(x\)

證明

所以所有節點的\(D(p)\)的和

其實就是當\(ql \leq l \)且\( r \leq qr\)時

還需要暴力操作的上限

如果修改區間完整包含\(p\)

操作後\(D(p)\)至少會\(-1\)

證明

如果不完全包含:

對於這樣的一個區間\(v\)

\(D(v)\)最多也只會\(+1\)

同時這種節點的數量是\(O(logN)\)量級的

增加的複雜度上限:\(O((N+Q)logN)\)

問題.Extreme

同樣的區間問題

但額外加了區間加值的操作

\(N,Q \leq 3 \cdot 10^5\)

d函數感覺會爆炸

一次區間加值

就可能造成數字種類增加\(O(n)\)量級

好像酶辦法做了

其實還是可以!

改一下定義

我們把\(D(p)\)的定義

重新改成以\(p\)為根的子樹中

節點上\(mx\)與\(mx_p\)不同的節點數

區間取min

對一個遞迴終止的節點\(u\)來說

\(u\)上某節點的\(mx\)可能從與\(mx_p\)相同變相異

這樣對所有節點的\(D\)會增加\(O(logN)\)

 

整筆操作下來\(\Sigma D\)增加的量級是\(O(log^2N)\)

首先遞迴終止的節點會有\(O(logN)\)個

暴力往下

這件事代表我往下把某些

\(mx\)與\(mx_p\)不同的節點改成\(mx_p\)

這其實是消耗\(D(p)\)的

所以單就區間取\(min\)而言

對\(\Sigma D\)的貢獻是\(O(Qlog^2N)\)

區間加值

遞迴終止的節點一樣會有\(O(logN)\)個

每個終止節點的子樹中,\(mx\)都會一起增加

最差的情況就是這\(O(logN)\)個節點都對\(\Sigma D\)有貢獻

 

另外走下來有一些沒完全覆蓋的節點

增加的\(D\)也是\(O(logN)\)

複雜度

根據剛剛的分析

總複雜度會是\(O(NlogN+Qlog^2N)\)

 

但聽說跑起來非常快

可能跟一個\(log\)差不多

小結論

其實吉如一線段樹比起特定的資料結構

更算是一種新的概念

我們只是修改了線段樹上遞迴終止的條件

只要確保暴力造成的複雜度可以接受

那就是可以用的解法

感謝聆聽~

進階資結

By Lai Hong An

進階資結

  • 280