树上问题训练记录 2025.2

Minimum spanning tree for each edge

https://www.gxyzoj.com/d/hzoj/p/4509

最小生成树+LCA

原题,LCA在生成树上找边权最大的边即可

Information Reform

https://www.gxyzoj.com/d/hzoj/p/4510

树形dp

很新的一种 dp 方式,原理上可以说是预支代价,就是将可能产生的代价提前计算

这个题涉及最短路径,可以先 LCA 或 floyd 求出,接下来考虑什么情况会产生贡献

整个贡献分成两部分,一个是放置的位置,一个是路径,所以可以设 fi,u 表示在点 i 放一个,对 u 及其子树内代价的最小值

首先,对于点 u 贡献一定是 disi,u+k,对于 u 子树内的一点 v 分两种情况:

  1. v 也由 i 转移,那么在之前计算时已经统计过在 i 放的代价了,所以要 k

  2. v 由其他点转移,但是如果去枚举这个点,时间复杂度是 O(n4) 的,但是可以记录一个pv,就是当前点的最优转移

接下来考虑如何统计答案,根一定是它的最优决策点

其他的依然是两种情况,如果沿用上面的决策点,记为 pos,因为上面的决策点已经算过贡献了,就不用再加了,另一种是本身的决策点,因此在 fpos,ukfpu,u 中取 min 即可

点击查看代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,k,a[200],edgenum,head[200],d[200][200];
int ans[200];
struct edge{
	int to,nxt;
}e[400];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
ll f[200][200];
int p[200];
void dfs(int u,int fa)
{
	for(int i=1;i<=n;i++)
	{
		f[i][u]=a[d[i][u]]+k;
	}
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		for(int j=1;j<=n;j++)
		{
			f[j][u]+=min(f[p[v]][v],f[j][v]-k);
		}
	}
	p[u]=1;
	for(int i=1;i<=n;i++)
	{
		if(f[i][u]<f[p[u]][u]) p[u]=i;
	}
}
void dfs1(int u,int fa,int pos)
{
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		if(f[p[v]][v]<f[pos][v]-k) ans[v]=p[v];
		else ans[v]=pos;
		dfs1(v,u,ans[v]);
	}
}
int main()
{
	scanf("%d%d",&n,&k);
	for(int i=1;i<n;i++)
	{
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			d[i][j]=1e9;
		}
		d[i][i]=0;
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add_edge(u,v);
		add_edge(v,u);
		d[u][v]=d[v][u]=1;
	}
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
				d[i][j]=min(d[i][k]+d[k][j],d[i][j]);
			}
		}
	}
	dfs(1,0);
	printf("%lld\n",f[p[1]][1]);
	ans[1]=p[1];
	dfs1(1,0,p[1]);
	for(int i=1;i<=n;i++)
	{
		printf("%d ",ans[i]);
	}
	return 0;
}

「JZOI-1」旅行

https://www.gxyzoj.com/d/hzoj/p/LG7359

树形dp+LCA

先从简单的开始考虑,如果对于每组询问单独将链拎出来,依次标号,进行 dp,显然的,设 fi,0/1 为当前在走陆路/水路,转移显然:

{fi,0=min(fi1,0,fi1,1)fi,1=min(fi1,0+l,fi1,1)

但是,注意到,其实这个东西的转移时所关系到的状态只有两头是什么,因为唯一要考虑的船,可以认为是在这一段 1 中的任意一个点造的,而且这个东西算的是总时间,如果中途改变方向,我们可以直接将改变后计算的结果就当作当前方向

所以,假设当前已经算好了两组数据的值,只需要知道他们的左右端点就可以拼接

接下来看如何拼接,如果两边待拼的点有一个走陆地,就直接相加,否则,因为两边都造了船,要减去一次造船的贡献

就剩下预处理和统计答案了,先看答案分成了什么

路径是由起点到 lca,这一段跳父亲,和 lca 到终点,这一段从父亲到儿子组成的

所以涉及两个方向,都要先 DFS 处理出来

最后,统计答案可以倍增或树剖+线段树(应该没有人会这么闲吧)合并即可

点击查看代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,l,T,edgenum,head[200005];
const ll inf=1e18;
struct edge{
	int to,val,nxt,sp,typ;
}e[400005];
struct edge1{
	int u,v,w,sp;
}a[200005];
void add_edge(int u,int v,int w,int sp,int typ)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	e[edgenum].val=w;
	e[edgenum].sp=sp;
	e[edgenum].typ=typ;
	head[u]=edgenum;
}
struct node{
	ll a,b,c,d;
//	l  0,0,1,1
//  r  0,1,0,1
//0->路 1->水
}d[2][200005][20];
int f[200005][20],dep[200005];
node merge(node x,node y)
{
	node tmp;
	if(y.d==1e18) return x;
	if(x.d==1e18) return y;
	tmp.a=min(min(x.a+y.a,x.a+y.c),min(x.b+y.a,x.b+y.c-l));
	tmp.b=min(min(x.a+y.b,x.a+y.d),min(x.b+y.b,x.b+y.d-l));
	tmp.c=min(min(x.c+y.a,x.c+y.c),min(x.d+y.a,x.d+y.c-l));
	tmp.d=min(min(x.c+y.b,x.c+y.d),min(x.d+y.b,x.d+y.d-l));
	return tmp;
}
void getdep(int u,int fa)
{
	dep[u]=dep[fa]+1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
//		printf("%d %d\n",u,v);
		if(v==fa) continue;
		getdep(v,u);
	}
}
void dfs(int u,int fa,int typ)
{
	f[u][0]=fa;
	for(int i=1;i<=19;i++)
	{
		f[u][i]=f[f[u][i-1]][i-1];
		d[typ][u][i]=merge(d[typ][u][i-1],d[typ][f[u][i-1]][i-1]);
	}
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		int x=e[i].val;
		if(e[i].typ==typ) x-=e[i].sp;
		else x+=e[i].sp;
		d[typ][v][0]=(node){e[i].val,inf,inf,l+x};
		dfs(v,u,typ);
	}
}
node lca(int x,int y)
{
	int fl=0;
	if(dep[x]<dep[y]) swap(x,y),fl=1;
	node res1,res2;
	res1=res2=(node){0,inf,inf,inf};
	for(int i=19;i>=0;i--)
	{
		if(dep[f[x][i]]>=dep[y])
		{
//			printf("1");
			res1=merge(res1,d[fl][x][i]);
			x=f[x][i];
		}
		if(x==y) return res1;
	}
//	printf("%lld %lld %lld %lld\n",res1.a,res1.b,res1.c,res1.d);
	for(int i=19;i>=0;i--)
	{
		if(f[x][i]!=f[y][i])
		{
			res1=merge(res1,d[fl][x][i]);
			res2=merge(res2,d[fl^1][y][i]);
			x=f[x][i],y=f[y][i];
		}
	}
	res1=merge(res1,d[fl][x][0]),res2=merge(res2,d[fl^1][y][0]);
	swap(res2.b,res2.c);
	res1=merge(res1,res2);
	return res1;
}
int main()
{
	scanf("%d%d%d",&n,&l,&T);
	for(int i=1;i<n;i++)
	{
		int u,v,w,sp,typ;
		scanf("%d%d%d%d%d",&u,&v,&w,&sp,&typ);
		if(typ==0) swap(u,v);
		add_edge(u,v,w,sp,0);
		add_edge(v,u,w,sp,0);
		a[i]=(edge1){u,v,w,sp};
	}
	getdep(1,0);
	edgenum=0;
	for(int i=1;i<=n;i++) head[i]=0;
	for(int i=1;i<n;i++)
	{
		int fl=0;
		if(dep[a[i].u]<dep[a[i].v]) fl=1;
		add_edge(a[i].u,a[i].v,a[i].w,a[i].sp,fl);
		add_edge(a[i].v,a[i].u,a[i].w,a[i].sp,fl);
	}
	dfs(1,0,0);
	dfs(1,0,1);
//	for(int i=1;i<=n;i++)
//	{
//		printf("%d ",dep[i]);
//	}
	while(T--)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		node tmp=lca(u,v);
		printf("%lld\n",min(min(tmp.a,tmp.b),min(tmp.c,tmp.d)));
	}
	return 0;
}

「DBOI」Round 1 人生如树

https://www.gxyzoj.com/d/hzoj/p/4512

LCA+二分+hash

先考虑放到数组怎么做,暴力一定是依次匹配,但是数很多,在询问很多的情况下会 T

因为是前 i 个数,而且每个数对应加的值固定,所以在固定起点时,可以二分

此时,匹配就不能暴力枚举了,因为每个位置的值确定,考虑哈希

接下来看如何把这个东西放到树上,依然是两段,一个是从 u 到 lca,一个是从 lca 到 v(此处的 v 是二分时长度是 mid 的数组的终点,不是原终点)

因为要保证数组的顺序,所以不能在求出 lca 到两点的哈希值后直接拼起来

但是从 lca 到 v 的这部分就是从父亲到儿子,可以直接相减

但是 u 到 lca 这部分,要从儿子跳父亲,为了避免重复,将 du,i 的意义改为 u 的父亲到 u 的第 2i 级祖先的哈希值,统计时再加上 au

对于 a 数组要加上的值,预处理即可

点击查看代码
#include<cstdio>
#include<algorithm>
#define ull unsigned long long
using namespace std;
int n,m,idx,edgenum,head[200005],a[200004];
struct edge{
	int to,nxt;
}e[400005];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
struct ques{
	int u1,v1,u2,v2;
}qs[100005];
int f[200005][20],dep[200005];
ull p1[200005],d[200005][20],d1[200005],p=13331,p2[200005];
void dfs(int u,int fa)
{
	dep[u]=dep[fa]+1,d1[u]=d1[fa]*p+a[u];
	f[u][0]=fa,d[u][0]=a[fa];
	for(int i=1;i<=19;i++)
	{
		f[u][i]=f[f[u][i-1]][i-1];
		d[u][i]=d[u][i-1]*p1[1<<(i-1)]+d[f[u][i-1]][i-1];
	}
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
	}
}
int lca(int x,int y)
{
	if(dep[x]<dep[y]) swap(x,y);
	for(int i=19;i>=0;i--)
	{
		if(dep[f[x][i]]>=dep[y])
		{
			x=f[x][i];
		}
		if(x==y) return x;
	}
	for(int i=19;i>=0;i--)
	{
		if(f[x][i]!=f[y][i])
		{
			x=f[x][i];
			y=f[y][i];
		}
	}
	return f[x][0];
}
int fl;
ull get(int u,int v,int x)
{
	int lc=lca(u,v),tmp=x-1,now=u;
	if(-dep[lc]*2+dep[u]+dep[v]+1<x)
	{
		fl=1;
		return 0;
	}
	fl=0;
	ull sum=a[u];
	for(int i=19;i>=0;i--)
	{
		if(tmp>=(1<<i)&&dep[f[now][i]]>=dep[lc])
		{
			tmp-=(1<<i);
			sum=sum*p1[1<<i]+d[now][i];
			now=f[now][i];
		}
	}
	if(x>dep[u]-dep[lc]+1)
	{
		tmp=x-(dep[u]-dep[lc]+1);
		sum*=p1[tmp];
		tmp=dep[v]-(dep[lc]+tmp),now=v;
		for(int i=19;i>=0;i--)
		{
			if(tmp>=(1<<i))
			{
				tmp-=(1<<i);
				now=f[now][i];
			}
		}
		sum+=d1[now]-d1[lc]*p1[dep[now]-dep[lc]];
	}
	return sum;
}
bool check(int id,int x)
{
	if(x==1)
	{
		if(a[qs[id].u1]+1==a[qs[id].u2]) return 1;
		return 0;
	}
	ull tmp1=get(qs[id].u1,qs[id].v1,x)+p2[x];
	if(fl) return 0;
	ull tmp2=get(qs[id].u2,qs[id].v2,x);
	if(fl) return 0;
	if(tmp1==tmp2) return 1;
	else return 0;
}
int main()
{
	scanf("%d%d%d",&n,&m,&idx);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add_edge(u,v);
		add_edge(v,u); 
	}
	idx=0;
	for(int i=1;i<=m;i++)
	{
		int opt;
		scanf("%d",&opt);
		if(opt==1)
		{
			int u1,v1,u2,v2;
			scanf("%d%d%d%d",&u1,&v1,&u2,&v2);
			qs[++idx]=(ques){u1,v1,u2,v2};
		}
		else
		{
			int u,w;
			scanf("%d%d",&u,&w);
			a[++n]=w;
			add_edge(u,n);
			add_edge(n,u);
		}
	}
	p1[0]=1;
	for(int i=1;i<=n;i++)
	{
		p1[i]=p1[i-1]*p;
		p2[i]=p2[i-1]*p+i;
//		printf("%llu %llu\n",p1[i],p2[i]);
	}
	dfs(1,0);
	for(int i=1;i<=idx;i++)
	{
		int l=0,r=n;
		while(l<r)
		{
			int mid=(l+r+1)>>1;
			if(check(i,mid)) l=mid;
			else r=mid-1;
		}
		printf("%d\n",l);
	}
	return 0;
}

Mobile Phone Network

https://www.gxyzoj.com/d/hzoj/p/4514

树剖+线段树+最小生成树

先强制选上这 k 条边,接下来考虑哪些边会影响答案

首先可以跑一次最小生成树,加入的新边不会影响答案,因为不成环,而成环的边,要保证环上所包含的没有权值的边小于等于当前边

此时,树剖+线段树维护即可

点击查看代码
#include<cstdio>
#include<algorithm>
#define lid id<<1
#define rid id<<1|1
#define ll long long
using namespace std;
const int inf=2e9;
int n,m,k,f[500005],edgenum,head[500005];
int read()
{
	int x=0;
	char ch=getchar();
	while(ch<48||ch>57) ch=getchar();
	while(ch>=48&&ch<=57)
	{
		x=x*10+ch-48;
		ch=getchar();
	}
	return x;
}
struct node{
	int u,v,w;
}a[500005],b[500005];
int find(int x)
{
	if(f[x]!=x) f[x]=find(f[x]);
	return f[x];
}
struct edge{
	int to,nxt;
}e[2000005];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
int siz[500005],son[500005],dep[500005],top[500005];
int dfn[500005],idx,vis[500005];
inline void dfs(int u,int fa)
{
	siz[u]=1,f[u]=fa,dep[u]=dep[fa]+1;
	int maxn=-1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		siz[u]+=siz[v];
		if(siz[v]>maxn) maxn=siz[v],son[u]=v;
	}
}
inline void dfs1(int u,int t)
{
	dfn[u]=++idx,top[u]=t;
	if(son[u])
	{
		dfs1(son[u],t);
	}
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==f[u]||v==son[u]) continue;
		dfs1(v,v);
	}
}
struct seg_tr{
	int l,r,mx,lazy,tag;
}tr[4000004];
inline void build(int id,int l,int r)
{
	tr[id].l=l,tr[id].r=r,tr[id].mx=tr[id].lazy=inf;
	if(l==r) return;
	int mid=(l+r)>>1;
	build(lid,l,mid);
	build(rid,mid+1,r);
}
inline void pushdown(int id)
{
	tr[lid].mx=min(tr[lid].mx,tr[id].lazy);
	tr[lid].lazy=min(tr[lid].lazy,tr[id].lazy);
	tr[rid].mx=min(tr[rid].mx,tr[id].lazy);
	tr[rid].lazy=min(tr[rid].lazy,tr[id].lazy);
	tr[id].lazy=inf;
}
inline void update(int id,int l,int r,int x)
{
	if(l>r||tr[id].tag) return;
	if(tr[id].l==l&&tr[id].r==r)
	{
		tr[id].mx=min(tr[id].mx,x);
		tr[id].lazy=min(tr[id].lazy,x);
		tr[id].tag=1;
		return;
	}
	pushdown(id);
	int mid=(tr[id].l+tr[id].r)>>1;
	if(r<=mid) update(lid,l,r,x);
	else if(l>mid) update(rid,l,r,x);
	else update(lid,l,mid,x),update(rid,mid+1,r,x);
	tr[id].tag|=(tr[lid].tag&tr[rid].tag);
}
inline int query(int id,int x)
{
	if(tr[id].l==tr[id].r)
	{
		return tr[id].mx;
	}
	pushdown(id);
	int mid=(tr[id].l+tr[id].r)>>1;
	if(x<=mid) return query(lid,x);
	else return query(rid,x);
}
void add(int u,int v,int w)
{
	while(top[u]!=top[v])
	{
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		update(1,dfn[top[u]],dfn[u],w);
		u=f[top[u]];
	}
	if(dfn[u]>dfn[v]) swap(u,v);
	update(1,dfn[u]+1,dfn[v],w);
}
int main()
{
	n=read(),k=read(),m=read();
	for(int i=1;i<=n;i++) f[i]=i;
	for(int i=1;i<=k;i++)
	{
		int u,v;
		u=read(),v=read();
		f[find(u)]=find(v);
		a[i]=(node){u,v,0};
		add_edge(u,v);
		add_edge(v,u);
	}
	for(int i=1;i<=m;i++)
	{
		int u,v,w;
		u=read(),v=read(),w=read();
		b[i]=(node){u,v,w};
	}
	for(int i=1;i<=m;i++)
	{
		int v=b[i].v,u=b[i].u;
		if(find(u)!=find(v))
		{
			f[find(u)]=find(v);
			add_edge(u,v);
			add_edge(v,u);
			vis[i]=1;
		}
	}
	dfs(1,0);
	dfs1(1,0);
	build(1,1,n);
	for(int i=1;i<=m;i++)
	{
		if(!vis[i])
		{
			add(b[i].u,b[i].v,b[i].w);;
		}
	}
	ll ans=0;
	for(int i=1;i<=k;i++)
	{
		int u=a[i].u,v=a[i].v;
		if(dep[u]>dep[v]) swap(u,v);
		ll x=query(1,dfn[v]);
		if(x==inf)
		{
			ans=-1;
			break;
		}
		ans+=x;
//		printf("%d %d %d\n",u,v,x);
	}
	printf("%lld",ans);
	return 0;
}

Close Vertices

https://www.gxyzoj.com/d/hzoj/p/4514

前置知识:点分治

看过点分治之后这个东西就不难了,先考虑只有一个限制的时候怎么做

和模板题类似,都要先统计出每个点对于当前根的深度,但是如果正常双指针去加的话,会出现两条路径在同一个子树内的情况

考虑容斥,可以固定当前根枚举到的子节点 v 到它的这条边再看 v 子树内满足条件的点对数,相减即可

对于这种二维限制的,树状数组统计即可

点击查看代码
#include<cstdio>
#include<algorithm>
#define ll long long
using namespace std;
int n,len,lim,head[100005],edgenum;
struct edge{
	int to,nxt,val;
}e[200005];
ll ans[100005],c[100005];
void add_edge(int u,int v,int w)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	e[edgenum].val=w;
	head[u]=edgenum;
}
int lowbit(int x)
{
	return x & (-x);
}
void add(int x,int v)
{
	if(x>len+2) return;
	while(x<=len+2)
	{
		c[x]+=v;
		x+=lowbit(x);
	}
}
int query(int x)
{
	int res=0;
	while(x>0)
	{
		res+=c[x];
		x-=lowbit(x);
	}
	return res;
}
int siz[100005],sum,rt,mx[100005];
bool vis[100005];
void dfs1(int u,int fa)
{
	siz[u]=1,mx[u]=0;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa||vis[v]) continue;
		dfs1(v,u);
		siz[u]+=siz[v];
		mx[u]=max(mx[u],siz[v]);
	}
	mx[u]=max(mx[u],sum-siz[u]);
	if(mx[u]<mx[rt]) rt=u;
}
int dep[100005],dis[100005],cnt;
struct node{
	int d1,d2;
}a[100005];
bool cmp(node x,node y)
{
	if(x.d2!=y.d2) return x.d2<y.d2;
	return x.d1<y.d1;
}
void dfs2(int u,int fa)
{
	dep[u]=dep[fa]+1;
	a[++cnt]=(node){dep[u],dis[u]};
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa||vis[v]) continue;
		dis[v]=dis[u]+e[i].val;
		if(dis[v]>lim) dis[v]=lim+1;
		dfs2(v,u);
	}
}
ll clac(int u)
{
	cnt=0;
	dfs2(u,0);
	sort(a+1,a+cnt+1,cmp);
//	printf("a%d\n",u);
	ll tmp=0;
	for(int i=1;i<=cnt;i++)
	{
		add(a[i].d1+1,1);
//		printf("%d %d\n",a[i].d1,a[i].d2);
//		if(a[i].d1<=len&&a[i].d2<=lim) tmp++;
	}
	int l=1,r=cnt;
//	printf("a");
	while(l<r)
	{
		if(a[l].d2+a[r].d2<=lim)
		{
			add(a[l].d1+1,-1);
			tmp+=query(len-a[l++].d1+1);
		}
		else
		{
			add(a[r--].d1+1,-1);
		}
	}
	add(a[l].d1+1,-1);
	return tmp;
}
void dfs(int u,int fa)
{
	vis[u]=1;
	dis[u]=0,dep[0]=-1;
	ans[u]=clac(u);
//	printf("%d ",ans[u]);
//	printf("%d %d\n",u,fa);
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa||vis[v]) continue;
		dis[v]=e[i].val,dep[0]=0;
		ans[u]-=clac(v);
	}
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa||vis[v]) continue;
		sum=siz[v],rt=0;
		dfs1(v,-1);
		dfs1(rt,-1);
//		printf("%d %d %d\n",u,v,rt);
		dfs(rt,u);
	}
}
int main()
{
	scanf("%d%d%d",&n,&len,&lim);
	for(int i=2;i<=n;i++)
	{
		int v,w;
		scanf("%d%d",&v,&w);
		add_edge(i,v,w);
		add_edge(v,i,w);
	}
	sum=n,mx[0]=1e9;
	dfs1(1,-1);
	dfs1(rt,-1);
//	printf("%d",rt);
	dfs(rt,-1);
	ll res=0;
	for(int i=1;i<=n;i++)
	{
		res+=ans[i];
//		printf("%d ",ans[i]);
	}
	printf("%lld",res);
	return 0;
}

Nastia Plays with a Tree

https://www.gxyzoj.com/d/hzoj/p/4515

贪心trick题

这个题一个显然的结论就是加边前每个点的度数都小于等于 2,因为加和断的边数相同,所以只要让断的边数尽量小

因为最后都是要让点的度数到达一个范围,可以 DFS,先处理儿子,再处理父亲

假设当前点 u 的所有儿子都已经处理好了,考虑 u,如果 u 点先断掉与儿子相连的边,只会对 u 自己产生贡献

如果先断掉与父亲相连的,则有可能另外对父亲产生贡献,所以当度数不满足条件时,先断掉父亲是更优的

此时,对于儿子,只需留下两个就满足条件

点击查看代码
#include<cstdio>
#include<algorithm>
using namespace std;
int T,n,head[100005],edgenum,cnt1,cnt2;
struct edge{
	int to,nxt;
}e[200005];
struct node{
	int u,v;
}a[100005],add[100005],del[100005];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
int vis[100005],f[100005],d[100005],dep[100005];
int find(int x)
{
	if(f[x]!=x) f[x]=find(f[x]);
	return f[x];
}
void dfs(int u,int fa)
{
	dep[u]=dep[fa]+1;
	int d1=1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		if(!vis[v]) d1++;
	}
	if(d1<3) return;
	d1=2,vis[u]=1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa||vis[v]) continue;
		if(!d1) vis[v]=1;
		else d1--;
	}
}
int l[100005],r[100005];
int main()
{
//	freopen("1.txt","w",stdout);
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d",&n);
		edgenum=0;
		for(int i=1;i<=n;i++) d[i]=vis[i]=head[i]=l[i]=r[i]=0,f[i]=i;
		for(int i=1;i<n;i++)
		{
			int u,v;
			scanf("%d%d",&u,&v);
			add_edge(u,v);
			add_edge(v,u);
			a[i]=(node){u,v};
		}
		dfs(1,0);
		int sum=0;
		for(int i=2;i<=n;i++) sum+=vis[i];
		printf("%d\n",sum);
		if(sum==0) continue;
		for(int i=1;i<n;i++)
		{
			int v=a[i].v,u=a[i].u;
			if(dep[u]<dep[v]) swap(u,v);
			if(vis[u])
			{
				del[++cnt1]=a[i];
				continue;
			}
//			printf("%d %d\n",u,v);
			d[u]++,d[v]++;
			f[find(u)]=find(v);
		}
		for(int i=1;i<=n;i++)
		{
//			printf("%d ",d[i]);
			if(d[i]==1)
			{
				int x=find(i);
				if(l[x]) r[x]=i;
				else l[x]=i;
			}
			if(d[i]==0) l[i]=r[i]=i;
		}
		int lst=0;
		for(int i=1;i<=n;i++)
		{
//			printf("%d %d\n",l[i],r[i]);
			if(l[i])
			{
				if(lst) add[++cnt2]=(node){lst,l[i]};
				lst=r[i];
			}
		}
		for(int i=1;i<=sum;i++)
		{
			printf("%d %d %d %d\n",del[i].u,del[i].v,add[i].u,add[i].v);
		}
		cnt1=cnt2=0;
	}
	return 0;
}

[ARC165E] Random Isolation

https://www.gxyzoj.com/d/hzoj/p/4508

树形dp

实在太抽象了,其实可以将题目转化为对于一个随机的长度为 n 的排列,依次按顺序进行删点,统计有效的删点次数

假设当前的子树 x 作为一个可被操作的联通快出现的概率为 P(x),那么最终的答案就是 P(x) ,因为经过一次操作后,必然会分成新的树

根据期望的线性性,可以树形dp

如果要保证这个联通快的出现,必然要使得所有与之相连的边都断掉,所以需要额外记录边数

因为连向父亲的边不好统计,所以设状态为 fi,j,k 表示当前是节点 i,以 i 为根的联通快大小为 j,子树内与之相邻的边数为 k 的方案数

最后统计答案时,一个长度为 m 的序列在一个长度为 n 的序列前的概率为 m!n!(n+m)!,和对应方案数相乘即可

点击查看代码
#include<cstdio>
#define ll long long
using namespace std;
const int mod=998244353;
int n,m,edgenum,head[105];
struct edge{
	int to,nxt;
}e[205];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
ll f[105][105][105],d[105][105],fac[205],inv[205];
int siz[105];
ll qpow(ll x,int y)
{
	ll res=1;
	while(y)
	{
		if(y&1) res=res*x%mod;
		x=x*x%mod;
		y>>=1; 
	}
	return res;
}
void dp(int u,int v)
{
	f[v][0][1]=1;
	for(int i=0;i<=siz[u];i++)
	{
		for(int j=0;j<=siz[u];j++)
		{
			for(int k=0;k<=siz[v];k++)
			{
				for(int l=0;l<=siz[v];l++)
				{
					d[i+k][j+l]+=f[u][i][j]*f[v][k][l]%mod;
					d[i+k][j+l]%=mod;
				}
			}
		}
	}
	siz[u]+=siz[v];
	for(int i=0;i<=siz[u];i++)
	{
		for(int j=0;j<=siz[u];j++)
		{
			f[u][i][j]=d[i][j];
			d[i][j]=0;
		}
	}
}
void dfs(int u,int fa)
{
	f[u][1][0]=1,siz[u]=1;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		dp(u,v);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add_edge(u,v);
		add_edge(v,u);
	}
	dfs(1,0);
	fac[0]=inv[0]=1;
	for(int i=1;i<=2*n;i++)
	{
		fac[i]=fac[i-1]*i%mod;
		inv[i]=qpow(fac[i],mod-2);
//		printf("%d %d\n",fac[i],inv[i]);
	}
	ll ans=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=m+1;j<=siz[i];j++)
		{
			for(int k=0;k<=siz[i];k++)
			{
				int x=k+(i!=1);
				ans+=f[i][j][k]*fac[x]%mod*fac[j]%mod*inv[x+j]%mod;
				ans%=mod;
			}
		}
	}
	printf("%lld",ans);
	return 0;
}

「LNOI2014」LCA

https://www.gxyzoj.com/d/hzoj/p/1239

原,不说了

Tree and Queries

https://www.gxyzoj.com/d/hzoj/p/4516

显然的,这个东西可以通过 DFS 序拍成一个序列,询问变成查询区间,接下来看怎么做

它有两个性质,一个是位置范围,一个是出现次数,对于这种无法整段处理的东西,考虑莫队

每次加入或删除一个点,就在这个数的 cnt 对应的前缀和位置更新一下

因为莫队是依次添加或删除的,所以这个东西一定更新全了,最后时间复杂度 O(nn)

点击查看代码
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int n,m,a[100005],edgenum,head[100005];
struct edge{
	int to,nxt;
}e[200005];
void add_edge(int u,int v)
{
	e[++edgenum].nxt=head[u];
	e[edgenum].to=v;
	head[u]=edgenum;
}
int dfn[100005],siz[100005],idx,pos[100005],rnk[100005];
void dfs(int u,int fa)
{
	dfn[u]=++idx,siz[u]=1,rnk[idx]=u;
	for(int i=head[u];i;i=e[i].nxt)
	{
		int v=e[i].to;
		if(v==fa) continue;
		dfs(v,u);
		siz[u]+=siz[v];
	}
}
struct ques{
	int l,r,x,id;
}q[100005];
bool cmp(ques x,ques y)
{
	if(pos[x.l]!=pos[y.l]) return x.l<y.l;
	return x.r<y.r;
}
int sum[100005],ans[100005],cnt[100005];
void del(int x)
{
	x=rnk[x];
	cnt[a[x]]--;
	if(cnt[a[x]]>=0)
	{
		sum[cnt[a[x]]+1]--;
	}
}
void add(int x)
{
	x=rnk[x];
	cnt[a[x]]++;
	if(cnt[a[x]]>=0)
	{
		sum[cnt[a[x]]]++;
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	int blen=sqrt(n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		pos[i]=(i-1)/blen+1;
	}
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		add_edge(u,v);
		add_edge(v,u);
	}
	dfs(1,0);
	for(int i=1;i<=m;i++)
	{
		int u,k;
		scanf("%d%d",&u,&k);
		q[i]=(ques){dfn[u],dfn[u]+siz[u]-1,k,i};
	}
	sort(q+1,q+m+1,cmp);
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(l<q[i].l) del(l++);
		while(l>q[i].l) add(--l);
		while(r>q[i].r) del(r--);
		while(r<q[i].r) add(++r);
		ans[q[i].id]=sum[q[i].x];
	}
	for(int i=1;i<=m;i++)
	{
		printf("%d\n",ans[i]);
	}
	return 0;
}
posted @   wangsiqi2010916  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示