【bzoj3672】[Noi2014]购票 斜率优化dp+CDQ分治+树的点分治
题目描述
输入
第 1 行包含2个非负整数 n,t,分别表示城市的个数和数据类型(其意义将在后面提到)。输入文件的第 2 到 n 行,每行描述一个除SZ之外的城市。其中第 v 行包含 5 个非负整数 $f_v,s_v,p_v,q_v,l_v$,分别表示城市 v 的父亲城市,它到父亲城市道路的长度,票价的两个参数和距离限制。请注意:输入不包含编号为 1 的SZ市,第 2 行到第 n 行分别描述的是城市 2 到城市 n。
输出
输出包含 n-1 行,每行包含一个整数。其中第 v 行表示从城市 v+1 出发,到达SZ市最少的购票费用。同样请注意:输出不包含编号为 1 的SZ市。
样例输入
7 3
1 2 20 0 3
1 5 10 100 5
2 4 10 10 10
2 9 1 100 10
3 5 20 100 10
4 4 20 0 10
样例输出
40
150
70
149
300
150
题解
斜率优化dp+CDQ分治+树的点分治
设$f[x]$表示$x$到根的最小代价,那么有dp方程:$f[i]=min\{f[j]+(deep[i]-deep[j])·p[i]+q[i]\}$,其中$j$是$i$的祖先且$deep[i]-deep[j]\le l[i]$。
考虑将这个dp方程变形,得:$f[j]=p[i]·deep[j]+f[i]-p[i]·deep[i]-q[i]$。
这很明显是y=kx+b的形式,因此可以使用斜率优化来解决。其中$f$是y,$p$是k,$deep$是x,$f-p·deep-q$是b。
然而dp在树上进行。。。
由于问题在树上,因此不能直接维护凸包(貌似网上有种“可持久化凸包”的写法,然而不会写。。。 以及一种树剖套凸包的写法,然而是3个log的。。。)
考虑CDQ分治,一条深度递减的链,影响的范围是链底的子树中的所有点(不包括链底端点)。因此可以先选定某个点作为分治中心,先处理其上边的子树,即包含树根部分的子树(如果存在)(CDQ分治处理左区间),处理出其到当前树根的链的凸包。然后对于所有底端子树的点,在凸包中查找(CDQ分治处理左边对右边的影响)。由于斜率不是单调的,因此需要二分查找。最后递归处理其它子树(CDQ分治处理右区间)
这个点的选择依据:每个子树的大小不超过树的1/2,显然选择重心(其实这个过程是树的点分治)。
具体的一些细节:
由于更新的点是不包括重心的,所以如果重心不为根节点,则需要先使用重心以上到根节点的链取更新重心,然后再更新其它节点。
对于题目中的$l_v$的限制,可以先将所有底端的点(右区间)按照$deep-l$,即最小能够接受的深度从大到小排序,然后从下往上加入链的节点时,看有多少点对应的凸包恰好为当前凸包(即下一个点不满足条件)。这些点在当前的凸包中二分更新,然后再维护凸包。
由于链上点的$deep$是自带有序的,因此可以直接使用单调栈维护凸包。
最后再递归处理其它子树即可。
时间复杂度为$O(n\log^2n)$。
代码的solve中$x$代表当前树的根,而$rt$代表当前树的重心,不要弄混。
#include <cstdio> #include <cstring> #include <algorithm> #define N 200010 using namespace std; typedef long long ll; int head[N] , to[N << 1] , next[N << 1] , cnt , fa[N] , p[N]; int si[N] , ms[N] , sum , root , vis[N] , A[N] , ta , B[N] , tb , sta[N] , tot; ll deep[N] , q[N] , l[N] , f[N]; inline void add(int x , int y) { to[++cnt] = y , next[cnt] = head[x] , head[x] = cnt; } bool cmp(const int &a , const int &b) { return deep[a] - l[a] > deep[b] - l[b]; } void getroot(int x , int pre) { int i; si[x] = 1 , ms[x] = 0; for(i = head[x] ; i ; i = next[i]) if(!vis[to[i]] && to[i] != pre) getroot(to[i] , x) , si[x] += si[to[i]] , ms[x] = max(ms[x] , si[to[i]]); ms[x] = max(ms[x] , sum - si[x]); if(ms[x] < ms[root]) root = x; } void fill(int x , int pre) { int i; B[++tb] = x; for(i = head[x] ; i ; i = next[i]) if(!vis[to[i]] && to[i] != pre) fill(to[i] , x); } inline double slop(int a , int b) { return (double)(f[a] - f[b]) / (deep[a] - deep[b]); } inline void update(int x) { if(!tot) return; int l = 1 , r = tot - 1 , mid , ret = tot; while(l <= r) { mid = (l + r) >> 1; if(slop(sta[mid] , sta[mid + 1]) < p[x]) ret = mid , r = mid - 1; else l = mid + 1; } f[x] = min(f[x] , f[sta[ret]] - deep[sta[ret]] * p[x] + q[x]); } void solve(int x) { int i , j , rt; sum = si[x] , root = 0 , getroot(x , 0) , vis[rt = root] = 1; if(x != rt) si[x] -= si[rt] , solve(x); tot = ta = tb = 0; A[++ta] = rt; for(i = rt ; i != x ; i = fa[i]) { if(deep[rt] - l[rt] <= deep[fa[i]]) f[rt] = min(f[rt] , f[fa[i]] - deep[fa[i]] * p[rt] + q[rt]); A[++ta] = fa[i]; } for(i = head[rt] ; i ; i = next[i]) if(!vis[to[i]]) fill(to[i] , 0); sort(B + 1 , B + tb + 1 , cmp); for(i = j = 1 ; i <= ta ; i ++ ) { while(j <= tb && deep[A[i]] < deep[B[j]] - l[B[j]]) update(B[j ++ ]); while(tot > 1 && slop(sta[tot - 1] , sta[tot]) <= slop(sta[tot] , A[i])) tot -- ; sta[++tot] = A[i]; } while(j <= tb) update(B[j ++ ]); for(i = head[rt] ; i ; i = next[i]) if(!vis[to[i]]) solve(to[i]); } int main() { int n , i; scanf("%d%*d" , &n); for(i = 2 ; i <= n ; i ++ ) { scanf("%d%lld%d%lld%lld" , &fa[i] , &deep[i] , &p[i] , &q[i] , &l[i]); deep[i] += deep[fa[i]] , q[i] += deep[i] * p[i]; add(fa[i] , i) , add(i , fa[i]); } memset(f , 0x3f , sizeof(f)) , f[1] = 0; ms[0] = 1 << 30 , si[1] = n , solve(1); for(i = 2 ; i <= n ; i ++ ) printf("%lld\n" , f[i]); return 0; }