bzoj1767 树上dp斜率优化+二分

题目描述

oi国是一个温暖美丽的地方,其地图是一个树结构,树的根节点就是oi城的首都——orzboshi城。orzboshi城是国王boshi办公的地方,他每天都会收到来自其他城市的信息。oi国的信息传递的方式是这样的:
除了orzboshi城,每个城市都有一个信使,当i城产生一条信息的时候,信使i需要花 Si 的时间做准备,然后以 Vi 的速度向i城父节点跑去,到达下一座城市j的时候,信使可以选择继续跑,或者通知城j的信使j,让他继续送信。信使j要花 Sj 的时间做准备,然后以 Sj 的速度去送信。
boshi想要知道,所有小镇的信息最少要花多少时间才能送到orzboshi城呢?

输入描述

第一行:n,表示oi国有n座城市
接下来n-1行:x,y,z 表示x到y城之间有一条z米的道路
接下来n-1行:表示从2到n号城市的信使的准备时间S和速度V(跑1米要花V的时间)

输出描述

一行,n-1个数,表示2到n号城市的信息到orzboshi城的最短时间

数据范围

3N105 , 0Si109, , 1Vi109

题目分析

咋一看可能会觉得是一个有点水的dp,大概就是f[i]=min(f[j]+v[i]*(dis[i]-dis[j])+s[i]),dis[i]代表1到i的距离,然后j是i的祖先节点。
然后看向数据范围。。。肯定会TLE的吧啊喂!
虽然想到了斜率优化,但是这题没有决策单调性,用单调队列维护是行不通的。
于是看向英文的solution。。。决定和boshi大佬一起翻译一下(虽然大部分是boshi大佬的功劳。。。)那么说一说解法吧:

推斜率式

如果在考虑i的时候,dep[j]>dep[k],(dep:深度)并且选择j的决策更好,则有:
f[j]+v[i](dis[i]dis[j])+s[i]<f[j]+v[i](dis[i]dis[k])+s[i]
哇塞这个式子真好推。。。

f[j]f[k]dis[j]dis[k]<v[i]

注意,现在我们把 f[j]f[k]dis[j]dis[k] 记为g(j,k)

维护栈

要遍历这棵树就要dfs,可是。。。没有决策单调性(假如a的父节点是b,b的父节点是c,以此类推,则考虑b时选择c,在考虑a的时候选择b或者d都是有可能的,如果用单调队列维护的话,不能轻易弹出队首元素)
所以就维护栈好了。
总之,我们要维护斜率的单调性,单调递增,如图。
单调递增
既然已经维护好单调性了,我们要找最优解。二分就可以找到队首(。。。栈首?)最优决策(怎么找接下来再讲),更新f[i],然后我们要把i放到栈里。可是。。。递归回去之后这个栈还是要用的,怎么能随便改来改去呢?
于是我们二分找到更新后栈里面i要放的位置(二分方法接下来讲),将栈顶指针修改为那个位置,并将那个位置替换为i。在dfs了i的所有子节点后,我们把栈顶指针移回去,并将那个位置原来的元素弄回来(用中间变量暂时储存即可),就可以回溯了。

二分怎么搞

1.二分找队首最优决策

int sfind(int l,int r,LL x){
    int mid=0;
    while(l<=r){
        mid=(l+r)>>1;
        if(g(q[mid+1],q[mid])<v[x])l=mid+1;
        else if(g(q[mid],q[mid-1])>v[x])r=mid-1;
        else return mid;
    }
    return mid;
}

想一想推斜率式的前提是 j>k
所以如果 g(q[mid+1],q[mid])<v[x] ,说明q[mid+1]比q[mid]更优。
如果 g(q[mid],q[mid1])>v[x] ,说明q[mid-1]比q[mid]更优。
因此我们可以看作是寻找v[x]这条直线左移过程中可以碰到的第一个点。
第一个二分
2.二分更新后栈里面x要放的位置

int tfind(int l,int r,int x){//二分找x应插入到队尾的什么地方(mid后面)
    int mid=0;
    while(l<=r){
        mid=(l+r)>>1;
        if(g(q[mid+1],q[mid])<g(x,q[mid]))l=mid+1;
        else if(g(q[mid],q[mid-1])>g(q[mid],x))r=mid-1;
        else return mid;
    }
    return mid;
}

判断1
如果 g(q[mid+1],q[mid])<g(x,q[mid]) ,如图,为了维护斜率单调递增,x应该要往右移
判断2
如果 g(q[mid],q[mid1])>g(q[mid],x) ,如图,为了维护斜率单调递增,x应该向左移动
判断3
剩下的情况,如图,x插入到mid后面就可以了。(为什么这条线延长了?因为看向斜率式子:g(i,j)=g(j,i))

代码

#include<iostream>
#include<cstdio>
#include<climits>
#include<cstring>
#include<algorithm>
using namespace std;
#define LL long long
#define db double
LL read(){
    LL q=0,w=1;char ch=' ';
    while(ch!='-'&&(ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')w=-1,ch=getchar();
    while(ch>='0'&&ch<='9')q=q*10+(LL)(ch-'0'),ch=getchar();
    return q*w;
}
const int N=100050;
int n,tot,top;
int h[N],to[N<<1],ne[N<<1];LL lo[N<<1];
LL s[N],v[N],f[N],dis[N];
int q[N];
void add(int x,int y,LL z){to[++tot]=y,ne[tot]=h[x],h[x]=tot,lo[tot]=z;}
db g(int j,int k){return (db)(f[j]*1.0-f[k]*1.0)/(db)(dis[j]*1.0-dis[k]*1.0);}
int sfind(int l,int r,LL x){//二分找队首最优决策
    int mid=0;
    while(l<=r){
        mid=(l+r)>>1;
        if(g(q[mid+1],q[mid])<v[x])l=mid+1;
        else if(g(q[mid],q[mid-1])>v[x])r=mid-1;
        else return mid;
    }
    return mid;
}
int tfind(int l,int r,int x){//二分找x应插入到队尾的什么地方(mid后面)
    int mid=0;
    while(l<=r){
        mid=(l+r)>>1;
        if(g(q[mid+1],q[mid])<g(x,q[mid]))l=mid+1;
        else if(g(q[mid],q[mid-1])>g(q[mid],x))r=mid-1;
        else return mid;
    }
    return mid;
}
void dfs(int x,int las){
    int i,j,fg,kls,kltop;
    fg=sfind(1,top,x);
    f[x]=f[q[fg]]+(dis[x]-dis[q[fg]])*v[x]+s[x];//dp
    fg=tfind(1,top,x);
    kls=q[fg+1],kltop=top,top=fg+1,q[top]=x;//暂时储存被修改的top和q[fg+1]
    for(i=h[x];i;i=ne[i]){
        if(to[i]==las)continue;
        dis[to[i]]=dis[x]+lo[i],dfs(to[i],x);
    }
    top=kltop,q[fg+1]=kls;//改回来
}
int main()
{
    freopen("harbingers.in","r",stdin);
    freopen("harbingers.out","w",stdout);
    int i,x,y;LL z;
    n=read();
    for(i=1;i<n;i++)x=read(),y=read(),z=read(),add(x,y,z),add(y,x,z);
    for(i=2;i<=n;i++)s[i]=read(),v[i]=read();
    dfs(1,-1);
    for(i=2;i<=n;i++)printf("%lld ",f[i]);
    return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值