HDOJ 3507 Print Article (斜率DP)

这里大概说一下题意,这里有打印n个数,每连续打印一次的花费需要下面那个公式来计算,求最小的花费。

img

大概考虑一下应该可以知道这是一个DP类型的题,那么就推一下公式。

设 dp[i] 表示的是打印完前 i 个数需要的最小的花费。

那么可以得出下面这个公式:

dp[i]=min(dp[i],dp[j]+(sum[j]sum[i])2+M)

sum[i]是预处理的前缀和,方程不难很好理解。

再来看复杂度:n的范围是500000,而我们的方程是二维的,那么就会炸掉,想办法优化。

在DP方程中,首先枚举 i ,然后再枚举 j ,考虑优化。

dp[i]=min(dp[i],dp[j]+(sum[j]sum[i])2+M)

首先把方程展开,将min里面的的能拿出来的无关项都拿出来,可以得到:

展开: dp[i]=min(dp[i],dp[j]+sum[j]22sum[j]sum[i]+sum[i]2+M)

提项: dp[i]=min(dp[i],dp[j]+sum[j]22sum[j]sum[i])+sum[i]2+M (i 为枚举项)

单独考虑min里面的内容,将只跟 j 有关的项提出来,现在的 j 也就是我们需要找的最优项。

x=sum[j] , y=dp[j]+sum[j]2 , k=2sum[i] , g=dp[i] .

将min里面的内容可以转为: g=ykx

在这里k为已知项,因为我们已知 i ,那么 sum[i] 在这里就是已知的。

也就是 dp[i] 的值只跟当前 j 的 y 和 x 相关了。

假设 x 和 y 为坐标系上面的两个点,把等式 g=ykx 放入坐标系中,那么所求的值就为:

在斜率为 k 过点 (x,y) 的直线和 y 轴的交点,现在不考虑这个题,假设图上面有无数个点,所形成的图像就一堆平行的直线,与 y 的交点从负无穷到正无穷都有。

j 的值也就是坐标 (x,y) 有很多,那么哪一个是最优解的解就是我们关心的问题,如果是遍历每一个 j 点,那么就是 n2 的复杂度,优化就在这里.

在已经建好图的情况下考虑这个题,要求的最优解就是: 将斜率为 k 过点 (x,y) 的直线从 x 轴的正无穷处往左平移所碰到的第一个点,画一下图就能很好理解这个优化。

那么我们就可以把 j 里面所有的点在坐标系上排列出来,每一个最靠右的点就是直线平移是碰到的最优解,但是这个题还有一个问题,我们在枚举 i 的时候,斜率 k=sum[i] 是在变化的,画图可知,当斜率变化的直线平移时不一定是最靠右的点是最优解。继续考虑这个题,我们可以明显的看出, 在遍历 i 的时候 ,斜率 k=sum[i] 是单调递增的,也就是说,每一条直线都是往上抬的。

那么我们就可以维护一个凸包队列,每一次枚举 i ,取队列中的最优的元素来更新ans,然后用当前 i 来形成一个新的点 (x,y) 来维护下凸包,这里要注意维护凸包要从队列 尾取两个点来与新点 (x,y) 来维护下凸包。

这里我们详细理解一下怎么维护和取最优值。分为两部分:
第一步:对于当前 i 从队列里面取最优解,求的现在 i 最优解。
由于队列维护的是一个下凸包,队头元素对于现在 i 直线来说并不一定是最靠右的点,这里怎么判断是否是最靠右,那么就是: 当队列的元素与 i 点 连起来不会干扰队列的下凸包性质,那么这个元素就是对于 i 的最优元素,那么我们就可以每次取队首两个元素和 i 点比较是否满足下凸性质,如果不满足 那么此时的队首元素就是不满足最优解的,对于这个元素我们可以出队列 删除它,因为如果这个点和 i 都不满足 下凸包 性质,那么对于后面斜率越来越大的点也肯定不满足,那么删掉它就好了,直到取到满足 下凸包的 点来求得当前 i 的最优解。
第二步:加入当前的 i 点来维护整体 下凸包 。
这一步就要比上一步要简单一点,注意一个问题,要维护整体的 下凸包 性质就肯定要从 下凸包 的起点也就是队列的尾部开始更新,每次取队尾的最后两个元素来判断是否满足下凸包性质,直到满足的情况就更新队列尾部。

复杂度: 对于每一个 i 都只会进队列出队列一次,那么总的复杂度就是 o(n) 的,就可以解决这个题了。

img

#include<stdio.h>
#include<algorithm>
#include<string>
#include<string.h>
#include<queue>
#include<stack>
#include<math.h>
#include<iostream>
using namespace std;
int dp[500050],q[500050];
int n,m,sum[500050],a[500050];
int check(int j,int k,int i)   //取队尾两个元素来维护凸包
{
    int l=dp[j]+sum[j]*sum[j]-(dp[k]+sum[k]*sum[k]);
    int r=sum[i]*(2*sum[j]-2*sum[k]);
    if(l<=r)
        return 1;
    return 0;
}

int getup(int j,int k)       //getup和getdow方便做斜率的比较
{
    return dp[j]+sum[j]*sum[j]-(dp[k]+sum[k]*sum[k]);
}
int getdowm(int j,int k)
{
    return (2*sum[j]-2*sum[k]);
}
int getdp(int i,int j)
{
    return dp[j]+((sum[i]-sum[j])*(sum[i]-sum[j])+m);
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        int head,rear;
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        sum[0]=dp[0]=0;
        head=rear=0;

        for(int i=1;i<=n;i++)
            sum[i]=sum[i-1]+a[i];    //前缀和

        q[rear++]=0;

        for(int i=1;i<=n;i++)
        {
            while(head+1<rear&&check(q[head+1],q[head],i)==1)   //如果队首元素都不满足下凸包就删掉
            {
                head++;
            }
            dp[i]=getdp(i,q[head]);                         //维护dp[i]

            while(head+1<rear&&((getup(i,q[rear-1])*getdowm(q[rear-1],q[rear-2]))<=(getup(q[rear-1],q[rear-2])*getdowm(i,q[rear-1]))))         //从队尾来维护下凸包
            {
                rear--;
            }
            q[rear++]=i;                                //加入这个点
        }
        printf("%d\n",dp[n]);
    }
return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值