食物链(POJ 1182)运用并查集的方法解题

什么是并查集

并查集是用来管理元素分组情况的数据结构。并查集可以高效的进行如下操作。不过需要注意并查集虽然可以进行合并操作,但是无法进行分割操作。

  • 查询元素a和元素b是否属于同一组
  • 合并元素a和元素所在的组

并查集示意图

并查集结构

并查集是使用树型结构来实现的,但是不是二叉树,是森林结构。并查集中的每个分组右一颗树构成许多分组构成森林。
初始化:
将n元素表示为n个节点。
合并:
将一个组的根连接到另一个组上。
这里写图片描述
查询:
为了查询两个节点是否为同一组,需要沿着树往上,如果是同一组,那么他们一定有公共的父节点。

并查集实现应该注意的点

在树型结构中可能会发生退化的现象(退化是指树在构建时中心偏移,比如说在构建二叉树时每次都插入左子树,那么最终会构成链表而不是二叉树),因此在构建并查集时也要想办法避免退化现象的发生。

避免退化:

  • 记录每棵树的高度(rank)。
  • 合并时时rank小的树连接到rank大的树上

路径压缩:
对于每个节点,一旦向上走到了一次根节点,就把这个点到父亲的边改为直接连向根节点。
这里写图片描述

上述方法不仅针对查询的节点,在查询过程中经过的所有结构都改为连接到根节点上。
这里写图片描述

并查集的复杂度

上述的优化方法加入后并查集的查询速度会非常高。对n个元素的并查集进行一次操作的品均复杂度是O(α(n))。α函数是阿克曼函数的反函数。这是比O(log(n))更快。

并查集的实现

#include <iostream>

#define  MAX_N 1000
using namespace std;

int par[MAX_N], rank[MAX_N];  //par表示parent代表父亲编号,par[x]指的是x的父节点编号

//初始化n个元素
void init(int n) {
    for (int i = 0; i < n; i++) {
        par[i] = i;
        rank[i] = 0;
    }
}

//查询树的根
int find(int x) {
    if (par[x] == x) {
        return x;
    } else {
        return par[x] = find(par[x]);
    }
}

//合并x和y所属的集合
void unite(int x, int y) {
    x = find(x);
    y = find(y);
    if (x == y) return;
    if (rank[x] < rank[y]) {
        par[x] = y;
    } else {
        par[y] = x;
        if (rank[x] == rank[y]) rank[x]++;
    }
}
//x和y是否属于同一集合
bool same(int x,int y){
    return find(x) ==find(y);
}
int main() {

}

并查集问题——食物链(POJ 1182)

问题:
有N只动物,分别编号为1,2,……,N。所以动物都属于A,B,C中的其中一种。已知A吃B, B吃C,C吃A。按顺序给出下面两种信息共K条。

  • 第一种:x和y属于同一物种
  • 第二种:x吃y。

然而这些信息可能会出错。有可能有的信息和之前给的信息矛盾,也有可能给出的x和y不在1,2,……N的范围内。求在k条信息中有多少条是不正确的。计算过程中,我们将忽略诸如此类的错误信息。

限制条件
1N50000 1 ⩽ N ⩽ 50000
0K100000 0 ⩽ K ⩽ 100000

输入示例
N=100, K=7
信息:
第一种,x=100,y=1
第二种,x=1,y=2
第二种,x=2,y=3
第二种,x=3,y=3
第一种,x=1,y=3
第二种,x=3,y=1
第一种,x=5,y=5
输入示例
3(1,4,5条是不正确的)

思路:
对于每个动物i创建3个元素i-A, i-B,i-C,并用这3×N个元素建立并查集。这个并查集维护如下信息:

  • i-x表示 “i属于种类x”
  • 并查集的每一个组表示都发生或都不发生

比如:如果i-A和j-B在同一组里,就表示如果i属于种类A那么j一定属于种类B,如果j属于种类B,那么i一定属于种类A。因此对于每一条信息只需要按下面的操作就可以了。

  • 第一种,x和y属于同一物种,合并x-A和y-A、x-B和y-B、x-C和y-C.
  • 第二种,x吃y,合并x-A和y-B,x-B和y-C,x-C和y-A
    在合并之前需要判断合并后是否产生矛盾。例如在第一种的情况下需要检查比如x-A和y-B或者y-C是否在同一组。

代码如下:

//bingcaji.h
// Created by wyc on 18-7-19.
//

#ifndef UNTITLED2_BINGCAJI_H
#define UNTITLED2_BINGCAJI_H
#include <iostream>

#define  MAXN 1000
using namespace std;

int par[MAXN], ranks[MAXN];  //par表示parent代表父亲编号,par[x]指的是x的父节点编号

//初始化n个元素
void init(int n) {
    for (int i = 0; i < n; i++) {
        par[i] = i;
        ranks[i] = 0;
    }
}

//查询树的根
int find(int x) {
    if (par[x] == x) {
        return x;
    } else {
        return par[x] = find(par[x]);
    }
}

//合并x和y所属的集合
void unite(int x, int y) {
    x = find(x);
    y = find(y);
    if (x == y) return;
    if (ranks[x] < ranks[y]) {
        par[x] = y;
    } else {
        par[y] = x;
        if (ranks[x] == ranks[y]) ranks[x]++;
    }
}
//x和y是否属于同一集合
bool same(int x,int y){
    return find(x) ==find(y);
}

#endif //UNTITLED2_BINGCAJI_H

//main.cpp
#include <iostream>
#include "bingcaji.h"
#define MAX_N 50000
#define MAX_K 100000


using namespace std;

int N, K;
int T[MAX_N], X[MAX_K], Y[MAX_K];

//在这里省略了并查集的部分代码
void solve() {
    //输入信息
    cin>>N>>K;
    for(int i=0;i<K;i++){
        cin>>T[i]>>X[i]>>Y[i];
    }
    //初始化并查集
    //x,x+N,x+N*2 分别表示x-A,x-B,x-C
    init (N*3);
    int ans=0;
    for(int i=0;i<K;i++){
        int t=T[i];
        int x=X[i]-1,y=Y[i]-1;
        if(x<0 || x>=N || y<0 || y>=N){
            ans++;
            continue;
        }
        if(t==1){//x和y属于同一类型
            if(same(x,y+N) || same(x, y+2*N)){
                ans++;
            }else{
                unite(x,y);
                unite(x+N,y+N);
                unite(x+N*2,y+N*2);
            }
        }else{
            if(same(x,y) || same(x,y+2*N)){
                ans++;
            }else{
                unite(x,y+N);
                unite(x+N,y+2*N);
                unite(x+2*N,y);
            }

        }
    }
    cout<<ans;
}

int main(){
    solve();
    return 0;
}

输入数据:
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
输出数据:
3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值