清北学堂培训2019.4.29
Day 2(冯哲)
今天的内容主要分为一下几部分
二叉搜索树
二叉搜索树(BST)是用有根二叉树来存储的一种数据结构,在二叉树中每个节点代表一个数据。
每个节点包含一个指向父亲的指针,和两个指向儿子的指针。如果没有则为空。
每个节点还包含一个key值,代表他本身这个点的权值。
特征:
- 二叉搜索树的key值是决定树形态的标准。
- 每个点的左子树中,节点的key值都小于这个点。
- 每个点的右子树中,节点的key值都大于这个点。
PS:在接下来的介绍中,我们将以ls[x]表示x的左儿子,rs[x]表示x的右儿子,fa[x]表示x的父亲,key[x]表示x这个点的权值。
BST是一种支持对权值进行查询的数据结构,它支持:
(1)插入一个数,删除一个数,询问最大/最小值,询问第k大值。
(2)当然,在所有操作结束后,它还能把剩下的数从小到大输出来。
查询最大值/最小值:
注意到BST左边的值都比右边小,所以如果一个点有左儿子,就往左儿子走,否则这个点就是最小值啦。
形象点就是使劲往左找(白眼)
最小值伪代码 ↓:
int FindMin() {int x = root;while (ls[x]){x = ls[x];}return key[x]; }
对BST删除时,都需要对树进行维护
(1)若直接删除该点,则在查询时很困难
(2)对这个结点的儿子进行考虑:
1.若无儿子,则直接删除
2.若x有一个儿子,直接把x的儿子接到x的父亲下面就行了
3.如果x有两个儿子,这种情况就比较麻烦了
1>定义x的后继y,是x右子树中所有点里,权值最小的点
2>找这个点可以x先走一次右儿子,再不停走左儿子
3>如果y是x的右儿子,那么直接把y的左儿子赋成原来x的左儿子,然后用y替代x的位置
"找点"伪代码:
int Find(int x) {int now = root;while(key[now]! = x)if (key[now] < x) now = rs[now]; else now = ls[now];return now; }
这里的总代码貌似很麻烦qwq【老师都不想写】
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #include<cstring> #include<string> #include<cmath> #include<ctime> #include<set> #include<vector> #include<map> #include<queue>#define N 300005 #define M 8000005#define mid ((l+r)>>1)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i,j,m,n,p,k,ls[N],rs[N],sum[N],size[N],a[N],root,tot,fa[N]; void ins(int x)//插入一个权值为x的数字 {sum[++tot]=x;//用tot来表示二叉树里的节点个数 size[tot]=1;if (!root) root=tot;//没有节点else{int now=root; //从根开始for (;;){++size[now]; if (sum[now]>sum[tot]) //判断和当前节点的大小 {if (!ls[now]) {ls[now]=tot; fa[tot]=now;break;}else now=ls[now];} else{if (!rs[now]){rs[now]=tot; fa[tot]=now;break;}else now=rs[now];}}} }int FindMin() {int now=root;while (ls[now]) now=ls[now];return sum[now]; }void build1()//暴力build的方法,每次插入一个值 {for (i=1;i<=n;++i) ins(a[i]); }int Divide(int l,int r) {if (l>r) return 0;ls[mid]=Divide(l,mid-1);rs[mid]=Divide(mid+1,r);fa[ls[mid]]=fa[rs[mid]]=mid; fa[0]=0;sum[mid]=a[mid];size[mid]=size[ls[mid]]+size[rs[mid]]+1;return mid; }void build2()//精巧的构造,使得树高是log N的 {sort(a+1,a+n+1);root=Divide(1,n);tot=n; }int Find(int x)//查询值为x的数的节点编号 {int now=root;while (sum[now]!=x&&now)if (sum[now]<x) now=rs[now]; else now=ls[now];return now; }int Findkth(int now,int k) {if (size[rs[now]]>=k) return Findkth(rs[now],k);else if (size[rs[now]]+1==k) return sum[now];else Findkth(ls[now],k-size[rs[now]]-1);//注意到递归下去之后右侧的部分都比它要大 }void del(int x)//删除一个值为x的点 {int id=Find(x),t=fa[id];//找到这个点的编号 if (!ls[id]&&!rs[id]) {if (ls[t]==id) ls[t]=0; else rs[t]=0; //去掉儿子边for (i=id;i;i=fa[i]) size[i]--; }elseif (!ls[id]||!rs[id]){int child=ls[id]+rs[id];//找存在的儿子的编号 if (ls[t]==id) ls[t]=child; else rs[t]=child;fa[child]=t;for (i=id;i;i=fa[i]) size[i]--;}else{int y=rs[id]; while (ls[y]) y=ls[y]; //找后继 if (rs[id]==y) {if (ls[t]==id) ls[t]=y; else rs[t]=y;fa[y]=t;ls[y]=ls[id];fa[ls[id]]=y;for (i=id;i;i=fa[i]) size[i]--;size[y]=size[ls[y]]+size[rs[y]];//y的子树大小需要更新 }else //最复杂的情况 {for (i=fa[y];i;i=fa[i]) size[i]--;//注意到变换完之后y到root路径上每个点的size都减少了1int tt=fa[y]; //先把y提出来 if (ls[tt]==y){ls[tt]=rs[y];fa[rs[y]]=tt;} else{rs[tt]=rs[y];fa[rs[y]]=tt;} //再来提出x if (ls[t]==x){ls[t]=y;fa[y]=t;ls[y]=ls[id];rs[y]=rs[id];}else{rs[t]=y;fa[y]=t;ls[y]=ls[id];rs[y]=rs[id];}size[y]=size[ls[y]]+size[rs[y]]+1;//更新一下size }} }int main() {scanf("%d",&n);for (i=1;i<=n;++i) scanf("%d",&a[i]);build1();printf("%d\n",Findkth(root,2));//查询第k大的权值是什么 del(4);printf("%d\n",Findkth(root,2)); }
插入就相对简单了,使得根结点的左子树严格小于他,右子树严格大于他,这样就再插入时比较就可以了。
课件是这么说的:
伪代码:
void insert(intval) {key[+ + tot] = val; ls[tot] = rs[tot] = 0;int now = root;//当前访问的节点为now.for(; ; ){if (val < key[now])if (!ls[now]) ls[now] = x, fa[x] = now, break; else now =ls[now];else if (!rs[now]) rs[now] = x, fa[x] =now, break; else now = rs[now];} }
下面讲一个小扩展的例题(说说思路就行辽)
求第k大的值(如果不能求的话它将会被堆完爆)
对每个节点在多记一个size[x]表示x这个节点子树里节点的个数。
从根开始,如果右子树的size ≥ k,就说明第k大值在右侧,
往右边走,如果右子树size + 1 = k,那么说明当前这个点就是第k大值。
否则,把k减去右子树size + 1,然后递归到左子树继续操作。
来个伪代码叭:
int Findth(int now, int k) {if (size[rs[now]] >= k){return Findth(rs[now], k);}else if (size[rs[now]] + 1 == k){return key[now];}else{return Findth(ls[now], k - size[rs[now]] - 1);} }
还有就是有关遍历的问题;
注意到权值在根的左右有明显的区分。
做一次中序遍历就可以从小到大把所有树排好了。
中序遍历:不停的访问左儿子,直到最后再退回父结点(递归),再走右儿子【类似于我们教练讲的左头右】
遍历伪代码:
inline int DFS(int now) {if (ls[now]){DFS(ls[now]);}print(key[now]);if (rs[now]){DFS(rs[now]);} }
还有一个良好的总结:
二叉堆
堆是一种特殊的二叉树,并且是一棵满二叉树。
第i个节点的父亲是i/2,这样我们就不用存每个点的父亲和儿子了。
二叉搜索树需要保持树的中序遍历不变,而堆则要保证每个点比两个儿子的权值都小。
对于二叉堆来说,主要有这么几个步骤:
建堆:
首先是要建出这么一个堆,最快捷的方法就是直接O(N log N)排一下序。
反正堆的所有操作几乎都是O(log N)的(qwq)。之后可以对这个建堆进行优化。
求最小值:
可以发现每个点都比两个儿子小,那么最小值显然就是a[1]
插入:
首先我们先把新加入的权值放入到n + 1的位置。
然后把这个权值一路往上比较,如果比父亲小就和父亲交换。
注意到堆的性质在任何一次交换中都满足。
而删除最小值只要把一个权值改到无穷大就能解决
解决定位问题
一般来说,堆的写法不同,操作之后堆的形态不同.
所以一般给的都是改变一个权值为多少的点.
假设权值两两不同,再记录一下某个权值现在哪个位置。
在交换权值的时候顺便交换位置信息。
删除权值:
理论上来说删除一个点的权值就只需要把这个点赋成inf 然后down一次。
但是这样堆里的元素只会越来越多.
我们可以把堆里第n号元素跟这个元素交换一下。
然后n--,把堆down一下就行了。
(PS:up(上浮)。down(下沉)。)
那么现在来考虑一种新的建堆方法。
倒序把每个节点都down一下.
正确性肯定没有问题。
复杂度n/2 + n/4 * 2 + n/8 * 3 + .... = O(n)
搞一个手写堆代码:
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #include<cstring> #include<string> #include<cmath> #include<ctime> #include<set> #include<vector> #include<map> #include<queue>#define N 300005 #define M 8000005#define ls (t<<1) #define rs ((t<<1)|1) #define mid ((l+r)>>1)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i,j,m,n,p,k,a[N];int FindMin() {return a[1]; }void build1() {sort(a+1,a+n+1); }void up(int now) {while (now&&a[now]<a[now/2]) swap(a[now],a[now/2]),now/=2; }void ins(int x) {a[++n]=x; up(n); }void down(int now) {while (now*2<=n){if (now*2==n){if (a[now]>a[now*2]) swap(a[now],a[now*2]),now*=2; }else{if (a[now]<=a[now*2]&&a[now]<=a[now*2+1]) break;if (a[now*2]<a[now*2+1]) swap(a[now],a[now*2]),now*=2;else swap(a[now],a[now*2+1]),now=now*2+1; }} }void del(int x) {swap(a[x],a[n]); --n;up(x);down(x); }void change(int x,int val) {if (a[x]>val){a[x]=val;up(x);}else{a[x]=val;down(x);} }void build2() {for (i=n/2;i>=1;--i) down(i); }int main() { scanf("%d",&n);for (i=1;i<=n;++i) scanf("%d",&a[i]);build2(); }
这里介绍一下<set>的几个库函数:
C++ Sets(操作库)
集合(Set)是一种包含已排序对象的关联容器
begin( )
返回指向第一个元素的迭代器
clear( )
清除所有元素
count( )
返回某个值元素的个数
empty( )
如果集合为空,返回true
end( )
返回指向最后一个元素的迭代器
equal_range( )
返回集合中与给定值相等的上下限的两个迭代器
erase( )
删除集合中的元素
find( )
返回一个指向被查找到元素的迭代器
get_allocator( )
返回集合的分配器
insert( )
在集合中插入元素
lower_bound( )
返回指向大于(或等于)某值的第一个元素的迭代器
key_comp( )
返回一个用于元素间值比较的函数
max_size( )
返回集合能容纳的元素的最大限值
rbegin( )
返回指向集合中最后一个元素的反向迭代器
rend( )
返回指向集合中第一个元素的反向迭代器
size( )
集合中元素的数目
swap( )
交换两个集合变量
upper_bound( )
返回大于某个值元素的迭代器
value_comp( )
返回一个用于比较元素间的值的函数
【优点:set可以实时维护有序的数组】
丑数
丑数指的是质因子中仅包含2, 3, 5, 7的数,最小的丑数
是1,求前k个丑数。
K ≤ 6000.
主要分两种思路解题
- 打表大法好!没有什么是打表解决不了的 (大误)
- 考虑递增的来构造序列.x被选中之后,接下来塞进去x * 2, x * 3, x * 5, x * 7.如果当前最小的数和上一次选的一样,就跳过.
复杂度O(K log N). 【还靠谱点qwq】
区间RMQ问题
区间RMQ问题是指这样一类问题。
给出一个长度为N的序列,我们会在区间上干的什么(比如单点加,区间加,区间覆盖),并且询问一些区间有关的信息(区间的和,区间的最大值)等。
这里介绍几种方法:
ST表
ST表是一种处理静态区间可重复计算问题的数据结构,一般也就求求最大最小值 。(没有其余多的用处)
ST表的思想是先求出每个[i, i + 2k)的最值。注意到这样区间的总数是O(N log N)的.
预处理(十分重要的一步)
不妨令fi,j为[i, i + 2j)的最小值。
那么首先fi,0的值都是它本身。
而fi,j= min(fi,j-1, fi+2j-1,j-1)【玄学操作qwq】
这样在O(N log N)的时间内就处理好了整个ST表
询问:
比如我们要询问[l, r]这个区间的最小值.
找到最大的k满足2k ≤ r - l + 1.
取[l, l + 2k), [r - 2k+ 1, r + 1)这两个区间。
注意到这两个区间完全覆盖了[l, r],所以这两个区间最小值较小的一个就是[l, r]的最小值。
因为注意到每次询问只要找区间就行了,所以复杂度是O(1).
ST表代码:
#include <cstdio> #include <algorithm> #include <cstring> #include <iostream> #include <cstring> #include <string> #include <cmath> #include <ctime> #include <set> #include <vector> #include <map> #include <queue>#define N 300005 #define M 8000005 #define K 18#define ls (t << 1) #define rs ((t << 1) | 1) #define mid ((l + r) >> 1)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i, j, m, n, p, k, ST[K + 1][N], a[N], Log[N];int Find(int l, int r) {int x = Log[r - l + 1];return max(ST[x][l], ST[x][r - (1 << x) + 1]); }int main() {scanf("%d", &n);for (i = 1; i <= n; ++i)scanf("%d", &a[i]);for (i = 1; i <= n; ++i)ST[0][i] = a[i];for (i = 1; i <= K; ++i)for (j = 1; j + (1 << i) - 1 <= n; ++j)ST[i][j] = max(ST[i - 1][j], ST[i - 1][j + (1 << (i - 1))]);for (i = 1; (1 << i) < N; ++i)Log[1 << i] = i;for (i = 1; i < N; ++i)if (!Log[i])Log[i] = Log[i - 1];printf("%d\n", Find(1, 3)); }
PS:ST表确实是一个询问O(1)的数据结构,但是它的功效相对也较弱.
例如每次求一个区间的和,利用前缀(布吉岛)可以做到O(N) - O(1).而ST表却无法完成。
据说好像做题有个这样的步骤:
(蕴含哲学qwq)
给出一个序列,支持对某个点的权值修改,或者询问某个区间的最大值. N, Q ≤ 100000.
我们会十分快乐的发现:ST表不行(妈耶那我凉了呀)
这就引入一个名叫线段树的东西
其实线段树被称为区间树比较合适 ,本质是一棵不会改变形态的二叉树
树上的每个节点对应于一个区间[a, b](也称线段),a,b通常为整数.
来几个实例:
(n=10)
(n=9)
我们注意到线段树的结构有点像分治结构,深度也是O(log N)的.
区间拆分:
区间拆分是线段树的核心操作,我们可以将一个区间[a, b]拆分成若干个节点,使得这些节点代表的区间加起来是[a, b],并且相互之间不重叠.
所有我们找到的这些节点就是”终止节点”.
区间拆分的步骤:
从根节点[1, n]开始,考虑当前节点是[L, R].如果[L, R]在[a, b]之内,那么它就是一个终止节点.否则,分别考虑[L, Mid],[Mid + 1, R]与[a, b]是否有交,递归两边继续找终止节点.
延迟更新:(标记-lazy标记)
类似于线段树,向下传值更新。
向下传值时(代码):inc+=inc【t】
相对加的inc就是祖先的和和自己原本的inc
线段树的代码:
#include <cstdio> #include <algorithm> #include <cstring> #include <iostream> #include <cstring> #include <string> #include <cmath> #include <ctime> #include <set> #include <vector> #include <map> #include <queue>#define N 300005 #define M 8000005#define ls (t * 2) #define rs (t * 2 + 1) #define mid ((l + r) / 2)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i, j, m, n, p, k, add[N * 4], sum[N * 4], a[N], ans, x, c, l, r;void build(int l, int r, int t) {if (l == r)sum[t] = a[l];else{build(l, mid, ls);build(mid + 1, r, rs);sum[t] = max(sum[ls], sum[rs]); } }void modify(int x, int c, int l, int r, int t) {if (l == r)sum[t] = c;else{if (l <= x && x <= mid)modify(x, c, l, mid, ls);elsemodify(x, c, mid + 1, r, rs);sum[t] = max(sum[ls], sum[rs]); } }void ask(int ll, int rr, int l, int r, int t) {if (ll <= l && r <= rr)ans = max(ans, sum[t]); else{if (ll <= mid)ask(ll, rr, l, mid, ls); if (rr > mid)ask(ll, rr, mid + 1, r, rs); } }int main() {scanf("%d", &n);for (i = 1; i <= n; ++i)scanf("%d", &a[i]);build(1, n, 1);modify(1, 5, 1, n, 1);ask(1, 5, 1, n, 1); }
树状数组
树状数组是一种用来求前缀和的数据结构
记lowbit(x)为x的二进制最低位.
例子:lowbit(8) = 8, lowbit(6) = 2
求lowbit:
int lowbit(int x) {return x&-x; }
对于原始数组A,我们设一个数组C.
C[i]=a[i-lowbit(i)+1]+...+a[i]
i > 0的时候C[i]才有用.C就是树状数组.
求和与更新:
求和:
树状数组只能够支持询问前缀和。我们先找到C[n],然后我们发现现在,下一个要找的点是n - lowbit(n),然后我们不断的减去lowbit(n)并累加C数组.
我们可以用前缀和相减的方式来求区间和.
询问的次数和n的二进制里1的个数相同.则是O(log N).
更新:
现在我们要修改Ax的权值,考虑所有包含x这个位置的区间个数.
从C[x]开始,下一个应该是C[y = x + lowbit(x)],再下一个是C[z = y + lowbit(y)]...
注意到每一次更新之后,位置的最低位1都会往前提1.总复杂度也为O(log N).
留一个树状数组的题:
洛谷P1908 逆序对
TLE25分代码(果然是我太天真qwq)
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; long long s[500001]; int main() {int n,a;scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%Ld",&s[i]);}for(int i=1;i<=n;i++){for(int j=1;j<i;j++){if(s[j]>s[i]){a++;}}}printf("%d",a);return 0; }
后来用了个离散化qwq还是TLE25分【还是吸了氧气qwq】
// luogu-judger-enable-o2 #include <iostream> #include <cstdio> #include <algorithm> using namespace std; struct m {int x,y,z; }s[500001]; int qwq(m a,m b) {return a.x<b.x; } int QWQ(m a,m b) {return a.y<b.y; } int main() {int n,k=1,c=1;scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&s[i].x);s[i].y=i;}sort(s+1,s+1+n,qwq);for(int i=1;i<=n;i++){if(s[i].x==s[i-1].x){s[i].z=s[i-1].z;}else{s[i].z=c;c++;}}sort(s+1,s+1+n,QWQ);c=0;for(int i=1;i<=n;i++){for(int j=1;j<i;j++){if(s[j].x>s[i].x){c++;}}}printf("%d",c); }
最后最后终于用树状数组AC了
#include <cstdio> #include <algorithm> #include <cstring> #include <iostream> #include <cstring> #include <string> #include <cmath> #include <ctime> #include <set> #include <vector> #include <map> #include <queue>#define N 500005 #define M 8000005#define ls (t << 1) #define rs ((t << 1) | 1) #define mid ((l + r) >> 1)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i, j, m, n, p, k, C[N], a[N], b[N];long long ans;int lowbit(int x) {return x & -x; }void ins(int x, int y) //修改a[x]的值,a[x]+=y; {for (; x <= n; x += lowbit(x))C[x] += y; //首先需要修改的区间是以x为右端点的,然后下一个区间是x+lowbit(x),以此类推 }int ask(int x) //查询[1,x]的值 {int ans = 0;for (; x; x -= lowbit(x))ans += C[x]; //我们询问的]是[x-lowbit(x)+1,x,然后后面一个区间是以x-lowbit(x)为右端点的,依次类推return ans; }int main() {scanf("%d", &n);for (i = 1; i <= n; ++i)scanf("%d", &a[i]), b[++b[0]] = a[i];sort(b + 1, b + b[0] + 1);b[0] = unique(b + 1, b + b[0] + 1) - (b + 1);for (i = 1; i <= n; ++i)a[i] = lower_bound(b + 1, b + b[0] + 1, a[i]) - b;for (i = 1; i <= n; ++i)ans += (i - 1) - ask(a[i]), ins(a[i], 1);printf("%lld\n", ans); }
紧赶慢赶终于到了并查集了
N个不同的元素分布在若干个互不相交集合中,需要多次进行以下3个操作:
1. 合并a,b两个元素所在的集合 Merge(a,b)
2. 查询一个元素在哪个集合
3. 查询两个元素是否属于同一集合 Query(a,b)
并查集的总代码:
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #include<cstring> #include<string> #include<cmath> #include<ctime> #include<set> #include<vector> #include<map> #include<queue>#define N 300005 #define M 8000005#define ls (t<<1) #define rs ((t<<1)|1) #define mid ((l+r)>>1)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i,j,m,n,p,k,fa[N],deep[N];int get(int x) //不加任何优化的暴力 {while (fa[x]!=x) x=fa[x];return x; }//路径压缩 /* int get(int x) { return fa[x]==x?fa[x]:fa[x]=get(fa[x]); }//按秩合并 void Merge(int x,int y) {x=get(x); y=get(y); 使用的是最暴力的getif (deep[x]<deep[y]) fa[x]=y;else if (deep[x]>deep[y]) fa[y]=x;else deep[x]++,fa[y]=x; }*/ void Merge(int x,int y) {x=get(x); y=get(y); //找到x,y所在连通块的顶点fa[x]=y; }int Ask(int x,int y) {x=get(x); y=get(y);if (x==y) return 1;//表示连通 return 0; }int main() {scanf("%d",&n);for (i=1;i<=n;++i) fa[i]=i,deep[i]=1;Merge(2,3);//合并2,3所在的连通块printf("%d\n",Ask(2,3));//询问2,3是否在同一个连通块里 }
这里用一个例题带大家看看(自己理解叭)
洛谷P3367 【模板】并查集
#include<bits/stdc++.h> using namespace std; int n,m; int x,y,z; int a,b; int fa[200001]; int find(int x) {if(fa[x]!=x){fa[x]=find(fa[x]);}return fa[x]; } void unionn(int r1,int r2) {fa[r2]=r1; } int main() {cin>>n>>m;for(int i=1;i<=n;i++){fa[i]=i;}for(int i=1;i<=m;i++){cin>>x>>y>>z;a=find(y);b=find(z);if(x==1){if(a!=b){unionn(a,b);}}if(x==2){if(a==b){cout<<"Y"<<endl;}else{cout<<"N"<<endl;}}} }
还讲到了路径压缩和按秩合并
路径压缩:
第一种优化看起来很玄学,我们在寻找一个点的顶点的时候,显然可以把这个点的父亲赋成他的顶点,也不会有什么影响.
看起来玄学,但是他的复杂度是O(N log N)的。
证明非常复杂,有兴趣的同学可以自行翻阅论文。
路径压缩的修改部分主要在Getroot部分
就一行代码:
int get(int x) { return fa[x]==x?fa[x]:fa[x]=get(fa[x]); }
按秩合并:
对每个顶点,再多记录一个当前整个结构中最深的点到根的深度deepx.
注意到两个顶点合并时,如果把比较浅的点接到比较深的节点上.
如果两个点深度不同,那么新的深度是原来较深的一个.
只有当两个点深度相同时,新的深度是原来的深度+1.
注意到一个深度为x的顶点下面至少有2x个点,所以x至多为log N.
那么在暴力向上走的时候,要访问的节点至多只有log个
按秩合并的修改部分在于Merge,其他部分都与暴力相同
伪代码:
void Merge(int x,int y) {x=get(x); y=get(y); 使用的是最暴力的getif (deep[x]<deep[y]) fa[x]=y;else if (deep[x]>deep[y]) fa[y]=x;else deep[x]++,fa[y]=x; }
将两者进行比较:
无论是时间,空间,还是代码复杂度,路径压缩都比按秩合并优秀.
值得注意的是,路径压缩中,复杂度只是N次操作的总复杂度为O(N log N)。按秩合并每一次的复杂度都是严格O(log N)的.
两种方式可以一起用,复杂度会降的更低.
最后的最后LCA
在一棵有根树中,树上两点x, y的LCA指的是x, y向根方向遇到到第一个相同的点 我们记每一个点到根的距离为deepx.
注意到x, y之间的路径长度就是deepx+ deepy- 2 * deepLCA.
int Find_LCA(int x,int y) //求x,y的LCA {int i,k;if (deep[x]<deep[y]) swap(x,y);x=Kth(x,deep[x]-deep[y]); //把x和y先走到同一深度 if (x==y) return x;for (i=K;i>=0;--i) //注意到x到根的路径是xa1a2...aic1c2...ck//y到根的路径是 yb1b2...bic1c2...ck 我们要做的就是把x和y分别跳到a_i,b_i的位置,可以发现这段距离是相同的. if (fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];return fa[x][0]; }
原始求法:
两个点到根路径一定是前面一段不一样,后面都一样.
注意到LCA的深度一定比x, y都要小.
利用deep,把比较深的点往父亲跳一格,直到x, y跳到同一个点上.
这样做复杂度是O(len).
倍增法
考虑一些静态的预处理操作.
像ST表一样,设fa i,j为i号点的第2j个父亲。
自根向下处理,容易发现
求第K个祖先
首先,倍增可以求每个点向上跳k步的点.
利用类似快速幂的想法.
每次跳2的整次幂,一共跳log次.
求LCA:
首先不妨假设deepx < deepy.
为了后续处理起来方便,我们先把x跳到和y一样深度的地方.
如果x和y已经相同了,就直接退出.
否则,由于x和y到LCA的距离相同,倒着枚举步长,如果x, y的第2^j个父亲不同,就跳上去.样,最后两个点都会跳到离LCA距离为1的地方,在跳一步就行了.
时间复杂度O(N log N)
#include<cstdio> #include<algorithm> #include<cstring> #include<iostream> #include<cstring> #include<string> #include<cmath> #include<ctime> #include<set> #include<vector> #include<map> #include<queue>#define N 300005 #define M 8000005 #define K 18#define ls (t<<1) #define rs ((t<<1)|1) #define mid ((l+r)>>1)#define mk make_pair #define pb push_back #define fi first #define se secondusing namespace std;int i,j,m,n,p,k,fa[N][K+1],deep[N];vector<int>v[N];void dfs(int x) //dfs求出树的形态,然后对fa数组进行处理 {int i;for (i=1;i<=K;++i) //fa[x][i]表示的是x向父亲走2^i步走到哪一个节点 fa[x][i]=fa[fa[x][i-1]][i-1]; //x走2^i步相当于走2^(i-1)步到一个节点fa[x][i-1],再从fa[x][i-1]走2^(i-1)步 for (i=0;i<(int)v[x].size();++i){int p=v[x][i];if (fa[x][0]==p) continue;fa[p][0]=x;deep[p]=deep[x]+1; //再记录一下一个点到根的深度deep_x dfs(p);} }int Kth(int x,int k) //求第k个父亲,利用二进制位来处理 {for (i=K;i>=0;--i) //k可以被拆分成logN个2的整次幂 if (k&(1<<i)) x=fa[x][i];return x; }int Find_LCA(int x,int y) //求x,y的LCA {int i,k;if (deep[x]<deep[y]) swap(x,y);x=Kth(x,deep[x]-deep[y]); //把x和y先走到同一深度 if (x==y) return x;for (i=K;i>=0;--i) //注意到x到根的路径是xa1a2...aic1c2...ck//y到根的路径是 yb1b2...bic1c2...ck 我们要做的就是把x和y分别跳到a_i,b_i的位置,可以发现这段距离是相同的. if (fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];return fa[x][0]; }int main() {scanf("%d",&n);for (i=1;i<n;++i){int x,y;scanf("%d%d",&x,&y);v[x].pb(y); v[y].pb(x);}dfs(1);printf("%d\n",Find_LCA(3,5)); }
总结:
LCA能发挥很大的用处
倍增这一算法的时空复杂度分别为O(N log N) - O(log N) O(N log N).
对于求第K个祖先,利用长链剖分以及倍增算法,可以做到O(N log N) - O(1) O(N log N).
对于求LCA,利用ST表以及欧拉序可以做到O(N log N) - O(1) O(N log N)
qwq,终于整完了
例题会少一些(望谅解)
转载于:https://www.cnblogs.com/gongcheng456/p/10792399.html
清北学堂培训2019.4.29相关推荐
- 清北学堂培训2019.4.4
第一次培训,心情有点激动(尽管没了清明节),还见到了各地的dalao们,十分开森 Day 1(李昊dalao) 上午篇 上午呢,主要讲了关于高精,快速幂,膜模意义下的运算,筛素数,费马小定理以及欧拉定 ...
- 清北学堂培训2019.4.7
Day 4(没错,又是他→钟皓曦) 上午,考了个试(悲惨爆零[好像是多加了几个文件夹]) 下午是题目讲解和概率期望的知识(貌似网上好像能搜到他的课件) 这里也没什么奇特的,主要是一些定义的知识(这里采 ...
- 清北学堂培训2019.4.6
Day 3(还是我们熟悉的钟皓曦大佬) 讲了神奇的组合数问题(据说各个网站的组合数问题都是钟皓曦出的,所以他说都是好题) 组合数需要用到下面两个原理和两个神奇的东西 加法原理:具有性质A的事件有m个, ...
- 清北学堂培训2019.4.28
Day 1(冯哲) 今天的内容很杂但却都是基础知识 主要分为下面几个点 枚举 枚举也称作穷举,指的是从问题所有可能的解的集合中一一枚举各元素.用题目中给定的检验条件判定哪些是无用的,哪些是有用的.能使 ...
- 7月清北学堂培训 Day 5
今天是钟皓曦老师的讲授~ 动态规划 动态规划的三种实现方法: 1.递推: 2.递归: 3.记忆化: 举个例子: 斐波那契数列:0,1,1,2,3,5,8-- Fn = Fn-1 + Fn-2 1.我们 ...
- 清北学堂(2019 4 28 ) part 1
今天主要用来铺路,打基础 枚举 没什么具体算法讲究,但要考虑更优的暴力枚举方法,例如回文质数,有以下几种思路: 1.挨个枚举自然数,再一起判断是否是回文数和质数,然而一看就不是最优 2.先枚举质数再判 ...
- 8月清北学堂培训 Day4
今天上午是赵和旭老师的讲授~ 概率与期望 dp 概率 某个事件 A 发生的可能性的大小,称之为事件 A 的概率,记作 P ( A ) . 假设某事的所有可能结果有 n 种,每种结果都是等概率,事件 A ...
- 五一清北学堂培训之Day 3之DP
今天又是长者给我们讲小学题目的一天 长者的讲台上又是布满了冰红茶的一天 ---------------------------------------------------------------- ...
- 7月清北学堂培训 Day 1
基础算法 1. 模拟算法 面向测试算法 模拟算法的关键就是将人类语言翻译成机器语言. 要做到以下两点: 1.优秀的读题能力: 2.优秀的代码能力: 程序流程图: 读入,循环处理指令,输出: 读题是很重 ...
最新文章
- 机器学习丨15个最流行的GitHub机器学习项目
- 【学术相关】94年的博士后又拿到了这个金奖!原来是他的学弟
- Dom4j遍历解析XML测试
- Kafka监控架构设计
- tornado-简介和原理
- 如何通向“广义人工智能”?LSTM 提出者之一Sepp Hochreiter:将符号 AI 与神经 AI 相结合...
- DB2新建编目及删除编目
- chartControl控件常用属性总结
- 四层和八层电梯控制系统Proteus仿真设计,51单片机,附仿真和Keil C代码
- 我们穷极一生,究竟追寻的是什么?
- 三款适用于企业建站的CMS建站系统
- 《tensorflow实战》6——强化学习之策略网络
- 想不到 HR 都在 GitHub 捞人!五位开源大牛分享成长经历(文末福利)
- IR2184死区时间介绍
- 家居家装行业人群洞察白皮书.pdf
- 16春季计算机应用基础,西交16春季《计算机应用基础》在线作业及答案
- 基于Python的百度AI人脸识别API接口(可用于OpenCV-Python人脸识别)
- idea去掉不想commit的文件
- 【信号处理】python按原理实现BPSK、QPSK、QAM信号调制
- React高阶组件探究
热门文章
- 重磅!继“智能+”120页PPT,阿里+毕马威发布4份智能经济报告(附免费下载)
- 中国科学院大学计算机学科评估,中国科学院大学学科评估结果排名
- 图形学基础 | 计算机图形学MOOC学习笔记
- svn服务器 提交文件超慢,TortoiseSVN提交速度极慢
- 传统客服压力山大,如何应用智能客服打造直击痛点的解决方案?
- 极客星球 | 前端工程化之路的探索与实践
- E-office OA 任意文件下载漏洞复现
- Python建立线性回归模型进行房价预测
- js + leetcode刷题:No.914 卡牌分组
- 域名中不应出现下划线