[斜率优化][JZOJ5355]保命

题目描述

为了加快社会主义现代化,建设新农村,农夫约(Farmer Jo)决定给农庄做一些防火措施,保障自己、猫、奶牛的生命安全。
农夫约的农庄里有N+1 座建筑,排成了一排,编号为0~N。对于0 <=i < N,建筑i 有w[i]头奶牛居住,与建筑i+1 距离为d[i]。建筑N 已装有消防栓,现在,农夫约决定再给k 个建筑安装消防栓,以减小安全隐患。
当火灾来临时,所有奶牛会从所在建筑开始,向大编号方向逃生,直到遇上第一个消防栓(如果本来就在消防栓处,就不用跑了)。农夫约定义了一个隐患值val:所有奶牛逃生距离之和。
农夫约希望让隐患值尽可能小,需要你给他设计一个好方案。
这里写图片描述

分析

本来出题人出的是近似算法,但是出烂了,可以用dp做。
我们先列出朴素方程嘛。设f[i][j]表示在第i个位置放了第j个消防栓的距离和最小值。
f[i][k]=min(f[j][k-1]+calc(j+1,i)),calc表示j+1到k的所有位置的牛到k的距离和,这个值的计算是可以O(1)的。
设s1[i]=s1[i-1]+d[i].
s2[i]=s2[i-1]+w[i];
s3[i]=s3[i-1]+s2[i-1]*d[i-1];
那么calc(x,y)=s3[y]-s3[x-1]-s2[x-1]*(s1[y-1]-s1[x-2])
仔细看,转移式里面只有一项既和y有关,又和x有关,即s1[y-1]*s2[x-1],然后s1是递增的,就可以斜率优化了。
斜率优化一般就是你化出一个转移方程,比如上面那个,然后用单调队列维护一个决策队列,就是把所有的有用的j存到里面,队头如果比下一个对于当前点更劣,就弹掉。每次做完当前点,我们插入它,在这里表现为f[i][k-1],然后要把队尾一些没有用的点弹掉。
什么是没有用的点呢?我们把转移方程里面只和i有关的常数项省略掉,然后剩下的东西,和i有关的数看成未知数,那么决策j就变成了一条直线,它和其他决策是会有交点的。那么当前做完了i,决策i和队尾的交点,在决策i和队尾前一个的交点之后,就证明了队尾没用,因为在任何情况下,队尾前一个或i总有一个比队尾优。(把s1[i-1]当成自变量,它不断变大,看看几条直线的分布情况)
化式子比较关键,如果是不等式,其中变号很容易弄错,因为会把负数当成正数,项也特别多,很容易漏或者下标搞错。
那么就乱搞了。
——————10.23更新
继续来总结,今天又做了一道题,吃了大亏…
注意到优化的时候,如果斜率单调,就可以线性维护,但是此时要注意插入记得要去除不合法的线段,上凸壳和下凸壳是有区别的,做题的时候一定要画图,谨慎。
思路,就是维护好直线,然后查询的时候弹掉一些,插入的时候把一定没有用的线段弹掉。注意“没有用”的条件。另外,求交点一定要注意double.

代码

#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<set>
using namespace std;
#define fo(i,j,k) for(i=j;i<=k;i++)
#define fd(i,j,k) for(i=j;i>=k;i--)
typedef long long ll;
typedef double db;
const int N=1e6+5,M=22,mo=998244353;
int n,m,i,j,k,p,dl[N],l,r,g[M][N],lst,w[N],d[N],prt[N];
ll f[M][N],s1[N],s2[N],s3[N],ans,tmp;
int read()
{
    int x=0;
    char ch=getchar();
    while (ch<'0'||ch>'9') ch=getchar();
    while (ch>='0'&&ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x;
}
ll calc(int i,int j)
{
    return s3[j]-s3[i-1]-s2[i-1]*(s1[j-1]-s1[max(i-2,0)]);
}
db cross(int i,int k)
{
    return (f[p][k]-s3[k]+s2[k]*s1[k-1]-(f[p][i]-s3[i]+s2[i]*s1[i-1]))*1.0/((db)s2[k]-(db)s2[i]);
}
int main()
{
    freopen("t1.in","r",stdin);
//  freopen("t1.out","w",stdout);
    scanf("%d %d",&n,&m);
    fo(i,1,n) w[i]=read(),d[i]=read();
    fo(i,1,n+1)
    {
        s1[i]=s1[i-1]+d[i];
        s2[i]=s2[i-1]+w[i];
        s3[i]=s3[i-1]+s2[i-1]*d[i-1];
    }
    fo(i,0,m) fo(j,1,n+1) f[i][j]=1e17;
    fo(j,1,n+1) f[1][j]=s3[j];
    fo(i,2,m)
    {
        p=i-1;
        l=1;r=1;
        dl[1]=0;
        fo(j,1,n+1)
        {
            //pop();
            while (l<r&&f[i-1][dl[l]]+calc(dl[l]+1,j)>f[i-1][dl[l+1]]+calc(dl[l+1]+1,j)) l++;

            f[i][j]=f[i-1][dl[l]]+calc(dl[l]+1,j);
            g[i][j]=dl[l];

            //push();
            while (l<r&&cross(dl[r-1],dl[r])>cross(dl[r],j)) r--;
            dl[++r]=j;
        }
    }
    ans=1e17;
    fo(i,0,n)
    {
        tmp=f[m][i]+calc(i+1,n+1);
        if (ans>tmp)
        {
            lst=i;
            ans=tmp;
        }
    }
    printf("%lld\n",ans);
    prt[0]=0;
    fd(i,m,1)
    {
        prt[++prt[0]]=lst-1;
        lst=g[m][lst];
        m--;
    }
    fd(i,prt[0],1) printf("%d ",prt[i]);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值