修剪草坪——十分详尽的题解

来自一本通5.5 例3luogu

好习惯,先看数据范围

#### 数据范围与提示

对于全部数据,$1\leq N\leq 10^5,0\leq E_i \leq 10^9$ 。

那么要用long long是显然的了。N的大小让人怀疑有log,但是其实没有

可能是考虑到deque巨大的常数吧反正我不喜欢用STL

不多说,直接上代码。边看边讲。

part1 朴素DP

前置

max函数不能写const ll&,可能是因为临时变量的问题。

#include<cstdio>
#define ll long long
int n,k;
ll max(const ll a,const ll b){return a>b?a:b;}
ll maxx[100005],e[100005],sum[100005],max2[100005];
main函数

前缀和便于查询连续的奶牛效率值之和

int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;++i)scanf("%lld",e+i),sum[i]=sum[i-1]+e[i];
	//*/
	if(n*k<=1e7){part2();return 0;}
	//*/
	part3();
	return 0;
}

part1·1

设$f_{i,j}$表示前前i头奶牛, 有j头奶牛(包括第i頭)与i连在一起,所能取得的最大效率

$maxx_i$表示考虑了前i头奶牛所取得的最大效率

for(int i=1;i<=n;++i){//前i头奶牛, 有多少奶牛(包括第i頭)与i连在一起 
	F[i][0]=maxx[i-1];//该位不选奶牛的话,上一位方案随意,不会受到k限制
	for(int j=1;j<=i&&j<=k;++j){
		F[i][j]=max(F[i][j],maxx[i-j-1]+sum[i]-sum[i-j]);
		maxx[i]=max(maxx[i],F[i][j]);//记下前i头奶牛的最大效率值便于查询
	}
}
printf("%lld",maxx[n]);

part1·2

发现上面输出的是maxx数组而非F数组,直接干掉F数组

for(int i=1;i<=n;++i){//上面的优化成这样 
	//F[i][0]=maxx[i-1];
	for(int j=1;j<i&&j<=k;++j){
		maxx[i]=max(maxx[i],maxx[i-j-1]+sum[i]-sum[i-j]);
	}
	if(i<=k)maxx[i]=max(maxx[i],sum[i]);//其实就是sum[i] 
}

至此能得70分,注意main函数里加上一句:(暴力得省一)

if(n*k<=1e7){part();return 0;}

因为$ n*k $的复杂度,所以保守估计$ 10^7 $,结果能得70分

开个O2,把限度放大一点,能得貌似80。不过我不敢这么浪

网上到处都说60分,@houyq的代码也是60分,由此可见该冒险的时候可以试试

part2

上面maxx数组只有1维,且后面的状态都是由前面的状态转移而来

看出来这是一道线性DP所以直接想到单调队列(不是)

part2·1

尝试继续优化,定义新数组把状态记下来免得多次计算(卡常级别的优化)

其实这么做的动机是因为,对于每个i,+sum[i]是不变的,maxx[i]一开始是0

变的东西只有这个[i-j+1]和[i-j],他俩随着j变而变

所以单独开个max2[]把两个东西合并了记下来,

max2[]没有什么实际意义,是为了分离变量和“不变暂时不变的量”而创造的新量

for(int i=1;i<=n;++i){//上面的“优化”成这样 
	for(int j=1;j<=i&&j<=k;++j){
		maxx[i]=max(maxx[i],max2[i-j]+sum[i]);
	}
	if(i<=k)maxx[i]=sum[i];
	max2[i]=maxx[i-1]-sum[i];
	//定义新数组, 因为第19行的i-j-1和i-j总是差1 
}

注意到第19行:

18	for(int j=1;j<i&&j<=k;++j){
19      maxx[i]=max(maxx[i],maxx[i-j-1]+sum[i]-sum[i-j]);
20	}

于是到了下一个要点:

part2·2

发现max()操作每次都有一个 +sum[i] 的操作,不顺眼,干掉

第二重循环需要倒着来,不顺眼,正着来

......

其实是因为发现是个滑动窗口查询区间最大值而改的

还有这一行 if(i<=k)maxx[i]=sum[i]; 不好,很复杂,抽出来

变成了这个样子

for(int i=1;i<=k;++i)
maxx[i]=sum[i],max2[i]=maxx[i-1]-sum[i];
for(int i=k+1;i<=n;++i){
	for(int j=1;j<=k;++j){//i-j∈[i-k,i-1]
		maxx[i]=max(maxx[i],max2[i-j]+sum[i]);
	}
	max2[i]=maxx[i-1]-sum[i];
}

至此,复杂度依然没变(常数大大减小,但是没太大用处)

$ However,$这给我们理解下面的代码带来了极大的方便

正片:单调队列优化

part3·1 要讲了

欸还没讲呢

先用个Max进一步铺垫一下,更直观

for(int i=1;i<=k;++i)
maxx[i]=sum[i],max2[i]=maxx[i-1]-sum[i];
for(int i=k+1,Max,j;i<=n;++i){//上面的优化成这样 
	for(j=i-k,Max=0;j<=i-1;++j){//i-j∈[i-k,i-1]
		if(Max<max2[j])Max=max2[j];
	}
	maxx[i]=Max+sum[i];
	max2[i]=maxx[i-1]-sum[i];//定义新数组 
}

我们写了一行注释:

//i-j∈[i-k,i-1]

1和k是常数。

我们想到,这个区间只会随着i的增加向右移动

而我们要的是区间内部的最大值

那么这显然是个滑动窗口问题

上板子,单调队列

ll dq[200005];int qh=1,qt=2;//deque::[qh,qt)
for(int i=1;i<=k;++i){
    maxx[i]=sum[i];
    const ll tmp=maxx[i-1]-sum[i];
    while(qh<=qt&&tmp>dq[qt])qt--;//单调性维护 
    dq[++qt]=tmp;
}
for(int i=k+1;i<=n;++i){//上面的优化成这样 
    maxx[i]=dq[qh]+sum[i];
    const ll tmp=maxx[i-1]-sum[i];
    while(qh<=qt&&tmp>dq[qt])qt--;//单调性维护 
    dq[++qt]=tmp;
    while(qh<=qt&&qh<=i-k)qh++;//时效性维护 
}

上面的代码看起来可以AC并不可以AC但是我并不打算再改下去了

因为我枯了

因为它会给选手带来极大的困惑并且它能通过小样例

我们推荐这种写法,即让队列记录编号而不是队首直接充当编号。

因为队列是一个容器,它的head和tail应该是private,对外不可见的,应该是没有实际意义的,面向过程的一个中间量,而容器本身盛放的东西才有意义

因此我写的代码中的qh做编号直接与i-k比较是一种极其耗费脑力的写法

不如用STL做法或者如下(手写的deque,详见下面的AC代码):

for(int i=1;i<=k;++i){
	maxx[i]=sum[i];
	max2[i]=maxx[i-1]-sum[i];
	while(!dq.empty()&&max2[i]>max2[dq.tail()])dq.popt();//单调性维护 
	dq.push(i);
}
for(int i=k+1;i<=n;++i){//上面的优化成这样 
	maxx[i]=max2[dq.head()]+sum[i];
	max2[i]=maxx[i-1]-sum[i];
	while(!dq.empty()&&max2[i]>max2[dq.tail()])dq.popt();//单调性维护 
	dq.push(i);
	while(!dq.empty()&&dq.head()<=i-k)dq.poph();//时效性维护 
}

非常符合C++面向对象的精髓所在(C才是面向过程)

还有许多其他做法,比如记录谁不选。因为我们要的是思维难度稍小一点的做法,所以不再赘述。(一上来就取反谁受得了啊)

总结:DP多练啊。不练,会的东西看不出来,也掌握不了

附上AC代码,手写的deque,常数小,小至近STL的 $\frac{1}{4}$,真不错:

#include<cstdio>
#define ll long long
int n,k;
ll max(ll a,ll b){return a>b?a:b;}
ll maxx[100005],e[100005],sum[100005],max2[100005];
void part(){
	/*/
	for(int i=1;i<=n;++i){//前i头奶牛, 该位时有多少奶牛连在一起 
		F[i][0]=maxx[i-1];
		for(int j=1;j<=i&&j<=k;++j){
			F[i][j]=max(F[i][j],maxx[i-j-1]+sum[i]-sum[i-j]);
			maxx[i]=max(maxx[i],F[i][j]);
		}
	}//*/
	for(int i=1;i<=n;++i){//上面的优化成这样 
		//F[i][0]=maxx[i-1];
		for(int j=1;j<i&&j<=k;++j){
			maxx[i]=max(maxx[i],maxx[i-j-1]+sum[i]-sum[i-j]);
		}
		if(i<=k)maxx[i]=max(maxx[i],sum[i]);//其实就是sum[i] 
	}
	//for(int i=1;i<=n;++i)printf("%lld ",maxx[i]);
	printf("%lld",maxx[n]);
}
void part2(){
	/*/ 
	for(int i=1;i<=n;++i){//上面的优化成这样 
		for(int j=1;j<=i&&j<=k;++j){
			maxx[i]=max(maxx[i],max2[i-j]+sum[i]);
		}
		if(i<=k)maxx[i]=sum[i];
		max2[i]=maxx[i-1]-sum[i];
		//定义新数组, 因为第19行的i-j-1和i-j总是差1 
	}//*/
	//*/
	for(int i=1;i<=k;++i)
	maxx[i]=sum[i],max2[i]=maxx[i-1]-sum[i];
	for(int i=k+1;i<=n;++i){//上面的优化成这样 
		for(int j=1;j<=k;++j){//i-j∈[i-k,i-1]
			maxx[i]=max(maxx[i],max2[i-j]+sum[i]);
		}
		max2[i]=maxx[i-1]-sum[i];//定义新数组 
	}//*/
	printf("%lld",maxx[n]);
}
struct deque{
	//deque::[qh,qt)
	private:int qh,qt,dq[100005];
	public:deque(){qh=qt=0;} 
	bool empty(){return qh>=qt;}
	int head(){return dq[qh];}
	int tail(){return dq[qt-1];}
	void poph(){qh++;}
	void popt(){qt--;}
	void push(const int& x){dq[qt++]=x;} 
}dq;
void part3(){
	/*/
	for(int i=1;i<=k;++i)
	maxx[i]=sum[i],max2[i]=maxx[i-1]-sum[i];
	for(int i=k+1,Max,j;i<=n;++i){//上面的优化成这样 
		for(j=i-k,Max=0;j<=i-1;++j){//i-j∈[i-k,i-1]
			if(Max<max2[j])Max=max2[j];
		}
		maxx[i]=Max+sum[i];
		max2[i]=maxx[i-1]-sum[i];//定义新数组 
	}//*/
	for(int i=1;i<=k;++i){
		maxx[i]=sum[i];
		max2[i]=maxx[i-1]-sum[i];
		while(!dq.empty()&&max2[i]>max2[dq.tail()])dq.popt();//单调性维护 
		dq.push(i);
	}
	for(int i=k+1;i<=n;++i){//上面的优化成这样 
		maxx[i]=max2[dq.head()]+sum[i];
		max2[i]=maxx[i-1]-sum[i];
		while(!dq.empty()&&max2[i]>max2[dq.tail()])dq.popt();//单调性维护 
		dq.push(i);
		while(!dq.empty()&&dq.head()<=i-k)dq.poph();//时效性维护 
	}
	printf("%lld",maxx[n]);
}
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;++i)scanf("%lld",e+i),sum[i]=sum[i-1]+e[i];
	//*/
	if(n*k<=1e7){part2();return 0;}//部分分保底
	//*/
	part3();
	return 0;
}

上一篇:poj 1936(水题)


下一篇:cookie