最大子段和问题及其扩展

最大子段和问题及其扩展

普通最大子段和

\(f_i\)表示以\(i\)结尾的最大子段和,则\(f_i=\max\lbrace 0,f_{i-1}\rbrace+a_i\)

单点修改最大子段和

带单点修改时,用数据结构维护,设\(t[x].lx,t[x].rx,t[x].mx\)分别表示区间\([t[x].l,t[x].r]\)的从左端点为起点的最大子段和,以右端点为起点的最大子段和,区间最大子段和,则容易得到:

\[\left\{ \begin{aligned} t[x].lx&=\max\lbrace t[lc].lx,t[lc].sum+t[rc].lx\rbrace \\ t[x].rx&=\max\lbrace t[rc].rx,t[rx].sum+t[lc].rx\rbrace \\ t[x].mx&=\max\lbrace t[lc].mx,t[rc].mx,t[x].lx,t[x].rx,t[x].sum,t[lc].rx+t[rc].lx\rbrace \end{aligned} \right. \]

食用线段树维护,复杂度\(O(n\log n)\),单点修改线段树自然支持,单次修改\(O(\log n)\)
这样可以查找任意子区间的最大子段和,单次查找\(O(\log n)\)

最小子段和

最小子段和类似于最大子段和,设\(f_i\)表示以\(i\)结尾的最小子段和,则\(f_i=\min\lbrace 0,f_{i-1}\rbrace +a_i\)

环形最大子段和

最大子段和有两种情况,一种是不跨环,此时就是一般最大子段和,另一种是跨环,正难则反,求一个最小子段和,用总和将其减去即可得到。

例子:*****###*,其中是最大子段和,###是最小子段和

两段最大子段和

\(f_i,g_i\)分别表示以\(i\)为结尾和以\(i\)为开头的最大子段和,则有:

\[\left\{ \begin{aligned} f_i=\max\lbrace 0,f_{i-1}\rbrace+a_i\\ g_i=\max\lbrace 0,g_{i+1}\rbrace+a_i\\ \end{aligned} \right. \]

求出解之后,设\(f'_i=\max_{k=1}^if_k\)\(g'_i=\max_{k=i}^ng_k\),然后枚举\(i\)\(f'_i+g'_i\)的最大值即可。时间复杂度\(O(n)\)

环形两段最大子段和

与环形最大子段和类似,情况如下:设X是最大子段和中的元素,#不是

  1. XXXX##XXXXX##

  2. XX####XXXX##XX

同样,求普通的两段最大子段和\(p\),两段最小子段和\(q\)\(Ans=\max\lbrace p,\sum_{i=1}^na_i-q\rbrace\)

int f[N],g[N],f1[N],g1[N],s[N],t[N],s1[N],t1[N],sum,a[N],ans1[N],ans2[N],n,ans=-1e9;
	memset(f,0xcf,sizeof f);
	memset(g,0xcf,sizeof f);
	memset(s,0x3f,sizeof f);
	memset(t,0x3f,sizeof f);
	memset(f1,0xcf,sizeof f);
	memset(g1,0xcf,sizeof f);
	memset(s1,0x3f,sizeof f);
	memset(t1,0x3f,sizeof f);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)sum+=a[i];
	for(int i=1;i<=n;i++)f[i]=max(0,f[i-1])+a[i],f1[i]=max(f1[i-1],f[i]);
	for(int i=n;i>=1;--i)g[i]=max(0,g[i+1])+a[i],g1[i]=max(g1[i+1],g[i]);
	for(int i=1;i<=n;i++)s[i]=min(0,s[i-1])+a[i],s1[i]=min(s1[i-1],s[i]);
	for(int i=n;i>=1;--i)t[i]=min(0,t[i+1])+a[i],t1[i]=min(t1[i+1],t[i]);
	for(int i=1;i<=n;i++)ans1[i]=g1[i+1]+f1[i];
	for(int i=1;i<=n;i++)ans2[i]=sum-s1[i]-t1[i+1];
	for(int i=1;i<=n;i++)if(!ans2[i])ans2[i]=-0x3f3f3f3f;
	for(int i=1;i<n;i++)ans=max(ans,max(ans1[i],ans2[i]));
	cout<<ans;

限制端点最大子段和

这是一道限制端点的最大子段和题:

给定长度为\(n\)的序列\(a\)\(m\)个询问,每次询问给出\(l_1,r_1,l_2,r_2(l_1\le l_2,r_1\le r_2)\),求满足左端点在\([l_1,r_1]\),右端点在\([l_2,r_2]\)的最大子段和。

分析

通过这个问题,我们可以发现情况分为两种:

  1. \(r_1\le l_2\)

此时容易看出\([r_1,l_2]\)是必须计算在内的,故我们用\([l_2,r_2].lx+[l_1,r_1].rx+[r_1,l_2].sum-a[r_1]-a[l_2]\)即可求出解。

  1. \(r_1>l_2\)

不妨设最终答案的左右端点为\(l,r\),则分四种情况讨论。

  1. \(l\in[l_2,r_1],r\in[l_2,r_1]\),这种情况的最大值是\([l_2,r_1].mx\)
  2. \(l\in [l_1,l_2],r\in[l_2,r_1]\),这种情况的最大值是\([l_1,l_2].rx+[l_2,r_1].lx-a[l_2]\)
  3. \(l\in[l_2,r_1],r\in[r_1,r_2]\),这种情况的最大值是\([l_2,r_1].rx+[r_1,r_2].lx-a[r_1]\)
  4. \(l\in[l_1,l_2],r\in[r_1,r_2]\),这种情况和\(r_1\le l_2\)相似,类比可得最大值是\([l_1,l_2].rx+[l_2,r_1].sum+[r_1,r_2].lx-a[l_2]-a[r_1]\)

由此,我们建立一颗线段树即可解决问题。

using namespace std;
#define N 500050
struct node{
	int l,r,lx,rx,mx,sum;
}t[N<<2];
int a[N],n,m;
char F[20]; 
#define lc x<<1
#define rc x<<1|1
inline void read(int &x){
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')w=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		s=s*10+ch-'0'; 
		ch=getchar();
	}
	x=s*w;
}
inline void write(int x){
 	if(!x)putchar('0');
    int tmp=x>0?x:-x;
    if(x<0)putchar('-');
    int cnt=0;
    while(tmp>0){
        F[cnt++]=tmp%10+'0';
        tmp/=10;
    }
    while(cnt>0)putchar(F[--cnt]) ;
}


void merge(node &a,node &b,node &c){
	a.lx=max(b.lx,b.sum+c.lx);
	a.rx=max(c.rx,b.rx+c.sum);
	a.mx=max(a.lx,max(a.rx,b.rx+c.lx));
	a.mx=max(a.mx,max(b.mx,c.mx));
	a.sum=b.sum+c.sum;
}
void build(int l,int r,int x){
	t[x].l=l,t[x].r=r;
	if(l==r){
		t[x].lx=t[x].rx=t[x].mx=t[x].sum=a[l];
		return ;
	}
	int mid=l+r>>1;
	build(l,mid,lc);
	build(mid+1,r,rc);
	merge(t[x],t[lc],t[rc]);
}
node find(int l,int r,int x){
	if(l<=t[x].l&&t[x].r<=r){
		return t[x];
	}
	node ans={0,0,-0x3f3f3f3f,-0x3f3f3f3f,-0x3f3f3f3f,0};
	node a=ans,b=ans;
	int mid=t[x].l+t[x].r>>1;
	if(l<=mid){
		a=find(l,r,lc); 
	}
	if(mid<r) {
		b=find(l,r,rc);
	}
	merge(ans,a,b);
	return ans;
}
int query(int l1,int r1,int l2,int r2){
	if(r1<=l2){
		return find(l1,r1,1).rx+find(r1,l2,1).sum+find(l2,r2,1).lx-a[r1]-a[l2];
	}
	node x=find(l2,r1,1),y=find(l1,l2,1),z=find(r1,r2,1);
	return max(x.mx,max(x.sum+z.lx+y.rx-a[l2]-a[r1],max(x.lx+y.rx-a[l2],x.rx+z.lx-a[r1])));
}
int main(){
	//freopen("data.in","r",stdin);
	//freopen("data.out","w",stdout);
	int tt;read(tt);
	while(tt--){
		read(n);
		for(int i=1;i<=n;i++)read(a[i]);
		build(1,n,1);
		read(m);
		while(m--){
			int l1,r1,l2,r2;
			read(l1),read(r1),read(l2),read(r2);
			write(query(l1,r1,l2,r2));
		}
	}
}

去重最大子段和

在线算法,发现维护去重似乎非常困难,考虑将序列离线下来。有了这个离线的条件,由于没有修改操作,我们就可以考虑对询问顺序开始魔改处理了。

1e5常见的做法无非三种可能:\(O(n\sqrt n),O(n\log n),O(n\log^2 n)\)。我们来一个个考虑:

\(O(n\sqrt n)\):莫队 or 根号分治。莫队的话,由于离线后去重便转化为在某个区域内算上贡献,涉及区间操作,套个数据结构复杂度就变成\(O(n\sqrt n\log n)\)起步,无法承受。而根号分治明显就不行(显然应该不会存在什么高效的分类方式)。

\(O(n\log n),O(n\log^2 n)\):涉及根号和区间操作,明显应该是线段树/树状数组/\(Splay\)。因为\(Splay\)超大常数,并且最大子段和存在一个由线段树维护的分治做法,我们首先来考虑线段树。

然后再来考虑,如果是线段树该怎么做。利用离线操作,一个套路是:

对询问的某端点进行排序,高效维护某个端点单向移动且支持查询以这个移动端点为端点的区间的答案。

加上去重的特殊性,我们考虑将所有的询问按\(r\)排序,并且令最初的\(r\)指针为\(1\),不断向后移动并处理询问。

维护一个答案序列(也许可以理解为贡献序列)\(b\)
考虑移动指针\(r\)加入一个新的数会怎么样,显然\(a_r\)产生贡献的范围是:\([pre_{r}+1,r]\),其中\(pre_i\)表示值为\(a_i\)的数的上一次出现位置,可以在\(O(n)\)内预处理出。

所以我们将\(b_i,i\in[pre_r+1,r]\)全部加上\(a_r\),这样我们就满足,在任意时刻,\(b_x\)表示\([x,r]\)的去重后和。

再来考虑如何求解最大子段和问题。

首先,因为此时的\(b_i\)已经代表了\([i,r]\)的去重后和,那么我们就可以查个最大值即可统计出右端点为\(r\)的最大子段和。

受这种思想的启发,回溯一个阶段,当\(r'=r-1\)的时候,我们也可以查个最大值得到右端点为\(r-1\)的最大子段和。以此类推,进行若干次操作即可得到答案。

但,显然这个做法有优化的空间,我们明显可以保存\(r-1\)及其之前的最大值,也即维护区间历史最大值,每一次加入操作之后更新一下历史最大值即可。

这样我们就得到了一个做法:

  1. 读入询问,离线,统计\(pre\)
  2. 建立线段树,维护区间最大值,区间历史最大值
  3. 将询问按右端点排序,建立指针\(r\)并不断右移,每次插入就在\([pre_r+1,r]\)上加上\(a_r\)。每次查询就查找\([ask[i].l,ask[i].r](ask[i].r=r)\)的历史最大值。

至此,我们得到了一个\(O(n\log n)\)的优秀算法。

#define N 100050
#define ll long long
int n,m,a[N],pre[N],b[N];
ll ans[N];
struct node{
	int l,r;ll mx,hmx,lz,hlz;
}t[N<<2];
struct Ask{
	int l,r,id;
	bool operator<(const Ask b){
		return r==b.r?l<b.l:r<b.r;
	}
}ask[N];
#define lc x<<1
#define rc x<<1|1
void read(int &x){
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')w=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		s=s*10+ch-'0';
		ch=getchar();
	}
	x=s*w;
}
void pushup(int x){
	t[x].mx=max(t[lc].mx,t[rc].mx);
	t[x].hmx=max(t[x].hmx,t[x].mx);
}
void pushdown(node &a,node &b,node &c){
	b.hmx=max(b.hmx,b.mx+a.hlz);
	c.hmx=max(c.hmx,c.mx+a.hlz);
	b.hlz=max(b.hlz,b.lz+a.hlz);
	c.hlz=max(c.hlz,c.lz+a.hlz);
	b.mx+=a.lz;
	c.mx+=a.lz; 
	b.lz+=a.lz;
	c.lz+=a.lz;
	a.lz=a.hlz=0;
}
void pushdown(int x){
	pushdown(t[x],t[lc],t[rc]);
}
void build(int l,int r,int x){
	t[x]={l,r,0,0,0,0};
	if(l==r)return ;
	int mid=l+r>>1;
	build(l,mid,lc);
	build(mid+1,r,rc);
}
ll find(int l,int r,int x){
	if(l<=t[x].l&&t[x].r<=r){
		return t[x].hmx;
	}
	pushdown(x);
	ll ans=0ll;
	int mid=t[x].l+t[x].r>>1;
	if(l<=mid)ans=max(find(l,r,lc),ans);
	if(mid<r)ans=max(find(l,r,rc),ans);
	pushup(x);
	return ans;
}
void change(int l,int r,ll k,int x){
	if(l<=t[x].l&&t[x].r<=r){
		t[x].lz+=k;
		t[x].hlz=max(t[x].hlz,t[x].lz);
		t[x].mx+=k;
		t[x].hmx=max(t[x].hmx,t[x].mx);
		return ;
	}
	pushdown(x);
	int mid=t[x].l+t[x].r>>1;
	if(l<=mid)change(l,r,k,lc);
	if(mid<r)change(l,r,k,rc);
	pushup(x);
}
void init(){
	read(n);
	build(1,n,1);
	for(int i=1;i<=n;i++)read(a[i]);
	for(int i=1;i<=n;i++)pre[i]=b[a[i]],b[a[i]]=i;
	read(m);
	for(int i=1;i<=m;i++)read(ask[i].l),read(ask[i].r),ask[i].id=i;
	sort(ask+1,ask+m+1);
}
void solve(){
	int l=1;
	for(int r=1;r<=n;++r){
		change(pre[r]+1,r,a[r],1);
		while(ask[l].r==r&&l<=m){
			ans[ask[l].id]=find(ask[l].l,ask[l].r,1);
			l++;
		}
	}
}
int main(){
	//freopen("data.in","r",stdin);
	//freopen("data.out","w",stdout);
	init();
	solve();
	for(int i=1;i<=m;i++){
		printf("%lld\n",ans[i]);
	}
}

posted @ 2022-12-20 22:05  spdarkle  阅读(79)  评论(0编辑  收藏  举报