1 条题解

  • 0
    @ 2025-8-24 21:40:11

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar b6e0_

    搬运于2025-08-24 21:40:11,当前版本为作者最后更新于2021-07-12 23:26:33,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    题目链接:https://www.luogu.com.cn/problem/P2664

    有几篇 O(n)\mathcal O(n) 解法的题解,但是都写的过于不清晰,甚至我会了还是看不懂,于是我重新从头到尾讲一遍 O(n)\mathcal O(n) 的算法,尽量包含到每个细节。虽然不能完全确定我的和已经在这儿的题解完全一样,但是大致肯定都是一样的。

    在本篇题解中,称节点 ii 的颜色为 aia_i,节点 ii 的答案为 sumisum_i(如题面中)。

    首先对求 sumisum_i 的问题做出一个转换:对于每种颜色 cc,计算有多少条从 ii 出发的路径不包含这种颜色,记为 ti,ct_{i,c}(此处看 tt 数组是 n2n^2 的,这个疑问下面再解决)。那么 nti,cn-t_{i,c} 就是有多少条从 ii 出发的路径包含颜色 cc。这段的总体思想就是考虑每种颜色的贡献,再反过来算。

    对于一种颜色 cc,如果将所有颜色为 cc 的节点删除,与它连接的边也删除,那么整棵树会断成一些小的连通块(也就是其他题解中的“小树”)。对于任意一个连通块,它内部任意两个点之间在原树上的路径都不会有颜色 cc。相反地,对于两个点,如果它们在不同的连通块内,那么它们之间在原树上的路径中一定有颜色 cc。所以,对于一个大小为 ss 的连通块中的所有点 xx,需要把 tx,ct_{x,c} 加上 ss

    如果第一重循环枚举了一个颜色,那么不管怎样都要遍历整棵树(即使有不遍历的方法也会非常复杂,大概这么觉得吧),时间复杂度就变为了 O(n2)\mathcal O(n^2),不能接受。于是,我们需要在一遍 dfs 中同时处理多种颜色,计算多种颜色的答案。

    设当前枚举到了节点 xx,它有若干子节点 y1,y2,y_1,y_2,\cdots。可以发现,如果我们删除掉所有颜色为 axa_x 的节点,那么一定存在一些连通块的“顶部”是 y1,y2,y_1,y_2,\cdots。下面的图表示了一个子节点 yy 的情况。红色圈起来的部分是一个以 yy 为顶部的连通块,它的大小是 yy 的子树大小减去 yy 的子树中所有颜色为 cc 的子树的大小。也就是说,我们在一个节点只处理一个颜色,即当前节点的颜色,也只需要处理这个颜色。如果没懂的话见代码实现难点的部分。

    于是,我们能在一遍 dfs 中知道需要给 tt 数组的哪些位置加上什么数。下面要解决的就是优化掉 tt 的一维并且快速做加法。

    在最后,sumisum_i 是(mm 为颜色种数):

    c=1m(nti,c)=nmc=1mti,c\sum_{c=1}^m(n-t_{i,c})=nm-\sum_{c=1}^mt_{i,c}

    于是我们只需要对于一个 ii,求出所有颜色的答案的和就行了。记为 tit_i。下面采用树上差分:设我们 dfs 到了 xx,枚举到了子节点 yy(也就是这个连通块的顶是 yy),连通块的大小为 ss,那么在节点 yy 处加 ss,子树内最靠上的颜色为 cc 的节点处减 cc。你肯定没看懂,看下图(如何求答案、正确性、复杂度证明等在图下):

    最后 txt_x 是从 xx 到根节点路径上的差分值的和。这样,上图中那些 cc 的子树内就不会受到这个连通块带来的贡献(抵消了),xx 及外面更不会受到贡献(xx 确实不应该受到贡献)。

    有关这个解法的复杂度,看起来一个一个给 yy 子树内的 cc 打标记很慢,其实,在一个节点以“cc”的身份被打过标记后就再也不会以“cc”的身份被打标记了。以“yy”的身份打标记也显然只会有一次,所以总复杂度是 O(n)\mathcal O(n) 的。至此,这个问题被完美解决。

    代码实现还有几个不太好处理的细节:

    • 如何快速计算 yy 的子树内那些如图中 cc 的子树大小的和,也就是连通块相比 yy 的子树缺失的部分。为解决这个问题,我们以颜色为下标开一个桶 bxb_x(即代码中的 colsiz[x])记录那些极大的、根节点的颜色为 xx 的子树大小和。在 dfs 到 xx,准备递归进入它的一个儿子 yy 前记录下 baxb_{a_x} 的值(代码中的 psiz),在回来时比较一下现在的 baxb_{a_x} 与之前的 baxb_{a_x},差就是连通块相比 yy 的子树缺失的部分的大小。
    • 对于上面的 bbcolsiz)数组的更新,在每一个儿子 yy 算出连通块的大小 ss(代码中的 nsiz)时,都将 baxb_{a_x} 加上 ss。最后在枚举完儿子后再加 11(节点 xx)。
    • 在打差分标记时,如何找出 yy 子树内如图中的那些 cc 节点打上减的标记。对于每种颜色 cc 开一个 vector(代码中的 v[c]),记录下所有(不管是否在 yy 的子树)根节点颜色为 cc 且极大的子树的根节点。在需要打标记时,由于刚刚递归进 yy 的子树,所以 yy 的子树内那些需要打标记的节点肯定在 vaxv_{a_x} 中的最前面。从 vaxv_{a_x} 的后面往前面循环,使用 dfs 序判断是否在 yy 的子树内,每找到一个符合要求的就打上标记并由于它不再极大了,执行 pop_back。在循环完所有子节点后,将 xx 压进 vaxv_{a_x} 中即可。
    • 在第一遍 dfs 执行完后,对于每种颜色还剩下根节点那儿的一个连通块。利用当前记录下的 colsiz[c]v[c] 等信息在主函数内打标记即可,具体见代码。

    完整代码:

    #include<bits/stdc++.h>
    using namespace std;
    struct edge
    {
    	int to,nxt;
    }e[200005];
    int h[100005],a[100005],dfn[100005],siz[100005],colsiz[100005],cnt;
    long long cf[100005],dep[100005];
    bool buc[100005];
    vector<int>v[100005];
    inline int read()
    {
    	char c=getchar();
    	int x=0;
    	while(c<'0'||c>'9')
    		c=getchar();
    	while(c>='0'&&c<='9')
    	{
    		x=(x<<3)+(x<<1)+c-'0';
    		c=getchar();
    	}
    	return x;
    }
    void write(long long x)
    {
    	if(x>9)
    		write(x/10);
    	putchar(x%10+'0');
    }
    inline void adde(int x,int y)
    {
    	e[++cnt].to=y;
    	e[cnt].nxt=h[x];
    	h[x]=cnt;
    }
    void dfs(int x,int f)
    {
    	siz[x]=1;
    	dfn[x]=++cnt;
    	for(int i=h[x];i;i=e[i].nxt)
    		if(e[i].to!=f)
    		{
    			int psiz=colsiz[a[x]];//记录下递归前的个数
    			dfs(e[i].to,x);
    			siz[x]+=siz[e[i].to];
    			int nsiz=siz[e[i].to]+psiz-colsiz[a[x]];//此处意为siz[e[i].to]-(colsiz[a[x]]-psiz)
    			colsiz[a[x]]+=nsiz;
    			cf[e[i].to]+=nsiz;//打上加的标记
    			while(v[a[x]].size()&&dfn[v[a[x]].back()]>dfn[x])//从后往前找
    			{
    				cf[v[a[x]].back()]-=nsiz;
    				v[a[x]].pop_back();
    			}
    		}
    	colsiz[a[x]]++;
    	v[a[x]].push_back(x);
    }
    void dfs2(int x,int f)//根据差分数组计算最终答案
    {
    	dep[x]=dep[f]+cf[x];
    	for(int i=h[x];i;i=e[i].nxt)
    		if(e[i].to!=f)
    			dfs2(e[i].to,x);
    }
    int main()
    {
    	int n=read(),m=0,tot=0,i,x,y;
    	for(i=1;i<=n;i++)
    	{
    		a[i]=read();
    		m=max(m,a[i]);
    		buc[a[i]]=true;//记录一个颜色是否出现在a[i]中
    	}
    	for(i=1;i<n;i++)
    	{
    		x=read();
    		y=read();
    		adde(x,y);
    		adde(y,x);
    	}
    	cnt=0;
    	dfs(1,0);
    	for(i=1;i<=m;i++)//处理包含1的那些连通块
    		if(buc[i])
    		{
    			tot++;
    			cf[1]+=n-colsiz[i];
    			for(int j=0;j<v[i].size();j++)
    				cf[v[i][j]]-=n-colsiz[i];
    		}
    	dfs2(1,0);
    	for(i=1;i<=n;i++)
    	{
    		write(1ll*n*tot-dep[i]);
    		putchar('\n');
    	}
    	return 0;
    }
    
    • 1

    信息

    ID
    2472
    时间
    1000ms
    内存
    125MiB
    难度
    6
    标签
    递交数
    0
    已通过
    0
    上传者