1. 程式人生 > 其它 >P2042 [NOI2005] 維護數列 題解

P2042 [NOI2005] 維護數列 題解

一道奆資料結構題,需要有較高的碼力和基礎的資料結構。

一看過去就會發現這是道資料結構題,然後這道題實際上就是平衡樹的板子題只是有各種奇怪的操作而已。我用的是 FHQ Treap。

其實吧這道題本來我不打算寫題解的,畢竟還是比較顯然的資料結構題,但是這道題的眾多坑點讓我還是決定寫篇題解,本篇題解採用拆解程式碼的方式貼程式碼,最後會有我對這道題用 FHQ Treap 寫這道題的坑點總結。


分析一下這道題,會發現實際上就是插入,刪除,反轉,推平,區間查詢和,全域性查詢最大子段和。

前置知識:線段樹求最大子段和,例題是 GSS1

好的現在我認為你應該會了這個 trick。

這道題我們可以仿照線段樹求最大子段和的方式,在每個節點維護以下 11 個值:l,r

左右兒子,Size 子樹大小,val 當前這個節點的權值,Key 就是隨機的值,pre,aft 表示前後綴最大和,sum 表示區間和,Maxn 表示最大子段和。然後還要維護 flag 表示推平懶標記,rev 表示翻轉懶標記。

於是你會發現如果直接開 \(4 \times 10^6\) 是會炸空間的,因此這裡我們需要垃圾回收,就是刪除節點的時候將不用的節點記錄下來以便重複使用,這塊的總複雜度是 \(O(點數)\) 的。

首先貼一下結構體:

const int MAXN = 5e5 + 5;
int n, q, Root, a[MAXN], cnt_Node;
struct node
{
    int l, r, Size, val, Key;
    int pre, aft, sum, Maxn;
    bool flag, rev;
    #define l(p) tree[p].l
    #define r(p) tree[p].r
    #define Size(p) tree[p].Size
    #define val(p) tree[p].val
    #define Key(p) tree[p].Key
    #define pre(p) tree[p].pre
    #define aft(p) tree[p].aft
    #define sum(p) tree[p].sum
    #define Maxn(p) tree[p].Maxn
    #define flag(p) tree[p].flag
    #define rev(p) tree[p].rev
}tree[MAXN];
stack <int> Rub;

Root 是根,cnt_Node 是當前樹的節點個數(不含刪除,就是拿來開點用的),Rub 是垃圾回收用的。

然後這道題有一個很大的坑點就是 最大子段和 不能為空,也就是說你必須選一個,因此我們需要考慮對 pre,aft,Maxn 做一點手腳:

在 Update(Pushup) 和 新建節點(Make_Node) 的時候,由於所有區間至少要選一個,所以一開始規定 Maxn = val 但是 preaft可以為 0 的(因為你已經選了一個了),然後按照正常的做法更新 Maxn,注意這裡的 Maxn 是絕對不能和 0 取大的!

還有一點需要注意的是普通線段樹寫法 Update(Pushup) 的時候節點本身是沒有權值的,但是 FHQ Treap 裡面是有的,因此不能忘記把這個節點合併進去。

貼一下 Update 函式:

void Update(int p)
{
    if (!p) return ;
    Size(p) = Size(l(p)) + Size(r(p)) + 1; sum(p) = sum(l(p)) + sum(r(p)) + val(p);
    pre(p) = Max(pre(l(p)), Max(sum(l(p)) + val(p) + pre(r(p)), 0));
    aft(p) = Max(aft(r(p)), Max(sum(r(p)) + val(p) + aft(l(p)), 0));
    Maxn(p) = Max(val(p), aft(l(p)) + val(p) + pre(r(p)));
    if (l(p)) Maxn(p) = Max(Maxn(p), Maxn(l(p)));
    if (r(p)) Maxn(p) = Max(Maxn(p), Maxn(r(p)));
} // 就是 Pushup

然後是新建節點 Make_Node 函式:

int Make_Node(int val)
{
    int tmp = 0; if (Rub.empty()) tmp = ++cnt_Node; else { tmp = Rub.top(); Rub.pop(); } // 重複利用
    l(tmp) = r(tmp) = 0; val(tmp) = val; Size(tmp) = 1;
    sum(tmp) = Maxn(tmp) = val; pre(tmp) = aft(tmp) = Max(val, 0);
    flag(tmp) = rev(tmp) = 0; Key(tmp) = rand();
    return tmp;
} // 新建節點

還有一個下傳懶標記的 Spread(Pushdown) 函式,這塊會順便帶上打翻轉懶標記和推平懶標記的兩個函式 ReverseCover

Reverse 就是正常的翻轉,考慮到 FHQ Treap 的中序遍歷就是原序列,直接交換左右子樹就好了,往下打懶標記。

這裡需要注意兩點:

  1. 翻轉的時候子樹不能直接打懶標記,是需要看情況的,因為翻轉兩次就是沒有翻轉。
  2. 注意翻轉的同時前後綴也被翻轉了,因此也是需要交換的。

然後是 Cover,這個函式往下推平的時候需要注意推平的值就是這個點的 val,以及兒子的 Maxn 必須要選一個,pre,aft 可選可不選。

寫了這兩個函式就能寫 Spread 了,這三個函式程式碼如下:

void Cover(int p, int val)
{
    if (!p) return ;
    val(p) = val; sum(p) = Size(p) * val;
    pre(p) = aft(p) = ((val > 0) ? sum(p) : 0);
    Maxn(p) = (val > 0) ? sum(p) : val; flag(p) = 1;
} // 區間推平

void Reverse(int p)
{
    if (!p) return ;
    std::swap(l(p), r(p));
    std::swap(pre(p), aft(p));
    rev(p) ^= 1; // 注意不能直接賦值為 1
} // 區間反轉

void Spread(int p)
{
    if (!p) return ;
    if (flag(p))
    {
        if (l(p)) Cover(l(p), val(p));
        if (r(p)) Cover(r(p), val(p));
        flag(p) = 0;
    }
    if (rev(p))
    {
        if (l(p)) Reverse(l(p));
        if (r(p)) Reverse(r(p));
        rev(p) = 0;
    }
} // 就是 Pushdown

然後這道題有一個全域性的坑點,好像有兩組資料是有的,就是操作的時候有時操作的區間長度是為 0 的,這個點也會坑到很多人,因此以上所有函式都需要在最開始加一個判斷節點是否為空

好的現在有了以上的基礎函式,可以開始寫各類我們需要的函數了。


首先看 Split 函式,這裡的函式按照大小分裂即可。

然後是 Merge 函式,這塊的話就是正常 Merge,但是當你往下合併的時候哪棵樹要往下合併哪棵樹就需要 Spread

void Split(int now, int val, int &x, int &y)
{
    if (now == 0) { x = y = 0; return ; }
    Spread(now);
    if (Size(l(now)) + 1 <= val) { x = now; Split(r(now), val - Size(l(now)) - 1, r(now), y); }
    else { y = now; Split(l(now), val, x, l(now)); }
    Update(now);
}

int Merge(int x, int y)
{
    if (!x || !y) return x + y;
    if (Key(x) < Key(y))
    {
        Spread(x); r(x) = Merge(r(x), y);
        Update(x); return x;
    }
    else
    {
        Spread(y); l(y) = Merge(x, l(y));
        Update(y); return y;
    }
}

然後是插入序列 Insert 函式,但是首先我們需要一個 Build 函式來建樹。

這個建樹就是仿照線段樹二分遞迴建樹,Insert 函式應該是基操了,就是將前面 pos 個拿出來然後三棵樹合併。

但顯然這裡也有坑點,先看程式碼:

int Build(int l, int r)
{
    if (l == r) return Make_Node(a[l]);
    int mid = (l + r) >> 1;
    int x = Build(l, mid);
    int y = Build(mid + 1, r);
    return Merge(x, y); // 注意這三句話
} // 遞迴建樹

void Insert()
{
    int pos = Read(), len = Read();
    for (int i = 1; i <= len; ++i) a[i] = Read();
    int x, y; Split(Root, pos, x, y);
    Root = Merge(Merge(x, Build(1, len)), y);
}

看見上面打註釋的三句話了嗎?如果你直接寫成 return Merge(Build(l, mid), Build(mid + 1, r));,可能程式會先執行後面的 Build(mid + 1, r),這樣子你可能就會掛掉了。

加可能的原因是有些人這樣寫是不會掛掉的(比如我這份程式碼),但是有些人會。

然後是 Delete 函式,就是正常的把需要的區間拉出來直接斃了,這裡會加上垃圾回收函式:

void Recycle(int p)
{
    Rub.push(p);
    if (l(p)) Recycle(l(p));
    if (r(p)) Recycle(r(p));
} // 垃圾回收

void Delete()
{
    int pos = Read(), len = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); Root = Merge(x, z); Recycle(y);
}

接下來看區間翻轉和區間推平操作,同樣也是將區間拉出來操作:

void Change_Cover()
{
    int pos = Read(), len = Read(), val = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); Cover(y, val);
    Root = Merge(Merge(x, y), z);
}

void Change_Reverse()
{
    int pos = Read(), len = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); Reverse(y);
    Root = Merge(Merge(x, y), z);
}

最後就是兩個查詢操作了,不用我多說了吧:

void Ask_sum()
{
    int pos = Read(), len = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); printf("%d\n", sum(y));
    Root = Merge(Merge(x, y), z);
}

void Ask_Maxn()
{
    printf("%d\n", Maxn(Root));
}

接下來總結一下這道題的坑點所在:

  1. 由於最大子段和不能為空,因此 pre,aft,Maxn 需要細節操作。
  2. 由於 FHQ Treap 本身的節點也是有權值的,因此也要合併進去。
  3. Reverse 操作的時候不能忘記交換前後綴。
  4. 每次操作之前一定要看一眼節點是不是空的。
  5. 插入的時候需要先建樹再合併。
  6. 因為空間限制較小,需要垃圾回收。

以上就是我遇到的所有坑點,如果還有的話就需要自己總結了。


Github:CodeBase-of-Plozia

Code:

/*
========= Plozia =========
    Author:Plozia
    Problem:P2042 [NOI2005] 維護數列
    Date:2021/12/28
========= Plozia =========
*/

#include <bits/stdc++.h>
using std::stack;
using std::string;

typedef long long LL;
const int MAXN = 5e5 + 5;
int n, q, Root, a[MAXN], cnt_Node;
struct node
{
    int l, r, Size, val, Key;
    int pre, aft, sum, Maxn;
    bool flag, rev;
    #define l(p) tree[p].l
    #define r(p) tree[p].r
    #define Size(p) tree[p].Size
    #define val(p) tree[p].val
    #define Key(p) tree[p].Key
    #define pre(p) tree[p].pre
    #define aft(p) tree[p].aft
    #define sum(p) tree[p].sum
    #define Maxn(p) tree[p].Maxn
    #define flag(p) tree[p].flag
    #define rev(p) tree[p].rev
}tree[MAXN];
stack <int> Rub;

int Read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = sum * 10 + (ch ^ 48);
    return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

int Make_Node(int val)
{
    int tmp = 0; if (Rub.empty()) tmp = ++cnt_Node; else { tmp = Rub.top(); Rub.pop(); }
    l(tmp) = r(tmp) = 0; val(tmp) = val; Size(tmp) = 1;
    sum(tmp) = Maxn(tmp) = val; pre(tmp) = aft(tmp) = Max(val, 0);
    flag(tmp) = rev(tmp) = 0; Key(tmp) = rand();
    return tmp;
} // 新建節點

void Cover(int p, int val)
{
    if (!p) return ;
    val(p) = val; sum(p) = Size(p) * val;
    pre(p) = aft(p) = ((val > 0) ? sum(p) : 0);
    Maxn(p) = (val > 0) ? sum(p) : val; flag(p) = 1;
} // 區間推平

void Reverse(int p)
{
    if (!p) return ;
    std::swap(l(p), r(p));
    std::swap(pre(p), aft(p));
    rev(p) ^= 1;
} // 區間反轉

void Update(int p)
{
    if (!p) return ;
    Size(p) = Size(l(p)) + Size(r(p)) + 1; sum(p) = sum(l(p)) + sum(r(p)) + val(p);
    pre(p) = Max(pre(l(p)), Max(sum(l(p)) + val(p) + pre(r(p)), 0));
    aft(p) = Max(aft(r(p)), Max(sum(r(p)) + val(p) + aft(l(p)), 0));
    Maxn(p) = Max(val(p), aft(l(p)) + val(p) + pre(r(p)));
    if (l(p)) Maxn(p) = Max(Maxn(p), Maxn(l(p)));
    if (r(p)) Maxn(p) = Max(Maxn(p), Maxn(r(p)));
} // 就是 Pushup

void Spread(int p)
{
    if (!p) return ;
    if (flag(p))
    {
        if (l(p)) Cover(l(p), val(p));
        if (r(p)) Cover(r(p), val(p));
        flag(p) = 0;
    }
    if (rev(p))
    {
        if (l(p)) Reverse(l(p));
        if (r(p)) Reverse(r(p));
        rev(p) = 0;
    }
} // 就是 Pushdown

void Split(int now, int val, int &x, int &y)
{
    if (now == 0) { x = y = 0; return ; }
    Spread(now);
    if (Size(l(now)) + 1 <= val) { x = now; Split(r(now), val - Size(l(now)) - 1, r(now), y); }
    else { y = now; Split(l(now), val, x, l(now)); }
    Update(now);
}

int Merge(int x, int y)
{
    if (!x || !y) return x + y;
    if (Key(x) < Key(y))
    {
        Spread(x); r(x) = Merge(r(x), y);
        Update(x); return x;
    }
    else
    {
        Spread(y); l(y) = Merge(x, l(y));
        Update(y); return y;
    }
}

int Build(int l, int r)
{
    if (l == r) return Make_Node(a[l]);
    int mid = (l + r) >> 1;
    int x = Build(l, mid);
    int y = Build(mid + 1, r);
    return Merge(x, y);
} // 遞迴建樹

void Recycle(int p)
{
    Rub.push(p);
    if (l(p)) Recycle(l(p));
    if (r(p)) Recycle(r(p));
} // 垃圾回收

void Insert()
{
    int pos = Read(), len = Read();
    for (int i = 1; i <= len; ++i) a[i] = Read();
    int x, y; Split(Root, pos, x, y);
    Root = Merge(Merge(x, Build(1, len)), y);
}

void Delete()
{
    int pos = Read(), len = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); Root = Merge(x, z); Recycle(y);
}

void Change_Cover()
{
    int pos = Read(), len = Read(), val = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); Cover(y, val);
    Root = Merge(Merge(x, y), z);
}

void Change_Reverse()
{
    int pos = Read(), len = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); Reverse(y);
    Root = Merge(Merge(x, y), z);
}

void Ask_sum()
{
    int pos = Read(), len = Read();
    int x, y, z; Split(Root, pos - 1, x, y);
    Split(y, len, y, z); printf("%d\n", sum(y));
    Root = Merge(Merge(x, y), z);
}

void Ask_Maxn()
{
    printf("%d\n", Maxn(Root));
}

int main()
{
    n = Read(), q = Read(); srand(time(0));
    for (int i = 1; i <= n; ++i) a[i] = Read();
    Root = Build(1, n);
    for (int i = 1; i <= q; ++i)
    {
        string str; std::cin >> str;
        if (str == "INSERT") Insert();
        if (str == "DELETE") Delete();
        if (str == "MAKE-SAME") Change_Cover();
        if (str == "REVERSE") Change_Reverse();
        if (str == "GET-SUM") Ask_sum();
        if (str == "MAX-SUM") Ask_Maxn();
    }
    return 0;
}