需要变换思路和使用技巧的点分治题——树上游戏

Sweetlemon

2019-04-12 10:46:49

Solution

[P2664 树上游戏](https://www.luogu.org/problemnew/show/P2664) 一道黑题,难度名副其实。 下面简述我的做法。 我的做法可能相当劣,时间复杂度是$\text{O}(n \log n)$,比不过$\text{O}(n)$的方法;而且代码比较长,达到了$210$行(带注释$242$行);使用了很多次$\mathrm{dfs}$,常数也不够优。但是也许我的思路会对您有所启发。 看到树上“路径”,且树形$\text{dp}$不好处理,于是我们考虑使用点分治。 “颜色数”一直是难以处理的统计量,这个统计量阻止了我们直接对“节点到根的路径”进行合并,而必须考虑其他的方法。 假设我们已经分治到原树的某一部分,并且我们当前只考虑经过(分治的)根(即当前重心,不妨记为$\mathrm{root}$)的路径。那么我们需要思考两个问题: 1. 如何计算当前分治区域内根的答案? 2. 如何计算当前分治区域内其他点的答案? 为了方便处理,我们把根的颜色单独拿出来讨论。 对于根本身,根的颜色的贡献是$\mathrm{size}(\mathrm{root})$;对于根的任何一个子孙顶点,根的颜色的贡献是$\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch})$,其中$\mathrm{branch}$是指该节点所在的根的子树。 接下来,如果遇到与根同色的节点,我们就当作无色处理。 第一个问题比较容易解决。 假设我们从根走到**某一**子孙节点,在走的过程中,如果出现了某种**新**颜色,那么这条路径答案就应该$+1$。也即我们统计的思路是,只在颜色第一次出现时计算。 如果在$\mathrm{root}\rightarrow x$的路径上,$\mathrm{color}(x)$只出现一次(即$x$的祖先都与$x$不同色),那么祖先到以$x$为根的子树内的所有点的路径上都会出现$\mathrm{color}(x)$,且第一次出现的位置是$x$。从而,$x$对根的答案的贡献为$x$所在子树内顶点的个数,即$\mathrm{size}(x)$。否则如果$x$与某一祖先同色,那么$x$的颜色就不是第一次出现,对根的答案没有贡献。 不妨设节点$x$的上述贡献为$f(x)$,那么$f(x)$为$\mathrm{size}(x)$($\mathrm{color}(x)$第一次出现)或$0$($x$为根或$\mathrm{color}(x)$不是第一次出现)。我们把它们累加,得到整棵树的贡献和$\sum{f}$。 那么第一个问题就解决了。该区域内根的答案$\mathrm{ans}(\mathrm{root})=\mathrm{size}(\mathrm{root})+\sum{f}$,即根节点处的答案就等于自身颜色的贡献与子孙节点颜色贡献之和。 第二个问题比较困难。 点分治的合并思想启示我们,借助刚才计算的$f$解决第二个问题。我们把路径$x\rightarrow y$拆成$x\rightarrow \mathrm{root}$和$\mathrm{root} \rightarrow y$两段。 对于某一定点$x$,如果只考虑所有的$x\rightarrow \mathrm{root}$,那么这些路径一共有$\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch})$条(每个$y$对应一条),每条路径完全相同,出现的不同颜色的个数都相等;于是,路径上出现的每一种颜色$t$对$x$的答案的贡献均为$\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch})$。 如果只考虑所有的$\mathrm{root} \rightarrow y$,这些路径中每条路径出现颜色数之和即为$\sum_{y\notin \mathrm{branch}}{f(y)}=\sum{f}-\sum_{i \in \mathrm{branch}}{f(i)}$,即$\sum{f}$减去所有$x$所在分支的点的$f$值。 但是,$x\rightarrow \mathrm{root}$和$\mathrm{root} \rightarrow y$中可能会有重复的颜色。具体地,如果某种颜色$t$在$x\rightarrow \mathrm{root}$中出现了,那么对于所有的在分支外部的、$\mathrm{color}(u)=t$的顶点$u$,它们的$f$值都不能再计入到答案里,因为这一种颜色已经在$x\rightarrow \mathrm{root}$上统计过了。 如果我们记$A$为在$x\rightarrow \mathrm{root}$上出现过的颜色的集合,那么答案就是 $$\sum{f}-\sum_{i \in \mathrm{branch}}{f(i)}-\sum_{u\notin \mathrm{branch}\text{且}\mathrm{color}(u)\in A}f(u)+\sum_{t\in A}(\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch}))$$ 上式已经包括了根的颜色对$x$的答案的贡献。 观察上式,我们发现要记录的量有: 1. $\sum{f}$。 2. $\mathrm{branch}$中所有点的$f$和,即$\sum_{i \in \mathrm{branch}}{f(i)}$。 3. 不在$\mathrm{branch}$中,但其颜色在$x\rightarrow \mathrm{root}$上出现过的点的$f$和,即$\sum_{u\notin \mathrm{branch}\text{且}\mathrm{color}(u)\in A}f(u)$。 4. $x\rightarrow \mathrm{root}$上所有颜色的贡献和,即$\sum_{t\in A}(\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch}))$。 对此,找到重心后,我们再进行三次$\mathrm{dfs}$。 第一次$\mathrm{dfs}$的任务是,计算出$\mathrm{size},\sum{f},\sum_{i \in \mathrm{branch}}{f(i)}$;并对每一种颜色$t$,计算所有颜色为$t$的节点的$f$和,即$\sum_{\mathrm{color}(u)=t}f(u)$。 第一次$\mathrm{dfs}$后,我们就可以得到$\mathrm{ans}(\mathrm{root})$了。 接下来开始枚举$\mathrm{root}$的子树,即枚举$\mathrm{branch}$。 对于每一个$\mathrm{branch}$,先进行第二次$\mathrm{dfs}$,任务是对每一种$\mathrm{branch}$中出现的颜色$t$,计算分支内所有颜色为$t$的节点$v$的$f$和,即$\sum_{v\in \mathrm{branch}\text{且}\mathrm{color}(v)=t}f(v)$。这样,我们就可以得到上面的第三个量: $$\sum_{u\notin \mathrm{branch}\text{且}\mathrm{color}(u)=t}f(u)=\sum_{\mathrm{color}(u)=t}f(u)-\sum_{v\in \mathrm{branch}\text{且}\mathrm{color}(v)=t}f(v)$$ 紧接着,进行第三次$\mathrm{dfs}$,任务便是计算答案。 先处理$\mathrm{ans}$式子的第二项,即我们把分支内的$f$从$\sum{f}$里暂时减掉,即把$\sum{f}$扣掉$\sum_{i \in \mathrm{branch}}{f(i)}$,待$\mathrm{dfs}$结束后再加回来。 第三、第四项合并处理。在$\mathrm{dfs}$的过程中,如果到达了一个与祖先都不同色的顶点$v$,就说明$t=\mathrm{color}(v)$是新出现的,那么$v$子树中的所有节点的集合$A$都会新增一个元素$t$。这样,第三项就增加了$\sum_{u\notin \mathrm{branch}\text{且}\mathrm{color}(u)=t}f(u)$,也即答案还需要扣除$\sum_{\mathrm{color}(u)=t}f(u)-\sum_{v\in \mathrm{branch}\text{且}\mathrm{color}(v)\in A}f(v)$;而第四项就增加了一个$\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch})$,也即答案还需要增加$\mathrm{size}(\mathrm{root})-\mathrm{size}(\mathrm{branch})$。那么我们把上述变化处理到对存储$\sum{f}$的变量处,函数即将返回时再恢复回来。 而如果$x$的颜色已经出现过了,那么我们不做上述处理。 经过这样的处理,$\mathrm{ans}$式子的每一项都在存储$\sum{f}$的变量处计算了,那么该变量的值就等于$\mathrm{ans}(x)$。 经过这样的遍历,我们就把所有答案都计算完毕了。 如上所述,我们按照点分治的基本思想,在每一层分治都作上述计算,把每一层的答案累加即可。 但在代码实现上还有一个较大的问题。由于点分治需要多次计算,因此数组必须及时清零,否则答案会不正确。 存储每个分支的$f$和的数组只需在第三次$\mathrm{dfs}$结束后顺手将该分支的位置设为$0$即可。对于存储每种颜色的$f$和的数组,可以用一个栈记录该层分治所出现的颜色,在往下递归分治前把栈中所有颜色的值清零即可。 而记录某特定分支中每一种颜色的$f$值和的数组就比较尴尬,因为它必须在计算每一个分支完毕后立即清零。我采用的方法是在第三次$\mathrm{dfs}$后再进行第四次$\mathrm{dfs}$,任务是清零该数组。 这样,这道题就完全解决了。当然,代码实现上还有许多细节,需要多多注意。例如,答案可能很大,需要开`long long`(否则只有$80$分)。 我的实现总用时[$1421\mathrm{ms}$(无`O2`)](https://www.luogu.org/record/show?rid=18133128) / [$919\mathrm{ms}$(`O2`)](https://www.luogu.org/record/show?rid=18133134)。 [带注释的代码](https://www.luogu.org/recordnew/show/18135537) ```cpp #include <cstdio> #include <cctype> #define MAXN 100005 #define MAXM 200005 #define MAXIOLG 25 using namespace std; typedef long long ll; //答案可能很大,记得开long long //下面为目隐团部分成员/读入输出优化 typedef ll io_t; //如月伸太郎, shintaro, No.7, 目缠, 记忆数字 io_t shin[MAXIOLG]; //濑户幸助, seto, No.2, 目盗, 读入优化 io_t seto(void); //楯山文乃, ayano, No.0, 目挂, 输出优化 void ayano(io_t x,char spliter='\n'); int fst[MAXN],nxt[MAXM],edges=0; //存图邻接表 int g[MAXM]; //图 int sz[MAXN],mnsz,root,tree_sz; //size数组, mnsz用于找重心, root为当前分治根, tree_sz为当前分治区域点数 int visited[MAXN]; //visited记录点是否已经被删除 int color[MAXN]; //每个点的颜色 int tree_color[MAXN],tree_color_n; //栈结构, 存储当前分治区域出现的颜色, 便于清空数组 //依次为每种颜色的f和、该种颜色在之前是否出现过、该分支每种颜色的f和、每一分支的f和、存储sum(f)的变量 ll color_f[MAXN],has_col[MAXN],branch_color_f[MAXN],branch_f[MAXN],tf; //答案数组 ll ans[MAXN]; void addedge(int u,int v); //加边 void solve(int x); //分治函数 void find_g(int x,int pa); //找重心 void dfs1(int x,int pa,int branch); //第一次dfs void dfs2(int x,int pa,int branch); //第二次dfs void dfs3(int x,int pa,int branch); //第三次dfs void dfs4(int x,int pa,int branch); //第四次dfs int main(void){ int n; //读入 n=seto(); for (int i=1;i<=n;i++) color[i]=seto(); for (int i=1;i<n;i++){ int u,v; u=seto(),v=seto(); addedge(u,v); addedge(v,u); } //点分治 sz[1]=n; solve(1); //输出 for (int i=1;i<=n;i++) ayano(ans[i]); return 0; } void solve(int x){ //分治函数 mnsz=tree_sz=sz[x]; find_g(x,0); //找重心 //清空栈 tree_color_n=0; const int root_col=color[root]; tree_color[tree_color_n++]=root_col; has_col[root_col]=1; tf=0; dfs1(root,0,0); //第一次dfs计算sz, tf, color_f, branch_f ans[root]+=(tf+tree_sz); //得到根的答案 for (int ei=fst[root];ei;ei=nxt[ei]){ //枚举分支 int v=g[ei]; if (visited[v]) continue; tf-=branch_f[v]; //扣除该分支f值(答案式第二项) dfs2(v,root,v); //第二次dfs计算branch_color_f dfs3(v,root,v); //第三次dfs计算ans dfs4(v,root,v); //第四次dfs清零branch_color_f tf+=branch_f[v]; //消除第二项的影响, 恢复tf变量 branch_f[v]=0; //清零branch_f } has_col[root_col]=0; //清零has_col //清零color_f while (tree_color_n){ tree_color_n--; color_f[tree_color[tree_color_n]]=0; } //递归分治 visited[root]=1; for (int ei=fst[root];ei;ei=nxt[ei]){ int v=g[ei]; if (visited[v]) continue; solve(v); } } void dfs1(int x,int pa,int branch){ //第一次dfs计算sz, tf, color_f, branch_f if (pa==root) branch=x; //处理branch变量 sz[x]=1; //sz初始化 int is_new_col=0,tcol=color[x]; if (!has_col[tcol]) //新出现的颜色, 先记录 is_new_col=has_col[tcol]=1; for (int ei=fst[x];ei;ei=nxt[ei]){ int v=g[ei]; if (visited[v] || v==pa) continue; dfs1(v,x,branch); sz[x]+=sz[v]; } if (is_new_col){ //新出现的颜色, 更新变量 if (!color_f[tcol]) //颜色在整个分治区域第一次出现, 入栈 tree_color[tree_color_n++]=tcol; color_f[tcol]+=sz[x]; //将贡献累加到color_f中 branch_f[branch]+=sz[x]; //将贡献累加到branch_f中 tf+=sz[x]; //将贡献累加到tf中 has_col[tcol]=0; //清掉has_col } } void dfs2(int x,int pa,int branch){ //第二次dfs计算branch_color_f int tcol=color[x],is_new_color=0; if (!has_col[tcol]) //新出现的颜色, 先记录 has_col[tcol]=is_new_color=1; for (int ei=fst[x];ei;ei=nxt[ei]){ int v=g[ei]; if (visited[v] || v==pa) continue; dfs2(v,x,branch); } if (is_new_color) //新出现的颜色, 清掉has_col并将贡献累加到branch_color_f中 has_col[tcol]=0,branch_color_f[tcol]+=sz[x]; } void dfs3(int x,int pa,int branch){ //第三次dfs计算ans ans[x]+=(tree_sz-sz[branch]); //根处的答案在前面未加入, 现在额外算 int tcol=color[x],is_new_color=0; if (!has_col[tcol]){ has_col[tcol]=is_new_color=1; //新出现的颜色,记录 tf-=(color_f[tcol]-branch_color_f[tcol]); //答案式子第三项 tf+=(tree_sz-sz[branch]); //答案式子第四项 } ans[x]+=tf; //累加答案 for (int ei=fst[x];ei;ei=nxt[ei]){ int v=g[ei]; if (visited[v] || v==pa) continue; dfs3(v,x,branch); } if (is_new_color){ //新出现的颜色,清掉has_col并恢复tf变量原来的值 has_col[tcol]=0; tf+=(color_f[tcol]-branch_color_f[tcol]); tf-=(tree_sz-sz[branch]); } } void dfs4(int x,int pa,int branch){ //第四次dfs清零branch_color_f branch_color_f[color[x]]=0; for (int ei=fst[x];ei;ei=nxt[ei]){ int v=g[ei]; if (visited[v] || v==pa) continue; dfs4(v,x,branch); } } void find_g(int x,int pa){ //找重心 sz[x]=1; //初始化size int mxsz=0; //x的子树或x以上部分的最大size for (int ei=fst[x];ei;ei=nxt[ei]){ int v=g[ei]; if (visited[v] || v==pa) continue; find_g(v,x); sz[x]+=sz[v]; if (sz[v]>mxsz) mxsz=sz[v]; } if (tree_sz-sz[x]>mxsz) mxsz=tree_sz-sz[x]; if (mxsz<mnsz) mnsz=mxsz,root=x; } void addedge(int u,int v){ //加边函数 edges++; g[edges]=v; nxt[edges]=fst[u],fst[u]=edges; } //下面为目隐团部分成员/读入输出优化 io_t seto(void){ //濑户幸助, seto, No.2, 目盗, 读入优化 io_t ans=0; int symbol=0; char ch=getchar(); while (!isdigit(ch)) (ch=='-')?(symbol=1):(0),ch=getchar(); while (isdigit(ch)) (ans=ans*10+(ch-'0')),ch=getchar(); return (symbol)?(-ans):(ans); } void ayano(io_t x,char spliter){ //楯山文乃, ayano, No.0, 目挂, 输出优化 if (!x){ putchar('0'),putchar(spliter); return; } if (x<0) putchar('-'),x=-x; int len=0; while (x){ io_t d=x/10; shin[len++]=x-d*10; //如月伸太郎, shintaro, No.7, 目缠, 记忆数字 x=d; } while (len){ len--; putchar(shin[len]+'0'); } putchar(spliter); } ```