【CF 463F】Escape Through Leaf

题意

  给你一棵 (n) 个点的树,每个节点有两个权值 (a_i,b_i)
  从一个点 (u) 可以跳到以其为根的子树内的任意一点 (v)(不能跳到 (u) 自己),代价是 (a_u imes b_v)
  求每个点跳到任意一个叶子的最小代价。
  (nle 10^5, -10^5le a_i,b_ile 10^5)

题解

暴力

  考虑暴力 (dp),设 (dp_i) 表示从叶子跳到 (i) 号点的最小代价。
  则有转移 (dp_u = min{a_u imes b_v + dp_v})
  复杂度 (O(n^2))

优化

  发现这是一个裸的斜率优化式子,转移为求多条直线 (y=kx+b) 在直线 (x=a_u) 处的最值((k=b_v, b=dp_v)
  好像就是需要维护一个支持动态插入直线的上凸包?是不是写个支持合并的凸包平衡树就行了?
  平衡树合并的方法就是启发式合并,把两个 splay 中小的 splay 里面的所有点暴力插入大的 splay。每个点被暴力重新插入后 其所在的 splay 至少增大一倍,所以最多被插入 (log n) 次,一次插入的复杂度最大是 (O(log n)),总时间复杂度就是 (O(n log^2 n))
  综上,可以写平衡树合并来通过此题,但是 CF 这场比赛 (2) 小时让你打 (7) 道题,你写这鬼玩意所需的时间肯定不止 (2) 小时……

  观察发现,我们只需要查询 (x=a_u) 处的最值,这个操作好像可以用李超树维护?
  于是我们只需要写个支持合并的李超树……
  李超树合并与平衡树同理(但是代码量短多了),用启发式合并,把两个李超树中 小的李超树里面的所有点 暴力插入大的李超树,时间复杂度与平衡树合并的复杂度差不多,都是 (O(nlog^2 n))
  代码好写就够了。


  upd:今天下午被这么一个问题搞自闭了
  因为有些线段树相关的题的空间是 (O(nlog n)) 甚至 (O(nlog^2 n)) 的,而动态开点实际上不会开很多点,所以你可能会适当减少线段树数组的大小。然而因为你少开空间有可能被卡,所以你可能会在本地尝试卡空间。这样做并不保险,你可能本机开了空间上限 (-10MB),但交上去不排除因为测评环境不同而 MLE 的可能,也就是说控制不当你就有爆 (0) 的风险。
  为了避免这种风险,我以前有一个用 vector 代替动态开点线段树数组的想法,这样你不但不用手算空间,甚至运行时间都会少一大截。
  然后我对这题测试了一波,发现当 (n) 达到 (1w) 出头时就开始 RE 了……
  更惊人的是 CF 竟然没有给出 RE 的错误原因(注意上面那个 RE 提交)
  
  zbl
  在本机面向数据和程序二分了一波,发现跑到 ( ext{ins}) 函数时炸了

#define ls tr[o].l
#define rs tr[o].r
struct Tree{
    line v; int l,r,siz;
    Tree(){v=line(); l=r=0, siz=1;}
};
vector<Tree> tr;
void ins(int &o, int l, int r, line v){
    if(!o) o=++cnt, tr.push_back(Tree());
    int mid=l+r>>1;
    if(cmp(tr[o].v, v, mid)) swap(tr[o].v,v);
    if(l==r || tr[o].v.k==v.k || !v.id) return;

    double x = cross(tr[o].v,v);
    if(x<l || x>r) return;
    if(v.k > tr[o].v.k) ins(ls,l,mid,v);
    else ins(rs,mid+1,r,v);
    pushup(o);
}

  经过调试,发现 vector ( ext{pushback}) 到第 (8192) 位后,(o) 这个变量炸了(一输出它就 RE,一用它就 RE,把输出换成其它变量或字符串都能正常输出)
  我就不是很懂了,(o) 这个变量到底是咋了?
  然后为了输出 (o) 这个变量调了一下午未果,请了很多人也没能解决
  有个大佬还主动用 linux 帮我输出 pdb 内部给的错误信息,然而得到的错误信息只有“堆空间删除后再利用”,其它的报错根本不明意义
  后来群里还有个人私聊我,说他以前用 vector 开线段树数组也因为这个问题 RE 了,一直没有解决。当时我以为开发者把 c++ STL 写锅了

  熬到晚上,有个福州一中的神爷给出了解释?!

  vector 是倍增式开空间,即当它 ( ext{pushback})(2^n(n∈Z)) 时,它会申请一个新的长度为 (2^{n+1}) 的空间,也就是把整个 vector 的地址平移了。
  vector 的地址平移会使你目前有关这个 vector 的指针全部失效,因为你的指针指向的是 vector 平移前的地址,现在 vector 已经移走了,你的指针就指空了,输出这个位置就炸了(即堆空间删除后再利用,因为地址本身可能与堆有关)。观察 (o) 这个引用指针指向了谁——发现就是 (ls,rs),即 (tr[o].l,tr[o].r),而 tr 这个 vector 在 ( ext{pushback}) 到第 (8192) 位后就平移了,所以 (o) 这个指针指的地址没了。
  换成数学用语就是说:(tr) 是一个 vector,设平移前 (tr) 的起点是一个地址 (a)(o=tr[x].l) 就相当于 ((a+x)->l)(tr) 平移后 (tr[o].l) 变成了 ((a'+x)->l),但你的 (o) 指针指向的地址还是 ((a+x)->l)
  所以对 vector 要慎用指针之类的东西,防止 vector 地址平移导致指针失效。

  看起来大功告成了?
  还没完,我改成这么写还是炸:

#define ls tr[o].l
#define rs tr[o].r
struct Tree{
    line v; int l,r,siz;
    Tree(){v=line(); l=r=0, siz=1;}
};
vector<Tree> tr;
int ins(int o, int l, int r, line v){
    if(!o) o=++cnt, tr.pb(Tree());
    int mid=l+r>>1;
    if(cmp(tr[o].v, v, mid)) swap(tr[o].v,v);
    if(l==r || tr[o].v.k==v.k || !v.id) return o;
 
    double x = cross(tr[o].v,v);
    if(x<l || x>r) return o;
    if(v.k > tr[o].v.k) ls = ins(ls,l,mid,v);
    else rs = ins(rs,mid+1,r,v);
    pushup(o);
    return o;
}

  炸的地方没变,还是开到第 (8192) 位就炸了
  然后我就开始喷:wcnm 这软件绝对写挂了
  冷静分析,发现问题可能出在这:

if(v.k > tr[o].v.k) ls = ins(ls,l,mid,v);
else rs = ins(rs,mid+1,r,v);

  有可能是赋值时先取了左边变量的地址,然后再计算右边变量,再赋到左边变量的地址上。这样的话,先前取的 (ls/rs) 的地址在赋值时还是指空了,所以大胆猜测这是炸的原因。
  解决方法很显然,用一个变量暂存返回值,再赋给 (ls/rs) 即可。具体见下方代码。

  总而言之就是 STL 的写法是真奇怪,经常在内存地址等各种难懂的地方出问题,用户体验不是很好。建议慎用 STL,考前一定要了解使用 STL 可能出现的各种 CE/RE 情况。
  不过用 vector 代替数组挺好的,看看测试结果就知道啦:
  (500w) 位数组开线段树
  vector 开线段树


#include<bits/stdc++.h>
#define ll long long
#define pb push_back
#define N 100005
#define lim 100001
#define Lim 200003
const ll inf = 1ll<<60;
using namespace std;
inline int read(){
	int x=0; bool f=1; char c=getchar();
	for(;!isdigit(c); c=getchar()) if(c=='-') f=0;
	for(; isdigit(c); c=getchar()) x=(x<<3)+(x<<1)+(c^'0');
	if(f) return x;
	return 0-x;
}
int n,a[N],b[N];
ll dp[N];
vector<int> g[N];
struct line{
	int k; ll b;
	bool id;
	line(){k=0, b=0, id=0;}
	line(int _k, ll _b, bool _id){k=_k, b=_b, id=_id;}
	ll getY(int x){return (ll)k*x+b;} 
};
inline bool cmp(line a, line b, int x){
	if(!a.id) return 1;
	return a.getY(x)>b.getY(x);
}
inline double cross(line a, line b){
	return (double)(a.b-b.b)/(b.k-a.k);
}
int rt[N]; 
struct LiChaoTree{
	#define ls tr[o].l
	#define rs tr[o].r
	struct Tree{
		line v; int l,r,siz;
		Tree(){v=line(); l=r=0, siz=1;}
	};
	vector<Tree> tr;
	int cnt;
	inline void pushup(int o){
		tr[o].siz = tr[ls].siz + tr[rs].siz + 1;
	}
	int ins(int o, int l, int r, line v){
		if(!o) o=++cnt, tr.pb(Tree());
		int mid=l+r>>1;
		if(cmp(tr[o].v, v, mid)) swap(tr[o].v,v);
		if(l==r || tr[o].v.k==v.k || !v.id) return o;
		
		double x = cross(tr[o].v,v);
		if(x<l || x>r) return o;
		if(v.k > tr[o].v.k){
			int wtf = ins(ls,l,mid,v);
			ls=wtf;
		}
		else{
			int wtf = ins(rs,mid+1,r,v);
			rs=wtf;
		}
		pushup(o);
		return o;
	}
	int merge(int x, int y, int l, int r){
		if(!x || !y) return x|y;
		ins(x,l,r,tr[y].v);
		int mid=l+r>>1;
		int wtf = merge(tr[x].l, tr[y].l, l, mid);   tr[x].l=wtf;
			wtf = merge(tr[x].r, tr[y].r, mid+1, r); tr[x].r=wtf;
		pushup(x);
		return x;
	}
	inline int merge(int x, int y){
		if(tr[x].siz<tr[y].siz) swap(x,y);
		return merge(x,y,1,Lim);
	}
	ll query(int o, int l, int r, int x){
		if(l==r) return tr[o].v.id ? tr[o].v.getY(x) : inf;
		int mid=l+r>>1; ll ret;
		if(x<=mid){
			if(!ls) return tr[o].v.getY(x);
			ret=query(ls,l,mid,x);
		}
		else{
			if(!rs) return tr[o].v.getY(x);
			ret=query(rs,mid+1,r,x);
		}
		return tr[o].v.id ? min(ret, tr[o].v.getY(x)) : ret;
	}
	#undef ls
	#undef rs
}sgt;
inline void ins(int x){
	int k=b[x]; ll b=dp[x]-(ll)lim*k;
	rt[x] = sgt.ins(rt[x], 1, Lim, line(k,b,1));
}
inline ll query(int x){
	return sgt.query(rt[x], 1, Lim, a[x]+lim);
}
void dfs(int u, int fa){
	bool flag=0;
	for(int i=0; i<g[u].size(); ++i){
		int v=g[u][i];
		if(v==fa) continue;
		flag=1;
		dfs(v,u);
		rt[u] = sgt.merge(rt[u], rt[v]);
	}
	if(flag) dp[u] = query(u);
	ins(u);
}
int main(){
	//freopen("1.in","r",stdin);
	//freopen("1.out","w",stdout);
	sgt.tr.pb(LiChaoTree::Tree());
	n=read();
	for(int i=1; i<=n; ++i) a[i]=read();
	for(int i=1; i<=n; ++i) b[i]=read();
	int u,v;
	for(int i=1; i<n; ++i){
		u=read(), v=read();
		g[u].pb(v), g[v].pb(u);
	}
	dfs(1,0);
	for(int i=1; i<=n; ++i) printf("%lld ",dp[i]);
	return 0;
}

  总结一点:不少斜率优化题都是单点求最值,所以可以用李超树这个简单的数据结构维护。但李超树并不能动态维护凸包面积,所以像【HAOI2011 防线修建】这种题就只能用平衡树做。

原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/cf463f.html