习题课1-3
等式
-
第一行整数T,表示数据组数
-
接下来会有T组数据,对于每组数据
-
第一行是两个整数n,m,表示变量个数和约束条件个数
-
接下来m行,每行三个整数a,b,e,表示第a个变量的和第b个变量的关系
-
若e=0则表示第a个变量不等于第b个变量
-
若e=1则表示第a个变量等于第b个变量
-
并查集(union-find)
-
将两个元素所在集合合并起来
-
查询两个元素是否在同一个集合中
-
注意e=1的操作要在e=0的操作之前
解法1
- 第i个元素所在集合ID(i)
- 把=1的条件提到前面来,相当于对约束条件排序
- 每次执行e=1时,若相应的两个元素为a和b,那么将所有满足ID©==ID(a)或者ID©==ID(b)的元素合并起来,也就是令所有ID©==ID(a)
- 之后执行e=0时,若相应的两个元素a和b有ID(a)==ID(b),则不合法,否则合法
- 时间复杂度O(nm)
解法1+
- set(i)表示编号为i的集合
- 对于操作=1
- 若set(id(a))的集合大小大于set(id(b)),交换a和b
- set(id(b))的集合大小等于set(id(b))&(并)set(id(a)) (意思让b是a和b的并集),并且对于任意x属于set(id(a)),让id(x)等于id(b)
- 白话意思就是小集合一个个拿出来加到大集合里面(启发式合并)
- 用一个变长数组保存
- C++:std::vector
- java:ArrayList
- python:list()
解法2
-
路径压缩和启发式合并
扫描二维码关注公众号,回复: 12476621 查看本文章 -
Father:每个节点的父亲,路径压缩的
-
Rank:节点的秩,用来启发式合并的
-
首先要排序,把所有等于1的操作换到前面来
-
int cnt = 0; // 把操作等于0的操作滞后,也可以把操作等于1的操作提前 for (int i = 0; i < (m-cnt); i++) { if (E.get(i) ==0){ while( E.get(m-1-cnt)==0 && (m-1-cnt)>i){ cnt++; } Collections.swap(A,i,m-1-cnt); Collections.swap(B,i,m-1-cnt); Collections.swap(E,i,m-1-cnt); } }
-
先找出A、B两个集合的根,路径压缩,(路径压缩,在往上寻找父亲节点的时候,把所有经过的节点都变成根节点(最早的祖先))
-
对于0操作,如果相等,则返回NO,因为此时已经执行完1操作,已经保存好完整的并查集关系
-
对于1操作,如果不相等,此时需要合并,father数组用于寻找根节点,rank表示并查集曾经达到的最大深度,
-
for (int i = 0; i < m; i++) { int setA = find(A.get(i)); int setB = find(B.get(i)); if (E.get(i) == 0){ if (setA==setB) { return "No"; } }else{ if (setA!=setB){ // 让B作为A的父节点 // 启发式合并 if (rank[setA]>rank[setB]){ int tmp = setA; setA = setB; setB = tmp; } father[setA] = setB; if (rank[setA]==rank[setB]){ rank[setB]++; } } } }
道路升级
-
从图中选出任意条边,使得任意两点都互相连通
-
并且任意两点的路径连起来的边权是最大的
-
最后希望选出的边数尽可能少
-
连通图(任意两点都可以互相连通)肯定有生成树,一定有最大生成树
-
任何不在最大生成树上的边,加入其他的边,都不会导致连通路径的边权变得更大,所以可以丢弃
解法1
-
kruskal算法
-
按边权大小从大到小排列,依次加入图中,用并查集维护连通性
-
disjoint(并查集英文名称)
-
没有用启发式合并,只用了路径压缩
-
static class UnionSet { int[] f; void init(int n){ f = new int[n+1]; for (int i = 1; i <= n; i++) { f[i] = i; rank[i] = 0; } } int find(int x){ return f[x]==x?x:(f[x]=find(f[x])); } boolean merge(int x,int y){ // 需要操作父亲节点,不然会丢失节点 int setX = find(x); int setY = find(y); if (setX!=setY){ f[setX] = setY; return true; } return false; } }
-
初始化并查集
-
怎样循环?逆序循环,因为权值是从小到达排列的,然后合并,如果合并成功,就把这条边加入
-
最后reverse边的集合,因为答案是正序输出的
-
if(find(x)!=find(y))
-
father(y) = father(x)
-
这种写法会丢失x以上的父节点关系,所以是错误的
二叉查找树
- 维护一个整数集合(数字可重复)
- 插入一个数字
- 删除一个数字
- 查询某个数字有多少个
- 查询最值
- 查询某个数字的前驱
问题分析
- 可用二叉树解决,为了保证时间复杂度,我们得使用平衡二叉树解决
- BBST有很多种,Treap、Splay、红黑树、AVL、SBT
- 推荐Treap(更优先)和Splay,前者好写速度快,后者好写功能强大
解法1
- c++的std::multiset
- java的TreeSet
- python的set(),需自己加点操作使得可重复
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YdMXruPo-1605228918387)(C:\Users\liusiping\AppData\Roaming\Typora\typora-user-images\image-20201111215106943.png)]
- 1:插入一个数字
- 2:删除一个数字
- 3:输出这个数字有多少个
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XAnL8VP6-1605228918388)(C:\Users\liusiping\AppData\Roaming\Typora\typora-user-images\image-20201111215251113.png)]
- 4:获取最小元素
- 5:查询某个数字的前驱,(lower_bound,二分查找)
- 找出它前面一个,找不到就输出None
- 百度:平衡树模板