翔哥的hu测 T1. 收集树叶(斜率优化dp||随机优化dp)

64 篇文章 0 订阅
61 篇文章 0 订阅

版权属于ZYXZYXZYX,想要引用此题(包括题面)的朋友请联系博主

这里写图片描述
这里写图片描述

题目来源:[CEOI 2004] Two sawmills

原题提交地址

分析:
首先要吐槽题面。。。

其次要注意:我们只在山上建两个额外的加工厂

显然是斜率优化dp
xue微复习一下斜率优化dp吧

方法一

显然有状态:
f[i][j] f [ i ] [ j ] 表示在位置 j j 建立第i座加工厂
f[i][j]=min(f[i1][k]+SUM(k+1,j)) f [ i ] [ j ] = m i n ( f [ i − 1 ] [ k ] + S U M ( k + 1 , j ) )
SUM(k+1,j) S U M ( k + 1 , j ) 表示 (k+1,j) ( k + 1 , j ) 的树运送到位置 j j 的加工厂的花费

首先我们要明确SUM如何计算
这里写图片描述
dis d i s 是距离前缀和
sum s u m 是树叶重量前缀和
ds[j]=ji=1sum[i]dis[i] d s [ j ] = ∑ i = 1 j s u m [ i ] d i s [ i ]
ds[j]=ji=1sum[i1]dis[i] d s ′ [ j ] = ∑ i = 1 j s u m [ i − 1 ] d i s [ i ]

如果我们在 j j i分别建立两个加工厂,那么 (j+1,j+2) ( j + 1 , j + 2 ) 的树就应该运送到 i i 加工厂

=(sum[j+1]sum[j])(dis[i]dis[j+1])+(sum[j+2]sum[j+1])(dis[i]dis[j+2]) = ( s u m [ j + 1 ] − s u m [ j ] ) ∗ ( d i s [ i ] − d i s [ j + 1 ] ) + ( s u m [ j + 2 ] − s u m [ j + 1 ] ) ∗ ( d i s [ i ] − d i s [ j + 2 ] )

=sum[j+1]dis[i]sum[j]dis[i]sum[j+1]dis[j+1]+sum[j]dis[j+1] = s u m [ j + 1 ] d i s [ i ] − s u m [ j ] d i s [ i ] − s u m [ j + 1 ] d i s [ j + 1 ] + s u m [ j ] d i s [ j + 1 ]
+sum[j+2]dis[i]sum[j+1]dis[i]sum[j+2]dis[j+2]+sum[j+1]dis[j+2] + s u m [ j + 2 ] d i s [ i ] − s u m [ j + 1 ] d i s [ i ] − s u m [ j + 2 ] d i s [ j + 2 ] + s u m [ j + 1 ] d i s [ j + 2 ]

=dis[i](sum[j+2]sum[j])(sum[j+1]dis[j+1]+sum[j+2]dis[j+2]) = d i s [ i ] ( s u m [ j + 2 ] − s u m [ j ] ) − ( s u m [ j + 1 ] d i s [ j + 1 ] + s u m [ j + 2 ] d i s [ j + 2 ] )
+(sum[j]dis[j+1]+sum[j+1]dis[j+2]) + ( s u m [ j ] d i s [ j + 1 ] + s u m [ j + 1 ] d i s [ j + 2 ] )

所以我们可以得到以下的计算函数:

ll SUM(int x,int y) {   
    return dis[y]*(sum[y-1]-sum[x])-(ds[y-1]-ds[x])+(_ds[y-1]-_ds[x]);
}
现在关键来了

我们现在需要化式子完成斜率优化
假设现在转移的是 i i 点,有两个转移点j,k,其中 k k 点更优,则有

f[k]+SUM(k,i)<f[j]+SUM(j,i)

f[j]+ds[j]ds[j](f[k]+ds[k]ds[k])>dis[i](sum[j]sum[k]) f [ j ] + d s [ j ] − d s ′ [ j ] − ( f [ k ] + d s [ k ] − d s ′ [ k ] ) > d i s [ i ] ( s u m [ j ] − s u m [ k ] )

f[j]+ds[j]ds[j](f[k]+ds[k]ds[k])sum[j]sum[k]>dis[i] f [ j ] + d s [ j ] − d s ′ [ j ] − ( f [ k ] + d s [ k ] − d s ′ [ k ] ) s u m [ j ] − s u m [ k ] > d i s [ i ]

get(j,k)=f[j]+ds[j]ds[j](f[k]+ds[k]ds[k])sum[j]sum[k] g e t ( j , k ) = f [ j ] + d s [ j ] − d s ′ [ j ] − ( f [ k ] + d s [ k ] − d s ′ [ k ] ) s u m [ j ] − s u m [ k ]

做法总结如下:

  • 用一个单调队列来维护解集
  • 转移:
    假设队列中从头到尾已经有元素 a,b,c a , b , c
    当要转移 i i 点的时候,如果get(a,b)<dis[i],那么说明 b b 点比a点更优, a a 点可以排除,于是a出队
    直到 get(x,y)>=dis[i] g e t ( x , y ) >= d i s [ i ] ,当前点就从 x x 转移
  • 入队:
    假设队列的尾部已经有元素a,b,c
    那么当 d d 要入队的时候,我们维护队列的上凸性质,
    即如果get(d,c)<get(c,b),那么就将 c c 点删除
    (我们要使get>dis[i],显然 get(d,c) g e t ( d , c ) 较小,是不会成为优秀转移点的)
    直到找到 get(d,x)>=get(x,y) g e t ( d , x ) >= g e t ( x , y ) 为止,并将 d d 点加入在该位置中

tip

注意:我们只在山上建两个额外的加工厂
所以我们要枚举加工厂个数转移
g是转移数组, f f 是答案数组,Q是辅助 g g 数组的队列,P是辅助 f f 数组的队列
注意get_pget_q的运用

#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long

using namespace std;

const int N=20005;
ll dis[N],sum[N],ds[N],_ds[N];
ll f[N],g[N];
int n,Q[N],P[N],tq=0,wq=0,tp=0,wp=0;

ll SUM(int x,int y) {   
    return dis[y]*(sum[y-1]-sum[x])-(ds[y-1]-ds[x])+(_ds[y-1]-_ds[x]);
}

double get_q(int j,int k) {
    return (double)(g[j]+ds[j]-_ds[j]-(g[k]+ds[k]-_ds[k]))/(double)(sum[j]-sum[k]);
}

double get_p(int j,int k) {
    return (double)(f[j]+ds[j]-_ds[j]-(f[k]+ds[k]-_ds[k]))/(double)(sum[j]-sum[k]);
}

int main() {
    scanf("%d",&n);
    for (int i=1;i<=n;i++) {
        int x,y;
        scanf("%d%d",&x,&y);
        sum[i]=sum[i-1]+(ll)x;
        dis[i+1]=dis[i]+(ll)y;
    }
    n++; sum[n]=sum[n-1]; dis[n+1]=dis[n];
    for (int i=1;i<=n;i++) {
        ds[i]=ds[i-1]+sum[i]*dis[i];
        _ds[i]=_ds[i-1]+dis[i]*sum[i-1];
    }

    memset(g,0,sizeof(g));
    for (int i=1;i<=3;i++) {
        for (int j=1;j<=n;j++) {
            while(tq<wq&&get_q(Q[tq],Q[tq+1])<dis[j]) tq++;
            f[j]=g[Q[tq]]+SUM(Q[tq],j);
            while(tp<wp&&get_p(P[wp],j)<get_p(P[wp-1],j)) wp--;
            P[++wp]=j;
        }
        memcpy(Q,P,sizeof(P));
        tq=tp; wq=wp;
        tp=wp=0;
        memcpy(g,f,sizeof(g));
    }       
    printf("%lld\n",f[n]);
    return 0;
}

方法二

随机化,参考自《浅谈随机化在信息学竞赛中的应用》(刘家骅)

我们建立一个矩阵P P[X,Y] P [ X , Y ] 表示第一个锯木场建立在 X X ,第二个锯木场建立在Y时的总运费
一开始时,矩阵的边长为 N N
我们随机寻找一定数量的点
(如下左图所示,取点数量应该充分利用时限并且注意效率,由于矩阵的大小一直在变化,推荐使用矩阵大小的定比确定取点数量)
计算出ta们的值,取其最小点,以这个点为新矩阵的中心,以现在矩阵的边长的34的长度为新矩形的边长
(如下右图所示)
从原来的矩阵中取出一块作为新矩阵的范围
(若新矩阵的范围出了原矩阵的边界就将其向里移动到原矩阵内)
然后继续在新矩阵中重复这样的操作,直至新矩阵足够小时,我们即可枚举新矩阵上的每一个点,取其中最小值作为答案

这里写图片描述

随意看一下代码吧

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<ctime>
#define ll long long

using namespace std;

const int N=20005;
ll n,w[N],d[N],dis[N],sum[N],co[N];
ll rx[210],ry[210],ans[210];

ll get(ll i,ll j) {
    return co[n+1]-sum[j]*(dis[i]-dis[j])-sum[i]*(dis[n+1]-dis[i]);
}

void solve() {
    double T=n*1.1;
    while (T>1) {
        for (ll i=1;i<=30;i++) {
            for (ll j=1;j<=80;j++) {
                ll nx=(ll)rx[i]*(1-(double)((rand()%((ll)(T+0.5)+1))/(double)n)+(double)((rand()%((ll)(T+0.5)+1))/(double)n));
                ll ny=(ll)ry[i]*(1-(double)((rand()%((ll)(T+0.5)+1))/(double)n)+(double)((rand()%((ll)(T+0.5)+1))/(double)n));
                while (ny==nx)
                    ny=(ll)ry[i]*(1-(double)((rand()%((ll)(T+0.5)+1))/(double)n)+(double)((rand()%((ll)(T+0.5)+1))/(double)n));
                if (nx<1||nx>n||ny<1||ny>n) continue;
                ll tmp=get(nx,ny);
                if (tmp<ans[i]) {
                    ans[i]=tmp;
                    rx[i]=nx;
                    ry[i]=ny;
                }
            }
        }
        T*=0.2;
    }
}

int main() 
{
    scanf("%d",&n);
    srand(time(NULL));
    for (ll i=1;i<=n;i++) {
        scanf("%lld%lld",&w[i],&d[i]);
        dis[i]=dis[i-1]+d[i-1];
        sum[i]=sum[i-1]+w[i];
        co[i]=co[i-1]+d[i-1]*sum[i-1];
    }
    sum[n+1]=sum[n];
    dis[n+1]=dis[n]+d[n];
    co[n+1]=co[n]+sum[n]*d[n];
    for (ll i=1;i<=80;i++) {
        rx[i]=rand()%n+1;        //随机的点 
        ry[i]=rand()%n+1;
        while (ry[i]==rx[i]) ry[i]=rand()%n+1;
        ans[i]=get(rx[i],ry[i]);
    }
    solve();
    ll Ans=2e9+7;
    for (ll i=1;i<=30;i++) Ans=min(Ans,ans[i]);
    printf("%lld",Ans);
} 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值