9.树上问题

树上问题

开题顺序: \(ALFBDKGC\)

\(A\) luogu P2515 [HAOI2010] 软件安装

\(B\) CF494D Birthday

  • 将式子拆成 \(2\sum\limits_{x \in \operatorname{Subtree}(v)}dis_{u,x}^{2}-\sum\limits_{i=1}^{n}dis_{u,i}^{2}\) 的形式。

  • \(\sum\limits_{i=1}^{n}dis_{u,i}^{2}\) 换根 \(DP\) 加完全平方公式是容易维护的。

  • 将前面的式子拆开,有 \(dis_{u,x}^{2}=(dis_{1,x}+dis_{1,u}-2dis_{1,\operatorname{LCA}(u,x)})^{2}=dis_{1,u}^{2}+dis_{1,x}^{2}+4dis_{1,\operatorname{LCA}(u,x)}^{2}+2dis_{1,u}dis_{1,x}-4dis_{1,u}dis_{{1,\operatorname{LCA}(u,x)}}-4dis_{1,x}dis_{{1,\operatorname{LCA}(u,x)}}\)

  • 将常量 \(u\) 提出去,需要额外处理出 \(\sum\limits_{y \in Subtree(x)}dis_{1,y},\sum\limits_{y \in Subtree(x)}dis_{1,y}^{2}\)

  • 剩下的部分涉及 \(\operatorname{LCA}\) 的求解,需要进行分讨。

    • \(u \notin \operatorname{Subtree}(v)\) 时是容易维护的。
    • \(u \in \operatorname{Subtree}(v)\) 时,发现 \(v \to u\) 这条链上的点作为 \(\operatorname{LCA}\) 时难以进行统计答案,需要将式子变形成 \(\sum\limits_{i=1}^{n}dis_{u,i}^{2}-2\sum\limits_{x \notin \operatorname{Subtree}(v)}dis_{u,x}^{2}\) ,然后完全平方公式展开即可。
    点击查看代码
    const ll p=1000000007;
    struct node
    {
    	ll nxt,to,w;
    }e[200010];
    ll head[100010],fa[100010],siz[100010],dep[100010],dis[100010],son[100010],top[100010],f[2][100010],g[2][100010],num[2][100010],n,cnt=0;
    void add(ll u,ll v,ll w)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	e[cnt].w=w;
    	head[u]=cnt;
    }
    void dfs1(ll x,ll father)
    {
    	siz[x]=1;
    	fa[x]=father;
    	dep[x]=dep[father]+1;
    	num[0][x]=dis[x];
    	num[1][x]=dis[x]*dis[x]%p;
    	for(ll i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=father)
    		{
    			dis[e[i].to]=(dis[x]+e[i].w)%p;
    			dfs1(e[i].to,x);
    			siz[x]+=siz[e[i].to];
    			son[x]=(siz[e[i].to]>siz[son[x]])?e[i].to:son[x];
    			num[0][x]=(num[0][x]+num[0][e[i].to])%p;
    			num[1][x]=(num[1][x]+num[1][e[i].to])%p;
    			f[0][x]=(f[0][x]+f[0][e[i].to]+siz[e[i].to]*e[i].w%p)%p;
    			f[1][x]=(f[1][x]+f[1][e[i].to]+2*e[i].w%p*f[0][e[i].to]%p+e[i].w*e[i].w%p*siz[e[i].to]%p)%p;
    		}
    	}
    }
    void dfs2(ll x,ll id)
    {
    	top[x]=id;
    	for(ll i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa[x])
    		{
    			g[0][e[i].to]=(g[0][x]+(n-2*siz[e[i].to]+p)*e[i].w%p)%p;
    			g[1][e[i].to]=(g[1][x]+e[i].w*e[i].w%p*(n-2*siz[e[i].to]+p)%p)%p;
    			g[1][e[i].to]=(g[1][e[i].to]+2*e[i].w%p*((g[0][x]-2*f[0][e[i].to]%p-e[i].w*siz[e[i].to]%p+2*p)%p))%p;
    			dfs2(e[i].to,(e[i].to==son[x])?id:e[i].to);
    		}
    	}
    }
    ll lca(ll u,ll v)
    {
    	while(top[u]!=top[v])
    	{
    		if(dep[top[u]]>dep[top[v]])  u=fa[top[u]];
    		else  v=fa[top[v]];
    	}
    	return dep[u]<dep[v]?u:v;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll m,u,v,w,rt,ans=0,i;
    	cin>>n;
    	for(i=1;i<=n-1;i++)
    	{
    		cin>>u>>v>>w;
    		add(u,v,w);  add(v,u,w);
    	}
    	dfs1(1,0);
    	g[0][1]=f[0][1];  g[1][1]=f[1][1];
    	dfs2(1,1);
    	cin>>m;
    	for(i=1;i<=m;i++)
    	{
    		cin>>u>>v;
    		rt=lca(u,v);
    		if(rt==v)
    		{   
    			w=(dis[u]-dis[v]+p)%p;
    			ans=(g[1][v]-f[1][v]+2*w%p*(g[0][v]-f[0][v]+p)%p+w*w%p*(n-siz[v])%p+p)%p;
    			ans=(g[1][u]-2*ans%p+p)%p;
    		}
    		else
    		{
    			ans=(dis[u]*dis[u]%p*siz[v]%p+num[1][v]+2*dis[u]%p*num[0][v]%p)%p;
    			ans=(ans+4*dis[rt]%p*dis[rt]%p*siz[v]%p)%p;
    			ans=(ans-4*dis[u]%p*dis[rt]%p*siz[v]%p+p)%p;
    			ans=(ans-4*dis[rt]%p*num[0][v]%p+p)%p;
    			ans=(ans*2%p-g[1][u]+p)%p;
    		}
    		cout<<ans<<endl;
    	}
    	return 0;
    }
    
    

\(C\) luogu P4757 [CERC2014] Parades

  • \(f_{x}\) 表示仅考虑 \(x\) 为根的子树内链的最大合并数量,观察到 \(f_{x} \ge \sum\limits_{y \in Son(x)}f_{y}\) ,且连接 \(x\) 和与 \(f_{y}\) 中最优决策链相交(边相交)的点一定不优。

  • 考虑记录以 \(x\) 为根的子树内不与 \(f_{x}\) 的任意一条最优决策链相交的点集 \(opt_{x}\) 来进行对 \(x \to u\) 的转移,贪心地任意选一个合法的进行转移即可。

  • 否则考虑 \(u \to x \to v\) 的转移,因 \(du \le 10\) ,直接暴力状压 \(DP\) 即可,更新过程类似 [AGC016F] Games on DAG 都是考虑新加入的 \(\operatorname{lowbit}\) 的贡献。

  • 最后对 \(opt_{x}\) 进行更新,将加入或不加入均不影响答案的儿子的 \(opt\) 拷贝过来即可。

  • 因全程遵循即使选择不合法的点对儿子的答案 \(-1\) 后对自己的答案 \(+1\) ,而总答案不变原则,可知上述做法正确性。

    点击查看代码
    struct node
    {
    	int nxt,to;
    }e[2010];
    int head[1010],vis[1010][1010],d[20][20],f[1010],g[1010],cnt=0;
    vector<int>son[1010],opt[1010];
    void add(int u,int v)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	head[u]=cnt;
    }
    int lowbit(int x)
    {
    	return (x&(-x));
    }
    void dfs(int x,int fa)
    {
    	f[x]=0;
    	son[x].clear();  opt[x].clear();
    	for(int i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x);
    			f[x]+=f[e[i].to];
    			son[x].push_back(e[i].to);
    			for(int j=0;j<opt[e[i].to].size();j++)
    			{
    				if(vis[x][opt[e[i].to][j]]==1)
    				{
    					f[x]++;
    					opt[e[i].to].clear();
    				}
    			}
    		}
    	}
    	int m=son[x].size(),tot=(1<<m)-1;
    	memset(d,0,sizeof(d));
    	for(int i=0;i<m;i++)
    	{
    		for(int j=i+1;j<m;j++)
    		{
    			int u=son[x][i],v=son[x][j];
    			for(int p=0;p<opt[u].size()&&d[i][j]==0;p++)
    			{
    				for(int q=0;q<opt[v].size();q++)
    				{
    					if(vis[opt[u][p]][opt[v][q]]==1)
    					{
    						d[i][j]=d[j][i]=1;
    						break;
    					}
    				}
    			}
    		}
    	}
    	g[0]=0;
    	for(int i=1;i<=tot;i++)
    	{
    		g[i]=g[i^lowbit(i)];
    		int k=__builtin_ctz(i);
    		for(int j=k+1;j<m;j++)
    		{
    			if(d[k][j]==1&&((i>>j)&1)==1)  g[i]=max(g[i],g[(i^lowbit(i))^(1<<j)]+1);
    		}
    	}
    	f[x]+=g[tot];
    	opt[x].push_back(x);
    	for(int i=0;i<m;i++)
    	{
    		if(g[tot]==g[tot-(1<<i)])
    		{
    			for(int j=0;j<opt[son[x][i]].size();j++)  opt[x].push_back(opt[son[x][i]][j]);
    		}
    	}
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	int t,n,m,u,v,i,j;
    	scanf("%d",&t);
    	for(j=1;j<=t;j++)
    	{
    		cnt=0;
    		memset(e,0,sizeof(e));
    		memset(head,0,sizeof(head));
    		memset(vis,0,sizeof(vis));
    		scanf("%d",&n);
    		for(i=1;i<=n-1;i++)
    		{
    			scanf("%d%d",&u,&v);
    			add(u,v);  add(v,u);
    		}
    		scanf("%d",&m);
    		for(i=1;i<=m;i++)
    		{
    			scanf("%d%d",&u,&v);
    			vis[u][v]=vis[v][u]=1;
    		}
    		dfs(1,0);
    		printf("%d\n",f[1]);
    	}
    	return 0;	
    }
    

\(D\) [ARC101E] Ribbons on Tree

  • 考虑正难则反,统计不合法的数量。具体地,设 \(F(S)\) 表示断掉 \(S\) 中的边后的方案数,则有 \(\sum\limits_{S \subseteq E}(-1)^{|S|}F(S)\) 即为所求。

  • 断掉这些边后原树分成了若干个大小为偶数的连通块,每个连通块内部两两配对,设大小为 \(2siz\) 则其内部的方案数为 \(\prod\limits_{i=1}^{siz}(2siz-1)\) ,记为 \(h_{siz}\)

  • 不妨统计以 \(x\) 为根的子树内 \(x\) 所在连通块大小为 \(i\) 时子树内部的贡献 \(f_{x,i}\)(包括容斥系数在内)。

  • 转移时考虑 \((x,y)\) 这条边是否断掉,分别有 \(\begin{cases} f_{x,i}' \gets -f_{x,i}f_{y,j}h_{j} \\ f'_{x,i+j} \gets f_{x,i}f_{y,j} \end{cases}\) ,需要辅助数组进行转移。

  • 最终,有 \(\sum\limits_{i=1}^{n}[i \bmod 2=0] \times f_{1,i}h_{i}\) 即为所求。

    点击查看代码
    const int p=1000000007;
    struct node
    {
    	int nxt,to;
    }e[10010];
    int head[5010],siz[5010],h[5010],f[5010][5010],g[5010],cnt=0;
    void add(int u,int v)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	head[u]=cnt;
    }
    void dfs(int x,int fa)
    {
    	siz[x]=1;
    	f[x][1]=1;
    	for(int i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x);
    			for(int j=1;j<=siz[x]+siz[e[i].to];j++)  g[j]=0;
    			for(int j=1;j<=siz[x];j++)
    			{
    				for(int k=1;k<=siz[e[i].to];k++)
    				{
    					g[j+k]=(g[j+k]+1ll*f[x][j]*f[e[i].to][k]%p)%p;
    					g[j]=(g[j]-1ll*f[x][j]*f[e[i].to][k]%p*h[k]%p+p)%p;
    				}
    			}
    			siz[x]+=siz[e[i].to];
    			for(int j=1;j<=siz[x];j++)  f[x][j]=g[j];
    		}
    	}
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	int n,u,v,ans=0,i;
    	cin>>n;
    	h[0]=1;
    	for(i=2;i<=n;i++)
    	{
    		cin>>u>>v;
    		add(u,v);  add(v,u);
    		if(i%2==0)  h[i]=1ll*h[i-2]*(i-1)%p;
    	}
    	dfs(1,0);
    	for(i=2;i<=n;i+=2)  ans=(ans+1ll*f[1][i]*h[i]%p)%p;
    	cout<<ans<<endl;
    	return 0;
    }
    

\(E\) luogu P3748 [六省联考 2017] 摧毁“树状图”

\(F\) luogu P2305 [NOI2014] 购票

\(G\) luogu P10805 [CEOI2024] 加油站

  • \(u \to fa_{v} \to v\)\(fa_{v} \to v\) 刚好需要加油时会对 \(fa_{v}\) 产生 \(siz_{v}\) 的贡献。

  • 考虑点分治,统计经过分治中心 \(rt\) 的路径 \(u \to rt \to v\) 的贡献。

  • 直接统计难以钦定 \(u,v\) 不在同一棵子树内的转移,不妨直接硬算,然后再对子树内部进行容斥。

  • 对于 \(u \to rt\) ,倍增容易维护出需要往上跳的最浅的点然后统计加油次数挂在相应节点上,此时不能直接进行统计贡献。

  • 对于 \(rt \to v\) ,因贡献产生方式与父亲节点有关,考虑将贡献挂到恰好需要加油才能到达的儿子节点。转移时考虑从根节点直接跳下来或根链上某个节点跳下来时的贡献,分别二分或维护链上前缀和维护。

  • 注意不要把原树上的子树大小和求重心时的子树大小搞混。

    点击查看代码
    struct node
    {
    	ll nxt,to,w;
    }e[140010];
    ll head[70010],ans[70010],siz[70010],cnt=0,n,k;
    void add(ll u,ll v,ll w)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	e[cnt].w=w;
    	head[u]=cnt;
    }
    void dfs(ll x,ll fa)
    {
    	siz[x]=1;
    	for(ll i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x);
    			siz[x]+=siz[e[i].to];
    		}
    	}
    }
    ll val(ll u,ll v)
    {
    	return siz[u]>siz[v]?siz[v]:n-siz[u];
    }
    struct Divide_On_Tree
    {
    	ll siz[70010],weight[70010],vis[70010],up[70010][20],dis[70010],num[70010],f[70010],sum[70010],center;
    	vector<ll>tmp,q;
    	void init(ll n)
    	{
    		center=0;
    		get_center(1,0,n);
    		get_siz(center,0);
    		divide(center);
    	}
    	void get_center(ll x,ll fa,ll n)
    	{
    		siz[x]=1;  weight[x]=0;
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0&&e[i].to!=fa)
    			{
    				get_center(e[i].to,x,n);
    				siz[x]+=siz[e[i].to];
    				weight[x]=max(weight[x],siz[e[i].to]);
    			}
    		}
    		weight[x]=max(weight[x],n-siz[x]);
    		if(weight[x]<=n/2)  center=x;
    	}
    	void get_siz(ll x,ll fa)
    	{
    		siz[x]=1;
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0&&e[i].to!=fa)
    			{
    				get_siz(e[i].to,x);
    				siz[x]+=siz[e[i].to];
    			}
    		}
    	}
    	ll find(ll x)
    	{
    		ll rt=x;
    		for(ll i=17;i>=0;i--)
    		{
    			if(dis[x]-dis[up[rt][i]]<=k)  rt=up[rt][i];
    		}
    		return rt;	
    	}
    	void get_dis(ll x,ll fa)
    	{
    		num[x]=1;  up[x][0]=fa;
    		for(ll i=1;i<=17;i++)  up[x][i]=up[up[x][i-1]][i-1];
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0&&e[i].to!=fa)
    			{
    				dis[e[i].to]=dis[x]+e[i].w;
    				get_dis(e[i].to,x);
    			}
    		}
    		if(dis[x]<=k)
    		{
    			for(ll i=1;i<=num[x];i++)  tmp.push_back(dis[x]);
    		}
    		else  num[find(x)]+=num[x];
    	}
    	void dfs(ll x,ll fa,vector<ll>&q,ll op)
    	{
    		f[x]=upper_bound(q.begin(),q.end(),k-dis[fa])-lower_bound(q.begin(),q.end(),k-dis[x]+1);
    		if(dis[x]>k)  f[x]+=sum[find(x)]-sum[find(fa)];
    		sum[x]=sum[fa]+f[x];
    		ans[fa]+=op*f[x]*val(fa,x);//将儿子的贡献算到父亲上
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0&&e[i].to!=fa)  dfs(e[i].to,x,q,op);
    		}
    	}
    	void divide(ll x)
    	{
    		vis[x]=1;  dis[x]=0;
    		q.clear();  q.push_back(0);
    		for(ll i=0;i<=17;i++)  up[x][i]=x;
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0)
    			{	
    				dis[e[i].to]=e[i].w;  tmp.clear();
    				get_dis(e[i].to,x);
    				ans[e[i].to]+=(num[e[i].to]-1)*val(e[i].to,x);// 除去自己到自己的贡献
    				sort(tmp.begin(),tmp.end());  
    				f[x]=sum[x]=0;  dfs(e[i].to,x,tmp,-1);
    				for(ll j=0;j<tmp.size();j++)  q.push_back(tmp[j]);
    			}
    		}
    		sort(q.begin(),q.end());
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0)  
    			{
    				f[x]=sum[x]=0;  dfs(e[i].to,x,q,1);
    			}
    		}
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(vis[e[i].to]==0)
    			{
    				center=0;
    				get_center(e[i].to,x,siz[e[i].to]);
    				get_siz(center,0);
    				divide(center);
    			}
    		}
    	}
    }D;
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll u,v,w,i;
    	cin>>n>>k;
    	for(i=1;i<=n-1;i++)
    	{
    		cin>>u>>v>>w;  u++;  v++;
    		add(u,v,w);  add(v,u,w);
    	}
    	dfs(1,0);
    	D.init(n);
    	for(i=1;i<=n;i++)  cout<<ans[i]<<endl;
    	return 0;
    }
    
    

\(H\) luogu P4220 [WC2018] 通道

\(I\) LibreOJ 3661. 「2021 集训队互测」蜘蛛爬树

\(J\) luogu P5642 人造情感(emotion)

\(K\) luogu P6830 [IOI2020] 连接擎天树

  • 由于环的存在,若要求 \(u,v\) 间有恰好三条路径则无解。

  • \(DFS\) 求连通块后考虑树边和基环树上环的构造即可。

    点击查看代码
    // #include"supertrees.h"
    int vis[1010],maxx,n;
    vector<int>pos,tree,circle;
    vector<vector<int> > e,ans;
    void build(vector<vector<int> > b);
    void dfs1(int x)
    {
    	vis[x]=1;
    	pos.push_back(x);
    	for(int i=0;i<n;i++)
    	{
    		maxx=max(maxx,e[x][i]);
    		if(e[x][i]!=0&&vis[i]==0)
    		{
    			dfs1(i);
    		}
    	}
    }
    void dfs2(int x)
    {
    	vis[x]=2;
    	tree.push_back(x);
    	for(int i=0;i<n;i++)
    	{
    		if(e[x][i]==1&&vis[i]==1)
    		{
    			dfs2(i);
    		}
    	}
    }
    bool check(vector<int>&p,int op)
    {
    	for(int i=0;i<p.size();i++)
    	{
    		for(int j=i+1;j<p.size();j++)
    		{
    			if(e[p[i]][p[j]]==op)  return false;
    		}
    	}
    	return true;
    }
    int construct(vector<vector<int> > p)
    {
    	n=p.size();  e=p;
    	ans.resize(n);
    	for(int i=0;i<n;i++)  ans[i].resize(n);
    	for(int i=0;i<n;i++)
    	{
    		if(vis[i]==0)
    		{
    			maxx=0;  pos.clear();
    			dfs1(i);
    			if(maxx==3||check(pos,0)==false)  return 0;
    			if(maxx==1)
    			{
    				for(int j=1;j<pos.size();j++)  ans[pos[0]][pos[j]]=ans[pos[j]][pos[0]]=1;
    			}
    			else
    			{
    				circle.clear();
    				for(int j=0;j<pos.size();j++)
    				{
    					if(vis[pos[j]]==1)
    					{
    						tree.clear();
    						dfs2(pos[j]);
    						if(check(tree,2)==false)   return 0;
    						for(int k=1;k<tree.size();k++)  ans[tree[0]][tree[k]]=ans[tree[k]][tree[0]]=1;
    						circle.push_back(tree[0]);
    					}
    				}
    				if(circle.size()<=2)  return 0;
    				for(int j=0;j<circle.size();j++)  
    				{
    					ans[circle[j]][circle[(j+1)%circle.size()]]=ans[circle[(j+1)%circle.size()]][circle[j]]=1;
    				}
    			}
    		}
    	}
    	build(ans);
    	return 1;
    }
    

\(L\) LibreOJ 6669.Nauuo and Binary Tree

\(M\) LibreOJ 2398. 「JOISC 2017 Day 3」自然公园

\(N\) CF1919G Tree LGM

\(O\) CF1919H Tree Diameter

posted @ 2025-01-21 21:06  hzoi_Shadow  阅读(262)  评论(0)    收藏  举报
扩大
缩小