题目链接:
题意:
给定 n 个数 c[i] ,和一个常数 M ,求 的最小值。
思路:
设 sum[i] 表示 c[i] 的前缀和(即c[1]+c[2]+...+c[i])。 (sum[0]=0,表示为空)
设 dp[i] 表示处理到第 i 位时候的最小花费。
dp[i]=min(dp[j]+(sum[i]-sum[j])^2+M) 其中 0<=j<i .
可以看出复杂度为O(n^2),在n<=5e5的情况下无法满足要求。
思考:能不能用更短的时间找到最优的转移点。
=> 斜率dp:
假设 i>j>k,计算dp[i]时,若 j 跳转比 k 跳转优秀,那么满足:
dp[j]+(sum[i]-sum[j])^2+M<dp[k]+(sum[i]-sum[k])^2+M
=>
左边的式子相当于以sum[x]为横坐标,以dp[x]+sum[x]^2为纵坐标的直线的斜率。
经分析,当满足下凸的性质时,最优转移一定在其中。
参考博客:斜率优化dp学习
所以维护这个性质,二分查找斜率比2*S[i]小的编号最大的点,就是最优的转移点O(nlogn)。由于S[i]也满足单调性,我们还可以直接维护一个单调队列就能解决这个问题O(n)。
tips:0点是自己构造的一个本不存在的点,相当于坐标系的原点(0,0),作用是能给第一个点一个初始的斜率,便于后续计算。
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 500000 + 100;
const ll MOD = 1e9 + 7;
int n, m;
ll sum[MAX];
ll dp[MAX];
ll q[MAX];
//斜率分子
ll up(int j, int k) {
return (dp[j] + sum[j] * sum[j]) - (dp[k] + sum[k] * sum[k]);
}
//斜率分母
ll down(int j, int k) {
return sum[j] - sum[k];
}
int main()
{
while (scanf("%d%d", &n, &m) != EOF)
{
for (int i = 1; i <= n; i++) {
scanf("%lld", &sum[i]);
}
for (int i = 1; i <= n; i++) {
sum[i] += sum[i - 1];
}
dp[0] = 0;
int left = 1, right = 1;
q[1] = 0;
for (int i = 1; i <= n; i++) {
//找到最优跳转点,若无法维护单调队列可用二分
while (left < right && (up(q[left + 1], q[left]) < 2 * sum[i] * down(q[left + 1], q[left])))
left++;
int front = q[left];
//状态转移
dp[i] = dp[front] + (sum[i] - sum[front])*(sum[i] - sum[front]) + m;
//维护下凸性质
while (left < right&&up(i, q[right])*down(q[right], q[right - 1]) <= down(i, q[right])*up(q[right], q[right - 1]))
right--;
q[++right] = i;
}
printf("%lld\n", dp[n]);
}
return 0;
}