题解:P5693 EI 的第六分块
CNS_5t0_0r2 · · 题解
本题是区间加正数 + 区间最大子段和,关于单点修改 + 区间最大子段和可以参见我一直在咕的线段树博客【2.3】例题
先思考一个问题:单点修改为什么不能直接转成区间加?
答:区间加完后我们不能直接确定一个区间的最优决策会变成什么,而单点修改是可以的(都是一个点了,肯定只能选当前位置或不选啦)。
因此,对于区间修改我们只能暴力修改子树,但这样复杂度就直接爆炸了。
不过,我们可以维护一些额外的信息,可以快速判断一个区间的决策是否会发生变化,从而减少暴力枚举子树时的枚举量。
首先我们得明确:一个决策是如何存储的?或者说,一个决策的形式是什么?
这就跟区间加的性质有关了。如果区间加
这提示我们,我们除了要把这个决策没有区间加时的值存下来,还需要把决策的区间长度
对于一个决策,如果其本来的值为
这显然可以看作两个以
而本题中有三个量需要决策(区间和决策永远是左右区间和相加),所以,只要
对于每个节点维护一个值
这样本题就做完了。
复杂度……不会证(可以看这篇题解)。
以下是一些注意事项:
-
本题不卡常。
-
交点横坐标只需要保留整数。
其余的可以参考代码中的注释(如果有不懂的地方可以回复我,我会随时更新)。
以下是代码(应该比较好懂吧):
#include<bits/stdc++.h>
#define int long long
#define mid ((l + r) >> 1)
#define lchild (root << 1)
#define rchild ((root << 1) + 1)
using namespace std;
const int N = 4e5 + 9,INF = 1e15;
int n,m;
int a[N];
struct linear_func{//一次函数,本质存储的是决策
int k,b;//k:最优决策区间的长度;b:最优决策的值
void add(int v){
b += k * v;
}
};
linear_func operator+(linear_func f1,linear_func f2){//两个一次函数相加
return (linear_func){f1.k + f2.k,f1.b + f2.b};
}
linear_func Max(linear_func f1,linear_func f2){//返回两个一次函数在 x = 0 (没有区间加操作)处取值更优者及两函数交点坐标
if(f1.k < f2.k || f1.k == f2.k && f1.b < f2.b)//不明白为什么不交换会WA,有知道的大佬欢迎私信告诉我!
swap(f1,f2);
return (f1.b >= f2.b) ? f1 : f2;
}
int intersection(linear_func f1,linear_func f2){//两个一次函数交点坐标
if(f1.k < f2.k || f1.k == f2.k && f1.b < f2.b)
swap(f1,f2);
if(f1.b >= f2.b)
return INF;
return (f2.b - f1.b + f1.k - f2.k - 1) / (f1.k - f2.k);
}
struct node{
linear_func sum,val;//区间和、区间最大子段和
linear_func lsum,rsum;//前缀最大和、后缀最大和
int x;//区间加超过了 x 就要重构子树
};
node operator+(node lc,node rc){//合并两个节点的信息
node ret;
ret.x = min(lc.x,rc.x);
ret.x = min(ret.x,intersection(lc.val,rc.val));
ret.x = min(ret.x,intersection(Max(lc.val,rc.val),lc.rsum + rc.lsum));
ret.x = min(ret.x,intersection(lc.lsum,lc.sum + rc.lsum));
ret.x = min(ret.x,intersection(rc.rsum,rc.sum + lc.rsum));
//这部分和单点修改 + 区间最大子段和是一样的,只是额外维护了一个最优决策的长度k。
ret.sum = lc.sum + rc.sum;
ret.val = Max(Max(lc.val,rc.val),lc.rsum + rc.lsum);
ret.lsum = Max(lc.lsum,lc.sum + rc.lsum);
ret.rsum = Max(rc.rsum,rc.sum + lc.rsum);
return ret;
}
struct KTT{
struct ktt_node{
node u;
int tag;
} t[N << 2];
bool in_range(int l,int r,int L,int R){
return L <= l && r <= R;
}
bool out_range(int l,int r,int L,int R){
return r < L || l > R;
}
void push_up(int root){
t[root].u = t[lchild].u + t[rchild].u;
}
void make_tag(int root,int v){
t[root].tag += v;
t[root].u.x -= v;
t[root].u.sum.add(v);
t[root].u.val.add(v);
t[root].u.lsum.add(v);
t[root].u.rsum.add(v);
}
void push_down(int root){
if(t[root].tag){
make_tag(lchild,t[root].tag);
make_tag(rchild,t[root].tag);
t[root].tag = 0;
}
}
void defeat(int root,int l,int r,int v){//将以root为根的子树暴力重构
if(v > t[root].u.x){//区间的决策发生了变化,暴力重构
int tmp = t[root].tag + v;//注意要把标记一起传下去,v包括了root节点祖先的标记
t[root].tag = 0;
defeat(lchild,l,mid,tmp);
defeat(rchild,mid + 1,r,tmp);
push_up(root);
}
else//否则可以直接更新区间信息,打标记即可
make_tag(root,v);
}
void build(int root,int l,int r){
if(l == r){
linear_func tmp = (linear_func){1,a[l]};//叶节点的区间长度为1,所以k就只能为1了(不用考虑叶节点不选的情况,如果出现了负数在 cout 处有取 max 操作可以忽略)
t[root].u = (node){tmp,tmp,tmp,tmp,INF};
return;
}
build(lchild,l,mid);
build(rchild,mid + 1,r);
push_up(root);
}
void update(int root,int l,int r,int L,int R,int v){
if(out_range(l,r,L,R))
return;
if(in_range(l,r,L,R)){
defeat(root,l,r,v);
return;
}
push_down(root);
update(lchild,l,mid,L,R,v);
update(rchild,mid + 1,r,L,R,v);
push_up(root);
}
node query(int root,int l,int r,int L,int R){
if(in_range(l,r,L,R))
return t[root].u;
push_down(root);
if(R <= mid)
return query(lchild,l,mid,L,R);
if(L > mid)
return query(rchild,mid + 1,r,L,R);
return query(lchild,l,mid,L,R) + query(rchild,mid + 1,r,L,R);
}
} ktt;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin >> n >> m;
for(int i = 1;i <= n;i++)
cin >> a[i];
ktt.build(1,1,n);
while(m--){
int op;
cin >> op;
if(op == 1){
int l,r,v;
cin >> l >> r >> v;
ktt.update(1,1,n,l,r,v);
}
else if(op == 2){
int l,r;
cin >> l >> r;
cout << max(ktt.query(1,1,n,l,r).val.b,0ll) << '\n';
}
}
return 0;
}