[BZOJ3636]教义问答手册
壹、题目
一个整数序列,给定若干询问,每个询问形如:在 \([l_i,r_i]\) 中选若干个长度为 \(L\) 的不相交的区间,使得其和最大。
贰、题解
比较容易写出 \(\mathcal O(n^2)\) 的 \(DP\),定义 \(f_{l,r}\) 表示区间 \([l,r]\) 的最大答案,那么就有转移方程
预处理 \(\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\)
多个区间询问的时候,除了考虑莫队、分块意外,还可以想一下能否使用整体二分,而整体二分的时候,对于一个区间,一般只考虑询问跨越区间中点的情况,其他情况递归处理即可。