暑期集训训练赛#1 A [哈希 or DFS序]

暑期集训训练赛#1 A [哈希 or DFS序]

题目描述

春天来了,万物复苏,动物们又到了发情的季节。蔡老板终于下定决心砍下了自家后院的两棵果树,并决定和自己喜欢的人一起分享果树上的果子。

这两棵果树一棵是长生果树另一棵是人参果树,两棵树上都有 n 个果子,编号为 1 n ,并分别由 n 1 段树枝连接起来。 为了把果子分成两份,蔡老板决定再两棵树上各砍一刀,分别砍断一根树枝把两棵树上的果子各分成两个部分。之后,对于每一棵果树,蔡老板会选择 1 号果子所在的那一部分。显然这样分果子一共有 ( n 1 ) 2 种分法,而蔡老板想知道,有多少种切割方法,使得蔡老师拿到的长生果和人参果具有相同的标号集合。

输入格式

第一行一个正整数 n 表示两棵树的大小。

接下来 n 1 行每行两个正整数表示长生果树上的边。

接下来 n 1 行每行两个正整数表示人参果树上的边。

输出格式

输出一行一个正整数表示蔡老板关心的切割方法的数目。

题解

非常讨厌搬运带有 L a T e X 公式的题目 ̄へ ̄

题目大意就是求有多少棵含有相同编号的子树。

1 暴力 43分

有一个很显然的暴力做法就是:枚举两棵树的每一条边,搜索判断边两边的树是否相同。代码还是很好写(虽然我写炸了),就不贴了。

2 正解

2.1 哈希 随机化算法

这道题卡哈希,xor哈希还得双蛤;还卡时间,map都不行,必须用unordered_map。

哈希的基本思路就是用一个数来表示一棵树的形态(这里的形态并不是形状相同,而是这棵树里包含了哪些数)。

树上的边都是无向边, [ u , v ] [ v , u ] 是等价的。因此选择的哈希必须要满足交换律。满足交换律的三种运算分别是:加法,乘法,异或。但是加法和乘法并不很可靠,比如对于加法,由 3和5构成的树 和由 2和6构成的树 会被认为是同一棵树;乘法同理。但是异或相对可靠,因为如果两棵子树相同,那么两棵子树对应的哈希值的每一个二进制位都要一一对应,因此错误的几率就会大大减小。

一定要双蛤!哈希要用unsigned long long!

#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<unordered_map>
#define N 200010
using namespace std;
int Last1[N<<1],Next1[N<<1],End1[N<<1],cnt1;
int Last2[N<<1],Next2[N<<1],End2[N<<1],cnt2;
unsigned long long Mat1[N],Mat2[N],Hash1[N],Hash2[N],Hash11[N],Hash22[N];
unordered_map<unsigned long long,bool>Mark,Markk;
void Ins1(int x,int y){
    End1[++cnt1]=y,Next1[cnt1]=Last1[x],Last1[x]=cnt1;
}
void Ins2(int x,int y){
    End2[++cnt2]=y,Next2[cnt2]=Last2[x],Last2[x]=cnt2;
}
void DFS1(int u,int fa){
    Hash1[u]=Mat1[u],Hash11[u]=Mat2[u];
    for(int i=Last1[u];i;i=Next1[i]){
        int v=End1[i];
        if(v==fa)continue;
        DFS1(v,u);
        Hash1[u]^=Hash1[v],Hash11[u]^=Hash11[v];
    }
}
void DFS2(int u,int fa){
    Hash2[u]=Mat1[u],Hash22[u]=Mat2[u];
    for(int i=Last2[u];i;i=Next2[i]){
        int v=End2[i];
        if(v==fa)continue;
        DFS2(v,u);
        Hash2[u]^=Hash2[v],Hash22[u]^=Hash22[v];
    }
}
int main(){
    srand(73939133);
    int n,Ans=0;scanf("%d",&n);
    for(int i=1;i<=n;i++){ 
        //使得随机数更加随机
        Mat1[i]=rand()*(rand()+i)+rand()%31;
        Mat2[i]=rand()*(rand()+i)+rand()%31; 
    } 
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        Ins1(u,v),Ins1(v,u);
    }
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        Ins2(u,v),Ins2(v,u);
    }
    DFS1(1,0);DFS2(1,0);
    for(int i=2;i<=n;i++){
        //从以1为根的树中去掉以i为根的子树
        //Mark[i],Markk[i]均表示存在i形态的子树
        Mark[Hash1[1]^Hash1[i]]=true;
        Markk[Hash11[1]^Hash11[i]]=true;
    }
    for(int i=2;i<=n;i++)
        if(Mark[Hash2[1]^Hash2[i]])
            if(Markk[Hash22[1]^Hash22[i]])Ans++;
    printf("%d",Ans);return 0;
}

2.2 DFS序 确定性算法

如果我们要判断两个序列 A B 中有多少相等的子串,可以先把 A 序列中的数字按照下标的顺序依次标号,再把 B 序列中的数字对应标号(这种方法的使用前提是 A 序列中的数字互不相同 B 序列中的数字可以有重复),看 B 序列中有多哪些部分的标号是连续的。

比如:

这里写图片描述

B 序列的标号中,标红部分的数字是连续的,那么就说明这一部分在 A 序列中也是出现过的。

以上是在序列中的做法,在树中同样如此。我们先把 A 树跑一遍 D F S ,让每个点都有一个 D F S 序(树上的每个节点编号显然是不同的),再把这个 D F S 序映射到 B 树上。如果 B 树中的某一棵子树有连续的编号,那么这棵子树在 A 中也出现过。

这里写图片描述

上图中,圈内的数字是B树中每个节点的编号,圈旁的数字是对应数字在A树中的DFS序。以2为根的子树中,新编号是连续的,为[2,5],所以这棵子树在A树中出现过;但是以3为根的子树的新编号不连续,所以就没有出现过。

这种做法显然更加优秀,代码也好写。用时更少,空间更小。

↑这是哈希做法

↑这是DFS序做法

#include<iostream>
#include<cstdio>
#define N 200100
using namespace std;
struct node{int l,r;}Ran[N];
int NewID[N],Size1[N],Size2[N],OldID[N],Time,Ans;
int End1[N<<1],Next1[N<<1],Last1[N<<1],cnt1;
int End2[N<<1],Next2[N<<1],Last2[N<<1],cnt2;
void DFS1(int u,int fa){
    NewID[u]=++Time,Size1[u]=1,OldID[Time]=u;
    for(int i=Last1[u];i;i=Next1[i]){
        int v=End1[i];
        if(v==fa)continue;
        DFS1(v,u);
        Size1[u]+=Size1[v];
    }
}
void DFS2(int u,int fa){
    Size2[u]=1;
    for(int i=Last2[u];i;i=Next2[i]){
        int v=End2[i];
        if(v==fa)continue;
        DFS2(v,u);
        Size2[u]+=Size2[v];
    }
}
void DFS3(int u,int fa){
    //我记录的是以u为根的子树中新编号的最小最大值
    //当 编号的范围==子树大小,那么这段编号就是连续的
    Ran[u].l=NewID[u],Ran[u].r=NewID[u];
    for(int i=Last2[u];i;i=Next2[i]){
        int v=End2[i];
        if(v==fa)continue;
        DFS3(v,u);
        Ran[u].l=min(Ran[u].l,Ran[v].l);
        Ran[u].r=max(Ran[u].r,Ran[v].r);
    }
    if(u!=1 && Ran[u].r-Ran[u].l+1==Size2[u])
        if(Size1[OldID[Ran[u].l]]==Size2[u])Ans++;
}
void Ins1(int x,int y){
    End1[++cnt1]=y,Next1[cnt1]=Last1[x],Last1[x]=cnt1;
}
void Ins2(int x,int y){
    End2[++cnt2]=y,Next2[cnt2]=Last2[x],Last2[x]=cnt2;
}
int main(){
//  freopen("ex_a3.in","r",stdin);
    int n;scanf("%d",&n);
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        Ins1(u,v),Ins1(v,u);
    }
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        Ins2(u,v),Ins2(v,u);
    }
    DFS1(1,0);DFS2(1,0);DFS3(1,0);
    printf("%d",Ans);return 0;
}

猜你喜欢

转载自blog.csdn.net/ArliaStark/article/details/81295377
今日推荐