「刷题记录」P7476 「C.E.L.U-02」苦涩
题目背景
回想起自己的过往的人生,\(\text{YQH}\) 觉得心中充满了苦涩。如果人生能再来一次,我一定会少做一些傻事,少真香几次,然后大胆地去追寻自己的爱。可惜没有这样一个机会了。
题目描述
在 \(\text{YQH}\) 的梦中,他看到自己过去的记忆正在不断浮现在自己脑中。这些记忆带给他的是满满的苦涩。他想要强行忘记一些来减轻自己的苦涩。
\(\text{YQH}\) 的脑中可以被分成 \(n\) 个片区,每个片区相当于一个存放记忆的可重集,初始为空。他将进行 \(m\) 次这三种操作:
操作 \(1\):区间 \(l\sim r\) 的片区中都浮现了一个苦涩值为 \(k\) 的记忆。
操作 \(2\):\(\text{YQH}\) 开始清理 \(l\sim r\) 片区的记忆。如果一个片区 \(k\in[l,r]\) 且 \(k\) 中苦涩值最大的记忆与 \(l\sim r\) 片区中苦涩值最大的记忆相等,则将这个苦涩值最大的记忆忘记。如果在同一个片区有多个相同的苦涩值最大的记忆,则只忘记一个。如果这些片区内没有记忆,则无视。
操作 \(3\):\(\text{YQH}\) 想知道,\(l\sim r\) 片区中苦涩值最大的记忆的苦涩值是多少,如果不存在,输出 \(-1\)。
输入格式
第一行两个数,\(n,m\)。
接下来 \(m\) 行,第一个数代表操作种类 \(op\),对于操作 \(1\),有三个数 \(l,r,k\),对于操作 \(2\) 或 \(3\),有两个数 \(l,r\)。
输出格式
对于每个操作 \(3\) 输出一行,代表答案。
样例 #1
样例输入 #1
5 4
1 1 3 2
1 2 4 3
2 3 3
3 1 3
样例输出 #1
3
样例 #2
样例输入 #2
6 6
1 1 6 2
1 3 3 2
1 3 4 3
2 3 4
3 3 3
3 4 4
样例输出 #2
2
2
提示
样例解释
样例解释一
下面为各操作之后 \(\text{YQH}\) 的大脑的状态:
第一次操作:\(\{2\},\{2\},\{2\},\varnothing,\varnothing\)
第二次操作:\(\{2\},\{2,3\},\{2,3\},\{3\},\varnothing\)
第三次操作:\(\{2\},\{2,3\},\{2\},\{3\},\varnothing\)
第四次操作询问 区间 \(1\sim 3\) 的最大值,所以答案是 \(3\)。
样例解释二
下面为各操作之后 \(\text{YQH}\) 的大脑的状态:
第一次操作:\(\{2\},\{2\},\{2\},\{2\},\{2\},\{2\}\)
第二次操作:\(\{2\},\{2\},\{2,2\},\{2\},\{2\},\{2\}\)
第三次操作:\(\{2\},\{2\},\{2,2,3\},\{2,3\},\{2\},\{2\}\)
第四次操作:\(\{2\},\{2\},\{2,2\},\{2\},\{2\},\{2\}\)
第五次操作询问 \(3\) 的最大值,所以答案是 \(2\)。
第六次操作询问 \(4\) 的最大值,所以答案是 \(2\)。
数据范围
Subtask | n | m | 特殊性质 |
---|---|---|---|
\(1(10pts)\) | \(\leq10^3\) | \(\le10^3\) | \(\diagdown\) |
\(2(20pts)\) | \(\leq5\times10^4\) | \(\leq5\times10^4\) | 没有操作 2 |
\(3(10pts)\) | \(\leq5\times10^4\) | \(\leq5\times10^4\) | 操作 2 中 \(l=r\) |
\(4(20pts)\) | \(\leq5\times10^4\) | \(\leq5\times10^4\) | \(\diagdown\) |
\(5(20pts)\) | \(\leq2\times10^5\) | \(\leq2\times10^5\) | 操作 2 中 \(l=r\) |
\(6(20pts)\) | \(\leq2\times10^5\) | \(\leq2\times10^5\) | \(\diagdown\) |
对于 \(100\%\) 的数据,\(n,m\le2\times10^5,k\le10^9\)
思路
一共有三个操作,别看他描述的花里胡哨的,其实就是区间加,区间删除,区间查询。
但是,由于区间加的操作,它的叶节点存储的不止有一个数字,而是一个数列,所以他和普通的线段树还不同,它的节点还要再套点其他东西
那么,我们要套什么呢?
它不是还要查询最大值吗,那我们就套一个堆
又有问题了,有区间操作,怎么下传懒标?将堆与孩子的堆进行合并——左偏树,想到这里,恭喜你,掉坑里了
左偏树合并是将整个堆都合并过去,他又不只有一个孩子,它和一个孩子合并了,那其他孩子呢?所以左偏树不行
既然传懒标很麻烦,那我们就不穿懒标了呗,这里介绍一个小优化——标记永久化
什么意思呢,就是标记不下放,永远留在当前位置
我们用堆来记录懒标,但是本题中标记永久化的不彻底,还有一种情况需要下传懒标,当堆顶元素与要删除的元素相等时,我们要下放懒标记
代码
定义线段树:
struct tree {
int len;// 代表区间长度
ll maxx;// 代表这个区间的最大值
priority_queue<ll> q;// 大根堆记录并维护懒标
}t[N << 4];
建树操作:
void build(int cur, int l, int r) {// 建树
t[cur].len = r - l + 1;// 求len
t[cur].maxx = -1;// 求max
t[cur].q.push(-1);// -1压堆底
if(l == r) return ;// 到达叶子节点
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
}
这些与普通线段树没什么差异
接下来就是标记永久化
void addlazy(int cur, ll v) {// 永久化懒标记
t[cur].q.push(v);// 插入懒标记
t[cur].maxx = t[cur].maxx > v ? t[cur].maxx : v;
// 将max值与懒标记比大小
}
更新操作
void pushup(int cur) {
t[cur].maxx = max(max(t[ls].maxx, t[rs].maxx), t[cur].q.top());
// 将左右孩子的最大值与懒标记的最大值比大小
}
插入操作
void insert(int cur, int l, int r, int ql, int qr, ll k) {// 插入
if(ql <= l && r <= qr) {
addlazy(cur, k);// 永久化标记
return ;
}
int mid = (l + r) >> 1;
if(ql <= mid) insert(ls, l, mid, ql, qr, k);
if(qr > mid) insert(rs, mid + 1, r, ql, qr, k);
pushup(cur);// pushup更新
}
查询操作
ll ask(int cur, int l, int r, int ql, int qr) {// 查询最大苦涩值
if(ql <= l && r <= qr) {
return t[cur].maxx;// 返回这个区间的最大苦涩值
}
ll ans = t[cur].q.top();// 懒标记的最大值
int mid = (l + r) >> 1;
if(ql <= mid) ans = max(ans, ask(ls, l, mid, ql, qr));
// 左子树的最大苦涩值
if(qr > mid) ans = max(ans, ask(rs, mid + 1, r, ql, qr));
// 右子树的最大苦涩值
return ans;
}
删除操作
void del(int cur, int l, int r, int ql, int qr, ll k) {
if(ql <= l && r <= qr && t[cur].maxx < k) return ;
// 如果这个区间的最大值都比不过k,那就没有要删除的
if(t[cur].q.top() == k) {// 最大的懒标记与k相等
t[cur].q.pop();// 弹出
pushdown(cur, l, r, ql, qr, k);// 下放懒标记
if(l == r) {// 到达叶子节点,修改最大值
t[cur].maxx = t[cur].q.top();
//叶结点的最大值就是懒标最大值
}
else pushup(cur);//更新最大值
return ;
}
int mid = (l + r) >> 1;
if(ql <= mid) del(ls, l, mid, ql, qr, k);
// 在左子树中删除
if(qr > mid) del(rs, mid + 1, r, ql, qr, k);
// 在右子树中删除
pushup(cur);
}
完整代码
#include <iostream>
#include <cstdio>
#include <queue>
#define ls (cur << 1)
#define rs (cur << 1 | 1)
typedef long long ll;
using namespace std;
const int N = 2e5 + 5;
int n, m;
struct tree {
int len;// 代表区间长度
ll maxx;// 代表这个区间的最大值
priority_queue<ll> q;// 大根堆记录并维护懒标
}t[N << 4];
inline ll read() {
ll x = 0;
int fg = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while(ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
void build(int cur, int l, int r) {// 建树
t[cur].len = r - l + 1;// 求len
t[cur].maxx = -1;// 求max
t[cur].q.push(-1);// -1压堆底
if(l == r) return ;// 到达叶子节点
int mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
}
void addlazy(int cur, ll v) {// 永久化懒标记
t[cur].q.push(v);// 插入懒标记
t[cur].maxx = t[cur].maxx > v ? t[cur].maxx : v;
// 将max值与懒标记比大小
}
void pushup(int cur) {
t[cur].maxx = max(max(t[ls].maxx, t[rs].maxx), t[cur].q.top());
// 将左右孩子的最大值与懒标记的最大值比大小
}
void pushdown(int cur, int l, int r, int ql, int qr, ll k) {
// 把懒标记下放到[ql,qr]以外的其他区间中
if(ql <= l && r <= qr) return ;// 永久化标记
int mid = (l + r) >> 1;
if(ql > mid) {// 全在右子树上
addlazy(ls, k);// 永久化标记
pushdown(rs, mid + 1, r, ql, qr, k);// 下放标记
}
else {
if(qr <= mid) {// 全在左子树上
addlazy(rs, k);// 永久化标记
pushdown(ls, l, mid, ql, qr, k);// 下放标记
}
else {// 跨左右子树
pushdown(ls, l, mid, ql, qr, k);
pushdown(rs, mid + 1, r, ql, qr, k);
}
}
pushup(cur);
}
void insert(int cur, int l, int r, int ql, int qr, ll k) {// 插入
if(ql <= l && r <= qr) {
addlazy(cur, k);// 永久化标记
return ;
}
int mid = (l + r) >> 1;
if(ql <= mid) insert(ls, l, mid, ql, qr, k);
if(qr > mid) insert(rs, mid + 1, r, ql, qr, k);
pushup(cur);// pushup更新
}
void del(int cur, int l, int r, int ql, int qr, ll k) {
if(ql <= l && r <= qr && t[cur].maxx < k) return ;
// 如果这个区间的最大值都比不过k,那就没有要删除的
if(t[cur].q.top() == k) {// 最大的懒标记与k相等
t[cur].q.pop();// 弹出
pushdown(cur, l, r, ql, qr, k);// 下放懒标记
if(l == r) {// 到达叶子节点,修改最大值
t[cur].maxx = t[cur].q.top();
//叶结点的最大值就是懒标最大值
}
else pushup(cur);//更新最大值
return ;
}
int mid = (l + r) >> 1;
if(ql <= mid) del(ls, l, mid, ql, qr, k);
// 在左子树中删除
if(qr > mid) del(rs, mid + 1, r, ql, qr, k);
// 在右子树中删除
pushup(cur);
}
ll ask(int cur, int l, int r, int ql, int qr) {// 查询最大苦涩值
if(ql <= l && r <= qr) {
return t[cur].maxx;// 返回这个区间的最大苦涩值
}
ll ans = t[cur].q.top();// 懒标记的最大值
int mid = (l + r) >> 1;
if(ql <= mid) ans = max(ans, ask(ls, l, mid, ql, qr));
// 左子树的最大苦涩值
if(qr > mid) ans = max(ans, ask(rs, mid + 1, r, ql, qr));
// 右子树的最大苦涩值
return ans;
}
int main() {
t[0].maxx = -1;
t[0].q.push(-1);
n = read();
m = read();
build(1, 1, n);
for(int i = 1; i <= m; ++i) {
int op = read();
int l, r, k;
if(op == 1) {
l = read(), r = read(), k = read();
insert(1, 1, n, l, r, k);// 插入节点
}
if(op == 2) {
l = read(), r = read();
k = ask(1, 1, n, l, r);// 找最大苦涩值
if(k != -1) del(1, 1, n, l, r, k);
// 如果有最大苦涩值,删除
}
if(op == 3) {
l = read(), r = read();
printf("%lld\n", ask(1, 1, n, l, r));// 找最大苦涩值
}
}
return 0;
}