一、并查集
知识点
并查集(Union-find Data Structure)是一种树型的数据结构。它的特点是由子结点找到父亲结点,用于处理一些不交集(Disjoint Sets)的合并及查询问题。
Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
Union:将两个子集合并成同一个集合。
重点,短命短思维巧
快速支持俩操作
树形式维护集合
根节点编号就是集合编号
每个点存父节点
并查集维护额外信息
并查集本质记住find函数,其他靠推导
按秩合并,用的少,小树接大树
1.合并集合
#include<iostream>
using namespace std;
int p[100010];
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
// 如果当前结点不是该集合祖宗结点就递归找。
//因为要优化路径压缩,找出来顺便都赋值给所有结点的父节点。时间复杂度降到O(1)
return p[x];
// 返回老爸一层层递归往上找嘛
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--){
char ops[2];
cin>>ops;
// 行末空格回车自动清,所以是数组
int a,b;
cin>>a>>b;
if(ops[0]=='M'){
if(find(a)!=find(b)){//find是看祖宗是不是一样,顺便路径压缩了
p[p[a]]=p[b];
// 如果祖宗结点不一样,直接把自己祖宗认另一个祖宗当爹,
//这样就合并了,大家祖宗都一样了
}
}else if(ops[0]=='Q'){
if(find(a)==find(b)) cout<<"Yes";
else cout<<"No";
puts("");
}
}
}
2.连通块中点的数量
#include<iostream>
using namespace std;
const int N=100010;
int p[N],size1[N];
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
// 如果当前结点不是该集合祖宗结点就递归找。
//因为要优化路径压缩,找出来顺便都赋值给所有结点的父节点。时间复杂度降到O(1)
return p[x];
// 返回老爸一层层递归往上找嘛
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++){
p[i]=i;
size1[i]=1;//刚开始自己是一个集合
}
while(m--){
char ops[2];
cin>>ops;
// 行末空格回车自动清,所以是数组
int a,b;
if(ops[0]=='C'){
cin>>a>>b;
if(find(a)!=find(b)){//find是看祖宗是不是一样,顺便路径压缩了
size1[p[b]]+=size1[p[a]];//先算结点数哈,不然都连一起了算个锤子啊,下面那个操作侵入
p[p[a]]=p[b];
// 如果祖宗结点不一样,直接把自己祖宗认另一个祖宗当爹,
//这样就合并了,大家祖宗都一样了
}
}else if(ops[1]=='1'){
cin>>a>>b;
if(find(a)==find(b)) cout<<"Yes";
else cout<<"No";
puts("");
}else{
cin>>a;
cout<<size1[find(a)]<<endl;//祖宗结点的size1才有意义,其他的算了吧
}
}
}
3.食物链
并查集维护额外信息
#include<iostream>
using namespace std;
int p[50010];
int dis[50010];
int find(int x){
if(p[x]!=x){
int t=find(p[x]);//要先求出并保存祖先结点,因为下面算距离要用
dis[x]+=dis[p[x]];
p[x]=t;//算完才能给父节点赋值,不然找不到父节点了
}
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
int res=0;
while(m--){
int ops;
cin>>ops;
int x,y;
cin>>x>>y;
if(x>n||y>n){
res++;
continue;
}
int px=find(x),py=find(y);
if(ops==1){
if(px==py&&(dis[y]-dis[x])%3){//在同一个集合就判断是不是模三意义下的同级关系,不是就res加
res++;
}else if(px!=py){//不在一个集合那就合并嘛
p[px]=py;
dis[px]=dis[y]-dis[x];
}
}else{
if(px==py&&(dis[y]-dis[x]+1)%3){//在同一个集合就判断是不是模三意义下的吃的关系,不是就res加
res++;
}else if(px!=py){
p[px]=py;
dis[px]=dis[y]+1-dis[x];
}
}
}
cout<<res;
return 0;
}
4.被围绕的区域
class Solution {
public:
static const int N=40010;
int p[N]={0};
int dx[4]={0,-1,0,1};
int dy[4]={1,0,-1,0};
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
void solve(vector<vector<char>>& board) {
int L=board.size();
int C=board[0].size();
for(int i=0;i<=L*C;i++){//这里要L*C是为了多出一个虚拟节点方便当
//做所有保留的O结点的父节点,标记用的
p[i]=i;
}
for(int i=0;i<L;i++){
for(int j=0;j<C;j++){
if(board[i][j]=='O'&&(i==0||i==L-1||j==0||j==C-1)){
p[find(L*C)]=find(i*C+j);//前后顺序可变,都是合体
//祖先是虚拟的'O'就不删
}else if(board[i][j]=='O'){
for(int k=0;k<4;k++){
if(i+dx[k]>=0&&j+dy[k]>=0&&i+dx[k]<L&&j+dy[k]<C&&board[i+dx[k]][j+dy[k]]=='O'){
p[find((i+dx[k])*C+j+dy[k])]=find(p[(i*C+j)]);
//不是边界的O就负责把四周的连通块合并,自己也合进去啊,所以都是别的来合它
//在不在一个连通块都是看祖先节点是不是一样
}
}
}
}
}
for(int i=0;i<L*C;i++){
if(p[find(i)]!=find(L*C)&&board[i/C][i%C]=='O'){
//看看祖先是不是虚拟
board[i/C][i%C]='X';
}
}
}
};
5.最长连续序列
class Solution {
public:
map<int,int>hash;//用于查看是否有过某个数字
static const int N=100010;
int p[N]={0};
int size1[N];//以某个下标的数在内的并且最大下标不超过这个数下标的最长连续序列长度
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int longestConsecutive(vector<int>& nums) {
int L=nums.size();
if(L==0) return 0;
if(L==1) return 1;
int res=1;
for(int i=0;i<L;i++){
size1[i]=1;
p[i]=i;
if(hash[nums[i]]) continue;//重复就不要了
hash[nums[i]]=i+1;//以1开始防止以0为下标hash无法判断到底有没有这个数
if(hash[nums[i]-1]&&hash[nums[i]+1]){
p[hash[nums[i]+1]-1]=i;//先看后的那个连通块有几个点,先以它做根来看最大有几个
size1[i]+=size1[hash[nums[i]+1]-1];//算上当前的点
int t=find(hash[nums[i]-1]-1);//看前一个的祖先是谁
p[i]=t;//更新当前这个点的爹成祖先,因为这样才知道它是哪个连通块啊,
//以祖先来判断是不是一个连通块
size1[t]+=size1[i];//最前面的祖先把后面的连通块点合计
res=max(res,size1[t]);
}else if(hash[nums[i]+1]){
p[hash[nums[i]+1]-1]=i;
size1[i]+=size1[hash[nums[i]+1]-1];
res=max(res,size1[i]);
}else if(hash[nums[i]-1]){
p[i]=find(hash[nums[i]-1]-1);
res=max(res,++size1[p[i]]);
}
}
return res;
}
};
6.岛屿数量
class Solution {
public:
static const int L=90010;
int p[L];
int dx[2]={1,0};
int dy[2]={0,-1};//只能俩方向,因为四个方向会重复算噢~
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int numIslands(vector<vector<char>>& grid) {
int res=0;
int M=grid.size(),N=grid[0].size();
for(int i=0;i<M*N;i++){
if(grid[i/N][i%N]=='1') res++;//没合并前看看有几个岛,注意是char噢
p[i]=i;
}
for(int i=0;i<M;i++){
for(int j=0;j<N;j++){
if(grid[i][j]=='1'){
for(int k=0;k<2;k++){
if(i+dx[k]>=0&&i+dx[k]<M&&j+dy[k]>=0&&j+dy[k]<N
&&grid[i+dx[k]][j+dy[k]]=='1'){
int r1=find((i+dx[k])*N+j+dy[k]);//祖先1
int r2=find(p[i*N+j]);//祖先2
if(r1!=r2){//不在一个连通块
p[r1]=r2;//合起来
res--;//二变一
}
}
}
}
}
}
return res;
}
};
7.除法求值
我麻了这题,写了几个小时你们猜吧,还是看答案之后搞的。。。
既然问题转变为图又与连接关系有关,那么很自然的想到并查集(并查集可以判断两点之间是否连通)。
class Solution {
public:
const static int L=210;
vector<double> val=vector(L,1.0);//并查集树中额外信息的存处,默认是1,自己跟祖先的比值
int p[L];
int idx=0;//配映射,因为下面值是字符
map<string,int>hash;
int find(int x){
if(p[x]!=x){
int t=p[x];//先拿父节点出来,待会要用,不然等一下路径压缩直接没了
p[x]=find(p[x]);
val[x]*=val[t];//自己到祖先的值等于自己到现在祖先的值乘现在祖先到后面祖先的值
}
return p[x];
}
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
int N=equations.size();
for(int i=0;i<N;i++){
if(hash.find(equations[i][0])==hash.end()){
hash[equations[i][0]]=idx++;//防止重复映射,没唯一标识你也无法判断集合一不一样了啊
}
if(hash.find(equations[i][1])==hash.end()){
hash[equations[i][1]]=idx++;
}
}
for(int i=0;i<idx;i++){//初始化
p[i]=i;
}
for(int i=0;i<N;i++){
int p1=find(hash[equations[i][0]]);
int p2=find(hash[equations[i][1]]);
if(p1!=p2){
p[p1]=p2;
val[p1]=values[i]*val[hash[equations[i][1]]]/val[hash[equations[i][0]]];
//换根后0的祖先变成1的祖先,0祖先的val值也相应变,
//看一个0能得几个1,再看有几个0,就知道和1祖先关系了
}
}
vector<double> res;
for(auto i:queries){
if(hash.find(i[0])==hash.end()||hash.find(i[1])==hash.end()){//并查集中没存过
res.push_back(-1.0);
}else if(find(hash[i[0]])==find(hash[i[1]])){//集合相同
res.push_back(val[hash[i[0]]]/val[hash[i[1]]]);
}else{//集合不同
res.push_back(-1.0);
}
}
return res;
}
};
8.账户合并
class Solution {
public:
static const int L=10010;
vector<int>p=vector<int>(L,1);
unordered_map<string,int> mp;//账户和它对应的idx
unordered_map<int,set<string>> ms;//存某idx或者说是根和它下面的账户,自动去重排序
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
vector<vector<string>> res=vector<vector<string>>();
int N=accounts.size();
for(int i=0;i<N;i++){
p[i]=i;
}
for(int i=0;i<N;i++){
for(int j=1;j<accounts[i].size();j++){
if(mp.find(accounts[i][j])==mp.end()){//看当前这个账户之前有木有,木有就加进去
mp[accounts[i][j]]=i;
}else{
p[find(i)]=find(mp[accounts[i][j]]);//有就祖宗认爹,待会一起合并去重的
}
}
}
for(int i=0;i<N;i++){
int t=find(i);//找祖宗,也就是看自己是哪个集合的
for(int j=1;j<accounts[i].size();j++){
ms[t].insert(accounts[i][j]);//按照集合来插入账户啊,合并去重了噢
}
}
for(auto i:ms){
vector<string> resSon;
resSon.push_back(accounts[i.first][0]);//先装名字
for(auto j:ms[i.first]){
resSon.push_back(j);
}
res.push_back(resSon);
}
return res;
}
};
9.冗余连接
class Solution {
public:
const static int L=2001;
vector<int> p=vector<int>(L,1);
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
vector<int> findRedundantConnection(vector<vector<int>>& edges) {
int N=edges.size();
for(int i=0;i<=N;i++){
p[i]=i;
}
vector<int>res;
for(int i=0;i<N;i++){
int t1=find(edges[i][0]);
int t2=find(edges[i][1]);
if(t1!=t2){
p[t1]=t2;
}else{
res=edges[i];//看看边的两头是不是已经在一个集合中了,在你再连边不是多余吗,找一下规律吧
}
}
return res;
}
};
10.省份数量
class Solution {
public:
static const int L=40010;
vector<int>p=vector<int>(L,1);
int find(int x){
if(p[x]!=x){
p[x]=find(p[x]);
}
return p[x];
}
int findCircleNum(vector<vector<int>>& isConnected) {
int N=isConnected.size();
int res=N;//---
for(int i=0;i<N*N;i++) p[i]=i;
for(int i=0;i<N*N;i++){
if(isConnected[i/N][i%N]){
int t1=find(i/N);//看第一个城市,也就是行数
int t2=find(i%N);//第二个城市,也就是列数
if(t1!=t2){
p[t1]=t2;
res--;
}
}
}
return res;
}
};