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 有优于分块的复杂度,但同时也对其使用产生了限制。

posted @ 2024-12-25 19:13  Tmbcan  阅读(1)  评论(0编辑  收藏  举报