[BZOJ3636]教义问答手册

壹、题目

传送门

一个整数序列,给定若干询问,每个询问形如:在 \([l_i,r_i]\) 中选若干个长度为 \(L\) 的不相交的区间,使得其和最大。

贰、题解

比较容易写出 \(\mathcal O(n^2)\)\(DP\),定义 \(f_{l,r}\) 表示区间 \([l,r]\) 的最大答案,那么就有转移方程

\[f_{l,r}=\max\{f_{l,r-1},f_{l,r-L}+\sum_{i=r-L+1}^ra_i\} \]

预处理 \(\mathcal O(n^2)\),询问 \(\mathcal O(1)\),同时空间复杂度和预处理同阶,询问较快,但是无法接受。

考虑利用 \(L\le 50\) 这个条件,使用整体二分 —— 对于一段区间 \([l,r]\),在这个区间中的询问 \(\lang ql,qr\rang\) 只有三种情况

  • 询问区间完全在 \(mid\) 左边,即 \(qr\le mid\)
  • 询问区间完全在 \(mid\) 右边,即 \(mid<ql\)
  • 询问跨越了 \(mid\),即 \(ql\le mid<qr\)

对于前两种,我们可以考虑递归进行解决,对于最后那种,其实就是讲询问劈成在 \([ql,mid]\) 以及 \([mid+1,qr]\) 两个区间选择的最大值,但是还有特殊情况——选了一段长度为 \(L\) 的区间跨越了 \(mid\),这个时候怎么考虑?回到我们原来定义的状态,\(f_{l,r}\) 如果在左边,那么它的右端点就只有 \(L\) 个是有用的,即 \([mid-L+1,mid]\),同样,如果 \(f_{l,r}\) 在右边,那么它的左端点亦只有 \(L\) 个有用,即 \([mid+1,mid+L]\),如果我们只处理这几个左、右端点,时间复杂度只有 \(\mathcal O(nL)\),同时我们可以将空间压下去。定义 \(fl_{i,j}\) 表示右边界距离区间中点距离为 \(i\) , 左边界下标为 \(j\) 的最大取值,同样定义 \(fr_{i,j}\) 表示左边界距离区间中点距离为 \(i\) , 右边界下标为 \(j\) 的最大取值,最后合并的时候再美剧一下选择了一段跨越了 \(mid\) 的区间的情况。

总时间复杂度 \(\mathcal O(nL\log n+qL)\).

考虑在分治的过程中计算所有过中点的询问的答案。假设当前的分治区间为 \([l,r]\),中点是 \(mid\)。然后我们先对于每一个 \(l’\),预处理出 \([l’,mid]\) 这个区间中选若干个长度为 \(L\) 的区间,并在末尾选了 \(x\) 个数 \((x\le L)\) 的最大值,还有对于每一个 \(r’\),处理出 \([mid+1,r’]\) 这个区间中选若干长度为 \(L\) 的区间,并在开头选了 \(x\) 个数 \((x\le L)\) 的最大值,这个都可以用一个 \(DP\) 求出。然后最后询问合并一下即可。

叁、代码

using namespace Elaina;

const int maxn=100000;
const int maxq=100000;
const int maxl=50;

int a[maxn+5],n,L,Q;
struct query{
    int l,r,id;
    query(){}
    query(const int L,const int R,const int I):l(L),r(R),id(I){}
};
query q[maxq+5],tmp[maxq+5];
int ans[maxq+5];

void input(){
	n=readin(1),L=readin(1);
	rep(i,1,n)a[i]=readin(1)+a[i-1];
	Q=readin(1);
    int l,r;
	rep(i,1,Q){
        l=readin(1),r=readin(1);
        q[i]=query(l,r,i);
	}
}

/** @brief 右边界距离区间中点距离为 @p i , 左边界下标为 @p j 的最大取值*/
int fl[maxl+5][maxn+5];
/** @brief 左边界距离区间中点距离为 @p i , 右边界下标为 @p j 的最大取值*/
int fr[maxl+5][maxn+5];

void solve_l(const int l,const int r){
    for(register int i=0;i<L;++i){
        // 确定一个 i 之后 j 的范围就变成 [l,r-i]
        fl[i][r-i+1]=0;
        for(register int j=r-i;j>=l;--j){
            fl[i][j]=fl[i][j+1];
            if(j+L-1<=r-i)// 如果选择的这一段 [j,j+L-1] 还在这个区间, 那么就试着选罢
                fl[i][j]=max(fl[i][j],fl[i][j+L]+a[j+L-1]-a[j-1]);
        }
    }
}
void solve_r(const int l,const int r){
    for(register int i=0;i<L;++i){
        // 确定一个 i 之后 j 的范围就变成 [l+i,r]
        fr[i][l+i-1]=0;
        for(register int j=l+i;j<=r;++j){
            fr[i][j]=fr[i][j-1];
            if(j-L+1>=l+i)// 如果选择的 [j-L+1,j] 还在区间中, 那么就试着选罢
                fr[i][j]=max(fr[i][j],fr[i][j-L]+a[j]-a[j-L]);
        }
    }
}

int counter;
void solve(const int l,const int r,const int ql,const int qr){
	if(r-l+1<L || ql>qr)return;
	register int mid=(l+r)>>1,cntl=ql-1,cntr=qr+1;

    solve_l(l,mid);solve_r(mid+1,r);
	
	for(register int t=ql;t<=qr;++t){
		++counter;
		if(q[t].r<=mid)tmp[++cntl]=q[t];
		else if(mid<q[t].l)tmp[--cntr]=q[t];
        else{
            register int id=q[t].id;
            ans[id]=fl[0][q[t].l]+fr[0][q[t].r];
            fep(i,Min(L-1,mid-q[t].l+1),max(1,mid+L-q[t].r))
                ans[id]=max(ans[id],fl[i][q[t].l]+fr[L-i][q[t].r]+a[mid+L-i]-a[mid-i]);
            // 对于一个 i, 选的区间就是 [mid-i+1,mid+L-i], 但是还得必须保证这个区间在 [q[t].l,q[t].r] 之间
        }
	}

    for(register int i=ql;i<=cntl;++i)q[i]=tmp[i];
    for(register int i=cntr;i<=qr;++i)q[i]=tmp[i];
    solve(l,mid,ql,cntl);
   	solve(mid+1,r,cntr,qr);
}

signed main(){
	input();
	solve(1,n,1,Q);
	for(register int i=1;i<=Q;++i)writc(ans[i],'\n');
	return 0;
}

肆、用到の小 \(\tt trick\)

多个区间询问的时候,除了考虑莫队、分块意外,还可以想一下能否使用整体二分,而整体二分的时候,对于一个区间,一般只考虑询问跨越区间中点的情况,其他情况递归处理即可。

posted @ 2021-02-17 15:55  Arextre  阅读(105)  评论(0编辑  收藏  举报