由于作者去年 tg 都没一等,文中有错误请大家多多提出。
斜率优化是用来求解一系列 dp 方程形如 $f_i=\displaystyle\min_{j=1}^{i}\{f_j+a_i+b_j+x_i\times y_j\}$(其中 $a_i$ 表示只和 $i$ 有关的项,$b_j$ 为只和 $j$ 有关的项,$x_i\times y_j$ 表示 $i$ 有关的项和 $j$ 有关的项相乘得到的项)。
显然转移方程中 $\min$ 换为 $\max$ 也同理。
考虑把转移方程化简,得到 $f_i=\displaystyle\min_{j=1}^{i}\{f_j+b_j+x_i\times y_j\}+a_i$,此时,我们把上式与一次函数的截距联系起来,令 $Y=f_j+b_j,k=-x_i,X=y_j$,此时方程即可化为 $f_i=\displaystyle\min_{j=1}^{i}\{Y-kX\}+a_i$。
此时,根据一次函数定义,可以得到 $Y-kX$ 表示的是经过经过点 $(X,Y)$ 的斜率为 $k$ 的直线的截距。所以我们只需要将斜率为 $k$ 的直线从下往上(如果是 $\max$ 就从上往下)贴合决策点,第一次经过的决策点即为更新 $f_i$ 最优的点。
通过观察以下图,可以发现中间的点 $B$ 和 $E$ 一定不会被任一斜率的最优直线经过,只有在下凸壳上的点才有可能对答案有贡献(大家别被下凸壳给吓到了,这里其实只是一个名词,与实际代码没有关系)。
如果 $k,x$ 均对于 $i$ 单调递增,那么那么可以用单调队列实现,如果仅 $x$ 递增,那么可以用二分实现,其他情况可以用平衡树/CDQ分治等算法优化,这里不展开(主要是作者根本不会 23333)。
模板
还是上式转移方程对应的代码,这里给出 $k,x$ 均对于 $i$ 递增的做法:
int f[N],q[N];
int hh = 0,tt = -1;
q[++tt] = 0;
// 这里 Y (i),X (i),K (i) 就是上式中推出的式子中的 Y,X,k
double slope (int i,int j) {
return (Y (j) - Y (i)) / (X (j) - X (i));
}
for (int i = 1;i <= n;i++) {
while (hh < tt && slope (q[hh],q[hh + 1]) < K (i)) hh++;
f[i] = f[q[hh]] + a[i] + b[q[hh]] + x[i] * y[q[hh]]; //转移方程
while (hh < tt && slope (q[tt - 1],q[tt]) > slope (q[tt],i)) tt--;
q[++tt] = i;
}
P2120
这里不选取任务安排系列的题目原因是非斜率优化部分过于麻烦。
下文中 $d_i$ 即为原题面中的 $x_i$。
考虑列出暴力转移方程 $f_i=\displaystyle\min_{j=0}^{i-1}\{f_j+c_i+\sum_{k=j+1}^{i}p_k\times(d_i-d_k)\}$。(这里 $k$ 的范围为 $i$ 其实会好写一些,后面有解释)
展开,得到 $*=\displaystyle\min_{j=0}^{i-1}\{f_j+c_i+(\sum_{k=j+1}^{i}p_k\times d_i)- (\sum_{k=j+1}^{i}p_k\times d_k)\}$。
继续化简得到 $*=\displaystyle\min_{j=0}^{i-1}\{f_j+c_i+d_i\times(\sum_{k=j+1}^{i}p_k)- (\sum_{k=j+1}^{i}p_k\times d_k)\}$。
令 $g_{1,i}=\displaystyle\sum_{j=1}^i p_j$,$g_{2,i}=\displaystyle\sum_{j=1}^i (p_j\times d_j)$。
那么得到 $*=\displaystyle\min_{j=0}^{i-1}\{f_j+c_i+d_i\times(g_{1,i}-g_{1,j})- (g_{2,i}-g_{2,j})\}$。
展开并添加括号得到 $*=\displaystyle\min_{j=0}^{i-1}\{f_j+g_{2,j}-d_i\times g_{1,j}\}+c_i+d_i\times g_{1,i}-g_{2,i}$。
令 $Y=f_j+g_{2,j},k=d_i,X=g_{1,j}$,那么 $\min$ 里的项就是 $\displaystyle\min_{j=0}^{i-1}\{Y-kX\}$,显然可以用前面的式子进行优化。
注意到 $k$ 关于 $i$ 递增而递增,所以我们可以用单调队列维护下凸壳。
下面给出核心代码:
LL Y (int i) {
return f[i] + g2[i];
}
LL X (int i) {
return g1[i];
}
LL K (int i) {
return x[i];
}
double slope (int i,int j) {
if (X (i) == X (j)) return Y (j) - Y (i) > 0 ? 9e18 : -9e18; // 特判斜率正负无穷大
return (Y (j) - Y (i)) / (X (j) - X (i));
}
x[++n] = 3e9; // 添加哨兵,这题数据比较恶心
for (int i = 1;i <= n;i++) g1[i] = g1[i - 1] + p[i],g2[i] = g2[i - 1] + p[i] * x[i];
int hh = 0,tt = -1;
q[++tt] = 0;
for (int i = 1;i <= n;i++) {
while (hh < tt && slope (q[hh],q[hh + 1]) < K (i)) hh++;
f[i] = f[q[hh]] + x[i] * (g1[i] - g1[q[hh]]) - (g2[i] - g2[q[hh]]) + c[i];
// cout << q[hh] << endl;
while (hh < tt && slope (q[tt - 1],q[tt]) > slope (q[tt],i)) tt--;
q[++tt] = i;
}
简单总结一下,我们对于上述类似的转移方程,我们设 $Y$ 等于仅和 $j$ 有关的项,$k$ 等于有关 $i$ 的项和有关 $j$ 的项相乘的项中的 $i$ 的项(就是 $x_i\times y_j$ 中的 $x_i$),$X$ 等于有关 $i$ 的项和有关 $j$ 的项相乘的项中的 $j$ 的项(就是 $x_i\times y_j$ 中的 $y_j$)。
这样就讲完了最最最模板的斜率优化了。
求个赞不过分吧 QaQ
/bx /bx /bx
%%%
%%%
我j组一等就直接谢天谢地了
你看你luogu发的牛牛(感觉%%%)
拉格朗日中值定理 或者 暴力凸包可能也是一种选择
%%%%%%%%%%%%%%%%%%%
%%%
qwq
因为它的本质就是一个斜率不等式
CDQ\平衡树 套斜率优化这种其实维护起来也不方便,这类题应该可以用一些更简单的方法维护
巨巨巨 %%%