【BZOJ - 4754】独特的树叶(树哈希)

题干:

JYY有两棵树A和B:树A有N个点,编号为1到N;树B有N+1个点,编号为1到N+1。JYY知道树B恰好是由树A加上一个叶

节点,然后将节点的编号打乱后得到的。他想知道,这个多余的叶子到底是树B中的哪一个叶节点呢?

 

Input

输入一行包含一个正整数N。

接下来N-1行,描述树A,每行包含两个整数表示树A中的一条边;

接下来N行,描述树B,每行包含两个整数表示树B中的一条边。

1≤N≤10^5

 

Output

输出一行一个整数,表示树B中相比树A多余的那个叶子的编号。如果有多个符合要求的叶子,输出B中编号最小的

那一个的编号

 

Sample Input

5 1 2 2 3 1 4 1 5 1 2 2 3 3 4 4 5 3 6

Sample Output

1

Hint

解题报告: 

哈希规则:子树u的哈希值由它的每一个子树vi的哈希值得来,首先将所有f(v)排个序(防止顺序不同造成影响),然后

f(u)=(size(u) \times \sum_i f(vi)W_{i-1} )mod MOD

W是事先选取的一个位权,MOD是模数,size(u)是子树u的大小。

这样DFS一遍可求出以1号节点为根时,所有子树的哈希值f(u)。

但是这是无根树,我们想求出以任意节点为根时整棵树的哈希值。

设fa[u]以1为根时u的父亲,则上面的f(u)也是以fa[u]为根时子树u的哈希值。

再求一个g(u)表示以u为根时子树fa[u]的哈希值。这个g(u)怎么求呢?再DFS一遍,对于每个节点,g(u)由g(fa[u])以及u的每个兄弟vi的f(vi)得来。但是直接暴力枚举的话在菊花图上是O(n^2)的,那怎么办呢?

对于每个节点u维护一个数组,存储它所有儿子的哈希值f(v),如果有父亲,则g(u)也在里面,把这个数组排好序,求出每个前缀的哈希值和每个后缀的哈希值。这时,以u为根时整棵树的哈希值就是整个数组的哈希值(再乘上子树大小n)。

此时求每个儿子v的g(v),就是从那个数组中间去掉f(v)后的哈希值,二分查找后把前缀哈希值和后缀哈希值拼起来就可以得到。记得乘上v为根时uu的size即n−size(v)。

这样就求出以每个节点为根的哈希值了。

把A的所有哈希值存到一个set里,然后枚举B的每个度为1的点u,求出以u为根它的唯一子树v的哈希值,如果set里有这个值,u就是所求的点之一。部分内容参考自:链接

首先明确一个问题,以u为根节点的树的Hash值=树的大小 * 子树Hash值的带权和 % MOD,也就是说这个根节点是以子树大小的身份参与到其中的。也就是HASH[u]其实重头戏在于u的孩子节点,而u在其中的作用只是size那一部分权重而已,其他的都与他无关。这是在做树的问题的时候,和其他问题不一样的一点,也是比较有特色的一点。 

g[v]代表以v为根时,子树u的Hash值。(也就是以v为根节点的 部分哈希和)那么g[v]由g[u]和v的一系列兄弟节点f[vi]带权得来。

AC代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<string>
#include<cmath>
#include<cstring>
#define F first
#define S second
#define ll long long
#define pb push_back
#define pm make_pair
using namespace std;
typedef pair<int,int> PII;
const int MAX = 2e5 + 5;
const ll MOD = 2000020331;
//const ll MOD = 2000004199;  用这个模数都没有问题!
const ll seed = 31;//
ll PW[MAX];
struct Edge {int v,ne;
} e[MAX];
int head[MAX],tot;
int n;
int deg[MAX];
bool isB;
void add(int u,int v) {if(isB) deg[u]++;e[++tot].v = v;e[tot].ne = head[u]; head[u] = tot;
}
int size[MAX],fa[MAX];
vector<ll> son[MAX],sl[MAX],sr[MAX];
ll f[MAX],g[MAX];
set<ll> vis;
ll dfs1(int cur,int rt) {ll res = 0;fa[cur] = rt; son[cur].clear();//必须要清空!! size[cur] = 1;for(int i = head[cur]; ~i; i = e[i].ne) {int v = e[i].v; if(v == rt) continue;ll tmp = dfs1(v,cur);son[cur].push_back(tmp);size[cur] += size[v]; }if(son[cur].empty()) return f[cur] = 1;//注意叶子结点的HASH值需要是1,而不能是0,也就是son数组中必须都是正数的HASH值。sort(son[cur].begin(),son[cur].end());int up = son[cur].size();for(int i = 0; i<up; i++) res = (res * seed + son[cur][i]) % MOD;	return f[cur] = size[cur] * res % MOD;
}
int ans;
void dfs2(int u) {if(fa[u]) {son[u].pb(g[u]);sort(son[u].begin(),son[u].end());}int up = son[u].size();sl[u].resize(up);sl[u][0] = son[u][0]; 	for(int i = 1; i<up; i++) {//好像不太能把初始化合并进来 sl[u][i] = (sl[u][i-1] * seed + son[u][i])%MOD;}sr[u].resize(up);sr[u][up-1] = son[u][up-1];for(int i = up-2; i>=0; i--) sr[u][i] = (sr[u][i+1] + son[u][i] * PW[up-i-1])%MOD; //其实不是求后缀的HASH值,而是前缀HASH的后缀和,所以要这么写 for(int i = head[u]; ~i; i = e[i].ne) {int v = e[i].v;if(v == fa[u]) continue;if(up == 1) {g[v] = 1; dfs2(v); break;}int p = lower_bound(son[u].begin(),son[u].end(),f[v]) - son[u].begin();g[v] = 0;if(p+1 < up) g[v] = sr[u][p+1];if(p-1 >= 0) g[v] = (g[v]+sl[u][p-1]*PW[up-1-p])%MOD;g[v] = g[v] * (n-size[v]) % MOD;if(isB && deg[v] == 1 && vis.find(g[v]) != vis.end()) ans = min(ans, v);dfs2(v);}if(!isB) vis.insert(sl[u][up - 1] * n % MOD);
}
int main()
{PW[0] = 1;for(int i = 1; i<MAX; i++) PW[i] = PW[i-1]*seed % MOD;cin>>n;memset(head,-1,sizeof head);for(int u,v,i = 1; i<n; i++) {cin>>u>>v;add(u,v);add(v,u);}dfs1(1,0);  dfs2(1);tot=0,isB=1,n++;memset(head,-1,sizeof head);for(int u,v,i = 1; i<n; i++) {cin>>u>>v;add(u,v);add(v,u);} dfs1(1,0);ans=1e9;if(deg[1] == 1 && vis.find(f[e[head[1]].v]) != vis.end()) ans = 1;dfs2(1);printf("%d\n",ans); return 0 ;
}

另一种及其简单的Hash方式:

我们只需要H_u = seed \times( \prod H_{v_i} + size[u])就可以了。

其中H_{v_i}是每一个子树的hash值
这个函数和上一个函数一样,支持换根,那么就不限于找重心了
那第一颗树所有的hash值丢进set中,第二颗树删除一个节点的hash值也可以用类似的方法弄出来。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<string>
#include<cmath>
#include<cstring>
#define F first
#define S second
#define ll long long
#define pb push_back
#define pm make_pair
using namespace std;
typedef pair<int,int> PII;
const int MAX = 2e5 + 5;
//const ll MOD = 2000020331;//这个模数在这个方法中可以直接扔掉了,因为根本不是质数所以肯定要不了,因为逆元的求法就不对。
const ll MOD = 2000004199;  //用这个模数都没有问题!
const ll seed = 13331;//可换! 233,2333,23333对于第二个模数都可以过
ll INV;
struct Edge {int v,ne;
} e[MAX];
int head[MAX],tot;
int n;
void adde(int u,int v) {e[++tot].v = v;e[tot].ne = head[u]; head[u] = tot;
}
inline ll qpow(ll a,ll b) {ll res = 1;while(b) {if(b&1) res = res * a % MOD;a = a *a % MOD;b >>= 1;		}return res;
}
inline ll add(ll x,ll y) {return (x+y)%MOD;}
inline ll mul(ll x,ll y) {return (x*y)%MOD;}
inline ll sub(ll x,ll y) {return (x-y+MOD)%MOD;}
ll H[MAX];
int size[MAX],du[MAX];
void dfs(int cur,int fa) {H[cur] = 1;size[cur] = 1;for(int i = head[cur]; ~i; i = e[i].ne) {int v = e[i].v;if(v == fa) continue;dfs(v,cur);size[cur] = add(size[cur],size[v]);H[cur] = mul(H[cur],H[v]);}H[cur] = add(H[cur],size[cur]);H[cur] = mul(H[cur],seed);
}
set<ll> ss;
bool isB;
void dfs1(int cur,int fa) {if(isB==0) ss.insert(H[cur]);//必须要先插入,因为根的情况。 ll Hall = mul(H[cur],INV),Hres;Hall = sub(Hall,n);for(int i = head[cur]; ~i; i = e[i].ne) {int v = e[i].v;if(v == fa) continue;Hres=mul(Hall,qpow(H[v],MOD-2));Hres=add(Hres,n-size[v]); Hres=mul(Hres,seed);//至此Hres代表除v的树的Hash值H[v]=mul(H[v],INV); H[v]=sub(H[v],size[v]);H[v]=mul(H[v],Hres);H[v]=add(H[v],n);H[v]=mul(H[v],seed);dfs1(v,cur);}
}
int main()
{INV = qpow(seed,MOD-2);cin>>n;tot=0;memset(head,-1,sizeof head);for(int u,v,i = 1; i<n; i++) scanf("%d%d",&u,&v),adde(u,v),adde(v,u);dfs(1,0);dfs1(1,0);tot=0;memset(head,-1,sizeof head);n++;for(int u,v,i = 1; i<n; i++) {scanf("%d%d",&u,&v);du[u]++,du[v]++;adde(u,v);adde(v,u);}isB=1;dfs(1,0);dfs1(1,0);for(int i = 1; i<=n; i++) {if(du[i] == 1) {ll Hres = mul(H[i],INV);Hres = sub(Hres,n);if(ss.find(Hres) != ss.end()) {printf("%d\n",i);return 0 ;}}}return 0 ;
}

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/439731.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一步步编写操作系统 36 一级页表与虚拟地址1

为了给大家说清楚分页机制&#xff0c;我们先在宏观上说下cpu地址变换过程&#xff0c;先让大家有个直观的印象&#xff0c;如果有不明白的地方也不要着急&#xff0c;适时地不求甚解&#xff0c;有助于从全局上将知识融会贯通&#xff08;这句话是我即兴说的&#xff0c;说得多…

动手学无人驾驶(4):基于激光雷达点云数据3D目标检测

上一篇文章《动手学无人驾驶&#xff08;3&#xff09;&#xff1a;基于激光雷达3D多目标追踪》介绍了3D多目标追踪&#xff0c;多目标追踪里使用的传感器数据为激光雷达Lidar检测到的数据&#xff0c;本文就介绍如何基于激光雷达点云数据进行3D目标检测。 论文地址&#xff1a…

一步步编写操作系统 37 一级页表与虚拟地址2

接上节&#xff0c;分页机制是建立在分段机制之上&#xff0c;与其脱离不了干系&#xff0c;即使在分页机制下的进程也要先经过逻辑上的分段才行&#xff0c;每加载一个进程&#xff0c;操作系统按照进程中各段的起始范围&#xff0c;在进程自己的4GB虚拟地址空间中寻找可有空间…

PointNet:3D点集分类与分割深度学习模型

之前的一篇博客《动手学无人驾驶&#xff08;4&#xff09;&#xff1a;基于激光雷达点云数据3D目标检测》里介绍到了如何基于PointRCNN模型来进行3D目标检测&#xff0c;作者使用的主干网是PointNet&#xff0c;而PointNet又是基于PointNet来实现的。今天写的这篇博客就是对Po…

计算机视觉那些事儿(1):基本任务

本文主要介绍深度学习在计算机视觉领域&#xff08;Computer vision&#xff09;基本任务中的应用&#xff0c;包括分类、检测、分割&#xff08;语义与实体)。 目录 引言 分类&#xff08;Classification&#xff09; 目标检测&#xff08;Object Detection&#xff09; T…

一步步编写操作系统 38 一级页表与虚拟地址3

接上&#xff0c;页是地址空间的计量单位&#xff0c;并不是专属物理地址或线性地址&#xff0c;只要是4KB的地址空间都可以称为一页&#xff0c;所以线性地址的一页也要对应物理地址的一页。一页大小为4KB&#xff0c;这样一来&#xff0c;4GB地址空间被划分成4GB/4KB1M个页&a…

《Python编程:从入门到实践》速查表

本文是Python畅销书《Python&#xff1a;从入门到实践》速查表。 随书配套视频观看地址&#xff1a;https://www.bilibili.com/video/av35698354 目录 1.Overview 2.Lists 3.Dictionaries 4.If and While Loops 5.Functions 6.Classes 7.Files and Exceptions 8.Testin…

一步步编写操作系统 39 二级页表1

前面讲述了页表的原理&#xff0c;并以一级页表做为原型讲述了地址转换过程。既然有了一级页表&#xff0c;为什么还要搞个二级页表呢&#xff1f;理由如下&#xff1a; 一级页表中最多可容纳1M&#xff08;1048576&#xff09;个页表项&#xff0c;每个页表项是4字节&#xf…

PointNet++详解与代码

在之前的一篇文章《PointNet&#xff1a;3D点集分类与分割深度学习模型》中分析了PointNet网络是如何进行3D点云数据分类与分割的。但是PointNet存在的一个缺点是无法获得局部特征&#xff0c;这使得它很难对复杂场景进行分析。在PointNet中&#xff0c;作者通过两个主要的方法…

一步步编写操作系统 40 内存分页下用户程序与操作系统的关系

分页的第一步要准备好一个页表&#xff0c;我们的页表是什么样子呢&#xff1f;现在我们要设计一个页表啦。 设计页表其实就是设计内存布局&#xff0c;不过在规划内存布局之前&#xff0c;我们需要了解用户进程与操作系统之间的关系。 前面讲保护模式时&#xff0c;我们知道…

一步步编写操作系统 41 快表tlb 简介

分页机制虽然很灵活&#xff0c;但您也看到了&#xff0c;为了实现虚拟地址到物理地址的映射&#xff0c;过程还是有些麻烦的。先要从CR3寄存器中获取页目录表物理地址&#xff0c;然后用虚拟地址的高10位乘以4的积做为在页目录表中的偏移量去寻址目录项pde&#xff0c;从pde中…

50个最有用的Matplotlib数据分析与可视化图

本文介绍了数据分析与可视化中最有用的50个数据分析图&#xff0c;共分为7大类&#xff1a;Correlation、Deviation、RankIng、Distribution、Composition、Change、Groups 原文链接&#xff1a;https://www.machinelearningplus.com/plots/top-50-matplotlib-visualizations-t…

视觉SLAM十四讲(1):预备知识

最近在学习高翔博士的《视觉SLAM十四讲》&#xff08;第二版&#xff09;&#xff0c;算是初学本书&#xff0c;配套资源还算蛮丰富的&#xff0c;有代码&#xff08;第一版和第二版都有&#xff09;&#xff0c;B站上也有高翔博士对第一版录制的讲解视频&#xff0c;真的是很贴…

一步步编写操作系统 43 汇编语言和c语言的理解

也许有的同学喜欢用汇编语言来实现操作系统&#xff0c;觉得用汇编来写程序似乎更简单直接&#xff0c;可控性比较强&#xff0c;有种“一切尽在掌握”的赶脚。而用c语言实现操作系统这件事&#xff0c;虽然轻松很多&#xff0c;但似乎隐约感觉到有些慌张。因为虽然c语言相对来…

视觉SLAM十四讲(2):初识SLAM

这一讲主要介绍视觉SLAM的结构&#xff0c;并完成第一个SLAM程序&#xff1a;HelloSLAM。 目录 2.1 小萝卜的例子 单目相机 双目相机 深度相机 2.2 经典视觉SLAM框架 2.3 SLAM问题的数学表述 2.4 编程实践 Hello SLAM 使用cmake 使用库 【高翔】视觉SLAM十四讲2.1 小…

一步步编写操作系统 44 用c语言编写内核1

先来个简单的&#xff0c;欢迎我们神秘嘉宾——main.c。这是我们第一个c语言代码。 1 int main(void) { 2 while(1); 3 return 0; 4 }它没法再简单啦&#xff0c;简单的程序似乎能帮助咱们更容易的理解所学的知识&#xff0c;哈哈&#xff0c;我说的是似乎&#xff0c;其实&…

从零实现一个3D目标检测算法(1):3D目标检测概述

本文是根据github上的开源项目&#xff1a;https://github.com/open-mmlab/OpenPCDet整理而来&#xff0c;在此表示感谢&#xff0c;强烈推荐大家去关注。使用的预训练模型也为此项目中提供的模型&#xff0c;不过此项目已更新为v0.2版&#xff0c;与本文中代码略有不同。 本文…

一步步编写操作系统 45 用c语言编写内核2

在linux下用于链接的程序是ld&#xff0c;链接有一个好处&#xff0c;可以指定最终生成的可执行文件的起始虚拟地址。它是用-Ttext参数来指定的&#xff0c;所以咱们可以执行以下命令完成链接&#xff1a; ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin …

使用OpenCV库快速求解相机内参

本文主要介绍如何使用OpenCV库函数求解相机内参。具体可查阅官网&#xff1a;https://docs.opencv.org/master/dc/dbb/tutorial_py_calibration.html。 关于相机内参的求解还有很多其它的工具&#xff0c;如使用MATLAB求解会更方便&#xff0c;直接调用MATLAB中的APP即可。 1.背…

一步步编写操作系统 46 用c语言编写内核3

再把上节代码贴出来&#xff0c; 1 //int main(void) { 2 int _start(void) { 3 while(1); 4 return 0; 5 }有没有同学想过&#xff0c;这里写一个_start函数&#xff0c;让其调用main函数如何&#xff1f;其实这是可以的&#xff0c;main函数并不是第一个函数&#xff0c;它实…