【总结】斜率优化DP
于是,XSC062开始写总结。
斜率优化DP
前置芝士
(夹带私货)
正文
我们以一道题为例。
打印文章
双倍经验 三倍经验
Solution
明显DP。
那么DP式就是:
\[\begin{aligned} f_i&=\min\{f_j+(s_i-s_j)^2+M\} \\ &=\min\{f_j+{s_i}^2-2\times s_i\times s_j+{s_j}^2+M\} \\ &=\min\{f_j-2\times s_i\times s_j+{s_j}^2\}+{s_i}^2+M \end{aligned} \]其中 \(s\) 为 \(c\) 的前缀和。
时间复杂度 \(\Theta(n^2)\),明显爆炸,所以我们需要优化。
在上一篇的 中,我们提到过,只有DP式中的与 \(i\) 有关的项能直接提出来时,我们才能使用单调队列优化,而这里的 \(s_i\) 与 \(s_j\) 相乘,无法使用单调队列优化。
我们思考,对于 \(f_i\) 来说,无非就是选出最优的 \(j\) 来构造它。
假设有 \(j\) 与 \(k\),如何判断 \(j\) 与 \(k\) 谁更优呢?
我们先钦定 \(j\) 优于 \(k\),且 \(j
那么可以得到:
\[f_j-2\times s_i\times s_j+{s_j}^2+{s_i}^2+M化简得:
\[f_j-2\times s_i\times s_j+{s_j}^2再将只与 \(j,k\) 有关的项放到左边,与 \(i\) 有关的项放到右边:
\[f_j-f_k+{s_j}^2-{s_k}^2<2\times s_i\times s_j-2\times s_i\times s_k \\ f_j-f_k+{s_j}^2-{s_k}^2<2\times s_i\times(s_j-s_k) \]左右两边同时 \(\div\) 与 \(i\) 无关的项 \(2\times(s_j-s_k)\) :
\[\dfrac{(f_j+{s_j}^2)-(f_k+{s_k}^2)}{(2\times s_j)-(2\times s_k)}如果满足上式 ,则 \(j
记住上面推式子的步骤,现在记不住等会儿再重复这个步骤推十几个式子也能记住的OwO
充要条件是什么鬼呀,我不会证呀QAQ
好了,接下来是斜率优化的重点部分——
在义务教育阶段,学生学习了一次函数,它的几何意义表示为一条直线,一次项的系数就是直线的斜率,只不过当直线与 \(x\) 轴垂直的时候无法表示。虽然没有明确给出斜率这个名词,但实际上思想已经渗透到其中。
直线对 \(x\) 轴的倾斜角 \(\alpha\) 的正切值 \(\tan\alpha\) 称为该直线的“斜率”,并记作 \(k\) ,公式为 \(k=\tan\alpha\)。
即 \(k=\tan\alpha=\dfrac{\Delta y}{\Delta x}=\dfrac{y_2-y_1}{x_2-x_1}\) 或 \(\dfrac{y_1-y_2}{x_1-x_2}\) 。
——选自 斜率_百度百科
XSC062看完了百度百科表示你TM在说些啥是不是欺负我六年义务教育的小学生恍然大悟
上面推出来的那个关于 \(j\) 和 \(k\) 的DP式,不就是求两个点 \((f_j+{s_j}^2,2\times s_j)\) 和 \((f_j+{s_k}^2,2\times s_k)\) 连成一条线之后的坡度吗!
本文后面的部分,\(x\) 的含义会在“点\((f_x+{s_x}^2,2\times s_x)\)”和“\(x\)这个下标”之间漂浮,请根据语境识别~
随后XSC062边打瞌睡边听GM讲课(特异功能),勉强算是明白了中心意思:
如图,假设有三个点 \(A,B,C\),以及 \(B,A\) 的斜率 \(k_1\),\(C,B\) 的斜率 \(k_2\)。

我们暂且把这个向外凸起的奇怪玩意儿称为一个上凸。
回到前面的我们得到的那个结论:
\[\dfrac{(f_j+{s_j}^2)-(f_k+{s_k}^2)}{(2\times s_j)-(2\times s_k)}如果满足上式 ,则 \(j
且 \(j\) 优于 \(k\)。
简单记忆为:
- 若点 \(x,y\) 的斜率比 \(s_i\) 小,则 \(x
且 \(x\) 优于 \(y\) 。 - 反之,若点 \(x,y\) 的斜率比 \(s_i\) 大,则 \(y
且 \(y\) 优于 \(x\)。
将 \(A,B\) 代入,我们得知:
- 若 \(k_1\leqslant s_i\),则 \(B\) 优于 \(A\)。
- 若 \(k_1>s_i\),则 \(A\) 优于 \(B\)。
- 若 \(k_2\leqslant s_i\),则 \(C\) 优于 \(B\)。
- 若 \(k_2>s_i\),则 \(B\) 优于 \(C\)。(人类的本质……
因为斜率=倾斜度=竖得有多高,明显 \(k_1>k_2\)。所以针对一个大小关系,\(k_1\) 和 \(k_2\) 之间只有可能有:
- \(s_i
,此时 \(B\) 优于 \(C\),\(A\) 优于 \(B\),\(A\) 为最优解。 - \(k_2
,此时 \(C\) 优于 \(B\),\(A\) 优于 \(B\),\(A,C\) 为最优解。 - \(k_2
,此时 \(C\) 优于 \(B\),\(B\) 优于 \(A\),\(C\) 为最优解。
发现了吗?\(B\) 永远都不可能是最优解。(可怜。。。
所以如果我们要维护一个最优解的序列,就可以不要 \(B\) 了。

那么,这个最优解序列里肯定已经不会存在“上凸”了。
如果是这样的“下凸”呢?

\(k_1\) 为 \(B,A\) 的斜率,\(k_2\) 为 \(C,B\) 的斜率。
将 \(A,B\) 代入,我们得知:
- 若 \(k_1\leqslant s_i\),则 \(B\) 优于 \(A\)。
- 若 \(k_1>s_i\),则 \(A\) 优于 \(B\)。
- 若 \(k_2\leqslant s_i\),则 \(C\) 优于 \(B\)。
- 若 \(k_2>s_i\),则 \(B\) 优于 \(C\)。
明显 \(k_1
- \(s_i
,此时 \(B\) 优于 \(C\),\(A\) 优于 \(B\),\(A\) 为最优解。 - \(k_1
,此时 \(B\) 优于 \(C\),\(B\) 优于 \(A\),\(B\) 为最优解。 - \(k_1
,此时 \(C\) 优于 \(B\),\(B\) 优于 \(A\),\(C\) 为最优解。
所以,在下凸的情况中,三个点都有可能是最优解,都需要保留。
现在呢,所有上凸都被去掉了,只剩下凸,所以大概最后的最优解序列就长这个样子:

好丑
反过来看就是lifan的脑袋了
观察发现,斜率是从左往右递增的。
所以,我们考虑用单调队列来当这个“最优解序列”。
维护队头
即保证队头元素为最优解。
设队头为 \(q_l\)。
如果 \(q_{l+1}\) 与 \(q_l\) 形成的斜率 \(\leqslant s_i\),根据上面推出来的玩意儿,得到 \(q_{l+1}\) 优于 \(q_l\)。
那还要 \(q_l\) 干啥,直接 l++。
更新DP值
\(f_i=f_{q_l}-2\times s_i\times s_{q_l}+{s_{q_l}}^2+{s_i}^2+M\)
维护队尾
即保证里面塞的点相邻两个的斜率递增。(听起来好怪。。。
设队尾为 \(q_r\),我们要往最优解队列里push一个 \(i\) 。
若队尾两个点 \(q_r,q_{r-1}\) 形成的斜率比 \(i,q_r\) 形成的斜率大,那么push(i)后,整个队列的斜率就不再单调递增,所以此时要将r--。(因为中讲到的 \(i\) 必须入队,只能委屈一下 \(q_r\) 了)
注意事项
众所周知,斜率是个浮点数。为了避免损失精度造成的一些惨案,我们交叉相乘,将分子、分母分开处理。
以及时刻都要保证队列中至少有两个点,因为要访问 \(q_l,q_{l+1}\) 和 \(q_r,q_{r-1}\)。
Code
#include
const int maxn=5e5+5;
int n,m,l,r;
int f[maxn],c[maxn],q[maxn];
int getDP(int i,int j){
return f[j]+m+(c[i]-c[j])*(c[i]-c[j]);
}
int getup(int j,int k){
//计算分子的值
return f[j]+c[j]*c[j]-f[k]-c[k]*c[k];
}
int getdown(int j,int k){
//计算分母的值
return (c[j]-c[k])*2;
}
int main(){
while(~scanf("%d%d",&n,&m)){
l=r=1;
//经验表明,凡是涉及到前缀和的单调队列都必须先在队列中push(0),因为sum[0]这个玩意
for(int i=1;i<=n;++i){
scanf("%d",&c[i]);
//懒得开前缀和数组,c自给自足
c[i]+=c[i-1];
//维护队头
//l
在对DP式变形时,我们最好将其化为 \(\dfrac{(j)-(k)}{(j)-(k)}>x\)
这个板子只适用于维护下凸包的情况。
当中间的符号为 \(>\) 时,我们会在这份代码上稍作改动,维护一个上凸包,后文会提到有关内容。
玩具装箱
双倍经验 三倍经验
噢噢噢,紫的,紫的!
Solution
从今往后我们就只讲怎么推式子,不再去证明下凸等性质了。
设 \(f_i\) 表示第 \(i\) 个玩具放完后的最小费用。
\[f_i=\min\{f_j+(i-j-1+\sum\limits_{k=i}^jC_k-L)^2\} \]为了让这个式子好拆,我们在一开始让 L++,并且 \(C\) 再次自给自足,为输入的 \(C\) 的前缀和数组。
于是式子就变成:
\[f_i=\min\{f_j+(i-j-L+C_i-C_j)^2 \]明显硬拆会死人。(反正我试过,比较适合用来发泄
所以我们把式子变成酱紫:
\[f_i=\min\{f_j+((C_i+i)-(C_j+j)-L)^2\} \]既然 \(C_i\) 与 \(i\),\(C_j\) 与 \(j\) 是对应的,那么直接预处理,给 \(C_i\) 加上 \(i\) 不就行了?
现在这个 \(C_i\) 的含义和实现就变得有点曲折难懂了。
具体实现如下:
for(int i=1;i<=n;++i){
scanf("%lld",&c[i]);
c[i]+=c[i-1];
}
for(int i=1;i<=n;++i)
c[i]+=i;
也就是说,\(C_i\) 是在前缀和的基础上加了一个 \(i\),注意不能把 \(i\) 也一起前缀和了。
然后式子就变成了这样:
\[\begin{aligned} f_i&=\min\{f_j+(C_i-C_j-L)^2\} \\ &=\min\{f_j+{C_i}^2+{C_j}^2+L^2-2\times C_i\times C_j-2\times C_i\times L+2\times C_j\times L\} \\ &=\min\{f_j+{C_j}^2-2\times C_i\times C_j+2\times C_j\times L\}+{C_i}^2+L^2-2\times C_i\times L \end{aligned} \]令 \(j\) 优于 \(k\) 且 \(j
得:
\[f_j+{C_j}^2-2\times C_i\times C_j+2\times C_j\times LCode
把上一题的代码中getup,getdown,getDP以及循环条件换一换就行了。
#include
#define int long long
const int maxn=5e4+5;
const int inf=0x3f3f3f3f;
int n,L,l,r;
int f[maxn],c[maxn],q[maxn];
int getDP(int i,int j){
return f[j]+(c[i]-c[j]-L)*(c[i]-c[j]-L);
}
int getup(int j,int k){
return f[j]-f[k]+2*c[j]*L-2*c[k]*L+c[j]*c[j]-c[k]*c[k];
}
int getdown(int j,int k){
return c[j]-c[k]<<1;
}
signed main(){
scanf("%lld%lld",&n,&L);
++L;
l=r=1;
for(int i=1;i<=n;++i){
scanf("%lld",&c[i]);
c[i]+=c[i-1];
}
for(int i=1;i<=n;++i){
c[i]+=i;
while(l
任务安排 1
双倍经验 三倍经验
Solution
感谢蓝书。这里按着蓝书上的思维走。
解法一
暴力。
此处的 \(t,c\) 为输入的 \(t,c\) 的前缀和数组。
设 \(f_{i,j}\) 为前 \(i\) 个任务分成 \(j\) 批的最小费用。
故 \(S\times j+t_i\) 为第 \(i\) 个任务的完成时间。
得出状态转移方程(\(k\) 枚举上一批任务结束位置):
\[f_{i,j}=\min\limits_{0\leqslant k时间复杂度 \(\Theta(n^3)\)
代码
#include
#include
const int maxn=5005;
int n,s,ans=1<<30;
int f[maxn][maxn];
int t[maxn],c[maxn];
int min(int x,int y){return x
亲测不开long long边T边WA拢共60,开了long long全MLE没分
解法二
脑子炸了,想了好久才想明白这个优化的正确性。
思考,以上代码需要 \(j\) 这一维的根本原因是什么?
因为我们无法确定之前已经划分了多少批,也就是无法确定 \(S\) 的个数。
换个角度思考,我们无法确定之前,却可以确定之后。
什么意思呢?如果我们在任务 \(i\) 处划分,那么任务 \(i\) 以及任务 \(i\) 以后的所有任务的执行时间都会延后 \(S\)。
因为 \(i\) 以后的状态也会使用 \(f_i\) 的值,我们在计算 \(f_i\) 时就将 \(S\) 提出来,提前把后面的 \(c\) 乘上不就行了?
中间的结果不对劲也无所谓,只要最后的答案是对的就行惹。
也就是说,我们没有直接求出每批任务的完成时刻,而是在一批任务“开始”对后续任务产生影响时,就先把费用累加到答案中。这是一种名为”费用提前计算“的经典思想。
——李煜东《算法竞赛进阶指南》
状态转移方程:
\[f_i=\min\limits_{0\leqslant j此处,\(f_i\) 没有具体含义。
代码
lyd貌似已经给了。。。算了,再写一遍巩固一下
#include
#include
#define int long long
const int maxn=5005;
int n,s;
int t[maxn],c[maxn],f[maxn];
int min(int x,int y){return x
任务安排 2
双倍经验
\(n\) 的范围变大了,\(n^2\) 过不了。
Solution
这不随手加个斜率优化的事儿吗。
我们继续瞎搞这个式子。
\[\begin{aligned} f_i&=\min\{f_j+(c_i-c_j)\times t_i+s\times(c_n-c_j)\} \\ &=\min\{f_j+c_i\times t_i-c_j\times t_i+s\times c_n-s\times c_j\} \\ &=\min\{f_j-c_j\times t_i-s\times c_j\}+c_i\times t_i+s\times c_n \end{aligned} \]令 \(j\) 优于 \(k\) 且 \(j
则有:
\[f_j-c_j\times t_i-s\times c_j然后就是老套路。
时间复杂度 \(\Theta(n)\)。
Code
lyd怎么又给了。。。毫无挑战性。。。
#include
#define int long long
const int maxn=300005;
int n,s,l,r;
int t[maxn],c[maxn],q[maxn],f[maxn];
int getDP(int i,int j){
return f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j]);
}
int getup(int j,int k){
return f[j]-f[k]-s*c[j]+s*c[k];
}
int getdown(int j,int k){
return c[j]-c[k];
}
signed main(){
scanf("%lld%lld",&n,&s);
l=r=1;
for(int i=1;i<=n;++i){
scanf("%lld%lld",&t[i],&c[i]);
t[i]+=t[i-1];
c[i]+=c[i-1];
}
for(int i=1;i<=n;++i){
while(l
任务安排 3
双倍经验 三倍经验
啊,又是刷水紫的一天
\(t\) 有可能是负数。
Solution
\(t\) (输入)有可能为负,代表着 \(t\)(前缀和)不再单调递增,用不单调的玩意作为单调队列的条件一看就十分不讲武德,这样维护出来的队头显然不是正确答案。
后面的 \(t,c\) 默认为前缀和。
好吧,我们感性证明一下为什么不能酱紫玩。(XSC062瞎想的,有错误请指出QwQ
就一个普通的例子,假设输入了一个负数,导致 \(t_i
那么在 \(i-1\) 时,它就被维护队头的操作剔掉了,但也许它凑巧就是 \(i\) 的最优解,呜呼哀哉。
于是我们不能删队头元素了。那怎么查询最优解呢?
单调队列里装的始终还是个具有单调性的下凸包,于是我们可以在队列中二分一个 \(pos\),\(pos\) 与 \(pos-1\) 形成的斜率比 \(t_i\) 小,\(pos+1\) 与 \(pos\) 形成的斜率比 \(t_i\) 大。
然后把 \(pos\) 当成 \(q_l\) 来处理就好惹。
队尾还是维护一个下凸。
时间复杂度 \(\Theta(n\log_2n)\)。
Code
这边强烈建议不要去动AcWing版本的那道。
最后两组数据堪称毒瘤。
卡long long乘法,卡__int128时限,卡double精度,非T即WA。
y总不想要他的horse了
反正我从凌晨1:00到1:30硬是没有搞出来。
#include
#include
#define int long long
const int maxn=300005;
int n,s,l,r;
int t[maxn],c[maxn],q[maxn],f[maxn];
int getDP(int i,int j){
return f[j]+t[i]*(c[i]-c[j])+s*(c[n]-c[j]);
}
int getup(int j,int k){
return f[j]-f[k]-s*c[j]+s*c[k];
}
int getdown(int j,int k){
return c[j]-c[k];
}
int BinarySearch(int val){
if(l==r)return q[l];
int L=l,R=r;
//二分整个单调队列
while(L>1;
if(getup(q[mid+1],q[mid])<=val*getdown(q[mid+1],q[mid]))
L=mid+1;
else R=mid;
}
return q[L];
}
signed main(){
scanf("%lld%lld",&n,&s);
l=r=1;
for(int i=1;i<=n;++i){
scanf("%lld%lld",&t[i],&c[i]);
t[i]+=t[i-1];
c[i]+=c[i-1];
}
for(int i=1;i<=n;++i){
int pos=BinarySearch(t[i]);
f[i]=getDP(i,pos);
while(l
土地购买
Solution
首先我们想明白一件事情:如果一块土地,有另一块土地的长和宽都比它大,那就不用再理它了,直接从总序列里剔除。
struct land{
int w,l;
bool operator<(const land q)const{
return w==q.w?l>q.l:w>q.w;
}
}a[maxn];
...
sort(a+1,a+n+1);
for(int i=1;i<=n;++i){
if(a[i].l>a[cnt].l)
a[++cnt]=a[i];
}
然后就推式子。
贪心地想,在前面的操作后,\(a\) 是一个 \(w\) 递减,\(l\) 递增的土地序列。
所以我们选择将连续的一段区间分为一组,这样的话,一个组里的一段连续的土地 \([x,y]\) 就只有 \(w_x\) 和 \(l_y\) 起了作用,又没有中间那一段的事了。
设 \(f_i\) 表示分配完第 \(i\) 块土地后的最小花费。
则有:
\[f_i=\min\{f_j+w_{j+1}\times l_i\} \]令 \(j\) 优于 \(k\)。
则有:
\[f_j+w_{j+1}\times l_iCode
#include
#include
using std::sort;
#define int long long
const int maxn=5e4+5;
struct land{
int w,l;
bool operator<(const land q)const{
return w==q.w?l>q.l:w>q.w;
}
}a[maxn];
int n,cnt,l,r;
int q[maxn],f[maxn];
int getup(int j,int k){return f[j]-f[k];}
int getdown(int j,int k){return a[k+1].w-a[j+1].w;}
int getDP(int i,int j){return f[j]+a[j+1].w*a[i].l;}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i)
scanf("%lld%lld",&a[i].w,&a[i].l);
sort(a+1,a+n+1);
for(int i=1;i<=n;++i){
if(a[i].l>a[cnt].l)
a[++cnt]=a[i];
}
n=cnt;
l=r=1;
for(int i=1;i<=n;++i){
while(l
仓库建设
双倍经验 三倍经验
Solution
辣鸡AcWing,毁我青春
设 \(f_i\) 表示在 \(i\) 工厂建立仓库的最小花费。
则有:
\[\begin{aligned} f_i&=\min\{f_j+\sum\limits_{k=j+1}^{i-1}[(x_i-x_k)\times p_k]+c_i\} \\ &=\min\{f_j+\sum\limits_{k=j+1}^{i-1}(x_i\times p_k)-\sum\limits_{k=j+1}^{i-1}(x_k\times p_k)\} \\ &=\min\{f_j+x_i\times\sum\limits_{k=j+1}^{i-1}p_k-\sum\limits_{k=j+1}^{i-1}(x_k\times p_k)\} \end{aligned} \]利用前缀和优化:设 \(a\) 为 \(p\) 的前缀和数组,\(b\) 为 \(x_i\times p_i\) 的前缀和数组。
则原式可化为:
\[\begin{aligned} f_i&=\min\{f_j+x_i\times(a_{i-1}-a_j)-(b_{i-1}-b_j)\} \\ &=\min\{f_j+x_i\times a_{i-1}-x_i\times a_j-b_{i-1}+b_j\} \\ &=\min\{f_j-x_i\times a_j+b_j\}+x_i\times a_{i-1}-b_{i-1} \end{aligned} \]令 \(j\) 优于 \(k\) 。
则有:
\[f_j-x_i\times a_j+b_jCode
想尝试自闭的同学们可以尝试做一下AcWing那个链接,非MLE即WA,爽到炸。
#include
#define int long long
const int maxn=1e6+5;
int n,l,r;
int a[maxn],b[maxn];
int f[maxn],x[maxn],p[maxn],c[maxn],q[maxn];
int getDP(int i,int j){
return f[j]+(a[i]-a[j])*x[i]-(b[i]-b[j])+c[i];
}
int getup(int j,int k){
return f[j]-f[k]+b[j]-b[k];
}
int getdown(int j,int k){
return a[j]-a[k];
}
signed main(){
scanf("%lld",&n);
l=r=1;
for(int i=1;i<=n;++i){
scanf("%lld%lld%lld",&x[i],&p[i],&c[i]);
a[i]=a[i-1]+p[i];
b[i]=b[i-1]+x[i]*p[i];
while(l
Cats Transport
双倍经验 三倍经验
经验:洛谷的题面比蓝书和AcWing上的标准多了,蓝书的题意不清。
Solution
研究表明,边猛灌养乐多边看蓝书有助于理解。
这道题难在推式子。(不然还能难在哪儿
设 \(A_i\) 表示要接到第 \(i\) 只猫的最早出发时间,也就是说,在此时出发,猫 \(i\) 的等待时间为 \(0\)。
即 \(A_i=T_i-\sum\limits_{j=1}^{H_i}D_j\),也就是出发时间=到达时间-经过时间。
此时我们可以把其他所有因素去掉,题意转换为:
已知在 \(\geqslant A_i\) 的时刻出发可以接到猫 \(i\) ,在 \(P\) 次出发次数的限制内接到所有猫,猫的等待时间之和最小是多少?
假设饲养员在 \(t\) 时刻出发,猫 \(i\) 的等待时间就是 \(t-A_i\)。
对 \(A_i\) 从小到大排序。
有脑子就能想明白,一次接一段连续的猫,花费自然是最小的。
若我们要接 \([k+1,j]\) 范围内的猫,它们的等待时间之和就是:
\[\begin{aligned} \sum\limits_{p=k+1}^{j}(A_j-A_p)&=[j-(k+1)+1]A_j-\sum\limits_{p=k+1}^{j}A_p \\ &=(j-k)\times A_j-(S_j-S_k) \\ &=j\times A_j-k\times A_j-S_j+S_k \end{aligned} \]其中 \(S\) 为 \(A\) 的前缀和。
设 \(f_{i,j}\) 表示前 \(i\) 个饲养员带走前 \(j\) 只猫的最小花费。
则有:
\[\begin{aligned} f_{i,j}&=\min\{f_{i-1,k}+j\times A_j-k\times A_j-S_j+S_k\} \\ &=\min\{f_{i-1,k}-A_j\times k+S_k\}+A_j\times j-S_j \end{aligned} \]将循环地枚举每个饲养员的循环变量 \(i\) 看做常量。
令 \(x\) 优于 \(y\),则有:
\[f_{i-1,x}-A_j\times x+S_x注意事项
\(f\) 初始化为极大值。
Code
#include
#include
#include
using std::sort;
#define int long long
const int maxn=1e5+5;
int n,m,p,h,t,l,r;
int f[105][maxn];
int a[maxn],s[maxn],d[maxn],q[maxn];
int getDP(int i,int j,int k){
return f[i-1][k]+j*a[j]-k*a[j]-s[j]+s[k];
}
int getup(int i,int x,int y){
return f[i-1][x]+s[x]-f[i-1][y]-s[y];
}
int getdown(int x,int y){
return x-y;
}
signed main(){
memset(f,0x3f,sizeof(f));
scanf("%lld%lld%lld",&n,&m,&p);
for(int i=2;i<=n;++i){
scanf("%lld",&d[i]);
d[i]+=d[i-1];
}
for(int i=1;i<=m;++i){
scanf("%lld%lld",&h,&t);
a[i]=t-d[h];
}
sort(a+1,a+m+1);
for(int i=1;i<=m;++i)
s[i]=s[i-1]+a[i];
f[0][0]=0;
for(int i=1;i<=p;++i){
l=r=1;
for(int j=1;j<=m;++j){
while(l
征途
双倍经验 三倍经验
Solution
那么就直接化掉 \(v\times m^2\) 叭awa
设 \(x\) 为当前休息站点与上一休息站点的距离, \(x_0\) 为 \(x_1\sim x_n\) 的平均数,\(S\) 为 \(x\) 的前缀和。
\[\begin{aligned} V\times m^2&=[(x_1-x_0)^2+(x_2-x_0)^2+\cdots+(x_m-x_0)^2]\times m \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2+(m\times {x_0})^2-2\times (x_0\times m)\times \sum\limits_{i=1}^{m}x_i \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2+{S_m}^2-2\times{S_m}^2 \\ &=m\times \sum\limits_{i=1}^{m}{x_i}^2-{S_m}^2 \end{aligned} \]锵锵!平均数什么的,不就无了喵?
其中 \(S_m\) 是一个定值。(即输入所有路的长度和)
唯一需要计算的,就是 \(\sum\limits_{i=1}^{m}{x_i}^{2}\),所以我们就来DP这个东西。
设 \(a\) 为输入道路长度的前缀和数组,\(f_{i,j}\) 表示第 \(i\) 次休息在 \(j\) 处时 \(\min\{\sum\limits_{k=1}^{i}{x_k}^2\}\) 的值,则有:
\[\begin{aligned} f_{i,j}&=\min\{f_{i-1,j}+(a_i-a_j)^2\} \\ &=\min\{f_{i-1,j}+{a_i}^2-2\times a_i\times a_j+{a_j}^2\} \end{aligned} \]令 \(j\) 优于 \(k\),则有:
\[f_{i-1,j}-2\times a_i\times a_j+{a_j}^2注意事项
不注意初始化还能注意哪儿。
将 \(f\) 初始化为极大值。
Code
在破烂 AcWing 上将 memset 改为初始化 f[0][i]=a[i]*a[i] 就可以过了www
不然会MLE,也许是因为没开滚动。(本来也用不着滚
#include
#include
#define int long long
const int maxn=3005;
int f[maxn][maxn];
int a[maxn],q[maxn];
int n,m,l,r,ans=1e18;
int getDP(int i,int j,int k){
return f[k-1][j]+(a[i]-a[j])*(a[i]-a[j]);
}
int getup(int j,int k,int i){
return f[i-1][j]-f[i-1][k]+a[j]*a[j]-a[k]*a[k];
}
int getdown(int j,int k){
return a[j]-a[k]<<1;
}
int min(int x,int y){return x
扩展题目-柠檬
双倍经验
Solution
是边写这篇边做的呢。
题面翻译成人话就是:
有一个含有 \(n\) 个元素的序列 \(s\)。将这个序列分成连续的若干段,定义每一段的价值为"在这一段当中任选某个元素的个数的平方再乘上这个元素"的最大值。求将 \(s\) 划分后的最大价值。
不难想到分成的每一段首尾元素必须相等。
比如有这样一个序列 \(x,a_1,a_2,a_3,\cdots,a_k,x,y\) 。假设我们求的是这一段包含 \(x\) 的个数的平方,那么完全可以将 \(y\) 从这一段中分离,单独为一段,明显更优。
而其他所有情况都是这种情况的拓展。
于是得到式子:
\[f_i=\max\{f_{j-1}+s_i\times(cnt_i-cnt_j+1)^2\} \]其中 \(cnt_i\) 为 \(s_i\) 已经出现了多少次。
然后就是套路。
\[\begin{aligned} f_i&=\max\{f_{j-1}+s_i\times({cnt_i}^2+{cnt_j}^2-2\times cnt_i\times cnt_j+2\times cnt_i-2\times cnt_j+1)\} \\ &=\max\{f_{j-1}+s_i\times{cnt_i}^2+s_i\times{cnt_j}^2-2\times s_i\times cnt_i\times cnt_j+2\times s_i\times cnt_i-2\times s_i\times cnt_j+s_i\} \\ &=\max\{f_{j-1}+s_i\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_j\}+s_i\times {cnt_i}^2+2\times s_i\times cnt_i+s_i \end{aligned} \]这次不是 \(\min\) 了,而是 \(\max\),还能这样玩吗?
不急,我们先按以前的方法试试,见机行事。
令 \(j\) 优于 \(k\),则有:
\[f_{j-1}+s_i\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_j>f_{k-1}+s_i\times {cnt_k}^2-2\times s_i\times cnt_i\times cnt_k-2\times s_i\times cnt_k \]经试验,硬搞搞不出来,因为那个 \(s_i\) 乘上的平方项。
那我们想一个办法,把 \(s_i\) 给搞掉就行了。
既然我们已知一段的首尾元素必须相等,那不就说明 \(s_i,s_j,s_k\) 可以相互替换吗?
继续搞。
\[f_{j-1}+s_j\times {cnt_j}^2-2\times s_i\times cnt_i\times cnt_j-2\times s_j\times cnt_j>f_{k-1}+s_k\times {cnt_k}^2-2\times s_i\times cnt_i\times cnt_k-2\times s_k\times cnt_k \\ f_{j-1}-f_{k-1}+s_j\times {cnt_j}^2-s_k\times {cnt_k}^2-2\times s_j\times cnt_j+2\times s_k\times cnt_k>2\times s_i\times cnt_i\times cnt_j-2\times s_i\times cnt_i\times cnt_k \\ \dfrac{f_{j-1}-f_{k-1}+s_j\times {cnt_j}^2-s_k\times {cnt_k}^2-2\times s_j\times cnt_j+2\times s_k\times cnt_k}{cnt_j-cnt_k}>2\times s_i\times cnt_i \]这里的符号变成 \(>\) 了。
证明得知,这里要维护的是一个斜率单调递减的上凸包最优解序列。
由于篇幅有限(实际上是因为懒),在这里就不把有关证明过程贴上来了,读者可以自行证明。
因为 \(s_i\) 必须与 \(s_j,s_k\) 相同,明显要针对每个 \(s_i\) 维护不同的最优解序列,在每次对应的序列中计算。
维护的斜率单调递减,而对于每一个相等的 \(s_i\), \(2\times s_i\times cnt_i\) 一定是单调递增的——
导致了这个序列大概长这个瓜样:斜率单调递减,但末尾的最小的斜率仍大于 \(2\times s_i\times cnt_i\)。
单调栈可以自行研究,因为作者瞄了一眼时间发现已经是明天了。(什么鬼
Code
#include
#include
//不用vector会MLE(但vector难调到像吃了shit一样
using namespace std;
#define int long long
#define top q[x].size()-1
const int maxn=1e5+5;
const int maxm=1e4+5;
int n;
vectorq[maxm];
int f[maxn],s[maxn];
int cnt[maxn],tot[maxm];
int getDP(int i,int j){
return f[j-1]+(cnt[i]-cnt[j]+1)*(cnt[i]-cnt[j]+1)*s[i];
}
int getup(int j,int k){
return f[j-1]-f[k-1]+s[j]*cnt[j]*cnt[j]-s[k]*cnt[k]*cnt[k]-2*s[j]*cnt[j]+2*s[k]*cnt[k];
}
int getdown(int j,int k){
return cnt[j]-cnt[k];
}
signed main(){
scanf("%lld",&n);
for(int i=1;i<=n;++i){
scanf("%lld",&s[i]);
cnt[i]=++tot[s[i]];
}
for(int i=1;i<=n;++i){
int x=s[i];
while(q[x].size()>=2&&getup(q[x][top-1],i)*getdown(q[x][top-1],q[x][top])>=getup(q[x][top-1],q[x][top])*getdown(q[x][top-1],i))
q[x].pop_back();
q[x].push_back(i);
while(q[x].size()>=2&&getDP(i,q[x][top])<=getDP(i,q[x][top-1]))
q[x].pop_back();
f[i]=getDP(i,q[x][top]);
}
printf("%lld",f[n]);
return 0;
}
总结
总结搞斜优题的步骤就是:
- 推DP式子
- 看看这个式子是不是人能化的
- 假设 \(j\) 由于 \(k\),将式子化成 \(\dfrac{(j)-(k)}{(j)-(k)}> \text{or}
的形式 - 单调队列/单调栈优化。
- 然后你就又多了一道紫色起步的题目。
end.