单调队列优化&斜率优化DP
单调队列优化
单调队列,顾名思义是值随着下标而单调递增/减的序列,是斜率优化、单调决策性优化的基础
显然可以用它来优化的DP具有以下性质:
- 转移方程是求最值
- 形如
f_i=max/min(f_j+g(i,j)) ,g(i,j) 的值单调 - 决策点有单调的上界或下界
单调队列中存储的是决策点,就是那些可以向
我们使用数组来模拟队列,保存队首为
- 将队首不符合要求的元素删掉(
hd++ ) - 根据队首的值进行DP转移
- 将队尾不如
i 优的决策删掉(tl-- ) - 存入新决策
i (q_{++tl}=i )
下面用例题来详细说明一下
例1:P3594
题面
长度不超过
由于这个题问的是区间长度,所以不妨定下区间右端点
设
朴素DP是遍历
显然,想让
首先考虑队首,一般情况下队首删除的是 “不符合规定的”,所以如果
然后考虑队尾,一般队尾删掉的是 “不如当前决策优的”,明确
最后考虑
答案就是对于每个
#include <bits/stdc++.h>
using namespace std;
long long a[2000005],s[2000005],q[2000005];
int main()
{
long long n,m,d;
scanf("%lld%lld%lld",&n,&m,&d);
for(long long i = 1;i <= n;i++)
{
scanf("%lld",&a[i]);
s[i] = s[i - 1] + a[i];
}
long long hd = 1,tl = 1,l = 1,maxa = d;
for(long long i = 1;i <= n;i++)
{
long long r = i;
while(hd <= tl && s[i] - s[i - d] > s[q[tl]] - s[q[tl] - d])
{
tl--;
}
q[++tl] = i;
while(hd <= tl && s[i] - s[l - 1] - (s[q[hd]] - s[q[hd] - d]) > m)
{
l++;
while(hd <= tl && q[hd] - l + 1 < d)
{
hd++;
}
}
maxa = max(maxa,r - l + 1);
}
printf("%lld",maxa);
return 0;
}
例2:P4954
题面
显然塔越细越高,所以每一层的宽度要小
如果我们从塔底开始堆,那么每一层都有一个宽度上限,这代表可能堆不完,所以从塔顶开始堆,这样只会有下限,一定可以堆完,每包干草的宽度采取倒序输入
设
转移条件为:
移项:
对于队首,找到所有决策点中离
转移的时候从
对于队尾,在
#include <bits/stdc++.h>
using namespace std;
long long a[100005],s[100005],q[100005],f[100005],g[100005];
int main()
{
long long n;
scanf("%lld",&n);
for(long long i = n;i >= 1;i--)
{
scanf("%lld",&a[i]);
}
for(int i = 1;i <= n;i++)
{
s[i] = s[i - 1] + a[i];
}
long long hd = 1,tl = 1;
for(long long i = 1;i <= n;i++)
{
while(hd <= tl && s[i] - s[q[hd]] >= g[q[hd]])
{
hd++;
}
g[i] = s[i] - s[q[hd - 1]];
f[i] = f[q[hd - 1]] + 1;
while(hd <= tl && s[i] + g[i] < s[q[tl]] + g[q[tl]])
{
tl--;
}
q[++tl] = i;
}
printf("%lld",f[n]);
return 0;
}
斜率优化
斜率:假设直线
斜率优化就是利用斜率的单调性来优化DP,有点抽象,通过例题理解
例题:P3195
题面
(一)代数法理解
设
为了方便表述,我们把
设有
此时我们把含有
为了形成斜率的格式,我们令
然后,把所有的点放到二维平面上,它们的下凸包就是所有可能的决策,原因可以通过三个点分类讨论来推广,这里不再赘述(注意并非所有的都是下凸包,上式要求点
我们要对一个问题进行优化,要关注两件事:正确性和单调性,对于斜率优化,正确性显然,而单调性我们看下凸包:
橙色线的斜率逆时针单调递减,灰蓝色的点不选是因为一定能找到两个湖蓝色的点使之不可能成为最优决策
下面来找最优决策点,这个点要满足与左边的点构成的边的斜率
(二)几何法理解
原本DP方程:
我们要使
显然上图中,红色点的截距最小,那么它就是最优决策点
(三)代码实现
使用单调队列维护下凸包点集,大致分为三步:
- 择优筛选,找到第一个斜率
> 2s_i 的边,此时q_{hd} 就是最优决策 - 利用
q_{hd} 更新f_i - 加入决策点
i ,随之更新点集,使其满足下凸包性质 (如果点集的倒数第二个点和i 形成的边可以把队尾删掉,那么就tl-- )
可以这样判断队尾可删:xl(q[tl - 1],i) <= xl(q[tl - 1],q[tl])(xl函数求斜率)
时间复杂度:
#include <bits/stdc++.h>
using namespace std;
long long n,m,s[50005],f[50005],q[50005];
long long X(long long a)
{
return s[a];
}
long long Y(long long a)
{
return f[a] + (s[a] + m) * (s[a] + m);
}
long long xl(long long a,long long b)
{
return (Y(b) - Y(a)) / (X(b) - X(a));
}
int main()
{
scanf("%lld%lld",&n,&m);
m++;
for(long long i = 1;i <= n;i++)
{
long long a;
scanf("%lld",&a);
s[i] = s[i - 1] + a + 1;
}
long long hd = 1,tl = 1;
for(long long i = 1;i <= n;i++)
{
while(hd <= tl - 1 && xl(q[hd],q[hd + 1]) <= 2 * s[i])
{
hd++;
}
f[i] = f[q[hd]] + (s[i] - s[q[hd]] - m) * (s[i] - s[q[hd]] - m);
while(hd <= tl - 1 && xl(q[tl - 1],i) <= xl(q[tl - 1],q[tl]))
{
tl--;
}
q[++tl] = i;
}
printf("%lld",f[n]);
return 0;
}
注意:因为至少两个点才能组成一条线,所以while循环条件为 (别问我怎么知道的喵
其实还可以按照决策单调性再优化,不过还没遇到这样的题,遇到了回来补
(四)一些注意点
- 写出朴素DP方程后,应该先推式子,看能否出现
\frac{Y(i)-Y(j)}{X(i)-X(j)} 的形式 - 根据最后推出的斜率方程,不定斜率是
\ge kk_i 还是\le kk_i ,来确定决策是在上凸包还是下凸包;还可以根据DP方程求max/min 知道维护上凸还是下凸(此处“/”两边分别对应) - 如果发现
X(i)=X(j) 的非严格单调情况,则求斜率的时候就不能直接除,应改成return Y(j) >= Y(i) ? inf : -inf - 有时队列初始需要加入一个初值,一般是
f_0
(五)单调性
如P3195,它的
如果
如果
均不单调时用平衡树维护前驱后继,查询
附:来自神犇的警句
从哪里转移,主要是看dp方程如何设的,而非是一定的,只是是一般在
q_{left} 的位置