[·V·e·n·u·s·] 长链剖分·杂题选做

一年前学的东西,但是直到最近几天才想明白。

CF526G Spiders Evil Plan

性质1:选的路径端点一定都是叶子

证明显然,因为不在叶子把它拓展到叶子边权和显然会增大。

我们先不考虑一定包含 x x x的限制。

c n t cnt cnt为叶子数。

那么如果 2 y < = c n t 2y<=cnt 2y<=cnt 最终的路径端点一定是由 2 y 2y 2y个叶子构成
.

否则显然可以存在一种方案取到所有的边。

性质2:对于任意一个选择的叶子集合,总有一种方法使得选出的边为这些叶子的虚树的边。

证明的话,如果某两条路径不相交,我们换一下端点就可以变得相交。这样最终所有路径都会相交,而所有路径的并自然是虚树的边集了。

考虑有 x x x怎么办,首先我们强制让 x x x被选,也就是说再选 2 y − 1 2y-1 2y1个叶子。

同时把 x x x作为根。那么虚树就是叶子到根的路径的并。

也就是说,我们要求 2 y − 1 2y-1 2y1个叶子,使得他们到根的路径并的权值之和最大。

性质3:我们把树做带权长链剖分,那么答案就是选前 2 y − 1 2y-1 2y1大的链。

首先这样选一定是最大的,并且同时因为长链剖分的性质,即每次挑最长的儿子,那么一定满足,如果 x x x所在链被选了,那么 x x x的链顶的父亲所在链也一定被选了。

但是有一个问题就是因为我们是强制 x x x选了,那么可能 x x x不在本来的虚树里,那么问题来了,是否有一个点一定在原本虚树里呢?。

性质4:直径端点一定在答案里。

也很对,因为长剖后第一条选的链肯定是直径端点。

综上所述,我们得到了一个一定出现在虚树中的点。

这样,我们就可以把直径端点作为树的根,因为端点有两个,所以分别以两个为根做一遍取最大值。

然后我们分析怎么保证 x x x一定被选。

不妨先把前 2 y − 1 2y-1 2y1大的链选了,-1是因为根也是叶子。

预处理 r k x rk_x rkx表示 x x x所在链的排名,也就是说第几大的。

如果 x x x已经被包含了,即 r k x ≤ 2 y − 1 rk_x\leq 2y-1 rkx2y1,那么直接就好了。

否则,有两种处理办法

1.从 x x x子树内选一个叶子打通到跟的路径,同时删掉排名为 2 y − 2 2y-2 2y2的路径。

2.求出 x x x向上走第一个被虚树覆盖的节点,不妨设为 z z z,可以倍增出来。

如果 z z z子树内只选了一个,那就把原来选的删掉换成 x x x子树内的叶子就好了。

如果不止一个,那么一定有一条链的权值较小,就会被第一种情况考虑到了。

综上所述,就在 O ( n l o g n ) O(nlogn) O(nlogn)的时间内解决了。

#include
using namespace std;
template <typename T>inline void read(T &x)
{
	x=0;char c=getchar();bool f=0;
	for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
	for(;c>='0'&&c<='9';c=getchar())
	x=(x<<1)+(x<<3)+(c^48);
	x=(f?-x:x);
}
const int N = 1e5+7;
int n,m;
struct edge
{
	int y,v,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y,int v)
{
	e[++t].y=y;
	e[t].v=v;
	e[t].next=link[x];
	link[x]=t;
}
struct machine
{
	int dis[N],root;	
	int fa[N][18];
	int dep[N],mxdep[N],son[N];
	void dfs(int x,int pre)
	{
		fa[x][0]=pre;
		for(int k=1;fa[x][k-1];k++)
		fa[x][k]=fa[fa[x][k-1]][k-1];
		for(int i=link[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(y==pre)continue;
			dep[y]=mxdep[y]=dep[x]+e[i].v;
			dfs(y,x);
			if(mxdep[y]>mxdep[x])
			{
				mxdep[x]=mxdep[y];
				son[x]=y;
			}
		}
	}
	int top[N];
	void Exdfs(int x,int topth)
	{
		top[x]=topth;
		if(!son[x])return;
		Exdfs(son[x],topth);
		for(int i=link[x];i;i=e[i].next)
		{
			int y=e[i].y;
			if(y==fa[x][0]||y==son[x])continue;
			Exdfs(y,y);
		}
	}
	int cnt=0;
	#define PII pair<int,int>
	#define mk(x,y) make_pair(x,y)
	#define X(x) x.first
	#define Y(x) x.second
	PII chain[N]; 
	int rk[N];
	int sum[N];
	void Build()
	{
		dfs(root,0);
		Exdfs(root,root);
		for(int i=1;i<=n;i++)
		if(top[i]==i)
		chain[++cnt]=mk(mxdep[i]-dep[fa[i][0]],i);
		sort(chain+1,chain+cnt+1);
		reverse(chain+1,chain+cnt+1);
		for(int i=1;i<=cnt;i++)
		rk[Y(chain[i])]=i;
		for(int i=1;i<=n;i++)
		rk[i]=rk[top[i]];
		for(int i=1;i<=cnt;i++)
		sum[i]=sum[i-1]+X(chain[i]);
	}
	int Jump(int x,int K)
	{
		for(int i=17;i>=0;i--)
		if(rk[fa[x][i]]>K) x=fa[x][i];
		return fa[x][0]; 
	}
	inline int getans(int x,int C)
	{
		C=C*2-1;
		if(C>cnt) return sum[cnt];
		if(rk[x]<=C) return sum[C];
		int p=Jump(x,C);
		return max(sum[C-1]+mxdep[x]-dep[Jump(x,C-1)],sum[C]-mxdep[p]+mxdep[x]);
	}
}Sol[2];
int dis[N][2];
void getdis(int x,int pre,int c)
{
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==pre)continue;
		dis[y][c]=dis[x][c]+e[i].v;
		getdis(y,x,c);
	}
}
int rot[2];
void getroot()
{
	getdis(1,0,0);
	for(int i=1;i<=n;i++)
	if(dis[i][0]>dis[rot[0]][0]) rot[0]=i;
	getdis(rot[0],0,1);
	for(int i=1;i<=n;i++)
	if(dis[i][1]>dis[rot[1]][1]) rot[1]=i;
}
int main()
{
	read(n);read(m);
	for(int i=1;i<n;i++)
	{
		int u,v,w;
		read(u);read(v);read(w);
		add(u,v,w);
		add(v,u,w);
	}
	getroot();
	Sol[0].root=rot[0];
	Sol[1].root=rot[1];
	Sol[0].Build();
	Sol[1].Build();
	int lst=0;
	while(m--)
	{
		int x,y;
		read(x);
		read(y);
		x=(x+lst-1)%n+1;
		y=(y+lst-1)%n+1;
		lst=max(Sol[0].getans(x,y),Sol[1].getans(x,y));
		printf("%d\n",lst);
	}
	return 0;
}

[WC2010]重建计划

首先是经典的0/1分数规划,二分答案后转化为求路径权值是否有 > 0 >0 >0的。

f [ x ] [ i ] f[x][i] f[x][i]为从 x x x向下走 y y y条边的最大边权。

那么转移为 f [ x ] [ i ] = m a x ( f [ y ] [ i − 1 ] + w [ x ] [ y ] ) f[x][i]=max(f[y][i-1]+w[x][y]) f[x][i]=max(f[y][i1]+w[x][y])

可以通过长链剖分+打标记的形式求出来。

那么答案为

a n s = max ⁡ i = L R ( max ⁡ ( f [ x ] [ i ] , f [ y ] [ j ] + f [ z ] [ i − j ] + w [ x ] [ y ] + w [ x ] [ z ] ) ) , i , j ∈ s o n [ x ] ans=\max_{i=L}^R(\max(f[x][i],f[y][j]+f[z][i-j]+w[x][y]+w[x][z])),i,j\in son[x] ans=maxi=LR(max(f[x][i],f[y][j]+f[z][ij]+w[x][y]+w[x][z])),i,json[x]

这个可以在短儿子向 x x x合并的时候顺便求出来。

但是有一个问题无法解决,就是我们要求一段区间内的最大值。

考虑用线段树维护。

长链剖分后,求出每个点的 d f s dfs dfs序,这样做的好处是一条链上的点在 d f s dfs dfs恰好为一个区间,且某个这条链上的点到链底的距离恰好为这个点在 d f s dfs dfs序上的位置到这个区间的最后一个位置的距离。

也就是说,这个线段树就可以当长链剖分的 d p dp dp数组来使用。

我们可以用 d p [ d f n [ x ] + i ] dp[dfn[x]+i] dp[dfn[x]+i]来存储 f [ x ] [ i ] f[x][i] f[x][i]的值,转移是非常方便的。

同时可以很好地处理一段区间的最大值,那么问题就解决了。

#include
using namespace std;
const int N = 1e6+7;
typedef long long LL;
typedef double db;
template <typename T>inline void read(T &x)
{
	x=0;char c=getchar();bool f=0;
	for(;c<'0'||c>'9';c=getchar()) f|=(c=='-');
	for(;c>='0'&&c<='9';c=getchar())
	x=(x<<1)+(x<<3)+(c^48);
	x=(f?-x:x);
}
int n,L,R;
struct edge
{
	int y,v,next;
}e[2*N];
int link[N],t=0;
void add(int x,int y,int v)
{
	e[++t].y=y;
	e[t].v=v;
	e[t].next=link[x];
	link[x]=t;
}
int len[N],son[N],val[N];
void dfs(int x,int pre)
{
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==pre)continue;
		dfs(y,x);
		if(len[y]+1>len[x])
		{
			len[x]=len[y]+1;
			son[x]=y;
			val[x]=e[i].v;
		}
	}
}
const db INF = 1e18;
db dp[N];
inline void clear(int k,int l,int r)
{
	dp[k]=-INF;
	if(l==r)return;
	int mid=(l+r)>>1;
	clear(k<<1,l,mid);
	clear(k<<1|1,mid+1,r);
}
inline void update(int k,int l,int r,int x,db v)
{
	dp[k]=max(dp[k],v);
	if(l==r)return;
	int mid=(l+r)>>1;
	if(x<=mid) update(k<<1,l,mid,x,v);
	else update(k<<1|1,mid+1,r,x,v);
}
db query(int k,int l,int r,int L,int R)
{
	if(l>R||r<L) return -INF;
	if(L<=l&&r<=R) return dp[k];
	int mid=(l+r)>>1;
	db res=-INF;
	if(L<=mid) res=max(res,query(k<<1,l,mid,L,R));
	if(R>mid) res=max(res,query(k<<1|1,mid+1,r,L,R));
	return res; 
}
db ans,mid;
int dfn[N],cnt=0; 
db f[N],g[N];
inline db push(int x,db v){return v-g[x];}
void solve(int x,int y,int w)
{
	int u=dfn[x],v=dfn[y];
	for(int i=1;i<=len[y]+1;i++)
	{
		db A=query(1,1,n,u+max(1,L-i),u+min(R-i,len[x]));
		ans=max(ans,w-mid+f[v+i-1]+g[u]+g[v]+A);

	}
	for(int i=1;i<=len[y]+1;i++)
	{
		db A=w-mid+f[v+i-1]+g[v];
		db B=g[u]+f[u+i];
		if(A>B) 
		{
			f[u+i]=push(u,A);
			update(1,1,n,u+i,f[u+i]);	
		}
	}
}
void getans(int x,int pre)
{
	if(!dfn[x]) dfn[x]=++cnt;
	int u=dfn[x];
	f[u]=g[u]=0;
	if(son[x])
	{
		getans(son[x],x);
		g[u]=g[u+1]+val[x]-mid;
		f[u]=push(u,0);
	}
	update(1,1,n,u,f[u]);
	for(int i=link[x];i;i=e[i].next)
	{
		int y=e[i].y;
		if(y==pre||y==son[x])continue;
		getans(y,x);
		solve(x,y,e[i].v);
	}
	if(len[x]>=L)
	ans=max(ans,g[u]+query(1,1,n,u+L,u+min(R,len[x])));
}
bool check(db v)
{
	clear(1,1,n);
	mid=v;
	ans=-INF;
	getans(1,0);
	return ans>=0;
}
int main()
{
	read(n);read(L);read(R);
	for(int i=1;i<n;i++)
	{
		int x,y,v;
		read(x);read(y);read(v);
		add(x,y,v);
		add(y,x,v);
	}
	dfs(1,0);
	db l=0,r=1e6;
	while(r-l>1e-5)
	{
		db Mid=(l+r)/2.0;
		if(check(Mid)) l=Mid;
		else r=Mid;
	} 
	printf("%.3lf\n",l);
	return 0;
}

[十二省联考 2019] 希望

真的很毒瘤啊。

首先因为连通块的交还是连通块,且对于一个连通块而言,点数-边数=1。

所以,我们用点的贡献,减掉边的贡献,就是答案。

f [ x ] [ i ] f[x][i] f[x][i]为在 x x x子树内,选择的包含 x x x的连通块的点中距离 x x x最远的距离是 i i i,选择连通块的方案数。

f [ x ] [ i ] = ∏ ( f [ y ] [ i − 1 ] + 1 ) f[x][i]=\prod (f[y][i-1]+1) f[x][i]=(f[y][i1]+1)

维护乘法标记和加法标记,这个可以很轻松地用长链剖分维护。

g [ x ] [ i ] g[x][i] g[x][i]为在 x x x的子树为,选择的包含 x x x的连通块的点中距离 x x x最远的距离是 i i i,选择连通块的方案数。

g [ x ] [ i ] = ( g [ f a [ x ] ] [ i − 1 ] + 1 ) [ ∏ y ∈ s o n [ f a [ x ] ] ! = x ( f [ y ] [ i − 2 ] + 1 ) g[x][i]=(g[fa[x]][i-1]+1)[\prod_{y\in son[fa[x]]!=x} (f[y][i-2]+1) g[x][i]=(g[fa[x]][i1]+1)[yson[fa[x]]!=x(f[y][i2]+1)

点的贡献自然是 ( f [ x ] [ L ] × g [ x ] [ L ] ) K (f[x][L]\times g[x][L])^K (f[x][L]×g[x][L])K

边的贡献是 ( f [ x ] [ L − 1 ] ] × g [ x ] [ L − 1 ] K ) (f[x][L-1]]\times g[x][L-1]^K) (f[x][L1]]×g[x][L1]K)

但是求 g g g却很麻烦。

我们可以把一个点的 g g g直接继承给长儿子,也算是倒着长链剖分了。

但是后边有点麻烦。

我们把 f a [ x ] fa[x] fa[x]的所有儿子列出来,那么就是一个前缀和一个后缀的乘积。

前缀好解决,因为我们就是正着 d p dp dp的。

但是反面呢?其实也能解决,只要我们第一遍求 f f f时倒着求,并记录下那时的 d p dp dp数组就好了。

细节却有很多。

你可能感兴趣的:(数据结构——树链剖分,深度优先,算法,动态规划)