Luogu EI 的第六分块 // KTT 学习记录
P5693 EI 的第六分块
题目描述
给定一个整数序列,支持区间加正整数以及查询区间最大子段和。
思路
使用线段树记录四个信息来维护答案:
- \(sum_i\):区间和;
- \(lmax_i\):最大前缀和;
- \(rmax_i\):最大后缀和;
- \(mx_i\):最大子段和。
信息合并时分类讨论:
- \(lmax = \max(lmax_{ls},sum_{ls}+lmax_{rs})\);
- \(rmax = \max(rmax_{rs},sum_{rs}+rmax_{ls})\);
- \(mx = \max(mx_{ls},mx_{rs},rmax_{ls}+lmax_{rs})\)。
信息处理
进行动态维护就要用 KTT 了,这是我们的重点内容。
现在每个信息记录的都不是一个具体值,而是一条一次函数:\(f(x)=kx+b\)。
其中 \(k\) 为最大子段的长度,\(x\) 为变化量,\(f(0)=b\) 为当前维护的具体值。
同时,对于两条函数,记录一个阈值 \(dx\),表示当前区间最大值是否在两个函数间进行交替。
关于交替阈值
前置知识:人教版八年级下册 19.2.3一次函数与方程、不等式
在对两条函数进行合并取最大值时,需要知道具体应该何时选取哪条函数。我们知道应该看函数的交点相对于区间的位置,来对取值情况分类讨论。
交替阈值就干了这样一件事情,维护时记录下何时应该对函数选取进行交替,并只在需要交替时交替,以此优化时间复杂度。
具体地,当区间加 \(q\) 时,函数向上进行了移动,函数的交点相对于区间进行了左右移动。此时我们令阈值 \(dx\) 减小,当 \(dx<0\) 时表示此时选取的函数要进行交替了。
具体减少多少呢,由于函数都满足 \(k\ge 1\),所以至少要令 \(dx-=q\)(当然最好是这个数,减多了重构次数就太多了)。
由于同一个区间可能有两个不同的函数进行维护,所以在合并区间时,阈值不仅要对左右区间取最小值,还需要包含当前两条函数的交点。
区间及函数合并
笔者个人建议写成重载运算符形式。
针对函数的操作,有求交点、函数合并、函数移动:
//struct Func
inline Func operator + (const Func&G) const{//函数合并
return Func(k+G.k,b+G.b);
}
inline ll operator & (const Func&G) const{//求交点
return (G.b-b)/(k-G.k);
}
inline void operator += (const ll&G){//函数向上移动
b += k*G;
}
区间合并时,我们在函数操作的基础上分类讨论即可,注意同时维护阈值信息:
//struct Tree
inline bool operator < (const Func&G) const{
//钦定两条函数的相对位置,方便判断有没有交点
return k==G.k && b<G.b || k<G.k;
}
inline void Merge_lx(Func x,Func y,Tree &tmp) const{//求lmax
if(x<y) swap(x,y);
if(x.b>=y.b) tmp.lx = x;//钦定过了函数位置,此时两条函数没有交点
else tmp.lx = y,tmp.dx = Min(tmp.dx,x&y);
}
//...
inline Tree operator + (const Tree&G) const{//区间合并
Tree tmp;tmp.sum = sum+G.sum; tmp.dx = Min(dx,G.dx);//注意维护阈值信息
Merge_lx(lx,sum+G.lx,tmp);Merge_rx(G.rx,G.sum+rx,tmp);
Merge_mx(G.mx,mx,tmp);Merge_mx(tmp.mx,rx+G.lx,tmp);
return tmp;
}
修改与重构
区间加按照正常的方式来,唯一不同的是在修改后需要对节点子树进行重构。
首先第一步肯定是下放标记:
//struct Tree
inline void operator += (const ll&G){//区间加
lx += G; rx += G; mx += G; sum += G; dx -= G;
}
//
inline void push_down(int p){//正常push_down
if(tag[p]){
tag[p<<1] += tag[p]; tr[p<<1] += tag[p];
tag[p<<1|1] += tag[p]; tr[p<<1|1] += tag[p];
tag[p] = 0;
}
}
然后再正常做修改:
//
inline void update(int l,int r,ll k){
l += P-1; r += P+1;//先push_down
for(int dep=DEP;dep;--dep) push_down(l>>dep),push_down(r>>dep);
while(l^1^r){
if(~l&1) tag[l^1]+=k,tr[l^1]+=k,rebuild(l^1);//别忘了重构
if(r&1) tag[r^1]+=k,tr[r^1]+=k,rebuild(r^1);
l>>=1;r>>=1;
tr[l] = tr[l<<1]+tr[l<<1|1];
tr[r] = tr[r<<1]+tr[r<<1|1];
}
for(l>>=1; l ;l>>=1) tr[l] = tr[l<<1]+tr[l<<1|1];
}
对于重构,从当前子树的根节点开始一层一层向下递推,直到没有节点需要重构为止:(模拟压栈常数也会更小一点)
//
inline void rebuild(int p){
if(tr[p].dx>=0) return ;
int head = 1,tail = 0;
st[++tail] = p; push_down(p);
while(tail>=head){//模拟压栈
int ttail = tail;
for(int j=tail,pos;j>=head;--j){
pos = st[j]; //看子节点的子树是否需要更新
if(tr[pos<<1].dx<0) st[++tail]=pos<<1,push_down(pos<<1);//注意push_down
if(tr[pos<<1|1].dx<0) st[++tail]=pos<<1|1,push_down(pos<<1|1);
}
head = ttail+1;
}//重新维护
do{ tr[st[tail]]=tr[st[tail]<<1]+tr[st[tail]<<1|1]; } while(--tail);
}
查询
正常做查询就可以了。
需要注意一点,区间合并时要按照左右顺序进行。
//
inline ll query(int l,int r){
l += P-1; r += P+1;//先push_down
for(int dep=DEP;dep;--dep) push_down(l>>dep),push_down(r>>dep);
Tree resl,resr;
while(l^1^r){
//注意左右区间的合并顺序
if(~l&1) resl = resl+tr[l^1];
if(r&1) resr = tr[r^1]+resr;
l>>=1;r>>=1;
}
return (resl+resr).mx.b;
}
KTT 的基本思路就是这样,将信息转换为函数进行处理,同时维护阈值进行重构。这使得 KTT 有优于分块的复杂度,但同时也对其使用产生了限制。