洛谷 P3628 特别行动队
洛谷题目页面传送门
题意见洛谷。
这题一看就是DP。。。
设\(dp_i\)表示前\(i\)个士兵的最大战斗力。显然,最终答案为\(dp_n\),DP边界为\(dp_0=0\),状态转移方程为\(dp_i=\max\limits_{j\in[0,i)}\left\{dp_j+a\left(\sum\limits_{k=j+1}^ix_k\right)^2+b\sum\limits_{k=j+1}^ix_k+c\right\}\)。
这其中要求区间和,可以先预处理出前缀和数组\(Sum\)。那么方程变为了\(dp_i=\max\limits_{j\in[0,i)}\left\{dp_j+a(Sum_i-Sum_j)^2+b(Sum_i-Sum_j)+c\right\}\)。
朴素地转移是\(\operatorname{O}\left(n^2\right)\)的,肯定会超时,于是要考虑优化。乍一看,这方程中\(\max\)的下界不动、上界在DP过程中随状态变量\(i\)单调递增,里面又有既和状态变量\(i\)又和决策变量\(j\)相关的项\((Sum_i-Sum_j)^2\)……这不就是斜率优化的标志吗?
我们假设有两个决策变量\(j,k\)满足\(j>k\)且\(j\)比\(k\)优,那么
即
去括号,得
即
把只关于决策变量的项放到左边来,既关于状态变量又关于决策变量的项放到右边去,得
由于\(j>k,x_i>0\),所以\(Sum_j-Sum_k>0\)(这就是要设\({j>k}\)的原因),那么可以把\(Sum_j-Sum_k\)除到左边去且不等号方向不变(同理,设\(j<k\)也可以,只不过不等号要变方向),得
这时,左边的东西似乎很眼熟……是什么呢……斜率!这不就相当于以\({dp_i+aSum_i^2-bSum_i}\)为点\({i}\)的纵坐标,以\({Sum_i}\)为点\({i}\)的横坐标的直角坐标系中,点\({k}\)与点\({j}\)之间直线的斜率吗?
我们令\(f(j,k)\)为这个斜率。现在设\(j,k,o\)是三个决策变量,满足\(j>k>o\),那么若\(f(j,k)\ge f(k,o)\),可以证明\(k\)永远不可能成为最优决策。因为当\(f(j,k)>2aSum_i\)时,\(j\)比\(k\)优,\(k\)不是最优的;当\(f(j,k)\le 2aSum_i\)时,结合\(f(j,k)\ge f(k,o)\)可以得出\(f(k,o)\le 2aSum_i\),即\(k\)不比\(o\)优,此时\(k\)也不是最优的。
所以我们要维护一个两个元素之间的直线的斜率严格单调递减的决策序列\(J\)(下标从\(1\)开始),即一个上凸壳,只有\(J\)里的决策才有可能最优。
但是\(J\)里有那么多决策,那个才是最优的呢?如果\(J\)中没有一个斜率\(\le2aSum_i\),即所有斜率都\(>2aSum_i\),显然\(J_{|J|}\)是最优决策;如果有,设\(J_j\)是第一个\(\le2aSum_i\)的斜率所对应的直线的左端点,那么\(J_j\)是最优决策。
不难想到,我们可以维护一个单调栈充当\(J\),装着对于目前的\(i\),所有可能最优的决策,然后二分找到最优决策。可是在DP的过程中,\(2aSum_i\)也是单调递减的(因为\(a<0\)),也就是说今朝一个斜率\(>2aSum_i\),那么它以后都要\(>2aSum_i\),即它对应的直线的左端点以后都不会成为最优决策,那就把这个废点弹出吧!用一个单调队列,在每次找最优决策时,不断地弹出队首(自带音效biubiubiu)直到第\(1\)个斜率\(\le2aSum_i\)为止。这样最优决策就是队首了。
\([1,n]\)内每个元素最多入队出队各一次,所以单调队列操作的总时间复杂度是\(\operatorname{O}(n)\),DP也是\(\operatorname{O}(n)\)的,整个就是\(\operatorname{O}(n)\)啦!
最后再唠叨\(2\)句:
- 这题用
int
会爆炸,要用long long
。 - 单调队列用STL里的
deque
比较慢,最好自己写。
讲了这么多,终于上代码了(逃
#include<bits/stdc++.h>
using namespace std;
#define int long long//防爆int
#define N 1000000
inline int sq(int x){return x*x;}//平方
int Sum[N+1]/*前缀和*/,q[N]/*单调队列*/,dp[N+1]/*dp[i]表示前i个士兵的最大战斗力*/,a,b,c;
inline double son(int j,int k)/*纵坐标之差,即斜率的分子*/{return (dp[j]+a*sq(Sum[j])-b*Sum[j])-(dp[k]+a*sq(Sum[k])-b*Sum[k]);}
inline double ma(int j,int k)/*横坐标之差,即斜率的分母*/{return Sum[j]-Sum[k];}
inline double f(int j,int k)/*斜率*/{return son(j,k)/ma(j,k);}
signed main(){
int n/*士兵个数*/,head=0,tail=0,i;scanf("%lld%lld%lld%lld",&n,&a,&b,&c);
for(i=1;i<=n;i++){
int x;scanf("%lld",&x);
Sum[i]=Sum[i-1]+x;//预处理前缀和
}
q[tail++]=0;//初始有一个决策0
for(i=1;i<=n;i++){//状态转移
while(head+1<tail&&f(q[head+1],q[head])>2*a*Sum[i])head++;//弹出废点
dp[i]=dp[q[head]]+a*sq(Sum[i]-Sum[q[head]])+b*(Sum[i]-Sum[q[head]])+c;//最佳决策是队首,转移
//转移完了之后,i成了i+1的决策,压入队列
while(head+1<tail&&f(i,q[tail-1])>=f(q[tail-1],q[tail-2]))tail--;//维护队尾单调性
q[tail++]=i;//入队
// printf("dp[%d]=%d\n",i,dp[i]);
}
printf("%lld",dp[n]);//答案为dp[n]
return 0;
}