DP优化:斜率优化 学习笔记

目录导航

前言

斜率优化这个技巧确实是个难点,花了差不多一个下午才搞懂基本的思路,切掉了一题例题(我太蒟了)。很多博客或多或少都有一些不足的地方,所以这里我尽力写一篇比较简单易懂的文章。(虽然可能并没有人看)


定义

我们先来看看我大wiki怎么说:

斜率优化(英语:Convex Hull Optimisation)是一种数形结合优化动态规划的思想,能够在不改变原有状态转移方程的情况下,大大提升效率。前提是状态转移方程中关于最优决策点项指数为1,及不含高次项且满足系数的单调递增,再借助笛卡尔平面直角坐标系将一个决策点抽象为一个坐标,使用几何方法进行求解。

看起来好像很麻烦的样子呢……

那我们先从一道例题讲起吧。


例题

传送门 HDU3507

题目描述:
Zero has an old printer that doesn't work well sometimes. As it is antique, he still like to use it to print articles. But it is too old to work for a long time and it will certainly wear and tear, so Zero use a cost to evaluate this degree.
One day Zero want to print an article which has N words, and each word i has a cost Ci to be printed. Also, Zero know that print k words in one line will cost.
Come From HDU
M is a const number.
Now Zero want to know the minimum cost in order to arrange the article perfectly.
Input
There are many test cases. For each test case, There are two numbers N and M in the first line (0 ≤ n ≤ 500000, 0 ≤ M ≤ 1000). Then, there are N numbers in the next 2 to N + 1 lines. Input are terminated by EOF.
Output
A single number, meaning the mininum cost to print the article.

题目大概就是说:

有一个长度为\(n\)的数字序列,可以分成若干段,每一段的代价是该段数字和的平方加上一个常数值\(m\),求其最小总代价值。

值得一提的是,这题的\(n\)范围较大,这就要求算法的复杂度达到\(O(n)\)\(O(nlogn)\)

那么,怎么做呢?

解题思路

首先抛开复杂度不提,先考虑纯朴素的动态规划算法
对于前\(n\)个数的最小分段代价\(f[i]\),显然可得知,\(f[i]\)可从\(f[1...n-1]\)其中之一继承而来,则转移方程为:
\(f[i]=\sum\limits^{i-1}_{j=1}min(f[j]+(sum[i]-sum[j])^2) +m\)
其中\(sum[i]\)表示前\(i\)个数字的前缀和。
显然,这个算法的时间复杂度\(O(n^2)\)的,肯定会超时。
那么,怎么解决这个问题?

这里,我们先假设:目前状态为\(i\),有两个待选择状态\(j,k(k<j<n)\),且选择\(j\)优于选则\(k\)
则根据转移方程,应当有:

\(f[j]+(sum[i]-sum[j])^2<f[k]+(sum[i]-sum[k])^2\)

用平方公式展开上式,得:

\(f[j]+sum[i]^2-2*sum[i]*sum[j]+sum[j]^2<f[k]+sum[i]^2-2*sum[i]*sum[k]+sum[k]^2\)

通过不等式移项,得:

\(f[j]-f[k]+sum[i]^2+sum[j]^2-sum[i]^2-sum[k]^2<2*sum[i]*sum[j]-2*sum[i]*sum[k]\)

合并同类项,得:

\(f[j]-f[k]+sum[j]^2-sum[k]^2<2*sum[i]*(sum[j]-sum[k])\)
两边同除以\((sum[j]-sum[k])\)得:
\(\frac{f[j]-f[k]+sum[j]^2-sum[k]^2}{sum[j]-sum[k]}<2*sum[i]\)

调整顺序得:

\(\frac{f[j]+sum[j]^2-(f[k]+sum[k]^2)}{sum[j]-sum[k]}<2*sum[i]\)

在这里我们设\(Y(i)=f[i]+sum[i],X(i)=sum[i]\),则原式变为:

\(\frac{Y(j)-Y(k)}{X(j)-X(k)}<2*X(i)\)

显而易见,这个算式是可以逆推的,也就是说,只要我们得到了上面的不等式,我们就可以知道:

无论什么时候,选择\(j\)都要比选择\(k\)更佳。反之,选择\(k\)更佳。
(事实上,这也是判断一道动态规划问题能否使用斜率优化基本式子。)

但是呢,仅仅知道这个式子,按照其性质,仍然需要\(O(n^2)\)的时间求解。所以,我们还得研究一下这个式子的性质
我们先来看一个叫做斜率的东西。

斜率亦称“角系数”,表示平面直角坐标系中表示一条直线对横坐标轴的倾斜程度的量。
直线对X 轴的倾斜角α的正切值\(tg(\alpha)\)称为该直线的“斜率”,并记作\(k,k=tg(\alpha)\)。规定平行于\(X\)轴的直线的斜率为零,平行于\(Y\)轴的直线的斜率不存在。对于过两个已知点\((x_1,y_1)\)\((x_2,y_2)\)的直线,若\(x_1≠x_2\),则该直线的斜率为\(k=\frac{y_1-y_2}{x_1-x_2}\)

简单来说,斜率就是一个函数的数学量,斜率越大,在坐标系中越斜。
且因为要构成一个函数图像,\(x\)\(y\)的值都必须是非严格递增的。
一个坐标系
这张图中,\(func1\)的斜率要比\(func2\)小。
这个时候,我们来看看斜率的公式:

\(k=\frac{y_1-y_2}{x_1-x_2}\)

欸,这个公式怎么跟我们刚刚推出来的那个公式有点像?
回顾一下:

\(\frac{Y(j)-Y(k)}{X(j)-X(k)}<2*X(i)\)

因为\(X(i)与Y(i)\)都是递增的,满足了函数的性质,
所以,我们刚才求到的式子左部就是函数图像上\(k\)\(j\)构成的函数的斜率。

\(Q:\)那么这和我们要解决的问题有什么关系呢?
\(QwQ\)别着急哟亲亲,先看下去嘛。

首先,我们按照我们上面对\(X(i),Y(i)\)的定义随便画出一个图像:

根据刚才的斜率计算式,显然有:
\(\frac{Y(j)-Y(k)}{X(j)-X(k)}>\frac{Y(i)-Y(j)}{X(i)-X(j)}\)
那么,对于现在我们要求解的状态\(f[t]\),可以分类讨论:

\(1) \frac{Y(j)-Y(k)}{X(j)-X(k)}>\frac{Y(i)-Y(j)}{X(i)-X(j)}>2*X(t)\)
\(k优于j,j优于i\)\((k>j>i)\)

\(2) \frac{Y(j)-Y(k)}{X(j)-X(k)}>2*X(t)>\frac{Y(i)-Y(j)}{X(i)-X(j)}\)
\(k优于j,i优于j\)\((k>j,i>j)\)

\(3) 2*X(t)>\frac{Y(j)-Y(k)}{X(j)-X(k)}>\frac{Y(i)-Y(j)}{X(i)-X(j)}\)
\(j优于k,i优于j\)\((i>j>k)\)
从上得出,\(j\)在任何情况下,都不可能是最优解。
所以,像\(j\)这种毒瘤吧,肯定是要切掉的对吧?

但是,如果是像下面这种图呢?
此处输入图片的描述
证明就不证了,直接说结论吧:\(j\)有可能成为最优解。

所以,我们可以考虑维护一个点集合,每加入一个点,去掉内部构成了上凸包的点,使得内部的点构成一个严格下凸的函数图像。

那么,在这坨数据点集合中,哪个是最优的呢?

根据图形特点,我们可以知道,继承集合中\(Y(i)\)最小的那个点一定是最优的。

而且,上面已经做过证明,在此题中,\(Y(i)\)单调上升的。
也就是说,我们只需维护一个单调队列,就可以轻松切掉这题。

每次循环时,先弹出队头不满足条件的点;
用队头的点(\(Y(i)\)最小)更新\(f[i]\)
加入当前点\(i\),同时弹出与\(i\)不构成下凸包函数的点。


Code

//对于求斜率的函数slope,
//如果使用除法的话可能会有精度缺失问题,
//所以我把原函数的X(y)-X(x)
//也就是sum[y]-sum[x],
//进行了通分,乘到了不等式的另一边。
#include <bits/stdc++.h>
#define Maxn 1000000
#define pow(x) x * x
using namespace std;
long long f[Maxn], sum[Maxn], que[Maxn];
int n, m;

long long slope(int x, int y) {
    return f[y] + pow(sum[y]) - f[x] - pow(sum[x]);
}

long long Sum(int x, int y) {
    return sum[y] - sum[x];
}

int work() {
    memset(sum, 0, sizeof sum);
    memset(que, 0, sizeof que);
    memset(f, 0, sizeof f);
    for(int i = 1; i <= n; i++) {
        cin >> sum[i];
        sum[i] += sum[i - 1]; 
    }
    int head = 0, tail = 0;
    que[head] = 0, f[0] = 0;
    for(int i = 1; i <= n; i++) {
        while(head < tail && slope(que[head], que[head + 1]) <= 2 * sum[i] * Sum(que[head], que[head + 1]))
            head++;
        f[i] = f[que[head]] + pow(Sum(que[head], i)) + m;
        while(head < tail && slope(que[tail - 1], que[tail]) * Sum(que[tail], i) >= slope(que[tail], i) * Sum(que[tail - 1], que[tail]))
            tail--;
        que[++tail] = i;
    }
    cout << f[n] << endl;
}
int main() {
    while(cin >> n >> m)
        work();
}

题外话:根据这个性质(\(Y(i)\)最小的点是最优继承点),对于\(X(i)\)非单调递增的题目,我们也可以通过二分查找完成。

后记

终于写完辣!这篇博客真的是写了很久呢(3~4个小时),但是对斜率优化的理解也加深了许多。在这里谢谢一个巨佬Orzzz的博客,我的这篇博客也有一些是参照\(ta\)的写的。时间匆忙,若有错误,还请多多包涵,在博客下方的评论提醒我哦。

猜你喜欢

转载自www.cnblogs.com/MaxDYF/p/10851233.html
今日推荐