您的位置:首页 > 其它

HDU 3507 Print Article(斜率DP入门理解)

2016-08-18 09:54 375 查看
Problem Description

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



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.

 

Sample Input

5 5
5
9
5
7
5

 

Sample Output

230

 

Author

Xnozero

 

Source

2010
ACM-ICPC Multi-University Training Contest(7)——Host by HIT

 
看了一篇斜率DP入门的博客,博客将此题奉为斜率DP入门题,于是跑去写了写,然后记录一下自己对于斜率DP的理解
先上代码:
#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#define Sint(n) scanf("%d",&n)
#define Sll(n) scanf("%I64d",&n)
#define Schar(n) scanf("%c",&n)
#define Sint2(x,y) scanf("%d %d",&x,&y)
#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)
#define Pint(x) printf("%d",x)
#define Pllc(x,c) printf("%I64d%c",x,c)
#define Pintc(x,c) printf("%d%c",x,c)
using namespace std;
typedef long long ll;
/*
单调队列维护向下凹的斜率曲线 (斜率单调递增)

对于斜率 单纯只看一个点的话就是 : dp[i]/sum[i] + sum[i]
对于每一个 i  sum[i]是定值 ,
根据dp[i]的定义(到i点为止的最小花费)
显然 dp是递增的,故斜率也是递增的(单调队列维护保证斜率单增)
所谓最优,dp[i]是越小越好,即斜率越小越好
根据队列维护的斜率的特点 ,显然队列中第一个点和i点的斜率是最小的
故 :dp[i] = getDP(i,q[head])即从队列中第一个点转化而来的dp是最小的

具体维护队列的操作在两处发生
当要求dp[i]的时候,
通过 sum[i] 在队首删除元素  维护斜率单增的点
当要添加i点的时候,
通过 相邻两斜率间满足单增  维护斜率单增的点
*/
const int N = 500007;
int dp
;
int sum
;
int q
;
int n,m;
int EX(int x)
{
return x*x;
}
int getDP(int i,int j)
{
return dp[j] + m + EX(sum[i]-sum[j]);
}
int getUP(int j,int k)//yj - yk
{
return (dp[j] + EX(sum[j])) - (dp[k] + EX(sum[k]));
}
int getDown(int j,int k)//xj - xk
{
return 2*(sum[j] - sum[k]);
}
int main()
{
while (Sint2(n,m) == 2)
{
for (int i = 1,x;i <= n;++i)
{
Sint(x);
sum[i] = sum[i-1] + x;
}
int head = 0,tail = 0;
q[tail++] = 0;//单调队列 (单增)
for (int i = 1;i <= n;++i)
{
//                  getup/getdown  <= sum[i]
while (head+1<tail&&getUP(q[head+1],q[head])<=sum[i]*getDown(q[head+1],q[head])) head++;
dp[i] = getDP(i,q[head]);
//          getup(i,q[tail-1])/getdown(i,q[tail-1]) <= getup(q[tail-1],q[tail-2])/getdown(q[tail-1],q[tail-2])
while (head+1<tail&&getUP(i,q[tail-1])*getDown(q[tail-1],q[tail-2])<=getUP(q[tail-1],q[tail-2])*getDown(i,q[tail-1]))tail--;
q[tail++] = i;
}
Pintc(dp
,'\n');
}
return 0;
}


大神博客:斜率优化DP         
(大神讲的很详细不多说,单纯记录下自己的奇思妙想)
首先说下对于作者给出的图:



我觉得作者想说的是,当斜率呈现如图的趋势,我们将k删除,而不是说队列中维护的点之间的斜率是这样
因为根据代码中维护队列的操作:
1.队首删除
//                  getup/getdown  <= sum[i]
while (head+1<tail&&getUP(q[head+1],q[head])<=sum[i]*getDown(q[head+1],q[head])) head++;
2.队尾删除
//          getup(i,q[tail-1])/getdown(i,q[tail-1]) <= getup(q[tail-1],q[tail-2])/getdown(q[tail-1],q[tail-2])
while (head+1<tail&&getUP(i,q[tail-1])*getDown(q[tail-1],q[tail-2])<=getUP(q[tail-1],q[tail-2])*getDown(i,q[tail-1]))tail--;


其实不难看出队列维护的是斜率单增的点
队列中的所有点连成图应该是这样的:



看代码发现斜率是单调递增之后,想了想为什么,
对于斜率,如果算两点之间就是getUP/getDown里面的式子,但是如果我们单看一个点的斜率(即是这个点的纵坐标除以横坐标)
那么一个点的斜率可以这样看:(dp[i]-sum[i]^2)/sum[i]  = dp[i]/sum[i] - sum[i]
显然sum[i]是定值,所以斜率具有的性质其实是dp[i]具有的性质(dp和斜率同单调性)
这里的dp[i]表示到第i个数为止的最小花费,显然dp[i]是单调递增的
然后dp求的是最小值,所以对于新出现的i点,不难看出它和队列中第一个点的斜率是最小的,那么求出的dp[i]也是最小的(dp和斜率同单调性)
同时,要想画斜率的图,显然x要是单增的,即这里的sum[i]是单增的
至于为什么取等号,这个根据题目来。
至于对于队列的维护,需要在两处进行操作:
当要求dp[i]的时候, 
通过 sum[i] 在队首删除元素  维护斜率单增的点  
当要添加i点的时候,
通过 相邻两斜率间满足单增  维护斜率单增的点

所以每次斜率优化DP只需要根据题目定义dp[i],然后根据dp[i]的状态转移确定斜率表达式
根据dp[i]的性质确定要维护的斜率的单调性和要选择队列第一个点还是最后一个点求dp[i]
说实话,本鶸连凸包都不会,以上纯属一只鶸渣的个人臆想。。。。如有理解错误 望指出。。。不胜感激。。。
另外,无聊写了双端队列deuqe实现的版本,跑的居然比模拟队列要快,也是震惊。。。
具体代码:
#define mem(a,x) memset(a,x,sizeof(a))
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<deque>
#include<set>
#include<stack>
#include<cmath>
#include<map>
#include<stdlib.h>
#include<cctype>
#include<string>
#define Sint(n) scanf("%d",&n)
#define Sll(n) scanf("%I64d",&n)
#define Schar(n) scanf("%c",&n)
#define Sint2(x,y) scanf("%d %d",&x,&y)
#define Sll2(x,y) scanf("%I64d %I64d",&x,&y)
#define Pint(x) printf("%d",x)
#define Pllc(x,c) printf("%I64d%c",x,c)
#define Pintc(x,c) printf("%d%c",x,c)
using namespace std;
typedef long long ll;
/*
斜率DP
*/
const int N = 500007;
int dp
;
int sum
;
int n,m;//n 个数  常数 m
int EX(int x) //平方
{
return x*x;
}
int getDP(int i,int j)
{
return dp[j] + m + EX(sum[i] - sum[j]);
}
int getUP(int j,int k) //斜率的分子 即 yj - yk
{
return (dp[j] + EX(sum[j])) - (dp[k] + EX(sum[k]));
}
int getDown(int j,int k)//斜率的分母 即 xj - xk
{
return 2*(sum[j] - sum[k]);
}
int main()
{
while (Sint2(n,m) == 2)
{
sum[0] = dp[0] = 0;
for (int i = 1,x;i <= n;++i)
{
Sint(x);
sum[i] = sum[i-1] + x;
}
//单调队列优化
deque<int>q;
q.push_back(0);//在尾部添加元素
for (int i = 1;i <= n;++i)
{
int a,b;
while (1)//在队首删除元素
{
if (q.size() < 2) break;
a = q.front();//第一个
q.pop_front();
b = q.front();//第二个
q.push_front(a);//删除的元素还回去
if (getUP(b,a)>sum[i]*getDown(b,a)) break;
q.pop_front();
}
//			cout<<q.front()<<endl;
dp[i] = getDP(i,q.front());
while (1)//在队尾删除元素
{
if (q.size() < 2) break;
a = q.back();//最后1个元素
q.pop_back();
b = q.back();//倒数第二个
q.push_back(a);
if (getUP(i,a)*getDown(a,b)>getUP(a,b)*getDown(i,a)) break;
q.pop_back();
}
q.push_back(i);
}
Pintc(dp
,'\n');
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  DP