HDU3507
题目大意:
给出N个单词,每个单词有个非负权值Ci,现要将它们分成连续的若干段,每段的代价为此段单词的权值和,还要加一个常数M,即(∑Ci)^2+M。现在想求出一种最优方案,使得总费用之和最小。
容易写出方程:
f[i]=min{f[j]+(s[i]-s[j])^2+M}
(0<=j<=i-1)
我们假设k<j<i。如果在j的时候决策要比在k的时候决策好,那么也是就是
dp[j]+M+(sum[i]-sum[j])^2 < dp[k]+M+(sum[i]-sum[k])^2。
因最小花费,所以优就是小于
两边移项一下,得到:
(dp[j]+num[j]^2 - (dp[k]+num[k]^2))/(2*(num[j]-num[k]))<sum[i]
把dp[j]-num[j]^2看做是yj
把2*num[j]看成是xj
那么yj-yk/xj-xk<sum[i]
左边是斜率的表示
说明j的决策比k的决策要好
#include <bits/stdc++.h>
using namespace std;
long long n,m,Q[500005],dp[500005],sum[500005];
double Slope(long long j,long long k) { //求斜率
return double((dp[j]+sum[j]*sum[j])-(dp[k]+sum[k]*sum[k]))/(2*sum[j]-2*sum[k]);
}//上面是数学公式推导
int main() {
while(scanf("%lld%lld",&n,&m)!=EOF){//非空
for(int i=1; i<=n; i++){ //扫一次
int tmp;cin>>tmp; //输入
sum[i]=sum[i-1]+tmp; //维护前缀和
}
int Left=1,Right=1,Q[1]=0,dp[0]=0;//定义左右界,队首是0,DP也是0
for(int i=1; i<=n; i++){ //扫一次N个数
while(Left<Right&&Slope(Q[Left],Q[Left+1])<=sum[i])Left++;//维护队首(删除非最优决策)
int Front=Q[Left]; //读出队首
dp[i]=dp[Front]+(sum[i]-sum[Front])*(sum[i]-sum[Front])+m;//计算当前dp
while(Left<Right&&Slope(Q[Right-1],Q[Right])>=Slope(Q[Right],i))Right--;//维护队尾(维护下凸包性质)
Q[++Right]=i; //入队
}
printf("%lld\n",dp[n]); //输出
}
return 0;
}
/*
Sample Input
5 5
5
9
5
7
5
Sample Output
230
*/
斜率优化dp每次更新一个位维护队首队尾,队首left:由化得公式知小于就是后者更优,队尾right:为何下凸包?因为化得的公式是斜率表达式,右端是sum值单增的,所以斜率也增,即左边表达式所表示斜率不断大于右边不段变大的sum值,确保能够维护再把i点压入,由此队列的值不一定是i,i+1相邻的都压入。最后再对栈中元素操作,此处再记第二反应,栈与队列可以一头一尾插入无穷大或0来使算法简便
更新解释(见博客):为什么是下凸包,因为k,i肯定有一个比j好,所以j就退队了,注意是j退光了之后再打入i的!
当我们要求解dp[t]时,如果可用的集合里存在这样三个点,位置关系如图所示:
综上,不管什么样的S[t+1],从j转移都不会是最佳方案。用一个数据结构维护一个凸包(下凸),每加入一个点就删去一些点,使其维持凸包的形态。最优转移一定在这个凸包中。