斜率优化一般使用单调队列辅助进行实现,用于优化 \(DP\) 的时间复杂度,比较抽象,须要读者有较高的数学素养。数组
本文例题连接函数
使用单调队列优化 \(DP\) ,一般能够解决型如: \(dp[i]=min(f(j))+g(i)\) 的状态转移方程。其中 \(f(i)\) 是只关于 \(i\) 的函数, \(g(j)\) 是只关于 \(j\) 的函数。朴素的解决方法是在第二层循环中枚举 \(j\) 来实现最小值,时间复杂度为 \(O(n^2)\) 。可使用单调队列来维护这个最小值实现 \(O(n)\) 的时间复杂度。优化
而斜率优化利用上述方法进行改进,实现对于型如: \(dp[i]=min(f(i,j))+g(i)\) 的状态转移方程。对比第一种状况,能够发现函数 \(f\) 函数与两个值 \(i,j\) 都有关,简单地使用单调队列是没法优化的。这时候就开始引入主题斜率优化了。ui
下面结合一道例题来具体详解。题目来自于 \(HNOI2008\) 省选题目。spa
有 \(n\) 个数字 \(C\),把它分为若干组,给出另外一个数 \(L\) ,每组的花费为\((i-j+\sum_{k=i}^jC_k-L)^2\),总花费为全部组的花费之和。求最小总花费。code
先考虑朴素的 \(dp\) 作法。blog
设 \(dp[i]\) 为将前 \(i\) 个数字分组后的最小花费。求和能够考虑使用前缀和来优化,设前缀和数组为 \(pre\) 。则状态转移方程能够写为:队列
\(dp[i]=Min(dp[j]+(sum[i]-sum[j])+(i-(j+1))-L)^2,0≤j<i)\)图片
便是:get
\(dp[i]=Min(dp[j]+(sum[i]-sum[j]+i-j-L-1)^2,0≤j<i)\)
那么 \(sum\) 数组能够初始化为:
for(int i = 1; i <= n; i++) { Quick_Read(val[i]); sum[i] = sum[i - 1] + val[i]; }
设 \(pre[i]=sum[i]+i\) ,再进一步设 \(l=L+1\) 那么状态转移方程能够写为:
\(dp[i]=Min(dp[j]+(pre[i]-pre[j]-l)^2,0≤j<i)\)
状态转移
int Get_Dp(int i, int j) { return dp[j] + (pre[i] - pre[j] - l) * (pre[i] - pre[j] - l); }
若枚举 \(j\) ,则时间复杂度为 \(O(n)^2\) ,时间复杂度不优。使用斜率优化能够对其进行优化。
假设当前枚举到 \(i\) ,须要获得 \(i\) 的状态。假设有两个决策点 \(j\) , \(k\) ,知足决策点 \(j\) 优于决策点 \(k\) 。用符号语言能够表达为:
\(dp[j]+(pre[i]-pre[j]-l)^2<dp[k]+(pre[i]-pre[k]-l)^2\)
展开得:
\(dp[j]+pre[i]^2+pre[j]^2+l^2-2\times pre[i]\times pre[j]-2\times l\times pre[i]+2\times l\times pre[j]<dp[k]+pre[i]^2+pre[k]^2+l^2-2\times pre[i]\times pre[k]-2\times l\times pre[i]+2\times l\times pre[k]\)
进一步整理得 :
\(dp[j]+pre[j]^2-dp[k]-pre[k]^2<(pre[i]-l)\times 2\times (pre[j] - pre[k])\)
观察可得:左边的式子只与 \(j\) 和 \(k\) 有关,但右边的式子还与 \(i\) 有关。也能够发现若知足上述式子,则会有 \(j\) 优于 \(k\) 。再分类讨论:
得到分子的函数:
int Get_Up(int j, int k) { return dp[j] + pre[j] * pre[j] - dp[k] - pre[k] * pre[k]; }
得到分母的函数:
int Get_Down(int j, int k) { return pre[j] - pre[k]; }
有了上述的一级结论,能够进一步推导出二级结论:
设 \(x,y\) 的斜率表示为 \(k(x,y)\) 。若存在三点 \(a,b,c\) ,有 \(k(a,b)>k(b,c)\) ,便是图像造成上凸的形状时,那么点 \(b\) 绝对不是最优的。
分类讨论:
那么就能够得出答案的点必须知足 \(k(a_1,a_2)<k(a_2,a_3)<...<k(a_{m-1},a_m)\) 。所有呈现出下凸状态,以下图。
这样下标递增,斜率递增的点集可使用单调队列来维护。
找出当前最优的点为 \(que[head]\) ,即队头元素。
while(Get_Up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * Get_Down(que[head + 1], que[head]) && head < tail) head++;
用当前点 \(i\) 来更新队列,使得该队列呈下凸之势。
while(Get_Up(que[tail], que[tail - 1]) * Get_Down(i, que[tail]) >= Get_Up(i, que[tail]) * Get_Down(que[tail], que[tail - 1]) && head < tail) tail--;
按照上述方法进行状态转移,获得的 \(dp[n]\) 就是当前的最优解。
代码比较短,一鼓作气。(注意要开 \(long\) \(long\))
#include <cstdio> #define int long long void Quick_Read(int &N) { N = 0; int op = 1; char c = getchar(); while(c < '0' || c > '9') { if(c == '-') op = -1; c = getchar(); } while(c >= '0' && c <= '9') { N = (N << 1) + (N << 3) + (c ^ 48); c = getchar(); } N *= op; } void Quick_Write(int N) { if(N < 0) { putchar('-'); N = -N; } if(N >= 10) Quick_Write(N / 10); putchar(N % 10 + 48); } const int MAXN = 5e5 + 5; int dp[MAXN]; int pre[MAXN], val[MAXN]; int n, l; int que[MAXN]; int head, tail; int Get_Dp(int i, int j) { return dp[j] + (pre[i] - pre[j] - l) * (pre[i] - pre[j] - l); } int Get_Up(int j, int k) { return dp[j] + pre[j] * pre[j] - dp[k] - pre[k] * pre[k]; } int Get_Down(int j, int k) { return pre[j] - pre[k]; } void Line_Dp() { head = 1; tail = 1; for(int i = 1; i <= n; i++) { while(Get_Up(que[head + 1], que[head]) <= 2 * (pre[i] - l) * Get_Down(que[head + 1], que[head]) && head < tail) head++; dp[i] = Get_Dp(i, que[head]); while(Get_Up(que[tail], que[tail - 1]) * Get_Down(i, que[tail]) >= Get_Up(i, que[tail]) * Get_Down(que[tail], que[tail - 1]) && head < tail) tail--; que[++tail] = i; } Quick_Write(dp[n]); } void Read() { Quick_Read(n); Quick_Read(l); l++; for(int i = 1; i <= n; i++) { Quick_Read(val[i]); pre[i] = pre[i - 1] + val[i] + 1; } } signed main() { Read(); Line_Dp(); return 0; }