传送门:点击打开链接
题意:要覆盖完n个点,要覆盖掉[i,j]的代价是(X[i]-X[j])*(X[i]-X[j])+W,求覆盖完所有点的最小代价
思路:这是一道非常裸的斜率优化。
网上讲解斜率优化的博客还是比较多的,斜率优化通常就是化简后,最后得到了一个斜率的式子
推荐个学习的链接
我稍微讲下对于单调队列,我一开始看着比较费劲的地方。。
(下面内容均建立在已经读完上面那个链接的基础之上。。
用g[i,j]表示点i,j的斜率。假如之前已经有点k,j。现在考虑把i点放进去
如果有g[j,i]<g[k,j],那么j一定不是最优的,现在我们来证明:
下面这张图是j需要被删除的情况。
如果g[j,i]<sum[i],那么根据之前我们列出的不等式的含义,可以知道i比j更优。
如果g[j,i]>=sum[i],那么就有g[k,j]>g[j,i]>=sum[i],根据定义,如果g[k,j]<sum[i],则j最优,反之,现在是>=sum[i],所以k点最优
所以我们能够发现,无论怎样,这种情况下的J都需要删掉。
综上所述,我们就知道,我们需要维护一个斜率越来越大的点的集合,如下图
我们接下来说一下,队列需要如何来维护。
下面的sum是针对上面那个地址来的,其实对应了本道题目的第i个位置
我们用单调队列来维护上面这个图。
删除队首没用的点:
取队首第一个点和第二个点,计算斜率,记为k
如果k<=sum,说明第二个点更优。所以我们就把队首那个点删掉,直到k>sum或者队列为空
由于随着i往后,sum会越来越大。我们可以发现,sum增大以后,我们刚刚删掉的点也不可能是最优的
所以就证明了我们删除队首的一些点的正确性。
新添加点:从队尾加入点,先看i与队列最后一个点的斜率记为a,队列最后一个点和最后第二个点的斜率记为b。
如果a<=b,那么就把最后一个点删掉,直到a>b或者队列中没有点。
为了减少我们思考复杂度,我们可以写好3个函数,geup(),getdown(),getdp(),之后的程序就很容易写了
PS:我们仔细再看下上面这道题为什么能用单调队列。
因为sum会越来越大,说白了就是你把斜率不等式写出来,当符号是<时候,右边是单调递增。当符号是>时候,右边是单调递减
只有这种情况,才可以使用单调队列来斜率优化。
如果sum不是单调的,那么就必须写凸包,然后再在凸包上三分答案。
#include <map>
#include <set>
#include <cmath>
#include <ctime>
#include <stack>
#include <queue>
#include <cstdio>
#include <cctype>
#include <bitset>
#include <string>
#include <vector>
#include <cstring>
#include <sstream>
#include <iostream>
#include <algorithm>
#include <functional>
#define fuck(x) cout<<"["<<x<<"]";
#define FIN freopen("input.txt","r",stdin);
#define FOUT freopen("output.txt","w+",stdout);
//#pragma comment(linker, "/STACK:102400000,102400000")
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int MX = 2e5 + 5;
LL A[MX], dp[MX], w;
int Q[MX], c, r;
LL getup(int i, int j) {
return dp[j - 1] + A[j] * A[j] - (dp[i - 1] + A[i] * A[i]);
}
LL getdown(int i, int j) {
return A[j] - A[i];
}
LL getdp(int i, int j) {
return dp[j - 1] + (A[i] - A[j]) * (A[i] - A[j]) + w;
}
int main() {
int T; //FIN;
scanf("%d", &T);
while(T--) {
int n;
c = 1; r = 0;
scanf("%d%lld", &n, &w);
for(int i = 1; i <= n; i++) {
scanf("%lld", &A[i]);
}
sort(A + 1, A + 1 + n);
for(int i = 1; i <= n; i++) {
while(r - c + 1 >= 2 && getup(Q[c], Q[c + 1]) <= 2 * A[i]*getdown(Q[c], Q[c + 1])) c++;
dp[i] = min(getdp(i, Q[c]), dp[i - 1] + w);
while(r - c + 1 >= 2 && getup(Q[r], i)*getdown(Q[r - 1], Q[r]) <= getdown(Q[r], i)*getup(Q[r - 1], Q[r])) r--;
Q[++r] = i;
}
printf("%lld\n", dp[n]);
}
return 0;
}
这里还介绍一种类似整体二分的思想的分治的方法,可以用来替代单调队列
不过单调队列总的复杂度是O(n),分治的方法总的复杂度是O(nlogn)
虽然变慢了一个log,但是代码编写上简单非常多。
void dfs(int l, int r, int dl, int dr) {
//[l,r]表示现在更新[l,r]区间dp[i]的最优值
//用j -> f(i),表示j是更新f(i)最优值的最优点
//那么[dl,dr]表示更新dp([l,r])的点,一定在[dl,dr]范围内
int mid = (l + r) >> 1;
int dm = dl;
int g = inf;
for (int i = dl; i <= dr; i++) {
if(g < dp[i] + f(i, mid)) {
g = dp[i] + f(i, mid);//记录更新dp[mid]的最优
dm = i;//记录更新dp[mid]的最优点
}
}
dp[mid] = g; //更新dp[mid]的值
//因为上文叙述的单调性,
//更新[l,mid-1]的最优点,一定在[dl,dm]范围内
if(l < mid) dfs(l, mid - 1, dl, dm);
//更新[mid+1,r]的最优点,一定在[dm,dr]范围内
if(mid < r) dfs(mid + 1, r, dm, dr);
//此份代码dfs顺序有点问题,并不正确,但是并不影响理解
}