题解:P5693 EI 的第六分块
AlanWalker_1 · · 题解
KTT
谨以此文纪念这一道远超能力范围的题
前言
warning:本文主要介绍做法,不含时间复杂度证明
感谢 @dead_X 的讲解。
本题沿用了线段树维护 区间子段和+单点修改 的思路,本质上就是对 前缀最大、区间和、后缀最大 那一套的变形。
Part 1
考虑 区间子段和+单点修改 是怎么做的。
在线段树单个节点维护的区间中,令
则 区间和并 (pushup) 的式子:
当修改操作针对单点时,只需要底层修改+向上合并即可。
但是我们发现,线段树节点维护的四个值描述的是区间的最优连续结构,而压缩掉了具体的选取方案,当我们使用懒标记的思想进行区间操作,无法确定子树内的信息变化是否会对当前节点产生影响,因此朴素的延迟下放无法保正确性。
Part 2
考虑区间和 +x 时如何变化
实际上是一个一次函数形式
同样的,
表示的是:当前这个值为 +x,这个值会变成
考虑为节点 +x 操作:
如果这一操作不足以使子树内合并时任何一次的
否则内部合并过程有变化,至少有一个量的表达式会发生改变,需要递归进子树内进行修改。
Part 3
根据上述思路,我们发现在每一次
对于每个节点,维护一个阀值
容易发现
如果一次 +x 的修改延迟下放,那么
Part 4
考虑实现
1.直线类
struct line
{
int k, b;
inline line operator+(const line &other) const
{
return line{k + other.k, b + other.b};
}
};
2.选取“当前值”较大的直线,并返回交点
pair<line, int> max(line a, line b)
{
if (a.k < b.k || (a.k == b.k && a.b < b.b))
swap(a, b);
if (a.b >= b.b)
return {a, INF};
return {b, (b.b - a.b) / (a.k - b.k)};
}
3.线段树节点定义+合并信息
struct node
{
line pre, suf, sum, tot;
int m;
inline node operator+(const node &other) const
{
node ans;
ans.m = min(m, other.m); //m取左右两边较小值
pair<line, int> res;
res = max(pre, sum + other.pre);
ans.pre = res.first;
ans.m = min(ans.m, res.second); //m取交点最小值
res = max(other.suf, suf + other.sum);
ans.suf = res.first;
ans.m = min(ans.m, res.second);
res = max(tot, other.tot);
ans.m = min(ans.m, res.second);
res = max(res.first, suf + other.pre);
ans.tot = res.first;
ans.m = min(ans.m, res.second);
ans.sum = sum + other.sum;
return ans;
}
} tr[N << 2];
4.修改作用在单点时
inline void work(int p, int w)
{
tag[p] += w;
tr[p].m -= w;
tr[p].pre.b += tr[p].pre.k * w; //作用在单点,修改“当前值”即可
tr[p].suf.b += tr[p].suf.k * w;
tr[p].tot.b += tr[p].tot.k * w;
tr[p].sum.b += tr[p].sum.k * w;
}
7.pushup & pushdown
inline void pushup(int p)
{ tr[p] = tr[lc] + tr[rc]; } //此处的加法为区间和并
inline void pushdown(int p)
{
if (!tag[p])
return;
int w = tag[p];
tag[p] = 0;
work(lc, w), work(rc, w);
}
6.建树
void build(int p, int l, int r)
{
if (l == r)
{
line t;
t.k = 1, t.b = a[l];
tr[p].m = INF, tag[p] = 0;
tr[p].pre = tr[p].suf = tr[p].tot = tr[p].sum = t;
return;
}
build(lc, l, mid), build(rc, mid + 1, r);
pushup(p);
}
7.更新
void update(int p, int l, int r, int ql, int qr, int w)
{
if (l >= ql && r <= qr) //区间被完全覆盖,对整棵子树操作
{
if (tr[p].m >= w) //修改量小于阀值,无需改变内部结构
{
work(p, w); //直接修改单点即可
return;
}
//修改量超过阀值,递归入子树修改
pushdown(p);
update(lc, l, mid, ql, qr, w), update(rc, mid + 1, r, ql, qr, w);
if (l != r)
pushup(p);
return;
}
pushdown(p);
if (ql <= mid)
update(lc, l, mid, ql, qr, w);
if (qr > mid)
update(rc, mid + 1, r, ql, qr, w);
pushup(p);
}
8.查询
//与普通线段树无异
node query(int p, int l, int r, int ql, int qr)
{
if (l >= ql && r <= qr)
return tr[p];
pushdown(p);
if (qr <= mid)
return query(lc, l, mid, ql, qr);
if (ql > mid)
return query(rc, mid + 1, r, ql, qr);
return
query(lc, l, mid, ql, qr) + query(rc, mid + 1, r, ql, qr);
//合并左右答案
}
9.主函数
signed main()
{
n = read(), q = read();
for (int i = 1; i <= n; i++)
a[i] = read();
build(1, 1, n);
while (q--)
{
int op = read();
if (op == 1)
{
int l = read(), r = read(), x = read();
update(1, 1, n, l, r, x);
}
else
{
int l = read(), r = read();
printf("%lld\n", max(0LL, query(1, 1, n, l, r).tot.b));
//注意query返回的是node,需要取实际值b。
}
}
return 0;
}
\完结撒花/
码字辛苦,点个赞再走吧