半彩三重奏(可撤销并查集)

108 篇文章 0 订阅

半彩三重奏

题目背景

帆帆不满足于只构建一棵深蓝之树,他觉得深蓝之树只有深蓝色太单调了,于是想给这棵树染上颜色。

题目描述

由于现在已经来到了魔幻的龙年,帆帆的深蓝之树已经被染上了颜色,结点 i i i 的颜色为 a i a_i ai

帆帆是一个喜新厌旧的人,在接下来的 q q q 天中,他每天都会改变他喜欢的颜色,第 i i i 天他喜欢的两种颜色是 x i , y i x_i,y_i xi,yi x i ≠ y i x_i\neq y_i xi=yi)。

但是为了照顾自己的树,他需要经常在树上移动,并且只会经过自己喜欢的颜色。

具体来说,第 i i i 天,帆帆会选择一个有序结点对 ( u , v ) (u,v) (u,v),然后沿着 u → v u\to v uv 的唯一简单路径移动,并且中间经过的结点(包含 u , v u,v u,v)颜色必须 ∈ { x i , y i } \in \{x_i,y_i\} {xi,yi} u u u 可以等于 v v v),之后可以抽取一次宵宫。

每一天你都需要抽取一个满命宵宫,然后告诉帆帆有多少种可能的选择有序结点对的方案。

输入格式

第一行两个正整数 n , q n,q n,q

接下来一行 n n n 个正整数 a 1 , a 2 , ⋯   , a n a_1,a_2,\cdots,a_n a1,a2,,an 表示每个点的颜色。

接下来一行 n − 1 n-1 n1 个正整数 p 2 , p 3 , ⋯   , p n p_2,p_3,\cdots,p_n p2,p3,,pn,表示对所有 2 ≤ i ≤ n 2\le i\le n 2in,树上存在一条边 ( p i , i ) (p_i,i) (pi,i)

接下来 q q q 行,每行两个正整数 x i , y i x_i,y_i xi,yi

输出格式

对于每组询问输出一行一个正整数表示答案。

样例 #1

样例输入 #1

5 3
1 2 1 3 3
1 1 2 2
1 2
1 3
2 3

样例输出 #1

9
6
9

提示

【样例 1 1 1 解释】

树的形态如图所示:

对于第一组询问,合法的路径有且仅有 { 1 , 2 , 3 } \{1,2,3\} {1,2,3} 中一点到另一点的路径。

对于第二组询问,合法的路径有且仅有 1 → 1 , 1 → 3 , 3 → 1 , 3 → 3 , 4 → 4 , 5 → 5 1\to 1,1\to 3,3\to 1,3\to 3,4\to 4,5\to 5 11,13,31,33,44,55

【子任务约束】

本题采用子任务捆绑测试。

对于 100 % 100\% 100% 的数据,有 1 ≤ n ≤ 1 0 6 1\le n\le 10^6 1n106 1 ≤ q ≤ 2 × 1 0 6 1\le q\le 2\times 10^6 1q2×106 1 ≤ a i , x , y ≤ n 1\le a_i,x,y\le n 1ai,x,yn x ≠ y x\neq y x=y。保证 p i < i p_i<i pi<i

子任务编号 n n n q q q特殊性质分值依赖子任务
Subtask #1 ≤ 100 \le 100 100 ≤ 100 \le 100 100 7 7 7
Subtask #2 ≤ 2000 \le 2000 2000 ≤ 2000 \le 2000 2000 18 18 18 1 1 1
Subtask #3 ≤ 1 0 5 \le 10^5 105 ≤ 2 × 1 0 5 \le 2\times 10^5 2×105A 5 5 5
Subtask #4 ≤ 1 0 5 \le 10^5 105 ≤ 2 × 1 0 5 \le 2\times 10^5 2×105B 19 19 19
Subtask #5 ≤ 1 0 5 \le 10^5 105 ≤ 2 × 1 0 5 \le 2\times 10^5 2×105 21 21 21 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4
Subtask #6 ≤ 2 × 1 0 5 \le 2\times 10^5 2×105 ≤ 5 × 1 0 5 \le 5\times 10^5 5×105 10 10 10 5 5 5
Subtask #7 ≤ 1 0 6 \le 10^6 106 ≤ 2 × 1 0 6 \le 2\times 10^6 2×106 20 20 20 6 6 6

特殊性质 A: p i = 1 p_i=1 pi=1

特殊性质 B: p i = i − 1 p_i=i-1 pi=i1

思路

题目大意

  • 给定一个 n n n 个节点的树,每个点 i i i 上有一个颜色 a i a_i ai
  • 每次询问,给定两个颜色 x , y x,y x,y。你需要找到树上有多少个点对 ( u , v ) (u,v) (u,v),满足 ( u , v ) (u,v) (u,v) 的简单路径上(包括 u , v u,v u,v),所有点的颜色要么是 x x x,要么是 y y y
  • ( u , v ) (u,v) (u,v) ( v , u ) (v,u) (v,u) 算两种不同的, u = v u=v u=v 时也算两种不同的。

具体做法

  • 首先,这道题比较典型的思路就是:我们把相同颜色的点合并(这个可以用并查集处理)并缩点,然后再按照原图的连线方式将它们连起来。
  • 但你如果单纯按照第一点那么做,最慢的地方就在于“拿出树中颜色为 x x x 和颜色为 y y y 的节点,进行连边”了。(因为它们会构成森林)
  • 所以我们得优化,我们发现:对于树上一条连着两个相同颜色节点的边,它被重复拿出来了好几次的贡献是可以预处理的。具体地,我们先把树上同一颜色并且互相连通的点,缩成一个连通块。这个我们可以用普通并查集来实现。然后我们就可以得到,单独在一个颜色中,可以互达的点对个数。
  • 设颜色 i i i s i s_i si 个连通块 a i , 1 , a i , 2 , ⋯   , a i , s i a_{i,1},a_{i,2},\cdots,a_{i,s_i} ai,1,ai,2,,ai,si,每个联通块的大小为 s i z i , j siz_{i,j} sizi,j,那么这个颜色中可以互达的点对个数 i n a n s i = ∑ j = 1 s i s i z i , j inans_i=\sum_{j=1}^{s_i}siz_{i,j} inansi=j=1sisizi,j这个是算相同颜色的
  • 那么不同颜色之间的怎么算呢?
    在这里插入图片描述
    然后,我们处理完一个颜色对之后,我们要把刚才我们所做的连边全部都撤销了。这样,才能保证处理下一个颜色对时不会发生影响。这个可以用栈来写
    在这里插入图片描述
    参考

自己的理解和遇到的问题

  • 它那里的答案是 i n a n s x + i n a n s y + s t a n s m a p [ ( x , y ) ] inans_x+inans_y+stans_{map[(x,y)]} inansx+inansy+stansmap[(x,y)],我们要特别注意一下,就是它这里的 i n a n s x inans_x inansx 的计算是 ∑ i = 1 s s i z i 2 \sum_{i=1}^s siz_i^2 i=1ssizi2,注意是有平方的,因为你自己模拟一下,同颜色的种类数就是这么多。
  • 因为本道题是多组测试数据,最好不要用 m e m s e t memset memset 很容易超时的。
  • 它这边是分为普通并查集和可撤销并查集
    注意,普通并查集的 find这么写:
int find1(int x){
    if(x!=p1[x])p1[x]=find1(p1[x]);
    return p1[x];
}

可撤销的并查集必须得:

int find(int x){
    if(p[x]==x)return x;
	return find(p[x]);
}

这两者的区别就是可撤销并查集不存在路径压缩,所以必须得那么写。(要不然会 49 p t s 49 pts 49pts

  • 如果直接对一个 pair 用 map 进行映射会 TLE,然后 pair 又是不能直接用
    unordered_map 映射的。所以说,我们先把这个 pair (x,y) 给映射成一个整数
    x*10^6+y,再用 unordered_map 进行映射。(这个得特别注意一下

因为我们规定颜色对 ( x , y ) (x,y) (x,y) x < y x<y x<y,所以我们当 a x < a i a_x<a_i ax<ai (其中, a a a 是颜色对),此时的颜色映射为 t m p = a x × 100000 + a i tmp=a_x\times100000+a_i tmp=ax×100000+ai,反过来就反过来。

  • 因为我们要离线处理,因此我们要用 v p vp vp v e c t o r vector vector 去装颜色点对。 v e c t o r vector vector 的索引就是哈希表的值。具体见:
if(!S.count(tmp)){//如果找不到此颜色,则开一个
    S[tmp]=++colp;
}
tp=S[tmp];
ve[tp].push_back({x,i});
  • 我们是把普通并查集用于求出各个连通块内部的内容,此时是用来缩点用的。
  • 以下是可撤销并查集的模板
void undo(){
    //然后撤销时,直接拿出栈顶的信息,进行复原,再弹栈即可。
    p[stk1.top()]=stk1.top();
    siz[stk2.top().first]=stk2.top().second;
    stk1.pop();
    stk2.pop();
}

for(int i=1;i<=colp;i++){
        for(int j=0;j<ve[i].size();j++){
            int xx=find(ve[i][j].x),yy=find(ve[i][j].y);
            sta[i]+=siz[xx]*siz[yy]*2;//连边
            
            int a=find(ve[i][j].x),b=find(ve[i][j].y);
            if(a!=b){
                if(siz[a]>siz[b])swap(a,b);
                stk1.push(a);//先记录下较小集合的父亲
                stk2.push({b,siz[b]});//再记录下较大集合的父亲和大小
                siz[b]+=siz[a];
                p[a]=b;
                
            }
        }
        for(int j=0;j<ve[i].size();j++)undo();
    }
  • 注意答案如果没有颜色对相连(也就是中间有其他颜色),此时答案就不用加上 s t a n s stans stans 了。

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<unordered_map>
#include<vector>
#include<stack>//可撤销并查集

#define int long long
#define x first
#define y second

using namespace std;

typedef pair<int,int>PII;

const int N = 1e6+10;

int a[N],p[N],siz[N],siz1[N];
int p1[N];
int n,m;
int col,tmp;
unordered_map<int,int>S;//颜色对的映射,类似哈希表
struct E{
    int x,y;
}e[N];
vector<E>ve[N];//存储下是 i 颜色对的边有哪些
int insans[N],sta[N];//insans表示颜色中可以互达的点对个数
//sta表示跨颜色之间的贡献
stack<int> stk1;//stk1表示合并的根节点
stack<PII> stk2;//stk2表示已合并到其他根的根大小
int colp,tp;//colp表示图上有多少颜色对在图上出现

int find(int x){
    // if(x!=p[x])p[x]=find(p[x]);
    // return p[x];
    if(p[x]==x)return x;
	return find(p[x]);
}

int find1(int x){
    if(x!=p1[x])p1[x]=find1(p1[x]);
    return p1[x];
}

void undo(){
    //然后撤销时,直接拿出栈顶的信息,进行复原,再弹栈即可。
    p[stk1.top()]=stk1.top();
    siz[stk2.top().x]=stk2.top().y;
    stk1.pop();
    stk2.pop();
}

signed main(){
    scanf("%lld%lld",&n,&m);
    
    for(int i=1;i<=n;i++){
        scanf("%lld",&a[i]);
        p1[i]=i;
    }
    
    for(int i=2;i<=n;i++){
        int x;
        scanf("%lld",&x);
        if(a[x]==a[i]){
            p1[find1(x)]=find1(i);
        }else{
            if(a[x]<a[i]){
                /*如果直接对一个 pair 用 map 进行映射会 TLE,然后 pair 又是不能直接用
                unordered_map 映射的。所以说,我们先把这个 pair (x,y) 给映射成一个整数 
                x*10^6+y,再用 unordered_map 进行映射。
                */
                tmp=a[x]*1e6+a[i];
            }else{
                tmp=a[i]*1e6+a[x];
            }
            
            if(!S.count(tmp)){//如果找不到此颜色,则开一个
                S[tmp]=++colp;
            }
            tp=S[tmp];
            ve[tp].push_back({x,i});
        }
    }
    
    
    for(int i=1;i<=n;i++){
        p1[i]=find1(i);
        siz1[p1[i]]++;
    }
    
    
    for(int i=1;i<=n;i++){
        if(p1[i]==i){
            insans[a[i]]+=siz1[i]*siz1[i];
        }
    }
    
    for(int i=1;i<=n;i++)p[i]=p1[i];
    
    for(int i=1;i<=n;i++)siz[i]=siz1[p1[i]];
    //前面的过程为预处理各个颜色缩点连线处理
    
    for(int i=1;i<=colp;i++){
        for(int j=0;j<ve[i].size();j++){
            int xx=find(ve[i][j].x),yy=find(ve[i][j].y);
            sta[i]+=siz[xx]*siz[yy]*2;//连边
            
            int a=find(ve[i][j].x),b=find(ve[i][j].y);
            if(a!=b){
                if(siz[a]>siz[b])swap(a,b);
                stk1.push(a);//先记录下较小集合的父亲
                stk2.push({b,siz[b]});//再记录下较大集合的父亲和大小
                siz[b]+=siz[a];
                p[a]=b;
                
            }
        }
        for(int j=0;j<ve[i].size();j++)undo();
    }
    
    
    while(m--){
        int x,y;
        scanf("%lld%lld",&x,&y);
        int ans=insans[x]+insans[y];
        if(x>y)swap(x,y);
        tmp=x*1e6+y;
        if(!S.count(tmp)){
            printf("%lld\n",ans);
        }else{
            //没有边连接
            tp=S[tmp];
            ans+=sta[tp];
            printf("%lld\n",ans);
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

green qwq

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值