離線算法

\[225\]賴泓安

自我介紹一下

建中\(225\)

我叫賴泓安

\(Handle:ancuber1031\)或類似的

\(Reference\)

謝一學長的簡報

OI Wiki

等等這個顏色的是題目連結

Abstract

先看看今天要幹麻

  • 莫隊算法
  • 整體二分搜
  • CDQ分治
  • Tarjan求LCA

我覺得code的template好醜

算法概念簡介

什麼是離線?

離線的相反是什麼?

當然就是在線啊

阿在線又是啥?

簡單來講就是秒讀秒回

問題一丟出來

你就馬上回答

比較:

在線:

每當輸入一個詢問,就馬上輸出結果

 

離線:

一次吃完所有詢問,

經過一些操作後求解,再全部輸出

普通莫隊

應該是分塊的東西

莫隊概念

考慮一個區間詢問的問題,假設\([l,r]\)的答案是\(ans(l,r)\)

如果對於\(ans(l-1,r),ans(l,r+1),ans(l+1,r),ans(l,r-1)\)

可以有效率的從\(ans(l,r)\)轉移

利用分塊的概念排序詢問

然後暴力轉移每筆詢問

莫隊作法

選定每塊大小為\(k\)

詢問左界依照所在的塊排序

同一塊內的詢問以右界大小排序

掃過排序好的詢問

每筆詢問都從上一塊暴力轉移

(一步步移動左右界)

複雜度

轉移複雜度\(O(P)\),塊數為\(S\),共有\(\frac{N}{k}\)塊

對於左界

在同一塊中的尋問每個最多移動\(k\)次,換塊總次數是\(N\)次

左界複雜度\(O(kQ+N)\)

右界則是塊數x\(N\),複雜度\(O(N \frac{N}{k})\)

總複雜度\(O((kQ+N+\frac{N^2}{k})P)\)

取最小:\(O(PN \sqrt{Q})\),此時\(k = \frac{N}{\sqrt{Q}}\)

Code

#pragma GCC optimize("Ofast,unroll-loops,no-stack-protector,fast-math")
#pragma GCC optimize("O3")
#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define ull unsigned long long
#define endl '\n'
#define pii pair<int,int>
#define pll pair<ll,ll>
#define p_q priority_queue

int n, Q, k;
int l = 1, r = 0;
int cur = 0;
vector<int> a;
vector<int> cnt(1e5+5,0), am(1e5+5,0);

struct query{
    int ql, qr, id;
    bool operator<(query b) {
        if (ql/k == b.ql/k) return qr < b.qr;
        return ql/k < b.ql/k;
    }
};

void sub(int v) {
    am[cnt[v]]--;
    cnt[v]--;
    am[cnt[v]]++;
    if (cur==cnt[v]+1 && am[cnt[v]+1]==0) cur = cnt[v];
}

void add(int v) {
    am[cnt[v]]--;
    cnt[v]++;
    am[cnt[v]]++;
    cur = max(cur,cnt[v]);
}

int main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin>>n>>Q;
    k = n/sqrt(Q);
    a.resize(n+1);
    vector<query> q(n);
    for (int i = 1; i <= n; ++i) cin>>a[i];
    for (int i = 0; i < Q; ++i) {
        cin>>q[i].ql>>q[i].qr;
        q[i].id = i;
    }
    sort(q.begin(),q.end());
    vector<int> ans(Q);
    am[0] = n;
    for (auto i : q) {
        while (r < i.qr) add(a[++r]);
        while (r > i.qr) sub(a[r--]);
        while (l < i.ql) sub(a[l++]);
        while (l > i.ql) add(a[--l]);
        ans[i.id] = cur;
    }
    for (int i = 0; i < Q; ++i) {
        cout<<ans[i]<<endl;
    }
    return 0;
}

\(Hint:\)壓常是個好東西:)

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

const int maxn = 2e5;

int n, Q, k, a, cnt = 0;
int arr[maxn+5], ans[maxn+5], num[maxn+5] = {0};
map<int,int> mp;

struct Query{
    int l, r, id;
    bool operator<(Query b) {
        if (l/k == b.l/k) return r < b.r;
        return l/k < b.l/k;
    }
} q[maxn+5];

inline void add(int p) {
    if (num[arr[p]]++ == 0) cnt++;
}

inline void sub(int p) {
    if (--num[arr[p]] == 0) cnt--;
}

signed main() {
    scanf("%d%d",&n,&Q);
    k = n/sqrt(Q);
    for (int i = 1; i <= n; ++i) {
        scanf("%d",&a);
        if (mp.find(a) != mp.end()) arr[i] = mp[a];
        else arr[i] = mp[a] = i;
    }
    for (int i = 1; i <= Q; ++i) {
        scanf("%d%d",&q[i].l,&q[i].r);
        q[i].id = i;
    }
    sort(q+1,q+Q+1);
    int l = 1, r = 0, ql, qr;
    for (int i = 1; i <= Q; ++i) {
        ql = q[i].l, qr = q[i].r;
        while(l < ql) sub(l++);
        while(r > qr) sub(r--);
        while(l > ql) add(--l);
        while(r < qr) add(++r);
        ans[q[i].id] = cnt;
    }
    for (int i = 1; i <= Q; ++i) printf("%d\n",ans[i]);
    return 0;
}

帶修改莫隊

普通莫隊加上時間維度!

說明

我們原本假設的是\([l,r]\)的答案是\(ans(l,r)\)

現在我們加上一個\(time\)的維度,代表經過了幾次修改

 

\(ans(l,r,time)\)代表區間\([l,r]\)中,經歷過\(time\)次修改的答案

 

轉移概念跟普通莫隊一樣

怎麼排序呢?

直接看結論

選定每塊大小(結論:\(n^{\frac{2}{3}}\))

第一順位依照左界所在的塊排序

第二順位以右界所在的塊排序

第三順位以\(time\)排序

複雜度

左右端點的塊不變,\(time\)會向右單調,\(O(n)\)

左右端點改變,\(time\)最大移動\(O(n)\)

左右端點可能的塊各是\(n^{\frac{1}{3}}\)

總複雜度:\(n^{\frac{1}{3}} \cdot n^{\frac{1}{3}} \cdot n\)

\(O(n^{\frac{5}{3}})\)

所以很爛:)

跟\(n^2\)沒差多少阿其實

回滾莫隊

差不多是被砍斷一隻手的意思

說明

剛剛的普通莫隊我們可以進行加入和刪除的動作

但假設今天我們只能做其中一個操作時怎麼辦?

RMQ

拿RMQ問題舉例

我們如果要求多筆詢問區間的最小值

可以很簡單的\(O(1)\)拓展區間

但刪除卻沒有一個有效率的方法

當然我知道RMQ都砸線段樹

回滾莫隊

這時候回滾莫隊可以保證\(O(N \sqrt{Q})\)內求解

且只需要用到加入操作

實作

實作時我們分成三種\(Case\)處理

  1. \(l,r\)在同一個分塊內
  2. \(r\)和上一筆詢問位於同一塊中
  3. \(r\)和上一筆詢問位於不同塊中

\(l,r\)位於同一塊內

反正塊不會太大(\(\frac{N}{\sqrt{Q}}\))

如果所有詢問都長這樣頂多也就\(O(N\sqrt{Q})\)

遇到這種直接暴力掃過區間即可

\(r\)和上一詢問同塊

這時候\(l\)一定遞增,右界作法相同

我們只要把左界拉回下一塊的第一個位置

(對每一塊都維護一個左端點到右界的答案)

然後拓展回到\(r\)即可

\(r\)和上一詢問不同塊

把左界設為下一塊的第一個

右界設為這一塊的最後一個

然後照普通莫隊拓展

Code

#pragma GCC optimize("Ofast")
#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
 
int n, k, Q;
int mn = 1e9+5, cmin = 1e9+5;
int arr[200005];
int ans[200005];
 
struct Query{
    int ql, qr, id;
    bool operator<(Query b) {
        if (ql/k == b.ql/k) return qr < b.qr;
        return ql/k < b.ql/k;
    }
} q[200005];
 
signed main() {
    scanf("%d%d",&n,&Q);
    k = n/sqrt(Q);
    for (int i = 1; i <= n; ++i) cin>>arr[i];
    for (int i = 1; i <= Q; ++i) {
        scanf("%d%d",&q[i].ql,&q[i].qr);
        q[i].id = i;
    }
    sort(q+1,q+Q+1);
    int l = 1, r = 0, pre = -1;
    for (int i = 1; i <= Q; ++i) {
        int cl = q[i].ql, cr = q[i].qr;
        if (cl/k == cr/k) {
            mn = 1e9+5;
            for (int j = cl; j <= cr; ++j) mn = min(mn,arr[j]);
        } else if (cl/k == pre) {
            l = (cl/k)*k+k;
            while(r < cr) cmin = min(arr[++r],cmin);
            mn = cmin;
            while(l > cl) mn = min(mn,arr[--l]);
        } else {
            l = (cl/k)*k+k, r = (cl/k)*k+k-1;
            pre = (cl/k);
            cmin = mn = 1e9+5;
            while(r < cr) cmin = min(arr[++r],cmin);
            mn = cmin;
            while(l > cl) mn = min(mn,arr[--l]);
        }
        ans[q[i].id] = mn;
    }
    for (int i = 1; i <= Q; ++i) printf("%d\n",ans[i]);
    return 0;
}

整體二分

如果每筆詢問可以二分搜

但詢問太多,每次都二分搜一次會TLE

可以吃完詢問在一起二分!

一般二分

在給一長度為\(n\)之序列\(arr\),求其中第\(k\)小的數

數字值域為\(1\) ~ \(C\)

除了直接排序之外還有什麼辦法?

對值域區間二分搜,

算有幾個數字小於\(mid\)(設為\(m\))

則對於每次猜測\(mid\)而言

 

如果\(m_{mid} < k,l = mid\)

如果\(m_{mid} > k,r = mid\)

複雜度:\(O(n log C)\)

現在題目一樣,但我要多次詢問第\(k\)小

 

如果我每筆詢問都二分搜一次

複雜度:\(O(Qn log  C)\)

有點慢

不如我一次二分搜全部

具體怎麼做?

對於當前處理的詢問

都猜它們的答案是\(mid\),然後一樣算\(m_{mid}\)

如果詢問的答案應該等於或更小就丟左邊,否則丟右邊

到\(l == r\)的時候就是當前詢問的答案

對原陣列做一樣的左右丟的操作

最多二分\(log  C\)層,\(O(Q)\)把詢問分兩邊

總複雜度:\(O((Q+N)log  C)\)

Code

struct Query {int id, k;};

int ans[N];//the answer of the i-th query
int check(int x);

void solve(int l, int r, vector<Query> q) {
    if (l == r) {
        for (auto i : q) ans[i.id] = l;
        return;
    }
    int mid = (l+r)>>1;
    int c = check(mid);
    vector<Query> ql, qr;//part
    for (auto i : q) {
        if (i.k <= c) ql.push_back(i);
        else qr.push_back({i.id,i.k-c});//important
    }
    solve(l,mid,ql), solve(mid+1,r,qr);
    return;
}

多次求區間第k小

換個方式維護陣列

但為了能高效率維護前綴,這邊我們需要使用BIT

例如:\([2,7,1,8,2,8,5,7,4]\)

 

\(Query\):\([l,r,k]\)

1.\([1,4,1]\)

2.\([2,3,2]\)

3.\([1,5,5]\)

 

\(C = 10\), 當前\(mid = 5\)

\([2,7,1,8,2,8,5,7,4]\)

\(C = 10\), 當前\(mid = 5\)

 

\(array\):\([1,0,1,0,1,0,1,0,1]\)

\(Pre:[0,1,1,2,2,3,3,4,4,5]\)

1.\([1,4,1]\):\(2-0 >  1\),\(ans<mid\),丟左邊

2.\([2,3,2]\):\(2-1  <  2\),\(ans>mid\),丟右邊

3.\([1,5,5]\):\(5-0 = 5\),\(ans=mid\),丟左邊

多次求區間第k小,帶修改

當前詢問依照時間順序做

遇到修改就當作刪除再加入數字

然後根據當前\(mid\)改\(array\)的值

就會變成維護動態前綴和

複雜度

序列長\(N\),詢問數\(Q\),值域\(C\)

每層需要\(O((Q+N)logN)\)

全部需要\(O((Q+N)logNlogC)\)

例題:

Code

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

#define endl '\n'
#define pb push_back
#define int long long

struct BIT{
    vector<int> a;
    int sz = 0;
    void init(int _n) { 
        sz = _n;
        a.assign(_n+5,0); 
    }
    void add(int p, int v) {
        while(p <= sz) {
            a[p] += v;
            p += p&-p;
        }
    }
    int sum(int p) {
        int ret = 0;
        while(p) {
            ret += a[p];
            p -= p&-p;
        }
        return ret;
    }
} bit;

struct Query{ int cmd, l, r, k, id; };

vector<int> ans(10005,1LL<<31);
vector<int> arr(50005);

void slv(int l, int r, vector<Query> &q) {
    if (l == r || q.empty()) {
        for (auto[cmd,ql,qr,k,id] : q) if(cmd) ans[id] = l;
        return;
    }
    int mid = (l+r)>>1;
    vector<Query> q1, q2;
    for (auto[cmd,ql,qr,k,id] : q) {
        if (cmd) {
            if (k <= bit.sum(qr)-bit.sum(ql-1)) q1.pb({cmd,ql,qr,k,id});
            else q2.pb({cmd,ql,qr,k-bit.sum(qr)+bit.sum(ql-1),id});
        } else {
            if (qr <= mid) {
                bit.add(ql,k);
                q1.pb({cmd,ql,qr,k,id});
            } else {
                q2.pb({cmd,ql,qr,k,id});
            }
        }
    }
    for (auto[cmd,ql,qr,k,id] : q1) if (!cmd) bit.add(ql,-k);
    slv(l,mid,q1), slv(mid+1,r,q2);
}

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int t, n, qq; 
    cin>>t;
    vector<Query> q;
    while(t--) {
        cin>>n>>qq;
        q.clear();
        bit.init(n);
        
        for (int i = 1; i <= n; ++i) {
            cin>>arr[i];
            q.pb({0,i,arr[i],1,0});
        }
        int cmd, l, r, k;
        for (int i = 1; i <= qq; ++i) {
            cin>>cmd>>l>>r;
            if (cmd == 1) {
                cin>>k;
                q.pb({1,l,r,k,i});
            } else if (cmd == 2) {
                q.pb({0,l,arr[l],-1,0});
                q.pb({0,l,r,1,0});
                arr[l] = r;
            } else {
                ans[i] = 7122;
            }
        }
        slv(-(1LL<<31),(1LL<<31),q);
        for (auto& i : ans) {
            if (i < (1LL<<31)) {
                cout<<i<<endl;
                i = (1LL<<31);
            }
        }
    }
    return 0;
}

CDQ分治

一種分治

概念性算法

可以用於解決和點對有關的問題

例如偏序關係

一維偏序

就很顯然,排序完直接算

二維偏序

除了之前教過的類\(merge  sort\)的分治算法

(其實這也可以算是一種CDQ分治)

 

我們還可以對\(x\)排序

然後依序加入點的時候BIT維護\(y\)的前綴和

 

都是\(O(NlogN)\)

由大到小排序\(x\), 相同時由小到大排序\(y\)

對\(x\)分治

合併區間時改成對\(y\)由大到小排序

用BIT維護\(z\)的前綴和

 

當然沒人說不行套兩層CDQ分治

實做思路

針對\(x\),假設我們用\(solve(l,r)\)求解\([l,r]\)

且已經用遞迴解決完\([l,mid],[mid+1,r]\)

這時統計對每個在遞迴樹右邊的點

有多少在遞迴樹左邊的點滿足偏序關係

實做思路

對兩邊的點

以\(y\)(由大到小)合併排序

對\(z\)維護前綴和

\(O(Nlog^2N)\)

Code

#pragma GCC optimize("Ofast")
#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

const int maxn = 1e5+5;

int n;
vector<int> ans(maxn,0);

struct Point { 
    int x, y, z, id; 
    bool operator<(Point b) {
        if (x == b.x) return y < b.y;
        return x > b.x;
    }
};

struct BIT {
    vector<int> arr;
    int sz;
    void init(int _n) {
        sz = _n;
        arr.assign(sz+5,0);
    }
    void upd(int p, int v) {
        while(p <= sz) {
            arr[p] += v;
            p += p&-p;
        }
    }
    int sum(int p) {
        int ret = 0;
        while(p > 0) {
            ret += arr[p];
            p -= p&-p;
        }
        return ret;
    }
} bit;

void solve(int l, int r, vector<Point> &a) {
    if (l == r) return;
    int mid = (l+r)>>1;
    solve(l,mid,a), solve(mid+1,r,a);
    int lp = l, rp = mid+1;
    vector<Point> tmp;
    while(lp <= mid && rp <= r) {
        if (a[lp].y > a[rp].y) {
            bit.upd(a[lp].z,1);
            tmp.pb(a[lp++]);
        } else {
            ans[a[rp].id] += (bit.sum(maxn)-bit.sum(a[rp].z));
            tmp.pb(a[rp++]);
        }
    }
    while(lp <= mid) {
        bit.upd(a[lp].z,1);
        tmp.pb(a[lp++]);
    }
    while(rp <= r) {
        ans[a[rp].id] += (bit.sum(maxn)-bit.sum(a[rp].z));
        tmp.pb(a[rp++]);
    }
    for (int i = l; i <= mid; ++i) bit.upd(a[i].z,-1);
    for (int i = l; i <= r; ++i) a[i] = tmp[i-l];
}

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin>>n;
    vector<Point> a(n);
    bit.init(maxn);
    for (int i = 0; i < n; ++i) {
        cin>>a[i].x>>a[i].y>>a[i].z;
        a[i].id = i;
    }
    sort(a.begin(),a.end());
    solve(0,n-1,a);
    for (int i = 0; i < n; ++i) cout<<ans[i]<<endl;
    return 0;
}

好我們回到離線

剛剛那個就只是分治

那它跟離線有什麼關係呢?

 

先看個例題

如果我們把刪除改成加入呢?

問題相當於

每次加入一個數字

問現在有多少個逆序數對

偏序關係

設\(p\)是位置,\(v\)是值

 

先不管加入的問題,如果\((i,j)\)是逆序對

那一定符合下面的情況

\( p_i < p_j  \&\&  v_i > v_j \)

\(or\)

\( p_i > p_j  \&\&  v_i < v_j \)

考慮加入時間

設\(t\)是加入時間

逆序對\((i,j)\)同時要滿足\(t_i \leq t_j\)

 

\( p_i < p_j  \&\&  v_i > v_j \)

\( p_i > p_j  \&\&  v_i < v_j \)

顯然的三維偏序

 

1. \(t_i \leq t_j\)

2. \( p_i > p_j\)

3. \(v_i < v_j \)

1. \(t_i \leq t_j\)

2. \( p_i < p_j\)

3. \(v_i > v_j \)

Code

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

#define ll long long
#define endl '\n'
#define pb push_back
#define int long long

const int maxn = 1e5+1;
int n, m, x;
vector<int> ans(100005,0);

struct node{
    int p, v, t = 0;
    bool operator<(node b) {
        if (t == b.t) return p < b.p;
        return t < b.t;
    }
};

struct BIT {
    vector<int> arr;
    int sz;
    void init(int _n) {
        sz = _n;
        arr.assign(sz+5,0);
    }
    void upd(int p, int v) {
        while(p <= sz) {
            arr[p] += v;
            p += p&-p;
        }
    }
    int sum(int p) {
        int ret = 0;
        while(p > 0) {
            ret += arr[p];
            p -= p&-p;
        }
        return ret;
    }
} bit;

//ti >= tj, pi > pj, vi < vj
void solve1(int l, int r, vector<node> &a) {
    if (l == r) return;
    int mid = (l+r)>>1;
    solve1(l,mid,a), solve1(mid+1,r,a);
    int lp = l, rp = mid+1;
    vector<node> tmp;
    while(lp <= mid && rp <= r) {
        if (a[lp].p < a[rp].p) {
            bit.upd(a[lp].v,1);
            tmp.pb(a[lp++]);
        } else {
            ans[a[rp].t] += bit.sum(maxn)-bit.sum(a[rp].v);
            tmp.pb(a[rp++]);
        }
    }
    while(lp <= mid) {
        bit.upd(a[lp].v,1);
        tmp.pb(a[lp++]);
    }
    while(rp <= r) {
        ans[a[rp].t] += bit.sum(maxn)-bit.sum(a[rp].v);
        tmp.pb(a[rp++]);
    }
    for (int i = l; i <= mid; ++i) bit.upd(a[i].v,-1);
    for (int i = l; i <= r; ++i) a[i] = tmp[i-l];
}

//ti >= tj, pi < pj, vi > vj
void solve2(int l, int r, vector<node> &a) {
    if (l == r) return;
    int mid = (l+r)>>1;
    solve2(l,mid,a), solve2(mid+1,r,a);
    int lp = l, rp = mid+1;
    vector<node> tmp;
    while(lp <= mid && rp <= r) {
        if (a[lp].p > a[rp].p) {
            bit.upd(a[lp].v,1);
            tmp.pb(a[lp++]);
        } else {
            ans[a[rp].t] += bit.sum(a[rp].v-1);
            tmp.pb(a[rp++]);
        }
    }
    while(lp <= mid) {
        bit.upd(a[lp].v,1);
        tmp.pb(a[lp++]);
    }
    while(rp <= r) {
        ans[a[rp].t] += bit.sum(a[rp].v-1);
        tmp.pb(a[rp++]);
    }
    for (int i = l; i <= mid; ++i) bit.upd(a[i].v,-1);
    for (int i = l; i <= r; ++i) a[i] = tmp[i-l];
}

signed main() {
    scanf("%lld%lld",&n,&m);
    vector<node> a(n);
    vector<int> pos(n+1);
    bit.init(maxn);
    for (int i = 0; i < n; ++i) {
        scanf("%lld",&a[i].v);
        a[i].p = pos[a[i].v] = i;
    }
    for (int i = m; i > 0; --i) {
        scanf("%lld",&x);
        a[pos[x]].t = i;
    }
    sort(a.begin(),a.end());
    solve1(0,n-1,a);
    sort(a.begin(),a.end());
    solve2(0,n-1,a);
    for (int i = 1; i <= m; ++i) ans[i] += ans[i-1];
    for (int i = m; i > 0; --i) printf("%lld\n",ans[i]);
    return 0;
}

記得開long long:(

Tarjan's LCA

對,又是他

以\(DFS\)為主要算法結構

在\(DFS\)的過程中

紀錄訪問順序

輔以\(DSU\)去處理詢問

定義一些我們需要的東西

\(bnd[i]\):存所有跟節點\(i\)有關的詢問

\(prt\)陣列:每個節點的\(boss\),預設是自己

性質

\(DFS\)時,\(u,v\)的\(LCA\)一定比\(u,v\)更晚被\(pop\)掉

 換個角度,對於\(u,v\)兩點

在\(DFS\)時,一定是先從\(u\)離開到\(LCA\)

然後從\(LCA\)走到\(v\)

對\(v\)而言,\(LCA\)會是目前時間最近的非初次造訪點

所以我們可以做些什麼呢?

當\(DFS\)完子樹,離開一個節點\(v\)時

我們就把\(prt[v]\)設成\(v\)的父節點

這樣有什麼效果?

對於一組詢問\((u.v)\)

假設\(u\)已經被訪問過

這時候我如果訪問到\(v\)

那\(u\)的當前祖先正好會是\(LCA\)

畫圖解釋

實做

void Tarjan(int u,int f){//f:父節點

    for all queries about u://處理和u有關的詢問
          //如果相對的詢問點v有訪問過:
                LCA(u,v) = find_boss(v)
                //這邊套用DSU
   
    for all children of u:
          //DFS
    
    prt[u] = f;
}

Code

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

#define pii pair<int,int>
#define endl '\n'
#define pb push_back

map<pii,int> mp;
vector<int> dep, prt;

int fb(int x) {
    if (x == prt[x]) return x;
    return prt[x] = fb(prt[x]);
}

void Tarjan(int u,int f,int d,vector<vector<int> > &g,vector<set<int> > &bnd){
    dep[u] = d;
    for (auto v : bnd[u]) {
        if (dep[v]) {
            mp[{u,v}] = mp[{v,u}] = fb(v);
        }
    }
    for (auto v : g[u]) {
        if (v != f) {
            Tarjan(v,u,d+1,g,bnd);
        }
    }
    prt[u] = f;
}

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, Q; cin>>n>>Q;
    vector<vector<int> > g(n+1);
    for (int i = 1; i < n; ++i) {
        int a, b; cin>>a>>b;
        g[a].pb(b);
        g[b].pb(a);
    }
    vector<set<int> > bnd(n+1);
    dep.assign(n+1,0), prt.assign(n+1,0);
    for (int i = 1; i <= n; ++i) prt[i] = i;
    vector< pii > q(Q);
    for (int i = 0; i < Q; ++i) {
        int a, b; cin>>a>>b;
        q[i] = {a,b};
        bnd[a].insert(b);
        bnd[b].insert(a);
    }
    Tarjan(1,1,1,g,bnd);
    for (auto [a,b] : q) {
        int lca = mp[{a,b}];
        cout<<dep[a]+dep[b]-2*dep[lca]<<endl;
    }
    return 0;
}

其他有趣的離線題

植物大戰殭屍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

const int maxn = 300005; 

vector<int> arr(10005,0);
vector<int> ans(1000005,0);

struct QUERY{
    int l, r, v, id;
    bool operator<(QUERY b) {
        return r < b.r;
    }
};

struct TRIE{
    vector<vector<int> > t;
    vector<int> nr;
    int cnt = 0;
    void init() {
        t.assign(maxn,vector<int>(2,0));
        nr.assign(maxn,0);
    }
    void upd(int p, int x, int d, int l) {
        nr[p] = l;
        int c = (x>>d)&1;
        if (!t[p][c]) t[p][c] = ++cnt;
        if (d >= 0) upd(t[p][c],x,d-1,l);
    }
    int query(int p, int x, int d, int l) {
        int c = ((x>>d)&1)^1;
        if (!t[p][c] || nr[t[p][c]] < l) c ^= 1;
        if (d == 0) return x^(c<<d);
        return (c<<d)^query(t[p][c],x,d-1,l);
    }
} trie;

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, Q; cin>>n>>Q;
    vector<QUERY> q(Q+1);
    for (int i = 1; i <= n; ++i) {
        cin>>arr[i];
        arr[i] ^= arr[i-1];
    }
    int l, r, v;
    for (int i = 1; i <= Q; ++i) {
        cin>>l>>r>>v;
        q[i] = {l,r,v,i};
    }
    sort(q.begin()+1,q.end());
    int p = 1;
    trie.init();
    for (int i = 0; i <= n; ++i) {
        while(p <= Q && q[p].r == i) {
            ans[q[p].id] = trie.query(0,q[p].v^arr[i],30,q[p].l);
            p++;
        }
        trie.upd(0,arr[i],30,i);
    }
    for (int i = 1; i <= Q; ++i) cout<<ans[i]<<endl;
    return 0;
}

Increasing Array Queries

#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 n, Q;

struct Query{
    int r, id;
};

struct SEG{
    vector<int> a, tg;
    void init(int _n) {
        a.assign(_n<<2,0);
        tg.assign(_n<<2,0);
    }
    void push(int p, int c) {
        if (tg[p]) {
            a[p] = tg[p]*(c+1);
            if (c) tg[p<<1] = tg[p];
            if (c) tg[p<<1|1] = tg[p];
            tg[p] = 0;
        }
    }
    void upd(int l, int r, int p, int ql, int qr, int v) {
        push(p,r-l);
        if (ql <= l && r <= qr) {
            tg[p] = v;
            return;
        }
        if (qr <= mid) upd(l,mid,p<<1,ql,qr,v);
        else if (ql > mid) upd(mid+1,r,p<<1|1,ql,qr,v);
        else {
            upd(l,mid,p<<1,ql,qr,v);
            upd(mid+1,r,p<<1|1,ql,qr,v);
        }
        a[p] = (tg[p<<1]?(tg[p<<1]*(mid-l+1)):a[p<<1]) + (tg[p<<1|1]?(tg[p<<1|1]*(r-mid)):a[p<<1|1]);
    }
    int sum(int l, int r, int p, int ql, int qr) {
        push(p,r-l);
        if (ql <= l && r <= qr) return a[p];
        if (qr <= mid) return sum(l,mid,p<<1,ql,qr);
        else if (ql > mid) return sum(mid+1,r,p<<1|1,ql,qr);
        else  return sum(l,mid,p<<1,ql,qr)+sum(mid+1,r,p<<1|1,ql,qr);
    }
} seg;

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    cin>>n>>Q;
    seg.init(n);
    vector<int> v(n+1,0);
    for (int i = 1; i <= n; ++i) {
        cin>>v[i];
        v[i] += v[i-1];
    }
    vector<vector<Query> > q(n+1);
    vector<int> ans(Q+1);
    for (int i = 1; i <= Q; ++i) {
        int l, r; cin>>l>>r;
        q[l].pb({r,i});
    }
    for (int i = n; i >= 1; --i) {
        int l = i, r = n+1;
        while(r-l > 1) {
            if (seg.sum(1,n,1,mid,mid) > v[i]-v[i-1]) r = mid;
            else l = mid;
        }
        seg.upd(1,n,1,i,l,v[i]-v[i-1]);
        for (auto [R,id] : q[i]) {
            ans[id] = seg.sum(1,n,1,i,R)-(v[R]-v[i-1]);
        }
    }
    for (int i = 1; i <= Q; ++i) {
        cout<<ans[i]<<endl;
    }
    return 0;
}

另一題有空補XD

離線

By Lai Hong An

離線

  • 270