線段樹知識亂講

Nekopedia發表於2024-11-08

前言

演算法競賽題目考察的是選手對於資料結構的選取與演算法的巧妙結合,而資料結構中線段樹扮演一個至關重要的角色,而近期(CSP 結束)在 hfu 的安排下我們需要自己弄一週的 ds,所以就有了這篇奇妙的部落格。

線段樹基礎知識

在我看來,線段樹其實就是在陣列的基礎上新增了一些額外的點,這些點用於維護原陣列的資訊,在此基礎上又新增一些點維護之前新增的點的資訊,就這麼一層一層往上直到最後只有一個點,而這個點也就管理了這整個陣列。而這些點之間的管理關係也就形成了一棵樹,故為線段樹。

然後考慮新增的這些點,如果這些點很隨意,最劣情況可能會被卡成一些神秘的東西,於是我們就讓第一層維護兩個點,第二層維護四個點,這樣我們只會建出 \(\log\) 層,並且增加的節點數是 \(O(n)\) 的。這樣線段樹就有了很優美的性質,也稱得上真正的線段樹了。

對於用線段樹維護資訊,我們不必在每次修改操作將相關的節點全部修改,但是我們需要知道我們應該去修改哪些地方。比如我在樹上走,走到了一個區間 \([l,r]\),如果這個區間被修改的區間包含,這就意味著我需要將這整個區間修改,可是直接修改是 \(O(len)\) 的,這時我們就可以先將這個地方的修改記下來,等到我查詢這個區間的時候在處理它。這樣線上段樹上找出的區間是 \(O(\log)\) 的,所以正常的修改就是找區間加上 \(O(1)\) 打標記。

而查詢和修改同理,都是在樹上走每次碰到一個區間能夠被全部算進貢獻就直接加入貢獻,否則就繼續往下走,但是在修改和查詢時都需要注意下放之前的標記,也就是我下一次走到有標記的節點時我會對其進行一些操作,這時原來的標記與現在的操作的兩個區間大機率是不同的,所以在操作前我們需要下放之前的標記。

然後放兩道板子。

P3372 【模板】線段樹 1

我們可以將線段樹給建出,過程就是一個遍歷樹的過程,只是到樹葉(也就是原陣列)時需要儲存資訊再一層一層傳回去。修改與查詢在此不再贅述。

void upd(int x){
	s[x] = s[ls] + s[rs];
}
void bld(int x, int l, int r){
	if(l == r)return (void)(s[x] = a[l]);
	int mid = l + r >> 1;
	bld(ls, l, mid), bld(rs, mid + 1, r);
	upd(x);
}
void pd(int x, int l, int r){
	if(! tg[x])return; int mid = l + r >> 1;
	tg[ls] += tg[x], tg[rs] += tg[x];
	s[ls] += tg[x] * (mid - l + 1);
	s[rs] += tg[x] * (r - mid);
	return (void)(tg[x] = 0);
}
void mdf(int x, int l, int r, int ql, int qr, ll y){
	if(ql <= l and r <= qr)return (void)(s[x] += y * (r - l + 1), tg[x] += y);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf(rs, mid + 1, r, ql, qr, y);
	upd(x);
}
ll qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return s[x];
	int mid = l + r >> 1; ll res = 0; pd(x, l, r);
	if(ql <= mid)res = qry(ls, l, mid, ql, qr);
	if(mid < qr)res += qry(rs, mid + 1, r, ql, qr);
	return res;
}

P3373 【模板】線段樹 2

這時有了乘法操作修改與下放似乎變得複雜,這時我們需要先分析不同操作的優先順序。

首先我們肯定對於乘法與加法分別打標記,然後對於一個加法標記,它的意義是讓區間加上一個數,如果直接加那麼乘法標記就會受影響。將乘法標記增加一定是錯的,所以我們應該將它下放。注意到乘法的優先順序是高於加法,所以在下放時要讓乘法標記先下放。

如果是有一個乘法標記,這時之前區間內的加法標記也應該乘上它,因為乘法分配律。所以乘法就直接全改了就行。

void upd(int x){
	s[x] = (s[ls] + s[rs]) % p;
}
void bld(int x, int l, int r){
	tg2[x] = 1;
	if(l == r)return (void)(s[x] = a[l]);
	int mid = l + r >> 1;
	bld(ls, l, mid), bld(rs, mid + 1, r);
	upd(x);
}
void pd(int x, int l, int r){
	int mid = l + r >> 1;
	tg1[ls] = (tg1[ls] * tg2[x] + tg1[x]) % p, tg1[rs] = (tg1[rs] * tg2[x] + tg1[x]) % p;
	s[ls] = (s[ls] * tg2[x] + tg1[x] * (mid - l + 1)) % p;
	s[rs] = (s[rs] * tg2[x] + tg1[x] * (r - mid)) % p;
	tg2[ls] = tg2[ls] * tg2[x] % p;
	tg2[rs] = tg2[rs] * tg2[x] % p;
	return (void)(tg1[x] = 0, tg2[x] = 1);
}
void mdf1(int x, int l, int r, int ql, int qr, ll y){
	if(ql <= l and r <= qr)return (void)(s[x] = y * s[x] % p, tg2[x] = tg2[x] * y % p, tg1[x] = tg1[x] * y % p);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf1(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf1(rs, mid + 1, r, ql, qr, y);
	upd(x);
}
void mdf2(int x, int l, int r, int ql, int qr, ll y){
	if(ql <= l and r <= qr)return (void)(s[x] = (s[x] + y * (r - l + 1)) % p, tg1[x] = (tg1[x] + y) % p);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf2(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf2(rs, mid + 1, r, ql, qr, y);
	upd(x);
}
ll qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return s[x];
	int mid = l + r >> 1; ll res = 0; pd(x, l, r);
	if(ql <= mid)res = qry(ls, l, mid, ql, qr);
	if(mid < qr)res += qry(rs, mid + 1, r, ql, qr);
	return res % p;
}

P1253 扶蘇的問題

這個題注意修改操作優先順序大於加法操作,每次修改操作時需要清空標記。然後就是不要在葉子節點\(\text {pushdown}\)

void upd(int x){
    c[x] = max(c[ls], c[rs]);
}
void build(int x, int l, int r){
    tg1[x] = max0810;
    if(l == r)return(void)(c[x] = a[l]);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pdtg1(int x){
    if(tg1[x] == max0810)return;
    tg2[ls] = tg2[rs] = 0;
    c[ls] = c[rs] = tg1[ls] = tg1[rs] = tg1[x];
    tg1[x] = max0810;
}
void pd(int x){
    pdtg1(x);
    if(! tg2[x])return;
    c[ls] += tg2[x]; c[rs] += tg2[x];
    tg2[ls] += tg2[x]; tg2[rs] += tg2[x];
    tg2[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int op, ll y){
    if(L <= l and r <= R){
        if(op ^ 1){
            c[x] += y; tg2[x] += y;
        }
        else{
            c[x] = tg1[x] = y;
            tg2[x] = 0;
        }
        return;
    }
    int mid = l + r >> 1; pd(x);
    if(L <= mid)mdfy(ls, l, mid, L, R, op, y);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, op, y);
    upd(x);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return c[x];
    int mid = l + r >> 1; ll res = max0810; pd(x);
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid) res = max(res, qry(rs, mid + 1, r, L, R));
    return res;
}

P4513 小白逛公園 3倍經驗見這篇(是的還是我)

線段樹維護區間最大子段和的板子題,支援單點修改。

考慮上傳答案的過程中需要維護的資訊,首先有區間最大子段和廢話,然後我們合併子區間時有可能從左邊區間的字尾最大加上右邊區間的字首最大轉移答案,所以還要記這兩個東西。然後更新前字尾時似乎又需要記一個區間和,不然沒法轉(讀者可自行思考)。然後查詢時就同時記錄這幾個資訊。單點修改就直接二分改了就行。

struct node{
	ll s, lm, rm, sum;
}sgt[N << 2];

void upd(node &x, node y, node z){
	x.sum = y.sum + z.sum;
	x.s = max(y.s, max(z.s, y.rm + z.lm));
	x.lm = max(y.lm, y.sum + z.lm), x.rm = max(z.rm, z.sum + y.rm);
}
void bld(int x, int l, int r){
	if(l == r)return(void)(sgt[x] = {a[l], a[l], a[l], a[l]});
	int mid = l + r >> 1;
	bld(ls, l, mid), bld(rs, mid + 1, r);
	upd(sgt[x], sgt[ls], sgt[rs]);
}
void mdf(int x, int l, int r, int pos, int y){
	if(l == r)return(void)(sgt[x] = {y, y, y, y});
	int mid = l + r >> 1;
	if(pos <= mid)mdf(ls, l, mid, pos, y);
	else mdf(rs, mid + 1, r, pos, y);
	upd(sgt[x], sgt[ls], sgt[rs]);
}
node qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return sgt[x];
	int mid = l + r >> 1; node res, res1, res2; bool o1(false), o2(false);
	if(ql <= mid)res1 = qry(ls, l, mid, ql, qr), o1 = true;
	if(mid < qr)res2 = qry(rs, mid + 1, r, ql, qr), o2 = true;
	if(! o1 or ! o2)res = o1 ? res1 : res2;
	else upd(res, res1, res2);
	return res;
}

P11071 「QMSOI R1」 Distorted Fate

首先拆位。觀察式子,裡面有一個按位或,說明了什麼?說明如果 \(a_i\) 中出現了一那麼後面的就都有值了,所以我們的任務就是每次找到區間內最左邊的一然後答案就是從第一個一開始到右端點的長度。

但是看到空間限制你會發現這題有點抽象,所以你拆位後只能共用一棵線段樹了。然後就是線段樹維護的是區間是否有 0/1、以及每次異或後可能存在的懶標記這三者都可以用 bool 型別的陣列維護。

但是這只是離線的,因為不會線上所以自己去看吧

struct qarray{
    int op, l, r, x; ll ans;
}q[N];
bool b[N], s0[N << 2], s1[N << 2], tg[N << 2];

#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    s0[x] = s0[ls] | s0[rs];
    s1[x] = s1[ls] | s1[rs];
}

void build(int x, int l, int r){
    tg[x] = 0;
    if(l == r)return(void)(s0[x] = b[l] ^ 1, s1[x] = b[l]);
    int mid = l + r >> 1;
    build(ls, l, mid), build(rs, mid + 1, r);
    upd(x);
}
void pd(int x){
    if(! tg[x])return ;
    tg[ls] ^= 1, tg[rs] ^= 1;
    swap(s0[ls], s1[ls]);
    swap(s0[rs], s1[rs]);
    tg[x] = 0;
}
void mdf(int x, int l, int r, int ql, int qr){
    if(ql <= l and r <= qr)return(void)(swap(s0[x], s1[x]), tg[x] ^= 1);
    int mid = l + r >> 1; pd(x);
    if(ql <= mid)mdf(ls, l, mid, ql, qr);
    if(mid < qr)mdf(rs, mid + 1, r, ql, qr);
    upd(x);
}

int nkp;
bool op;

int fd(int x, int l, int r){
    if(l == r)return s1[x] ? l : - 1; pd(x); if(! s1[x])return - 1; int mid = l + r >> 1;
    if(s1[ls])return fd(ls, l, mid);
    else return fd(rs, mid + 1, r);
}

void qry(int x, int l, int r, int ql, int qr){
    if(op)return ;
    if(ql <= l and r <= qr){
        int res = fd(x, l, r);
        if(~ res)nkp = res, op = true;
        return;
    }
    int mid = l + r >> 1; pd(x);
    if(ql <= mid)qry(ls, l, mid, ql, qr);
    if(op)return ;
    if(mid < qr)qry(rs, mid + 1, r, ql, qr);
}
const int p = 1073741824;

signed main(){
    n = rd(), m = rd();
    for(int i = 1; i <= n; ++i)a[i] = rd();
    for(int i = 1; i <= m; ++i){
        int x = rd(), l = rd(), r = rd(), y;
        q[i] = {x, l, r};
        if(x == 1)q[i].x = rd();
    }
    for(int bit = 0; bit < 31; ++bit){
        for(int i = 1; i <= n; ++i)b[i] = a[i] >> bit & 1;
        build(1, 1, n);
        for(int i = 1; i <= m; ++i)if(q[i].op == 1){
            int x = q[i].x >> bit & 1;
            if(! x)continue; mdf(1, 1, n, q[i].l, q[i].r);
        }else{
            op = false, nkp = q[i].r + 1; qry(1, 1, n, q[i].l, q[i].r);
            q[i].ans += 1ll * (q[i].r - nkp + 1) * (1ll << bit) % p;
            q[i].ans %= p;
        }
    }
    for(int i = 1; i <= m; ++i)if(q[i].op ^ 1)printf("%lld\n", q[i].ans);
    return 0;
}

線段樹的擴充1(可持久化)

可持久化其實就是加了一個可以訪問歷史版本的功能,就比如我詢問之前某次修改後的答案你可以和普通線段樹複雜度一樣 \(O(\log n)\) 回答。

考慮具體如何實現。其實每次修改操作,我們只會修改 \(O(\log n)\) 個點,於是我們就可以把這些點組成的鏈扯出來,對於每次操作重新“開”一顆線段樹,就像下面這樣。

我們把每次操作看成一個樹根,往下修改時就在原來的樹上走,如果一個點不修改,就直接繼承到現在的樹上,否則就新開一個節點。然後其他東西就該咋弄咋弄,正常往這上面套各種線段樹應該都行。

然後我們就不得不說一個非常重要的東西了。

可持久化權值線段樹(主席樹)

是的,我們如果讓權值線段樹可持久化,它就成了眾所周知的主席樹。這個東西能夠查詢靜態區間第 \(k\) 小。至於動態區間第 \(k\) 小嘛,在外面再套一個樹狀陣列不就行了?

先說靜態的。我們把輸入每一個數看成一次操作,而每次操作就是在值域中修改一個點。然後詢問的時候就可以看成字首和。在樹上走的時候,對於尋找當前區間 \([l,r]\) 中值域裡的第 \(k\) 大,我就正常線段樹二分,但是同時二分兩棵線段樹,對於一個 \(mid\),前一半的值域在 \([l,r]\) 中數的個數等價於 \([1,r]\) 中數的個數減去 \([1,l-1]\) 中數的個數。然後就可以直接做了。

P3834 板子 以及P1533 可憐的狗狗雙倍經驗(真就直接複製)

程式碼如下:

int n, m, a[N], b[N], tt;
int rt[N << 5], c[N << 5], nd, ls[N << 5], rs[N << 5];

void upd(int &x, int y, int l, int r, int p){
    x = ++nd; c[x] = c[y] + 1;
    if(l == r)return; int mid = l + r >> 1;
    if(p <= mid)rs[x] = rs[y], upd(ls[x], ls[y], l, mid, p);
    else ls[x] = ls[y], upd(rs[x], rs[y], mid + 1, r, p);
}
int qry(int x, int y, int l, int r, int k){
    if(l == r)return l; int mid = l + r >> 1, res = c[ls[x]] - c[ls[y]];
    if(res >= k)return qry(ls[x], ls[y], l, mid, k);
    return qry(rs[x], rs[y], mid + 1, r, k - res);
}

signed main(){
    n = rd(), m = rd();
    for(int i = 1; i <= n; ++i)a[i] = b[i] = rd();
    sort(b + 1, b + 1 + n);
    tt = unique(b + 1, b + 1 + n) - b - 1;
    for(int i = 1; i <= n; ++i)a[i] = lower_bound(b + 1, b + 1 + tt, a[i]) - b, upd(rt[i], rt[i - 1], 1, tt, a[i]);
    for(int i = 1; i <= m; ++i){
        int l = rd(), r = rd(), k = rd();
        printf("%d\n", b[qry(rt[r], rt[l - 1], 1, tt, k)]);
    }
    return 0;
}

P2617 Dynamic Rankings

然後考慮動態區間怎麼做。

我們可以給主席樹套一棵樹狀陣列,每次暴力修改完在樹狀陣列上維護,注意樹狀陣列記錄的其實只是每一棵主席樹的樹根,所以跑樹狀陣列每一個點時要在對應的主席樹上進行修改。查詢可類比上文,上文中我們記錄了兩個樹根,現在我們可以把樹狀陣列上的一些相關的樹根開兩個陣列記一下,每次二分就把所有樹的答案累加起來,其他部分與上面類似不再贅述。

程式碼如下:

namespace SGT{
    int nd, rt[N << 9], ls[N << 9], rs[N << 9], c[N << 9];
    void gotols(){
        for(int i = 1; i <= c1; ++i)q1[i] = ls[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = ls[q2[i]];
    }
    void gotors(){
        for(int i = 1; i <= c1; ++i)q1[i] = rs[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = rs[q2[i]];
    }
    void upd(int &x, int l, int r, int p, int y){
        if(! x)x = ++nd; c[x] += y;
        if(l == r)return; int mid = l + r >> 1;
        if(p <= mid)upd(ls[x], l, mid, p, y);
        else upd(rs[x], mid + 1, r, p, y);
    }
    int qryk(int l, int r, int k){
        if(l == r)return l; int mid = l + r >> 1, res = 0;
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        if(res >= k)return gotols(), qryk(l, mid, k);
        return gotors(), qryk(mid + 1, r, k - res);
    }
    int qryrk(int l, int r, int k){
        if(l == r)return 0; int mid = l + r >> 1, res = 0;
        if(k <= mid)return gotols(), qryrk(l, mid, k);
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        return gotors(), res + qryrk(mid + 1, r, k);
    }
}
using namespace SGT;
namespace BIT{
    int lb(int x){return x & - x;}
    void get(int l, int r){
        c1 = c2 = 0;
        for(int i = l - 1; i; i -= lb(i))q1[++c1] = rt[i];
        for(int i = r; i; i -= lb(i))q2[++c2] = rt[i];
    }
    void mdf(int p, int y){
        int x = lower_bound(b + 1, b + 1 + tt, a[p]) - b;
        for(; p <= n; p += lb(p))upd(rt[p], 1, tt, x, y);
    }
    int kth(int l, int r, int k){get(l, r); return qryk(1, tt, k);}
    int rk(int l, int r, int k){int kk = lower_bound(b + 1, b + 1 + tt, k) - b; get(l, r); return qryrk(1, tt, kk) + 1;}
}

P2839 [國家集訓隊] middle

首先分析題目性質,想想我們的答案有什麼特點。因為是找最大中位數,是不是應該可以二分啊。假設當前二分一個 \(mid\),有一個套路就是把大於等於它的賦值成一,否則為零,然後看求子段和是否大於等於零即可判斷。對於這道題,就是中間確定部分的可以用值域線段樹維護,左右兩邊再分別開一個線段樹維護前字尾最大子段和就行。

然後關於程式碼,就是放的我的遠古時期寫的我自己都看的難受神秘程式碼。

#include<bits/stdc++.h>
#define s(x) tr[x].sum
#define ls(x) tr[x].lson
#define rs(x) tr[x].rson
#define lm(x) tr[x].lmx
#define rm(x) tr[x].rmx
#define lc(x) tr[x].ncl
#define rc(x) tr[x].ncr
using namespace std;
const int N = 2e4 + 10;
int n, m, a[N], num[N], cnt, rt[N], idq, x1, x2, x3, x4;
int L(int x){return lower_bound(num + 1, num + cnt + 1, x) - num;}
struct tree
{
    int sum, lmx, rmx;
    int lson, rson;
    bool ncl, ncr; 
}tr[N * 40];
vector < int > c[N];
void upd(int x)
{
    s(x) = s(ls(x)) + s(rs(x));
    lm(x) = max(lm(ls(x)), s(ls(x)) + lm(rs(x)));
    rm(x) = max(rm(rs(x)), s(rs(x)) + rm(ls(x)));
}
int build(int l, int r)
{
    int x = ++idq;
    if(l == r)
	{
        if(L(a[l]) < 2)s(x) = lm(x) = rm(x) = - 1;
        else s(x) = lm(x) = rm(x) = 1;
        return x;
    }
    int mid = l + r >> 1; lc(x) = rc(x) = 1;
    ls(x) = build(l, mid); rs(x) = build(mid + 1, r);
    upd(x); return x;
}
int modify(int x, int y, int l, int r, int to, int val, bool cc)
{
    if(!cc)x = ++idq;
    if(l == r)
	{
        s(x) = lm(x) = rm(x) = val;
        return x;
    }
    int mid = l + r >> 1;
    if(to <= mid)
	{
        if(!rc(x)) rs(x) = rs(y);
        if(!lc(x))lc(x) = 1, ls(x) = modify(x, ls(y), l, mid, to, val, 0);
        else ls(x) = modify(ls(x), ls(y), l, mid, to, val, 1);
    }
    else
	{
        if(!lc(x)) ls(x) = ls(y);
        if(!rc(x))rc(x) = 1, rs(x) = modify(x, rs(y), mid + 1, r, to, val, 0);
        else rs(x) = modify(rs(x), rs(y), mid + 1, r, to, val, 1);
    }
    upd(x); return x;
}
int query(int x, int l, int r, int L, int R)
{
    if(L <= l and r <= R)return s(x);
    int mid = l + r >> 1; int ret = 0;
    if(L <= mid)ret += query(ls(x), l, mid, L, R);
    if(mid < R)ret += query(rs(x), mid + 1, r, L, R);
    return ret;
}
tree query1(int x, int l, int r, int L, int R)
{
    
    if(L <= l and r <= R)return tr[x];
    int mid = l + r >> 1;
    if(L <= mid and mid < R)
	{
        tree ret;
        tree le = query1(ls(x), l, mid, L, R);
        tree ri = query1(rs(x), mid + 1, r, L, R);
        ret.sum = le.sum + ri.sum;
        ret.lmx = max(le.lmx, le.sum + ri.lmx);
        return ret;
    }
    else if(L <= mid)return query1(ls(x), l, mid, L, R);
    else return query1(rs(x), mid + 1, r, L, R);
}
tree query2(int x, int l, int r, int L, int R)
{
    if(L <= l and r <= R)return tr[x];
    int mid = l + r >> 1;
    if(L <= mid and mid < R)
	{
        tree ret;
        tree le = query2(ls(x), l, mid, L, R);
        tree ri = query2(rs(x), mid + 1, r, L, R);
        ret.sum = le.sum + ri.sum;
        ret.rmx = max(ri.rmx, ri.sum + le.rmx);
        return ret;
    }
    else if(L <= mid)return query2(ls(x), l, mid, L, R);
    else return query2(rs(x), mid + 1, r, L, R);
}
bool check(int val)
{
    int sz = 0;
    if(x2 + 2 <= x3) sz = query(rt[val], 1, n, x2 + 1, x3 - 1);
    int sr = query1(rt[val], 1, n, x3, x4).lmx;
    int sl = query2(rt[val], 1, n, x1, x2).rmx;
    return (sl + sz + sr) >= 0;
}
int main()
{
    scanf("%d", &n);
    for(int i = 1; i <= n; i++)scanf("%d", &a[i]), num[++cnt] = a[i];
    sort(num + 1, num + cnt + 1);
    cnt = unique(num + 1, num + cnt + 1) - num - 1;
    for(int i = 1; i <= n; i++)c[L(a[i])].push_back(i);
    rt[1] = build(1,n);
    for(int i = 2; i <= cnt; i++)for(int j = 0; j < c[i - 1].size(); j++)
	{
	    int go = c[i - 1][j];
	    rt[i] = modify(rt[i], rt[i - 1], 1, n, go, - 1, rt[i] > 0);
	}
    scanf("%d", &m);
    int las = 0;
    int d[6];
    while(m--)
	{
        scanf("%d %d %d %d", &x1, &x2, &x3, &x4);
        d[1] = (x1 + las) % n;
        d[2] = (x2 + las) % n;
        d[3] = (x3 + las) % n;
        d[4] = (x4 + las) % n;
        sort(d + 1, d + 5);
        x1 = d[1] + 1, x2 = d[2] + 1, x3 = d[3] + 1, x4 = d[4] + 1;
        int l = 1, r = cnt;
        int ans = 0;
        while(l <= r)
		{
            int mid = l + r >> 1;
            if(check(mid))ans = mid, l = mid + 1;
            else r = mid - 1;
        }
        las = num[ans];
        printf("%d\n", las);
    }
    return 0;
}

P4137 Rmq Problem / mex

套路題。考慮我們每次記錄 \(val\) 上次存在的位置,查詢區間 \([l,r]\) 就是在 \(rt_r\) 子樹中找滿足出現時間小於 \(l\) 的最小 \(val\)。用主席樹維護最小值即可。

void upd(int &x, int y, int l, int r, int p, int val){
    if(! x)x = ++tot; if(l == r)return(void)(ans[x] = val); int mid = l + r >> 1;
    if(p <= mid)rs[x] = rs[y], upd(ls[x], ls[y], l, mid, p, val);
    else ls[x] = ls[y], upd(rs[x], rs[y], mid + 1, r, p, val);
    ans[x] = min(ans[ls[x]], ans[rs[x]]);
}
int qry(int x, int l, int r, int lim){
    if(l == r)return l; int mid = l + r >> 1;
    if(ans[ls[x]] < lim)return qry(ls[x], l, mid, lim);
    else return qry(rs[x], mid + 1, r, lim);
}

signed main(){
    // fileio(fil);
    n = rd() + 1, m = rd();
    for(int i = 1, x; i < n; ++i){
        x = rd() + 1; if(x > n)rt[i] = rt[i - 1];
        else upd(rt[i], rt[i - 1], 1, n, x, i);
    }
    for(int i = 1; i <= m; ++i){
        int l = rd(), r = rd();
        printf("%d\n", qry(rt[r], 1, n, l) - 1);
    }
    return 0;
}

線段樹的擴充2(線段樹合併)

注意合併要用動態開點線段樹,不然複雜度會變成 \(O(n\log^2n)\) 的,因為線段樹有 \(O(n\log n)\) 個節點,合併又是 \(O(\log n)\) 的。

怎麼合併呢?我們還是在樹上走,如果走到一點地方時存在節點(可能是一個或兩個)為空,就可以直接返回另一個節點,否則就往下遞迴,到葉子時合併回來即可。但是乍一看這不是爆炸了嗎?所以接下來我們需要口胡細緻分析一下複雜度。

容易發現:在合併兩棵樹的過程中走到有空節點的時候就返回了,相當於走一個點就會刪掉一些點,所以總複雜度不會超過總點數也就是 \(O(n\log n)\)

講題前先談談我對於線段樹合併的理解。首先為什麼需要合併線段樹,就是因為對於一些問題我們需要合併的資訊是 \(O(n)\) 級別的,對於這類式子我們肯定需要一種 \(O(\log)\) 的方式去最佳化。然後就是如何去理解合併的過程,因為如果真的每一個點都開一個線段樹那肯定爆炸沒得說。其實我們是把每個點對應在一段區間上,或是下標或是值域,然後對於每一個點的線段樹動態開點,最後是合併過程中並不是單純的加加減減,而是要根據你的式子去還原整個過程,或許你現在還不太懂,但等你看了後面某道題後你便會恍然大悟。

然後看一道板子。

P4556 [Vani有約會] 雨天的尾巴 /【模板】線段樹合併

觀察到是在樹上修改,於是可以聯想到某個經典套路,將樹上路徑轉化成差分。具體的,對於一條路徑 \((u,v)\),設 \(t=lca(u,v)\),則我可以把對於 \((u,v)\) 的操作看成對於 \((root,u)\)\((root,v)\) 的操作還有 \((root,t)\)\((root,fa_t)\) 的逆操作。然後考慮對每個點開線段樹。本題可以用顏色做線段樹下標,然後就似乎是單點修改(直接線段樹二分)加上線段樹合併。

然後稍微注意一下樹上合併時需要跑一遍 dfs 序,然後遞迴返回時合併。

void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1, sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
int lca(int u, int v){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        u = fa[tp[u]];
    }
    return dep[u] < dep[v] ? u : v;
}

void upd(int x){
    if(s[ls[x]] > s[rs[x]] or (s[ls[x]] == s[rs[x]] and col[ls[x]] < col[rs[x]]))return(void)(s[x] = s[ls[x]], col[x] = col[ls[x]]);
    s[x] = s[rs[x]], col[x] = col[rs[x]];
}
void md(int &p, int l, int r, int c, int y){
    if(! p)p = ++tot;
    if(l == r)return(void)(s[p] += y, col[p] = c);
    int mid = l + r >> 1;
    if(c <= mid)md(ls[p], l, mid, c, y);
    else md(rs[p], mid + 1, r, c, y);
    upd(p);
}
int merge(int a, int b, int l, int r){
    if(! a or ! b)return a | b;
    if(l == r)return s[a] += s[b], a;
    int mid = l + r >> 1;
    ls[a] = merge(ls[a], ls[b], l, mid);
    rs[a] = merge(rs[a], rs[b], mid + 1, r);
    upd(a); return a;
}
void dfs(int u){
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u])continue;
        dfs(v); rt[u] = merge(rt[u], rt[v], 1, 100000);
    }
    if(s[rt[u]])ans[u] = col[rt[u]];
}

signed main(){
    n = rd(), q = rd();
    for(int i = 1; i < n; ++i){
        int u = rd(), v = rd();
        add(u, v); add(v, u);
    }
    dfs1(1, 0); dfs2(1, 1);
    for(int i = 1; i <= q; ++i){
        int u = rd(), v = rd(), w = rd();
        md(rt[u], 1, 100000, w, 1); md(rt[v], 1, 100000, w, 1);
        int f = lca(u, v);
        md(rt[f], 1, 100000, w, - 1); md(rt[fa[f]], 1, 100000, w, - 1);
    }
    dfs(1);
    for(int i = 1; i <= n; ++i)printf("%d\n", ans[i]);
    return 0;
}

P3224 [HNOI2012] 永無鄉

怎麼感覺有點過於板子了?

對於求第 \(k\) 大的問題我們用值域線段樹,然後考慮怎麼解決連通性?然後你會發現直接並查集維護,然後並查集就是菊花圖,對每朵花的花心開一棵線段樹,然後正常合併即可。

int fd(int x){
    return x == f[x] ? x : f[x] = fd(f[x]);
}

void upd(int x){
    s[x] = s[ls[x]] + s[rs[x]];
}
void md(int &p, int l, int r, int y){
    if(! p)p = ++cnt;
    if(l == r)return(void)(++s[p]);
    int mid = l + r >> 1;
    if(y <= mid)md(ls[p], l, mid, y);
    else md(rs[p], mid + 1, r, y);
    upd(p);
}
int merge(int a, int b, int l, int r){
    if(! a or ! b)return a | b;
    if(l == r)return s[a] += s[b], a;
    int mid = l + r >> 1;
    ls[a] = merge(ls[a], ls[b], l, mid);
    rs[a] = merge(rs[a], rs[b], mid + 1, r);
    upd(a); return a;
}
int qry(int p, int l, int r, int y){
    if(l == r)return l;
    int mid = l + r >> 1, ans;
    if(s[ls[p]] >= y)ans = qry(ls[p], l, mid, y);
    else ans = qry(rs[p], mid + 1, r, y - s[ls[p]]);
    return ans;
}

void sol1(){
    int x = rd(), y = rd();
    x = fd(x), y = fd(y);
    if(x == y)return;
    f[y] = x; rt[x] = merge(rt[x], rt[y], 1, 100000);
}

signed main(){
    n = rd(), m = rd();
    for(int i = 1, x; i <= n; ++i)md(rt[i], 1, 100000, x = rd()), f[i] = i, pos[x] = i;
    for(int i = 1; i <= m; ++i)sol1();
    q = rd();
    for(int i = 1; i <= q; ++i){
        char c; cin >> c;
        if(c == 'B')sol1();
        else{
            int x = rd(), y = rd(); x = fd(x);
            if(s[rt[x]] < y){puts("-1"); continue;}
            printf("%d\n", pos[qry(rt[x], 1, 100000, y)]);
        }
    }
    return 0;
}

是不是覺得太簡單了?那麼來一點有難度的。

P5298 [PKUWC2018] Minimax

對於這道題我們可以很容易想到一個暴力 dp,設 \(f_{u,i}\) 表示 \(u\) 節點為第 \(i\) 大的數的機率。然後答案就是:

\[\sum_{i=1}^mi\times V_i\times f_{1,i}^2 \]

然後開始轉移,我們令當前點 \(u\) 的左兒子叫 \(l\),右兒子叫 \(r\),轉移式子也就呼之欲出:

\[f_{u,i}=f_{l,i}\times p_u\times\sum_{j<i}f_{r,j}+ f_{r,i}\times p_u\times\sum_{j<i}f_{l,j}+ f_{l,i}\times (1-p_u)\times\sum_{j>i}f_{r,j}+ f_{r,i}\times (1-p_u)\times\sum_{j>i}f_{l,j} \]

希望沒有筆誤

然後你就發現每次你都需要拿一段字首出來轉移,能不能最佳化一下啊?然而確實可以。考慮線段樹維護,下標就是排名(dp 的第二維)。現在你就把每個 \(f_i\) 都看成二叉樹上的點權,然後就可以合併了但是,

考慮這道題的合併並不是單純的加減,我們上面推出的轉移式中 \(f_i\) 是帶了係數的,所以在合併時我們還需要記一下係數,那麼係數又怎麼求呢?

考慮把上面的式子拆完,就是對於線段樹上一個點 \(i\),它的係數就是:

\[\sum_{k,j<i}p_k\times f_j+\sum_{k',j'>i}(1-p_{k'})\times f_{j'} \]

說人話就是左邊(下標)比它小的點乘上對應節點的機率 \(p\) 加上右邊的。所以按照上面的實現就好了。

此題小結

其實線段樹合併的本質就是最佳化狀態轉移。對於合併的順序需要注意與線段樹配套用的工具具體選擇,然後對於合併的式子其實就是轉移式子的復現,具體一點也就是拆分再乘法分配律合併同類項

擴充到一般的題,我們最開始可以考慮 native 的想法,寫出稍微暴力的轉移與維護方式,然後再觀察這些東西能不能透過什麼性質把他們變得具有一些優美的性質,比如可加性、可減性、單調性之類的,最後在思考如何套線段樹顯得優雅。

程式碼

const db eps = 1e-8;
const ll inf = 1e18;
const int N = 3e5 + 5, p = 998244353;

ll qmi(ll x, ll y){
	ll res = 1ll;
	for(; y; y >>= 1, x = x * x % p)if(y & 1)res = res * x % p;
	return res;
}

const ll ii = qmi(10000, p - 2);

int n, b[N], nd, m;
int rt[N << 5], ls[N << 5], rs[N << 5];
ll s[N << 5], tg[N << 5], f[N], a[N];
int fa[N], c[N], ch[N][2];

inline int jia(int x, int y){return x - p + y >= 0 ? x - p + y : x + y;}

void upd(int x){
	s[x] = jia(s[ls[x]], s[rs[x]]);
}
void updd(int x, ll y){
	if(! x)return;
	s[x] = s[x] * y % p;
	tg[x] = tg[x] * y % p;
}
void pd(int x){
	if(tg[x] == 1)return;
	updd(ls[x], tg[x]), updd(rs[x], tg[x]);
	return (void)(tg[x] = 1);
}
void addin(int &x, int l, int r, int pos){
	if(! x)tg[x = ++nd] = 1;
	if(l == r)return(void)(s[x] = 1);
	int mid = l + r >> 1; pd(x);
	if(pos <= mid)addin(ls[x], l, mid, pos);
	else addin(rs[x], mid + 1, r, pos);
	upd(x);
}
int merge(int x, int y, int l, int r, ll sx, ll sy, ll pp){
	if(! x or ! y)return updd(x | y, (x ? sx : sy)), x | y;
	int mid = l + r >> 1; pd(x), pd(y);
	ll lsl = s[ls[x]], rsl = s[rs[x]], lsr = s[ls[y]], rsr = s[rs[y]];
	ls[x] = merge(ls[x], ls[y], l, mid, jia(sx, rsr * (p + 1 - pp) % p), jia(sy, rsl * (p + 1 - pp) % p), pp);
	rs[x] = merge(rs[x], rs[y], mid + 1, r, jia(sx, lsr * pp % p), jia(sy, lsl * pp % p), pp);
	return upd(x), x;
}
void dfs(int u){
	if(! c[u])addin(rt[u], 1, m, a[u]);
	if(c[u] == 1)dfs(ch[u][0]), rt[u] = rt[ch[u][0]];
	if(c[u] == 2)dfs(ch[u][0]), dfs(ch[u][1]), rt[u] = merge(rt[ch[u][0]], rt[ch[u][1]], 1, m, 0, 0, a[u]);
}
void getans(int x, int l, int r){
	if(! x)return;
	if(l == r)return(void)(f[l] = s[x]);
	int mid = l + r >> 1; pd(x);
	getans(ls[x], l, mid); getans(rs[x], mid + 1, r);
}

int main(){
	rd(n);
	for(int i = 1; i <= n; ++i)rd(fa[i]), fa[i] ? ch[fa[i]][c[fa[i]]++] = i : 0;
	for(int i = 1; i <= n; ++i){
		rd(a[i]);
		if(c[i])a[i] = a[i] * ii % p;
		else b[++m] = a[i];
	}
	sort(b + 1, b + 1 + m);
	for(int i = 1; i <= n; ++i)if(! c[i])a[i] = lower_bound(b + 1, b + 1 + m, a[i]) - b;
	dfs(1); getans(rt[1], 1, m); ll ans = 0;
	for(int i = 1; i <= m; ++i)ans = jia(ans, 1ll * i * b[i] % p * f[i] % p * f[i] % p);
	wt(ans);
	return 0;
}

(為什麼沒有線段樹分裂?因為我不會感覺不太常見就先放了)

線段樹的擴充3(李超線段樹)

簡單介紹一下,李超線段樹維護了一些線段,它可以快速查詢位置上縱座標最大/小的線段編號以及數值。考慮這種東西就很適合去解決一些需要從區間中找最值轉移狀態的題。

然後講一下實現與應用。

李超線段樹的節點維護的是在 \(mid\) 處縱座標最大的線段,所以實際維護的時候它就很暴力。直接在樹上找到它的範圍然後就每次暴力比較線段就完了。分析複雜度,查詢是一隻 \(\log\),然後暴力更新的時候因為兩個一次函式最多隻有一個交點,所以它之後遞迴一個子區間,複雜度也是一隻 \(\log\),所以一共是兩隻 \(\log\)

查詢時候就從根走到對應的葉子節點,走的時候順路記一下最值就完了。

板子和他的雙倍經驗

const int N = 1e5 + 5, mod1 = 39989, mod2 = 1e9 + 1;
const ll INF = 1e18;
int n;
lb k[N], b[N];
int sgt[N << 2];

#define ls x << 1
#define rs x << 1 | 1
lb calc(int x, int pos){
    return k[x] * pos + b[x];
}
bool eq(lb a, lb b){
    lb eps = 1e-10;
    if(a - b < eps and b - a < eps)return true;
    return false;
}
bool cmp(int x1, int x2, int pos){
    lb a = calc(x1, pos), b = calc(x2, pos);
    return a > b || (a == b and x1 < x2);
}
void _upd(int tmp, int x, int l, int r){
    int mid = l + r >> 1;
    if(cmp(tmp, sgt[x], mid))swap(tmp, sgt[x]);
    if(cmp(tmp, sgt[x], l))_upd(tmp, ls, l, mid);
    if(cmp(tmp, sgt[x], r))_upd(tmp, rs, mid + 1, r);
}
void upd(int x, int l, int r, int L, int R, int tmp){
    if(L <= l and r <= R)return _upd(tmp, x, l, r);
    int mid = l + r >> 1;
    if(L <= mid)upd(ls, l, mid, L, R, tmp);
    if(R > mid)upd(rs, mid + 1, r, L, R, tmp);
}
int qry(int x, int l, int r, int pos){
    int mid = l + r >> 1, res = sgt[x];
    if(l == r)return res;
    int tmp = pos <= mid ? qry(ls, l, mid, pos) : qry(rs, mid + 1, r, pos);
    if(cmp(tmp, res, pos))swap(res, tmp);
    return res;
}

signed main(){
    n = rd();
    b[0] = - INF;
    int ans = 0, cnt = 0;
    for(int i = 1; i <= n; ++i){
        int op = rd();
        if(op == 0){
            int pos = (rd() + ans + mod1 - 1) % mod1 + 1;
            printf("%d\n", ans = qry(1, 1, mod1, pos));
        }
        else{
            int x1 = rd(), y1 = rd(), x2 = rd(), y2 = rd();
            x1 = (x1 + ans - 1) % mod1 + 1;
            y1 = (y1 + ans - 1) % mod2 + 1;
            x2 = (x2 + ans - 1) % mod1 + 1;
            y2 = (y2 + ans - 1) % mod2 + 1;
            if(x1 > x2)swap(x1, x2), swap(y1, y2);
            if(x1 == x2)k[++cnt] = 0, b[cnt] = max(y1, y2);
            else{
                k[++cnt] = (lb)(y1 - y2) / (x1 - x2);
                b[cnt] = y1 - k[cnt] * x1;
            }
            upd(1, 1, mod1, x1, x2, cnt);
        }
    }
    return 0;
}

然後講一些應用吧(?)

其實李超線段樹最主要的運用還是結合著斜率最佳化 dp 一起考,本來我想放在後面與 dp 的結合的,想想還是算了。但是我在這一塊的題寫的很少,所以就會講一點次要的。

P3081 [USACO13MAR] Hill Walk G

我們從 \((0,0)\) 開始走。如果直接建李超線段樹那麼當我們想要取下面部分的線段時就出問題了,所以我們就需要一個支援刪除的東西維護它。於是考慮平衡樹。

然後走的時候我們是橫座標排序,所以現在就只用考慮縱座標。在每次走到一個線段的終點時我們需要看可行區間中比當前線段低一層的是誰。然後就想到把線段全部丟到平衡樹裡面,透過某種排序方使這些線段從高到低排好序。

那麼如何判斷兩個線段的高低呢?我們透過下圖來分析。

如果我要比較上面兩條線段,我首先會去看它們右端點的位置。顯然後者更靠右,然後我們把它們的右端點連線起來(如圖紅線)。如果前者更高(如圖),那麼紅線的斜率就會小於後面那條線段的斜率,否則就會是圖片下半部分的情況。於是我們比較線段之間的優先順序時就可以先比較右端點再比較斜率了。

bool operator <(line x, line y){
	if(x.c < y.c)return 1ll * (y.c - x.c) * (y.d - y.b) < 1ll * (y.d - x.d) * (y.c - y.a);
	return 1ll * (x.c - y.c) * (x.d - x.b) > 1ll * (x.c - x.a) * (x.d - y.d);
}

注意如果直接用除法可能精度會炸。

然後就從左往右掃一遍,把線段該加入平衡樹的就加進來,該刪的就刪掉。如果刪掉的是當前所在的區間就在平衡樹裡查詢一個比它低一層的線段然後更新答案。實際實現時可以用 set 代替平衡樹。

int main(){
	rd(n);
	for(int i = 1; i <= n; ++i)rd(l[i].a, l[i].b, l[i].c, l[i].d), p[i - 1 << 1 | 1] = {l[i].a, l[i].b, i}, p[i << 1] = {l[i].c, l[i].d, i}, l[i].id = i;
	s.ins(l[1]); sort(p + 1, p + 1 + (n << 1));
	for(int i = 2; i <= (n << 1); ++i)
		if(p[i].x == l[p[i].id].a)s.ins(l[p[i].id]);
		else if(p[i].id == cur){
			set < line > :: iterator it = s.find(l[p[i].id]);
			if(it == s.begin())break;
			++ans; it--;
			cur = it -> id;
		}
		else s.del(l[p[i].id]);
	wt(ans);
	return 0;
}

本來有一道題的但是我把它放到樹剖套線段樹裡面了。

本來還有一道題但是我把它又放到 dp 最佳化裡面了。

所以因為各種原因,李超線段樹就寫到此為止吧!但是我能夠保證你能在後面的內容中找到李超線段樹的影子。

線段樹的擴充4(掃描線)

這裡只講狹義掃描線也就是掃面積並,至於廣義的掃面線等之後有空我會單獨寫一篇部落格詳細講解。

就簡單講講掃面積並。以從上往下掃為例,我們可以把圖形拆成若干平行於橫軸的代表加一減一的線段。每經過一個線段,這個奇形怪狀的圖形的橫截長度就會發生變化,我們可以每次維護這個變化,並統計出上一次的橫截長度對應的面積。然後對於經過線段,我們可以看成區間的加減,查詢就是詢問區間內非零個數。

板子

int n, cnt[N << 2], top;
ll f[N << 2], y[N << 2], ans;
struct node{
    ll x1, y1, y2;
    int op;
    bool operator < (const node &rhs)const {
        return x1 < rhs.x1;
    }
}a[N << 2];

#define ls x << 1
#define rs x << 1 | 1
void upd(int x, int l, int r){
    if(cnt[x])return (void)(f[x] = y[r] - y[l - 1]);
    f[x] = f[ls] + f[rs];
}
void modify(int x, int l, int r, int L, int R, int op){
    if(L <= l and r <= R){cnt[x] += op; upd(x, l, r); return;}
    int mid = l + r >> 1;
    if(L <= mid)modify(ls, l, mid, L, R, op);
    if(R > mid)modify(rs, mid + 1, r, L, R, op);
    upd(x, l, r);
}

signed main(){
    //freopen(, stdin);
    //freopen(, stdout);
    n = rd();
    for(int i = 1; i <= n; ++i){
        ll x1 = rd(), y1 = rd(), x2 = rd(), y2 = rd();
        a[++top] = {x1, y1, y2, 1}; y[top] = y1;
        a[++top] = {x2, y1, y2, - 1}; y[top] = y2;
    }
    sort(y + 1, y + 1 + top);
    sort(a + 1, a + 1 + top);
    n = unique(y + 1, y + 1 + top) - y - 1;
    for(int i = 1; i <= top; ++i){
        ans += f[1] * (a[i].x1 - a[i - 1].x1);
        int l = lower_bound(y + 1, y + 1 + n, a[i].y1) - y;
        int r = lower_bound(y + 1, y + 1 + n, a[i].y2) - y;
        modify(1, 1, n, l + 1, r, a[i].op);
    }
    printf("%lld", ans);
    return 0;
}

線段樹的擴充5(樹套樹)

維護高維資訊時,單純的線段樹顯然無法勝任,這時我們就需要簡潔好寫的 cdq 而不是使一樣的樹套樹。

樹套樹一般只套兩層,套三層是真的沒法寫。對於兩層的樹來說,一般裡層就是非常正常的樹,它會正常地維護資訊,然後外層的樹有一點變化,因為外層的樹不是維護一些資訊而是一些樹,所以為了方便我們就維護樹根的資訊。然後講一講修改。

其實也沒啥好講的,就是外層跑一遍,只要是有關聯的點都要修改!需要修改的就去裡層跑,然後遞迴往上更新就行。

查詢就正常查詢,找到區間後從直接返回答案變成查詢裡層。

有一道比板子簡單一點的題放在主席樹裡面了,這裡就直接放板子吧。然後這種東西講多少遍都不如自己看別人寫得好的程式碼然後自己想一遍。

板子

namespace SGT{
    int nd, rt[N << 9], ls[N << 9], rs[N << 9], c[N << 9];
    void gotols(){
        for(int i = 1; i <= c1; ++i)q1[i] = ls[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = ls[q2[i]];
    }
    void gotors(){
        for(int i = 1; i <= c1; ++i)q1[i] = rs[q1[i]];
        for(int i = 1; i <= c2; ++i)q2[i] = rs[q2[i]];
    }
    void upd(int &x, int l, int r, int p, int y){
        if(! x)x = ++nd; c[x] += y;
        if(l == r)return; int mid = l + r >> 1;
        if(p <= mid)upd(ls[x], l, mid, p, y);
        else upd(rs[x], mid + 1, r, p, y);
    }
    int qryk(int l, int r, int k){
        if(l == r)return l; int mid = l + r >> 1, res = 0;
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        if(res >= k)return gotols(), qryk(l, mid, k);
        return gotors(), qryk(mid + 1, r, k - res);
    }
    int qryrk(int l, int r, int k){
        if(l == r)return 0; int mid = l + r >> 1, res = 0;
        if(k <= mid)return gotols(), qryrk(l, mid, k);
        for(int i = 1; i <= c1; ++i)res -= c[ls[q1[i]]];
        for(int i = 1; i <= c2; ++i)res += c[ls[q2[i]]];
        return gotors(), res + qryrk(mid + 1, r, k);
    }
}
using namespace SGT;
namespace BIT{
    int lb(int x){return x & - x;}
    void get(int l, int r){
        c1 = c2 = 0;
        for(int i = l - 1; i; i -= lb(i))q1[++c1] = rt[i];
        for(int i = r; i; i -= lb(i))q2[++c2] = rt[i];
    }
    void mdf(int p, int y){
        int x = lower_bound(b + 1, b + 1 + tt, a[p]) - b;
        for(; p <= n; p += lb(p))upd(rt[p], 1, tt, x, y);
    }
    int kth(int l, int r, int k){get(l, r); return qryk(1, tt, k);}
    int rk(int l, int r, int k){int kk = lower_bound(b + 1, b + 1 + tt, k) - b; get(l, r); return qryrk(1, tt, kk) + 1;}
    int pre(int l, int r, int k){
        k = rk(l, r, k) - 1; return k > 0 ? b[kth(l, r, k)] : - INF;
    }
    int suf(int l, int r, int k){
        k = rk(l, r, k + 1); return k < r - l + 2 ? b[kth(l, r, k)] : INF;
    }
}

P4175 [CTSC2008] 網路管理

雖然這看上去很樹套樹+樹剖但是其實可以將答案差分再合併一下扔掉一個 \(\log\),這樣維護字首主席樹,查詢時就查四棵樹即可,最後就是 \(O(n\log^2n)\)。但是我在看完題後根本沒管太多,看到三隻 \(\log\) 能過就寫完樹套樹就水靈靈地過了((

void dfs1(int u, int f){
	dep[u] = dep[fa[u] = f] + 1, sz[u] = 1;
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to; if(v == f)continue;
		dfs1(v, u); sz[u] += sz[v];
		if(sz[son[u]] < sz[v])son[u] = v;
	}
}
void dfs2(int u, int tp){
	top[u] = tp; dfn[u] = ++tim, rk[tim] = u;
	if(son[u])dfs2(son[u], tp);
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to;
		if(v ^ fa[u] and v ^ son[u])dfs2(v, v);
	}
}

void gotols(){
	for(int i = 1; i <= c1; ++i)q1[i] = ls[q1[i]];
	for(int i = 1; i <= c2; ++i)q2[i] = ls[q2[i]];
}
void gotors(){
	for(int i = 1; i <= c1; ++i)q1[i] = rs[q1[i]];
	for(int i = 1; i <= c2; ++i)q2[i] = rs[q2[i]];
}
void upd(int &x, int l, int r, int pos, int y){
	if(! x)x = ++nd; s[x] += y;
	if(l == r)return; int mid = l + r >> 1;
	if(pos <= mid)upd(ls[x], l, mid, pos, y);
	else upd(rs[x], mid + 1, r, pos, y);
}
int getsum(){
	int sum = 0;
	for(int i = 1; i <= c1; ++i)sum -= s[q1[i]];
	for(int i = 1; i <= c2; ++i)sum += s[q2[i]];
	return sum;
}
int qryk(int l, int r, int k){
	if(l == r)return getsum() >= k ? l : 0; int mid = l + r >> 1, sum = 0;
	for(int i = 1; i <= c1; ++i)sum -= s[rs[q1[i]]];
	for(int i = 1; i <= c2; ++i)sum += s[rs[q2[i]]];
	if(k <= sum)return gotors(), qryk(mid + 1, r, k);
	return gotols(), qryk(l, mid, k - sum);
}

void get(int l, int r){
	for(int i = l - 1; i; i -= i & - i)q1[++c1] = rt[i];
	for(int i = r; i; i -= i & - i)q2[++c2] = rt[i];
}
void mdf(int pos, int y){
	int x = lower_bound(b + 1, b + 1 + m, a[rk[pos]]) - b;
	for(; pos <= n; pos += pos & - pos)upd(rt[pos], 1, m, x, y);
}
int kth(int u, int v,int k){
	c1 = c2 = 0;
	while(top[u] ^ top[v]){
		if(dep[top[u]] < dep[top[v]])swap(u, v);
		get(dfn[top[u]], dfn[u]);
		u = fa[top[u]];
	}
	if(dep[u] > dep[v])swap(u, v);
	get(dfn[u], dfn[v]); return qryk(1, m, k);
}

但是說實在的,實戰中這玩意用的是真的少,一般時候還是 cdq 或者轉化題意用複雜度更低的做法做。

線段樹的一些高階玩法

這一部分主要是一些其他的演算法、思想與線段樹的運用,其實最主要的思想還是尋找題目性質,然後線段樹最佳化維護資訊的過程。

那就先來看一些線段樹與樹剖的東西。

樹剖+線段樹

做過序列上給一些顏色求顏色段數量的題吧?如果我把它扔到樹上會怎麼樣呢?

P2486 [SDOI2011] 染色

結果是根本不會怎麼樣。直接樹剖把樹變成鏈做,套上線段樹就完了。

void add(int u, int v){
    e[++cnt] = {hd[u], v}; hd[u] = cnt;
}
void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top; dfn[u] = ++tim, rk[tim] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}

struct node{
    int len, l, r;
};
#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    lc[x] = lc[ls], rc[x] = rc[rs];
    s[x] = s[ls] + s[rs] - (rc[ls] == lc[rs]);
}
void build(int x, int l, int r){
    if(l == r)return(void)(lc[x] = rc[x] = a[rk[l]], s[x] = 1);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pd(int x){
    if(! lz[x])return;
    lc[ls] = rc[ls] = lz[x];
    lc[rs] = rc[rs] = lz[x];
    s[ls] = s[rs] = 1;
    lz[ls] = lz[rs] = lz[x]; lz[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int c){
    if(L <= l and r <= R)return(void)(lc[x] = rc[x] = lz[x] = c, s[x] = 1);
    int mid = l + r >> 1; pd(x);
    if(L <= mid)mdfy(ls, l, mid, L, R, c);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, c);
    upd(x);
}
node qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return {s[x], lc[x], rc[x]};
    int mid = l + r >> 1; node res, t; pd(x);
    res = {0, 0, 0};
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid){
        t = qry(rs, mid + 1, r, L, R);
        if(! res.len)return t;
        res.len += t.len - (rc[ls] == lc[rs]);
        res.r = t.r;
    }
    return res;
}
void mpath(int u, int v, int c){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        mdfy(1, 1, n, dfn[tp[u]], dfn[u], c);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    mdfy(1, 1, n, dfn[u], dfn[v], c);
}
int qpath(int u, int v){
    int pr1 = 0, pr2 = 0, ans = 0;
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v), swap(pr1, pr2);
        node res = qry(1, 1, n, dfn[tp[u]], dfn[u]);
        ans += res.len - (pr1 == res.r);
        pr1 = res.l; u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v), swap(pr1, pr2);
    node res = qry(1, 1, n, dfn[u], dfn[v]);
    ans += res.len - (pr1 == res.l) - (pr2 == res.r);
    return ans;
}

這道是不是太簡單了一點?那就加強一下!

P3979 遙遠的國度

這下看懂了!

其實顏色段的處理不成問題,我們需要思考的是樹根的變化應該如何高效處理。

這個時候我們就可以分類討論一下樹根的位置了。如果樹根就是詢問點那直接輸出全域性的答案;如果樹根不在當前詢問點 \(u\) 的子樹內,是不是對 \(u\) 根本沒有影響啊!最後就剩一種——在 \(u\) 的某棵子樹內,這時候我們肯定要先找到是哪一棵子樹,然後用全域性減去這棵子樹就行。

那麼如何快速找子樹呢?我們可以類比樹剖求 LCA 的過程,然後就做完了。找子樹和其他操作都是 \(O(n\log n)\) 的。

void add(int u, int v){
    e[++cnt] = {hd[u], v}; hd[u] = cnt;
}
void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top; dfn[u] = ++tim, rk[tim] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    mi[x] = min(mi[ls], mi[rs]);
}
void build(int x, int l, int r){
    if(l == r)return(void)(mi[x] = a[rk[l]]);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pd(int x){
    if(! lz[x])return;
    mi[ls] = mi[rs] = lz[x];
    lz[ls] = lz[rs] = lz[x]; lz[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int c){
    if(L <= l and r <= R)return(void)(mi[x] = lz[x] = c);
    int mid = l + r >> 1; pd(x);
    if(L <= mid)mdfy(ls, l, mid, L, R, c);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, c);
    upd(x);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return mi[x];
    int mid = l + r >> 1; ll res = INF; pd(x);
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid)res = min(res, qry(rs, mid + 1, r, L, R));
    return res;
}
void mpath(int u, int v, int c){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        mdfy(1, 1, n, dfn[tp[u]], dfn[u], c);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    mdfy(1, 1, n, dfn[u], dfn[v], c);
}
ll qtree(int u){
    if(u == root)return mi[1];
    if(dfn[root] < dfn[u] or dfn[root] >= dfn[u] + sz[u])return qry(1, 1, n, dfn[u], dfn[u] + sz[u] - 1);
    int v = root;
    while(tp[u] != tp[v]){
        v = tp[v];
        if(fa[v] == u)break;
        v = fa[v];
    }
    if(tp[u] == tp[v])v = rk[dfn[u] + 1];
    return min(qry(1, 1, n, 1, dfn[v] - 1), qry(1, 1, n, dfn[v] + sz[v], n));
}

好了也該來一道那啥一點的題了(

P2542 [AHOI2005] 航線規劃

如果你會 LCT 而且程式碼能力強的話建議直接無腦做就完了。

考慮不會 LCT 的人會怎麼做。如果用資料結構啥的維護刪邊可能不太現實,所以考慮倒著做就把刪邊變成了加邊。然後就是如何維護兩點之間橋的數量。我可以先把初始的橋給處理出來,跑一下 tarjan 然後圖就成了一棵樹,然後考慮加邊操作意味著什麼。如果現在需要將 \((u,v)\) 加入圖中,那麼樹上 \(u,v\) 之間的路徑上的橋就都沒了,因為構成環。所以每次加邊操作都可以看成區間賦值,然後樹剖套一個果的線段樹就做完了。

void addx(int u, int v){
    ex[++cntx] = {hdx[u], v}; hdx[u] = cntx;
}
void tarjan(int u, int fa){
    id[u] = low[u] = ++seq;
    st.push(u);
    for(int i = hdx[u]; i; i = ex[i].nxt){
        int v = ex[i].to;
        if(v == fa)continue;
        if(! id[v])tarjan(v, u);
        low[u] = min(low[u], low[v]);
    }
    if(low[u] == id[u]){
        ++ct;
        while(st.top() ^ u){
            col[st.top()] = ct;
            st.pop();
        }
        col[u] = ct; st.pop();
    }
}

void add(int u, int v){
    e[++cnt] = {hd[u], v}; hd[u] = cnt;
}
void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; sz[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == f)continue;
        dfs1(v, u); sz[u] += sz[v];
        if(sz[son[u]] < sz[v])son[u] = v;
    }
}
void dfs2(int u, int top){
    tp[u] = top; dfn[u] = ++tim, rk[tim] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
#define ls x << 1
#define rs x << 1 | 1
void upd(int x){
    s[x] = s[ls] + s[rs];
}
void build(int x, int l, int r){
    if(l == r)return(void)(s[x] = 1);
    int mid = l + r >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    upd(x);
}
void pd(int x, int l, int r){
    if(! lz[x])return;
    int mid = l + r >> 1;
    s[ls] = s[rs] = 0;
    lz[ls] = lz[rs] = lz[x]; lz[x] = 0;
}
void mdfy(int x, int l, int r, int L, int R, int c){
    if(L <= l and r <= R)return(void)(s[x] = c, lz[x] = 1);
    int mid = l + r >> 1; pd(x, l, r);
    if(L <= mid)mdfy(ls, l, mid, L, R, c);
    if(R > mid)mdfy(rs, mid + 1, r, L, R, c);
    upd(x);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return s[x];
    int mid = l + r >> 1, res = 0; pd(x, l, r);
    if(L <= mid)res = qry(ls, l, mid, L, R);
    if(R > mid)res += qry(rs, mid + 1, r, L, R);
    return res;
}
void mpath(int u, int v, int c){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        mdfy(1, 1, n, dfn[tp[u]], dfn[u], c);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    mdfy(1, 1, n, dfn[u] + 1, dfn[v], c);
}
int qpath(int u, int v){
    int res = 0;
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        res += qry(1, 1, n, dfn[tp[u]], dfn[u]);
        u = fa[tp[u]];
    }
    if(dep[u] > dep[v])swap(u, v);
    return res + qry(1, 1, n, dfn[u] + 1, dfn[v]);
}

signed main(){
    n = rd(), m = rd();
    for(int i = 1; i <= m; ++i){
        E[i].u = rd(), E[i].v = rd();
        if(E[i].u > E[i].v)swap(E[i].u, E[i].v);
        ++mp[mkp(E[i].u, E[i].v)];
    }
    while(1){
        q[++tot][0] = rd();
        if(q[tot][0] == - 1)break;
        q[tot][1] = rd(), q[tot][2] = rd();
        if(q[tot][1] > q[tot][2])swap(q[tot][1], q[tot][2]);
        if(! q[tot][0])--mp[mkp(q[tot][1], q[tot][2])];
    }
    --tot;
    for(int i = 1; i <= m; ++i){
        int u = E[i].u, v = E[i].v;
        if(mp[mkp(u, v)])addx(u, v), addx(v, u);
    }
    tarjan(1, 0);
    for(int i = 1; i <= m; ++i){
        int u = E[i].u, v = E[i].v;
        if(mp[mkp(u, v)] and col[u] ^ col[v])add(col[u], col[v]), add(col[v], col[u]);
    }
    for(int i = 1; i <= tot; ++i)q[i][1] = col[q[i][1]], q[i][2] = col[q[i][2]];

    dfs1(1, 0); dfs2(1, 1); build(1, 1, n);
    for(int i = tot; i; --i){
        if(q[i][0])ans[++tt] = q[i][1] == q[i][2] ? 0 : qpath(q[i][1], q[i][2]);
        else mpath(q[i][1], q[i][2], 0);
    }
    for(int i = tt; i; --i)printf("%d\n", ans[i]);
    return 0;
}

P4211 [LNOI2014] LCA

對於這種題我們需要翻譯題目資訊。

像文中的兩個點求 lca 深度的過程我們能不能用另一種視角去看待?我們可以看成一個點到根的路徑加一,另一個點再查詢其到一的路徑的權值和。然後就好做了。具體的講,我們可以把區間與點求 lca 的深度轉換成兩個字首和相減,這不就直接離線以後掃描線掃一遍的事?

void dfs1(int u, int f){
	dep[u] = dep[f] + 1, sz[u] = 1;
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to;
		dfs1(v, u); sz[u] += sz[v];
		if(sz[son[u]] < sz[v])son[u] = v;
	}
}
void dfs2(int u, int tp){
	top[u] = tp, dfn[u] = ++tim, rk[tim] = u;
	if(son[u])dfs2(son[u], tp);
	for(int i = hd[u]; i; i = e[i].nxt){
		int v = e[i].to; if(v ^ son[u])dfs2(v, v);
	}
}

int s[N << 2], tg[N << 2];
#define ls x << 1
#define rs x << 1 | 1

int ad(int x, int y){
	return x += y, x >= p ? x - p : x;
}

void upd(int x){
	s[x] = ad(s[ls], s[rs]);
}
void pd(int x, int l, int r){
	if(! tg[x])return; int mid = l + r >> 1;
	tg[ls] = ad(tg[ls], tg[x]), tg[rs] = ad(tg[rs], tg[x]);
	s[ls] = ad(s[ls], tg[x] * (mid - l + 1) % p);
	s[rs] = ad(s[rs], tg[x] * (r - mid) % p);
	return(void)(tg[x] = 0);
}
void mdf(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return(void)(s[x] = ad(s[x], (r - l + 1) >= p ? r - l + 1 - p : r - l + 1), ++tg[x]);
	int mid = l + r >> 1; pd(x, l, r);
	if(ql <= mid)mdf(ls, l, mid, ql, qr);
	if(mid < qr)mdf(rs, mid + 1, r, ql, qr);
	upd(x);
}
int qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r <= qr)return s[x];
	int mid = l + r >> 1, res = 0; pd(x, l, r);
	if(ql <= mid)res = qry(ls, l, mid, ql, qr);
	if(mid < qr)res = ad(res, qry(rs, mid + 1, r, ql, qr));
	return res;
}

void up(int u){
	while(top[u] ^ 1){
		mdf(1, 1, n, dfn[top[u]], dfn[u]);
		u = fa[top[u]];
	}
	mdf(1, 1, n, 1, dfn[u]);
}

int fd(int u){
	int sum = 0;
	while(top[u] ^ 1){
		sum = ad(sum, qry(1, 1, n, dfn[top[u]], dfn[u]));
		u = fa[top[u]];
	}
	return ad(sum, qry(1, 1, n, 1, dfn[u]));
}

struct qay{
	int x, id, o;
};
vector < qay > q[N];

int ans[N];

int main(){
	rd(n, Q);
	for(int i = 2; i <= n; ++i)rd(fa[i]), add(++fa[i], i);
	dfs1(1, 0), dfs2(1, 1);
	for(int i = 1, l, r, x; i <= Q; ++i){
		rd(l, r, x);
		q[++r].pb({++x, i, 1});
		q[l].pb({x, i, - 1});
	}
	for(int i = 1; i <= n; ++i){
		up(i);
		for(qay j : q[i])ans[j.id] = (ans[j.id] + p + j.o * fd(j.x)) % p;
	}
	for(int i = 1; i <= Q; ++i)wt(ans[i]), pc('\n');
	return 0;
}

P4069 [SDOI2016] 遊戲

感覺找的題是不是太簡單了

也許看到修改的式子中帶了一個 \(dis\) 你會馬上想到差分,但是仔細一想似乎不太對。因為它每次詢問路徑最小值,但是每次的操作相對獨立,這樣一看您覺得這是否更像線段覆蓋的題。

想通了後這道題的做法就呼之欲出了,直接樹剖套李超線段樹板子做完。但是加線段的地方還要在 lca 處分討一下。

void dfs1(int u, int f){
    dep[u] = dep[fa[u] = f] + 1; cnt[u] = 1;
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to; if(v == f)continue; dis[v] = dis[u] + e[i].w;
        dfs1(v, u); if(cnt[son[u]] < cnt[v])son[u] = v;
        cnt[u] += cnt[v];
    }
}
void dfs2(int u, int top){
    tp[u] = top;
    dfn[u] = ++seq; rk[dfn[u]] = u;
    if(! son[u])return; dfs2(son[u], top);
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to;
        if(v == fa[u] or v == son[u])continue;
        dfs2(v, v);
    }
}
int lca(int u, int v){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        u = fa[tp[u]];
    }
    return dep[u] < dep[v] ? u : v;
}

ll k[N], b[N], c[N << 3];
#define ls x << 1
#define rs x << 1 | 1
ll calc(int x, int pos){
    return k[x] * dis[rk[pos]] + b[x];
}
bool cmp(int x1, int x2, int pos){
    ll a = calc(x1, pos), b = calc(x2, pos);
    return a < b or (a == b and x1 < x2); 
}
void updd(int x, int l, int r){
    ll a = calc(sgt[x], l), b = calc(sgt[x], r);
    c[x] = min(c[x], min(a, b));
    c[x] = min(c[x], min(c[ls], c[rs]));
}
void _upd(int tmp, int x, int l, int r){
    int mid = l + r >> 1;
    if(cmp(tmp, sgt[x], mid))swap(tmp, sgt[x]);
    if(cmp(tmp, sgt[x], l))_upd(tmp, ls, l, mid);
    if(cmp(tmp, sgt[x], r))_upd(tmp, rs, mid + 1, r);
    updd(x, l, r);
}
void upd(int x, int l, int r, int L, int R, int tmp){
    if(L <= l and r <= R)return _upd(tmp, x, l, r);
    int mid = l + r >> 1;
    if(L <= mid)upd(ls, l, mid, L, R, tmp);
    if(R > mid)upd(rs, mid + 1, r, L, R, tmp);
    updd(x, l, r);
}
ll qry(int x, int l, int r, int L, int R){
    if(L <= l and r <= R)return c[x];
    int mid = l + r >> 1; ll res = min(calc(sgt[x], max(l, L)), calc(sgt[x], min(r, R)));
    if(L <= mid)res = min(res, qry(ls, l, mid, L, R));
    if(R > mid)res = min(res, qry(rs, mid + 1, r, L, R));
    return res;
}
void usum(int u, int v, int tmp){
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        upd(1, 1, n, dfn[tp[u]], dfn[u], tmp);
        u = fa[tp[u]];
    }
    if(dfn[u] < dfn[v])swap(u, v);
    upd(1, 1, n, dfn[v], dfn[u], tmp);
}
ll qsum(int u, int v){
    ll res = INF;
    while(tp[u] != tp[v]){
        if(dep[tp[u]] < dep[tp[v]])swap(u, v);
        res = min(res, qry(1, 1, n, dfn[tp[u]], dfn[u]));
        u = fa[tp[u]];
    }
    if(dfn[u] < dfn[v])swap(u, v);
    res = min(res, qry(1, 1, n, dfn[v], dfn[u]));
    return res;
}

signed main(){
    for(int i = 0; i < N << 3; ++i)c[i] = INF;
    n = rd(), m = rd(); int lcnt = 0;
    for(int i = 1; i < n; ++i){
        int u = rd(), v = rd(); ll w = rd();
        add(u, v, w); add(v, u, w);
    }
    dfs1(1, 0); dfs2(1, 1); b[0] = INF;
    for(int i = 1; i <= m; ++i){
        int op = rd(), u = rd(), v = rd();
        if(op ^ 1)printf("%lld\n", qsum(u, v));
        else{
            int t = lca(u, v);
            ll A = rd(), B = rd();
            k[++lcnt] = - A; b[lcnt] = A * dis[u] + B;
            usum(u, t, lcnt);
            k[++lcnt] = A; b[lcnt] = A * (dis[u] - 2 * dis[t]) + B;
            usum(t, v, lcnt);
        }
    }
    return 0;
}

線段樹與 dp

dp 有關的技巧與知識在此不贅述,可以翻一下我寫的 dp 的一些部落格。這裡是線段樹專題,所以我想講一點線段樹的東西。

首先是何時用線段樹的問題。如果 dp 式子中出現了需要蔥區間轉移的東西我們會先考慮記一下關鍵資訊,轉移時直接合並,當我們無法快速查詢或合併時我們就會上線段樹。

然後就是線段樹需要維護什麼?我們啟用線段樹的初衷是要維護一些不太好用陣列記錄的資訊,或是轉移或查詢的時候不方便的東西。所以我們在維護線段樹過程中就應該結合實際去記錄,而不是把原來的資訊原封不動扔進去。比如原來的資訊差分後有良好的性質我就只需要把差分的東西丟進去,查詢時在進行合併。

最後是選用什麼線段樹。當我們維護的資訊只有可合併的特點時我們就上最普通的線段樹,如果資訊具有一定的可加可減性可以考慮可持久化。如果是高維資訊就先看能不能 cdq,然後看樹套樹,最後考慮 kd-tree(雖然我不會這個,也就只能看看)。

P1442 鐵球落地

因為是我很早以前做過的,還寫了題解,所以就摘錄的當時題解的原話。

因為鐵球從高到低落下去,則每一個平臺最短時間的貢獻只會來源於它頭上的一個(或幾個)平臺,所以是線性的,於是考慮dp。

考慮按照由低到高的順序dp。令 \(dp[i][0/1]\) 表示從 \(x\) 軸到第 \(i\) 個平臺的左/右端點最短用時。

首先可以推出第 \(i\) 個平臺上任意一點 \((k,a_i.h)\)\(x\) 軸最短用時為:\(\min(k-a_i.l+dp[i][0],a_i.r-k+dp[i][1])\)

然後平臺間轉移:

\[dp[i][0]=\begin{cases}a_i.h&a_i.h<=H\\a_i.h-a_j.h+\min(a_i.l-a_j.l+dp[j][0],a_j.r-a_i.l+dp[j][1])&a_i.h-a_j.h<=H\end{cases} \]

就可以用資料結構維護 \(j\) 了。具體的,每次更新 \(i\) 的答案後用當前平臺編號覆蓋之前的平臺編號,但是資料太大需要離散化一下。維護用線段樹和 set 均可。

所以說這道題就這麼被我用 set 水過了。但是因為我非常負責所以還是放一個程式碼:

int n, m, x, y, f[N][2];
struct line{
    int h, l, r;
}a[N];
set < pair < int, int > > s;
bool cmp(line x, line y){
    return x.h < y.h;
}

void upd(int &ff, int nw, int h, int j){
    if(h - a[j].h > m)return;
    if(! j)return(void)(ff = h);
    ff = h - a[j].h + min(nw - a[j].l + f[j][0], a[j].r - nw + f[j][1]);
}

signed main(){
    n = rd(); m = rd(); x = rd(); y = rd();
    for(int i = 1; i <= n; ++i)a[i].h = rd(), a[i].l = rd(), a[i].r = rd();
    memset(f, INF, sizeof f); sort(a + 1, a + 1 + n, cmp);
    s.insert({INF, 0});
    for(int i = 1; i <= n; ++i){
        int lx = a[i].l, rx = a[i].r, hx =  a[i].h;
        auto lp = s.lower_bound({lx, 0}), rp = s.lower_bound({rx, 0});
        upd(f[i][0], lx, hx, lp -> second); upd(f[i][1], rx, hx, rp -> second);
        int tg = lp -> second;
        for(; lp -> first <= rx; lp = s.erase(lp)); s.insert({lx, tg}); s.insert({rx, i});
    }
    int res; upd(res, x, y, s.lower_bound({x, 0}) -> second);
    printf("%d", res);
    return 0;
}

看一道典題。

P4655 [CEOI2017] Building Bridges

這個 dp 似乎就很一眼。

\(f_i\) 表示從 \(1\) 走到 \(i\) 的最小代價,然後轉移方程就是:

\[f_i=\min_{j<i}\{f_j+(h_i-h_j)^2+\sum_{k=j+1}^{i-1}w_k\} \]

\(w\) 記一個字首和,並把式子拆一拆,就變成了:

\[f_i=\min_{j<i}\{f_j+h_i^2-2h_ih_j+h_j^2+s_{i-1}-s_j\} \]

然後把不變的提出來:

\[f_i=\min_{j<i}\{f_j-2h_ih_j+h_j^2-s_j\}+h_i^2+s_{i-1} \]

現在的目標就是如何快速查詢 \(\min\)。如果你對於每一個 \(j\) 把所有與其有關的東西看成一個函式,再把 \(i\) 有關的 \(h_i\) 看成一個變數,我是不是就把 \(\min\) 裡面的一坨抽象成了一個一次函式。我令 \(k=h_j,b=f_j+h_j^2-s_j\),然後這一坨就變成了 \(k\times h_i+b\),問題就變成在當前所有線段中查詢位置 \(h_i\) 處的最小值,於是就李超線段樹了。

int n;
ll h[N], w[N];
ll s[N], f[N];
namespace SGT{
	ll k[N], b[N];
	int sgt[M << 2];
	#define ls x << 1
	#define rs x << 1 | 1
	const int lim = M - 3;
	ll calc(int x, int pos){return k[x] * pos + b[x];}
	bool cmp(int x1, int x2, int pos){
		ll t1 = calc(x1, pos), t2 = calc(x2, pos);
		return t1 < t2;
	}
	void upd(int x, int l, int r, int id){
		int mid = l + r >> 1;
		if(cmp(id, sgt[x], mid))swap(sgt[x], id);
		if(cmp(id, sgt[x], l))upd(ls, l, mid, id);
		if(cmp(id, sgt[x], r))upd(rs, mid + 1, r, id);
	}
	void mdf(int x, int l, int r, int ql, int qr, int id){
		if(ql <= l and r <= qr)return(void)(upd(x, l, r, id));
		int mid = l + r >> 1;
		if(ql <= mid)mdf(ls, l, mid, ql, qr, id);
		if(mid < qr)mdf(rs, mid + 1, r, ql, qr, id);
	}
	int qry(int x, int l, int r, int pos){
		if(l == r)return sgt[x];
		int mid = l + r >> 1, res;
		res = pos <= mid ? qry(ls, l, mid, pos) : qry(rs, mid + 1, r, pos);
		return cmp(res, sgt[x], pos) ? res : sgt[x];
	}
}
using namespace SGT;

int main(){
	rd(n); for(int i = 1; i <= n; ++i)rd(h[i]); b[0] = inf;
	for(int i = 1; i <= n; ++i)rd(w[i]), s[i] = s[i - 1] + w[i];
	for(int i = 1; i <= n; ++i){
		int j = qry(1, 0, lim, h[i]);
		if(i ^ 1)f[i] = f[j] + h[j] * h[j] - s[j] - 2 * h[i] * h[j] + h[i] * h[i] + s[i - 1];
		k[i] = - 2 * h[i]; b[i] = f[i] + h[i] * h[i] - s[i];
		mdf(1, 0, lim, 0, lim, i);
	}
	wt(f[n]);
	return 0;
}

Problem - 1476F - Codeforces

考慮令 \(f_i\) 表示前 \(i\) 盞燈能夠照亮的最大字首。考慮轉移。

對於當前的燈有兩種情況,一種是前面的燈照不到它,這時就不用管直接跳過,還有就是照的到就更新當前最大值。

但實際上還有一種就是當前的朝左,然後能被它照到的就全向右,這時 \(f_i=\max_{i-p_i\le j<i}\{j+p_j\}\)

然後其實用 st 表就可以維護了,但是這道題有一定的價值所以我還是放到這裡了。

最後輸出方案就倒推一遍即可。

void init(int nn){
	f[0] = f[1] = 0;
	for(int i = 1; i <= nn; ++i)op[i] = 0;
}
void make_st(){
	for(int j = 1; j <= lg[n]; ++j)
		for(int i = 1; i + (1 << j) - 1 <= n; ++i)st[i][j] = max(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
}
int query(int l, int r){
	if(l > r)return 0;
	int j = lg[r - l + 1];
	return max(st[l][j], st[r - (1 << j) + 1][j]);
}

int find(int l, int r, int x){
	int id = - 1;
	while(l <= r){
		int mid = l + r >> 1;
		if(f[mid] >= x)id = mid, r = --mid;
		else l = ++mid;
	}
	return id;
}

void solve(int x){
	if(! x)return;
	if(f[x] == f[x - 1])return (void)solve(x - 1);
	if(f[x] == st[x][0] and f[x - 1] >= x)return (void)(op[x] = 1, solve(x - 1));
	int y = find(0, x - 1, x - p[x] - 1);
	for(int i = y + 1; i < x; ++i)op[i] = 1;
	solve(y);
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	int T; cin >> T;
	for(int i = 2; i < N; ++i)lg[i] = lg[i >> 1] + 1;
	while(T--){
		init(n);
		cin >> n;
		for(int i = 1; i <= n; ++i)cin >> p[i], st[i][0] = i + p[i];
		make_st();
		for(int i = 1; i <= n; ++i){
			f[i] = f[i - 1];
			if(f[i] >= i)f[i] = max(f[i], st[i][0]);
			int id = find(0, i - 1, i - p[i] - 1);
			if(id != - 1)f[i] = max(f[i], max(i - 1, query(id + 1, i - 1)));
		}
//		for(int i = 1; i <= n; ++i)cout << "f[" << i << "] = " << f[i] << ' '; cout << '\n';
		if(f[n] < n){
			cout << "NO" << '\n';
			continue;
		}
		cout << "YES" << '\n';
		solve(n);
		for(int i = 1; i <= n; ++i)cout << (op[i] ? 'R' : 'L');
		cout << '\n';
	}
	return 0;
}

線段樹最佳化建圖

顧名思義,就是在建圖過程中可能會出現點向區間連邊或區間向點連邊的情況,這時就需要線段樹最佳化建圖,只是圖上操作時複雜度多一隻 \(\log\)

Problem - 786B - Codeforces

這道題就是出現了上述情況,在此重點講建圖的方法。首先我們需要滿足區間能夠與點聯絡起來,這就需要線段樹的節點與點要相連。我們可以效仿線段樹維護資訊的方式,將樹上的節點一層一層連起來,最後葉子節點其實就代表真正的點。

其次是我們多連的這些虛邊不能影響答案。這時邊權的設計就需要仔細考慮。比如此題是求最短路,所以我把邊權設為零,那如果是跑網路流,我們的邊權(容量)就應該設成正無窮。

下面程式碼只展示連邊方法,相信能夠看到這裡的讀者都會最短路。

void build(int x, int l, int r){
    t[x].l = l, t[x].r = r;
    if(l == r){
        int n1 = l + (n << 3), n2 = x + (n << 2);
        add(n1, x, 0); add(x, n1, 0);
        add(n1, n2, 0); add(n2, n1, 0);
        return;
    }
    int mid = l + r >> 1, t = n << 2;
    add(x, ls, 0); add(x, rs, 0);
    add(ls + t, x + t, 0); add(rs + t, x + t, 0);
    build(ls, l, mid); build(rs, mid + 1, r);
}
void modify(int x, int L, int R, int u, ll w, bool op){
    int l = t[x].l, r = t[x].r, mid = l + r >> 1;
    int t1 = n << 2, t2 = n << 3;
    if(L == l and r == R){
        if(op)return (void)(add(x + t1, u + t2, w));
        else return (void)(add(u + t2, x, w));
    }
    if(R <= mid)modify(ls, L, R, u, w, op);
    else if(L > mid)modify(rs, L, R, u, w, op);
    else{
        modify(ls, L, mid, u, w, op);
        modify(rs, mid + 1, R, u, w, op);
    }
}
signed main(){
    //freopen(,stdin);
    //freopen(,stdout);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> q >> s;
    build(1, 1, n);
    for(int i = 1, op, u, v, l, r; i <= q; ++i){
        cin >> op >> u;
        ll w;
        if(op == 1){
            cin >> v >> w;
            add(u + (n << 3), v + (n << 3), w);
        }
        else{
            cin >> l >> r >> w;
            modify(1, l, r, u, w, op - 2);
        }
    }
    s += (n << 3);
    dijkstra();
    for(int i = 1, t = n << 3; i <= n; ++i){
        if(d[i + t] < 2e18)cout << d[i + t] << ' ';
        else cout << - 1 << ' ';
    }
    return 0;
}

P6348 [PA2011] Journeys

問題升級!如果是區間向區間連邊呢?如果把區間抓出來對應連是 \(O(\log^2)\) 的,然後 bfs 也是 \(O(\log^2)\) 的所以就會被卡。這時我們可以繼續建虛點,將區間與虛點連邊即可。

void build(int x, int l, int r){
    t[x].l = l, t[x].r = r;
    if(l == r)return (void)(id[l] = x);
    int mid = l + r >> 1, t = n << 2;
    add(x, ls, 0); add(x, rs, 0);
    add(ls + t, x + t, 0); add(rs + t, x + t, 0);
    build(ls, l, mid); build(rs, mid + 1, r);
}
void modify(int x, int L, int R, int u, bool op){
    int l = t[x].l, r = t[x].r, mid = l + r >> 1;
    if(l == L and r == R){
        if(op)return (void)(add(x + (n << 2), u, 0));
        return (void)(add(u, x, 0));
    }
    if(R <= mid)modify(ls, L, R, u, op);
    else if(L > mid)modify(rs, L, R, u, op);
    else{
        modify(ls, L, mid, u, op);
        modify(rs, mid + 1, R, u, op);
    }
}

signed main(){
    //freopen(,stdin);
    //freopen(,stdout);
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m >> s;
    build(1, 1, n);
    cnt = n << 3;
    for(int i = 1, l1, l2, r1, r2; i <= m; ++i){
        int u = ++cnt, v = ++cnt;
        cin >> l1 >> r1 >> l2 >> r2;
        add(v, u, 1); modify(1, l2, r2, u, 0); modify(1, l1, r1, v, 1);
        u = ++cnt, v = ++cnt;
        add(v, u, 1); modify(1, l1, r1, u, 0); modify(1, l2, r2, v, 1);
    }
    for(int i = 1, t = n << 2; i <= n; ++i){
        add(id[i], id[i] + t, 0);
        add(id[i] + t, id[i], 0);
    }
    s = id[s] + (n << 2);
    bfs();
    for(int i = 1; i <= n; ++i)cout << d[id[i]] << '\n';
    return 0;
}

然後車站分級大家應該都做過吧?如果把它的資料範圍改到 \(n=10^5\) 又該怎麼做?

考慮線段樹最佳化建圖。每一趟車相當於一些區間向一些點連邊,就像上面的題一樣,每次新建一個虛點,區間連向虛點、虛點連向單點。只不過因為要跑 toposort,所以線段樹上的邊需要改成單向邊,這時注意方向一定是從葉子連向根。程式碼懶得找就不放了。

線段樹的更難的應用(雜題)

P10856 【MX-X2-T5】「Cfz Round 4」Xor-Forces

一道非常不錯的題!

先找題目性質,然後發現操作一可以合併,所以只用考慮如何快速做完操作一併得到操作二的答案。先考慮如何在動態序列上維護顏色段,我們可以維護滿足 \(col_{i-1}=col_{i}=col_{i+1}\) 的數量再用總數減去就是答案。對於區間的端點我們暴力判斷就行因為不會超過 \(O(\log n)\)。那麼操作一又如何處理呢?

考慮每次操作後有的區間會交換但是有的卻保持相對靜止,然後建議隨便寫幾個數用紙和筆手摸一下操作。

你會發現我其實是把 \([0,1]\) 這個區間平移到了 \([6,7]\),其他可以類比。試想對於線段樹上一段長度為 \(len\) 的區間,我做操作一後出現的情況只會有 \(len\) 種。設異或上 \(sum\),若 \(sum>len\) 其實就可以看成 \(sum\bmod len\),因為把這些東西都放在二進位制下你會發現他多出來的那些數位上的一都沒有用,而 \(sum\) 取零到 \(len-1\) 時序列的變換方式又互不相同,所以有 \(len\) 種情況。然後我其實可以直接把這些情況記下來。時間複雜度是 \(\sum_{i=0}^{2^i\le len}\frac{len}{2^i}=2\times len\) 的,所以建樹總複雜度還是 \(O(n\log n)\) 的。

然後其實就直接線上段樹上走就行了,對於查詢到的區間,若當前操作的值小於等於就可以直接把答案取出來,否則就需要平移到同一層的某個點上,呼叫它的答案。對於平移操作我們又如何處理呢?

我們可以記錄一個陣列 \(jump_{l,i}\) 代表序列上左端點是 \(l\) 且線段樹節點大小為 \(2^i\) 的點是誰,然後跳的時候相當於二進位制下數位小於等於當前走到的點就直接呼叫平移後的答案,大於等於的部分就異或上區間左端點直接跳,最後中點的答案就暴力合併即可。

(如果有點暈建議結合上面的圖畫一下)

程式碼:

void bld(int x, int l, int r){
	int len = r - l, mid = l + r >> 1;
	jump[l][lg[len]] = x; f[x].rsz(len);
	if(l + 1 == r)return;
	bld(ls, l, mid), bld(rs, mid, r);
	for(int i = 0; i < len; ++i)
		if(i < (len >> 1))f[x][i] = f[ls][i] + f[rs][i] + (a[mid - 1 ^ i] == a[mid ^ i]);
		else f[x][i] = f[ls][i - (len >> 1)] + f[rs][i - (len >> 1)] + (a[mid - 1 ^ i] == a[mid ^ i]);
}
int qry(int x, int l, int r, int ql, int qr){
	if(ql <= l and r - 1 <= qr)return f[jump[l ^ (opt >> lg[r - l] << lg[r - l])][lg[r - l]]][opt % (1 << lg[r - l])];
	int mid = l + r >> 1, ansl(0), ansr(0); bool fl(false), fr(false);
	if(ql < mid)fl = true, ansl = qry(ls, l, mid, ql, qr);
	if(mid <= qr)fr = true, ansr = qry(rs, mid, r, ql, qr);
	return fl ? (fr ? (ansl + ansr + (a[mid ^ opt] == a[mid - 1 ^ opt])) : ansl) : ansr;
}

P2757 [國家集訓隊] 等差子序列

此題性質就是考慮只要滿足有三個數滿足條件就行。然後中間的這個數是最特殊的!因為輸入的數構成排列,假設我對當前點左邊的點全染黑色(1),右邊全染白色(0),如果 \(\exists t,col_{x+t}=1,col_{x-t}=0\) 那麼就說明找到了。

然後我們就考慮如何快速查詢與更新。如果像上面一樣給位置染色,我如何快速判斷呢?我可以在值域上染色,把這個 01 序列看成一個高位數,實際操作時我可以對於黑點和白點各維護一個高位數,然後就去比較他們的某幾位是否是迴文數。細想,這不就是雜湊判斷迴文串嗎?於是兩棵線段樹維護正/反雜湊就行了。

但是因為以前資料過水導致我用 bitset 艹過了,現在也懶得寫程式碼於是就咕了。

P4197 Peaks

觀察到限制是 \(\le x\) 所以我們直接離線下來按限制從小到大排序然後線段樹合併暴力做。

P7834 [ONTAK2010] Peaks 加強版考慮加強

現在你不可以離線。又應該這麼做呢?

其實做法還是基於此題的限制 \(\le x\)。因為有這個關係,所以我們可以建 kruskal 重構樹。這樣如果兩個點能夠互相抵達當且僅當它們的 lca 小於等於 x,於是我們就從當前點一直往上跳,直到不能再走到其他點。這一過程可以倍增實現,並且倍增同時能夠維護出最大值。然後跳到最高點後我們就用主席樹查詢即可。

struct Edge{
    int u, v; ll w;
    friend bool operator < (Edge a, Edge b){
        return a.w < b.w;
    }
}E[M];
struct edge{
    int nxt, to;
}e[M << 1];
void add(int u, int v){
    e[++cnt] = {hd[u], v}, hd[u] = cnt; ++in[v];
}
int fd(int x){
    return x == ff[x] ? x : ff[x] = fd(ff[x]);
}
void dfs(int u, int fa){
    lq[dfn[++tim] = u] = tim, f[u][0] = fa;
    for(int i = 1; (1 << i) <= n; ++i)f[u][i] = f[f[u][i - 1]][i - 1];
    for(int i = hd[u]; i; i = e[i].nxt){
        int v = e[i].to; if(v == fa)continue;
        dfs(v, u), sz[u] += sz[v];
    }
    if(! sz[u])sz[u] = 1; rq[u] = tim;
}

int rt[N], ls[N << 5], rs[N << 5], t[N << 5];
void upd(int &x, int y, int l, ll r, int pos, int val){
    if(! x)x = ++tot; t[x] = t[y] + val;
    if(l == r)return; ll mid = l + r >> 1;
    if(pos <= mid)rs[x] = rs[y], upd(ls[x], ls[y], l, mid, pos, val);
    else ls[x] = ls[y], upd(rs[x], rs[y], mid + 1, r, pos, val);
}
int kth(int x, int y, int l, ll r, int k){
    if(l == r)return l; ll mid = l + r >> 1, s = t[rs[y]] - t[rs[x]];
    if(k > s)return kth(ls[x], ls[y], l, mid, k - s);
    else return kth(rs[x], rs[y], mid + 1, r, k);
}
int qry(int u, int lim, int k){
    for(int i = 23; ~ i; --i)if(f[u][i] and c[f[u][i]] <= lim)u = f[u][i];
    if(sz[u] < k)return - 1;
    return kth(rt[lq[u] - 1], rt[rq[u]], 0, inf, k);
}

signed main(){
    // fileio(fil);
    int mod;
    mod = n = rd(), m = rd(), q = rd();
    for(int i = 1; i <= n; ++i)h[i] = rd();
    for(int i = 1; i <= (n << 1); ++i)ff[i] = i;
    for(int i = 1; i <= m; ++i){
        int u = rd(), v = rd(); ll w = rd();
        E[i] = {u, v, w};
    }
    sort(E + 1, E + 1 + m);
    for(int i = 1; i <= m; ++i){
        int u = fd(E[i].u), v = fd(E[i].v); ll w = E[i].w;
        if(u == v)continue;
        ff[u] = ff[v] = ++n; c[n] = w;
        add(n, u), add(n, v);
    }
    for(int i = 1; i <= n; ++i)if(! in[i])dfs(i, 0);
    for(int i = 1; i <= n; ++i){
        if(dfn[i] <= mod)upd(rt[i], rt[i - 1], 0, inf, h[dfn[i]], 1);
        else rt[i] = rt[i - 1];
    }
    ll lastans = 0;
    for(int i = 1; i <= q; ++i){
        ll u = (rd() ^ lastans) % mod + 1, lim = rd() ^ lastans, k = (rd() ^ lastans) % mod + 1;
        printf("%lld\n", lastans = qry(u, lim, k)); lastans = max(0ll, lastans);
    }
    return 0;
}

P4898 [IOI2018] seats 排座位

評價是:真的神仙題!

我們需要思考的是如何翻譯題目。我們假設已經被安排的座位是一(黑點),否則是零(白點)。正常想法是類比一維的時候,我們記錄區間內一的個數,然後看是否等於區間大小。這可以線段樹維護。但是考慮到調換座位的操作會對一段區間造成影響,如果我每個都要 \(O(1)\) 修改肯定就炸了,我們就需要一種可以合併的資訊去代替現在的。

區間操作我們可以想到區間加,那麼我們現在能不能記錄一些可加的資訊來描述合法情況?

我們來考慮每一個點的情況,假設當前狀態已經合法,現在是一個矩形然後四周可能有一圈白點。假設這個點是黑點,想想它滿足什麼性質時矩形合法而什麼時候矩形不合法。本題最妙的地方來了!我們可以關注它周圍點的狀態來確定是否合法。具體的,如果這個點左邊的點和上面的點有黑點,那麼他就只能是矩形的一部分,否則它就必定為矩形的左上角,而左上角顯然只有一個。也就是說我們要維護黑點左邊和上面都是白點的黑點的數量,當數量為一時滿足條件。

然後考慮白點周圍狀態。如果它周圍有至少兩個黑點那麼當前狀態就不合法。所以我們可以再記錄一下周圍有至少兩個黑點的白點的數量,當數量為零時滿足條件。

然後聰明如你,你會發現這兩個狀態需要同時滿足,所以可以把它們放在一起維護。最後就是支援區間加,查詢全域性最小值數量。修改操作其實就是暴力交換後周圍的一些點可能會有影響,把這些點重新算一遍就好了。

const int dir[4][2] = {1, 0, - 1, 0, 0, 1, 0, - 1};

int h, w, q;
vector < vector < int > > g;
int tt[N];

struct node{
	int x, y, tim;
}a[N];

int res[N << 2], cnt[N << 2], tg[N << 2];

#define ls x << 1
#define rs x << 1 | 1

void upd(int x){
	res[x] = min(res[ls], res[rs]);
	cnt[x] = cnt[ls] * (res[x] == res[ls]) + cnt[rs] * (res[x] == res[rs]);
}
void init(int x, int l, int r){
	if(l == r)return(void)(cnt[x] = 1);
	int mid = l + r >> 1;
	init(ls, l, mid), init(rs, mid + 1, r);
	upd(x);
}
void pd(int x){
	if(! tg[x])return;
	tg[ls] += tg[x], res[ls] += tg[x];
	tg[rs] += tg[x], res[rs] += tg[x];
	return(void)(tg[x] = 0);
}
void mdf(int x, int l, int r, int ql, int qr, int y){
	if(ql <= l and r <= qr)return(void)(res[x] += y, tg[x] += y);
	int mid = l + r >> 1; pd(x);
	if(ql <= mid)mdf(ls, l, mid, ql, qr, y);
	if(mid < qr)mdf(rs, mid + 1, r, ql, qr, y);
	upd(x);
}

int get(int x, int y){
	if(x < 0 or y < 0 or x >= h or y >= w)return inf;
	return a[g[x][y]].tim;
}
int loc(int x, int y){
	if(x < 0 or y < 0 or x >= h or y >= w)return - 1;
	return g[x][y];
}

void option(int i, int op){
	vector < int > lp; int t1, t2;
	lp.pb(get(a[i].x + 1, a[i].y));
	lp.pb(get(a[i].x, a[i].y + 1));
	lp.pb(t1 = get(a[i].x - 1, a[i].y));
	lp.pb(t2 = get(a[i].x, a[i].y - 1));
	sort(lp.bg(), lp.ed()); int lpos = lp[1];
	if(a[i].tim > lpos)mdf(1, 1, h * w, lpos, a[i].tim - 1, op);
	t1 = min(t1, t2); if(t1 > a[i].tim)mdf(1, 1, h * w, a[i].tim, t1 - 1, op);
}

int main(){
	rd(h, w, q); g.rsz(h); for(int i = 0; i < h; ++i)g[i].rsz(w);
	for(int i = 1; i <= h * w; ++i){
		rd(a[i].x, a[i].y);
		g[a[i].x][a[i].y] = a[i].tim = i;
		tt[i] = i;
	}
	init(1, 1, h * w);
	for(int i = 1; i <= h * w; ++i)option(i, 1);
	for(int i = 1, u, v; i <= q; ++i){
		rd(u, v); ++u, ++v;
		int used[12], cc;
		used[1] = u = tt[u], used[cc = 2] = v = tt[v];
		int xx = a[u].x, yy = a[u].y;
		for(int j = 0; j < 4; ++j){
			int tmp = loc(xx + dir[j][0], yy + dir[j][1]);
			if(~ tmp)used[++cc] = tmp;
		}
		xx = a[v].x, yy = a[v].y;
		for(int j = 0; j < 4; ++j){
			int tmp = loc(xx + dir[j][0], yy + dir[j][1]);
			if(~ tmp)used[++cc] = tmp;
		}
		sort(used + 1, used + 1 + cc); cc = unique(used + 1, used + 1 + cc) - used - 1;
		for(int j = 1; j <= cc; ++j)option(used[j], - 1);
		swap(a[u].tim, a[v].tim);
		swap(tt[a[u].tim], tt[a[v].tim]);
		for(int j = 1; j <= cc; ++j)option(used[j], 1);
		wt(cnt[1]); pc('\n');
	}
	return 0;
}

後記

從起稿開始(2024.11.6 16:00)到竣工(2024.11.8.15.26)一共花了我兩天時間。其間我寫了十道題左右,並重新梳理了暑假講的和國慶拔高的幾乎所有題目,當然一些太逆天的題我還是沒能寫完。從明天開始我就準備複習 dp 部分的內容,但可能很難再像這次寫下這洋洋幾千上萬字了。今天下午,隨著半期考試的落幕,沒有停課的同學們也會陸續歸隊,機房不再冷清。但這樣安靜投入的去做一件事情是真的很好!每天回宿舍都很疲憊,晚上冼個澡,躺下就睡,早上總是 6:30 準時醒,生物鐘就這樣形成。

不知又要到何時,我內心才能如此平靜?或許,改變自己的心態,既是一種挑戰,也是一份禮物。當你成功克服自我後,也許會收穫意想不到的驚喜!

但無論如何,ds 方面的內容算是弄完了。cdq 和整體二分我會在今天下午後半程複習完畢,下一次很系統的複習應該就要到 noip 後了,希望不要給自己留遺憾,也希望大家都能有光明的前途!