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;
}
posted @ 2022-04-17 18:46  Plozia  阅读(39)  评论(0编辑  收藏  举报