并查集kuangbin专题

本文深入探讨并查集算法的应用,包括初始化、路径压缩、合并操作等核心概念,并通过多个实例解析,如NOIP2010关押罪犯、NOI2002银河英雄传说、hdu3038序列求和、食物链问题及POJ1984带权并查集,展示了并查集在解决复杂关系问题中的强大能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

把内容迁到这里来了,作为一部分补充

初始化:

for(int i = 1;i <= n; ++i)
 f[i] = i;

路径压缩:

int find(int t)
{
    if(f[t] == t) return t;
    f[t] = find(f[t]);
    return f[t];
}

合并:

f[find(x)] = find(y);

NOIP2010关押罪犯:构造虚点
假设x号罪犯与y号罪犯是仇人,x+n号罪犯与y+n号罪犯是仇人,那么可以推出:如果x与y在同一个监狱了,则x+n号犯人与y+n号也在同一个监狱里,一定会发生冲突

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;

struct node
{
    int x,y,z;
}a[100010];

int n,m,f[50000];

bool cmp(node k,node p)
{
    return k.z > p.z;
}

int find(int x)
{
    if(f[x] == x) return x;
    f[x] = find(f[x]);
    return f[x];
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n * 2; ++i) f[i] = i;
    for(int i = 0;i < m; ++i) scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
    sort(a , a + m , cmp);
    for(int i = 0;i < m; ++i)
    {
        if(find(a[i].x) == find(a[i].y))
        {
            printf("%d\n",a[i].z);
            return 0;
        }
        f[find(a[i].x)] = find(a[i].y + n);
        f[find(a[i].y)] = find(a[i].x + n);
    }
    printf("0\n");
    return 0;
}

 

NOI2002银河英雄传说
在应用并查集的通识维护每列的总数以及每列中战舰到队列最前面的距离

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;

int f[30010],d[30010],last[30010];
int n,x,y;
char c;

int find(int x)
{
    if(f[x] == x) return x;
    int p = f[x];
    f[x] = find(f[x]);
    d[x] += d[p];
    return f[x];
}

int main()
{
    for(int i = 1;i <= 30000; ++i)
    {
        d[i] = 0;
        f[i] = i;
        last[i] = 1;
    }
    scanf("%d",&n);
    for(int i = 0;i < n; ++i)
    {
        cin>>c;
        scanf("%d%d",&x,&y);
        //cout<<c<<endl;
        if(c == 'M')
        {
            x = find(x);
            y = find(y);
            f[x] = y;
            d[x] += last[y];
            last[y] += last[x];
        }
        else
        {
            if(find(x) != find(y)) printf("-1\n");
            else
            {
                printf("%d\n",abs(d[x] - d[y]) - 1);
               // printf("%d %d",d[x],d[y]);
            }
        }
    }
    return 0;
}

hdu 3038 

题目:给定一个序列(事先不知道序列的值),然后询问,从第a个数到第b个数的和是多少,如果与之前的情况相冲突,则ans++

带权并查集

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;

typedef long long ll;
const int maxn = 200000 + 5;
int n, m;
int fa[maxn];
int sum[maxn];

int findz(int x)
{
	if (x == fa[x]) return x;
    int f = fa[x];
    fa[x] = findz(fa[x]);
    sum[x] += sum[f];
    return fa[x];
}

/**
把左端点a和右端点b当成两个不同的量
求出sum[a],sum[b],如果他们属于同一个根(左端点相同),
        只需要判断sum[a] + c 和sum[b] 是否相等即可
否则,无法找到矛盾,认为这个答案是合法的,进行更新
*/
bool unite(int x, int y, int c)  
{
	int a = findz(x), b = findz(y);
	if (a == b)
	{
		if (sum[x] + c != sum[y]) return 0;
		return 1;
	}
	else
	{
		fa[b] = a;
		sum[b] = sum[x] - sum[y] + c;
		return 1;
	}
}

int main()
{
	int a, b, c;
	while(~scanf("%d%d", &n, &m))
	{
		int ans = 0;
		/**
		sum数组记录前缀和
		*/
        for(int i = 0; i <= n; i++) fa[i] = i,sum[i] = 0;
		for(int i = 0; i < m; i++)
		{
			scanf("%d%d%d",&a,&b,&c);
			if(!unite(a - 1, b, c)) ans++;
		}
		printf("%d\n",ans);
	}
	return 0;
}

食物链

这是一个十分经典的种类并查集 && 带权并查集 问题

解法一:

#include<cstdio>
using namespace std;

const int maxn = 1e5 + 10;
int f[maxn],val[maxn];
int n,m,ans,t,x,y;

int findz(int x)
{
    if(f[x] == x) return x;
    int tmp = findz(f[x]);
    val[x] = (val[x] + val[f[x]]) % 3;
    return f[x] = tmp;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n; ++i)
        f[i] = i,val[i] = 0;
    ///val数组记录吃与被吃的关系 ,0表示与父亲同级,1表示吃父亲,2表示被父亲吃
    ans = 0;
    for(int i = 0;i < m;++i)
    {
        scanf("%d%d%d",&t,&x,&y);

        if(x > n || y > n)  ///第二个条件
        {
            ans++;
            continue;
        }

        int a = findz(x),b = findz(y);

        if(a == b)  ///父亲相同
        {
            if(t == 1 && val[x] != val[y]) ans++;
            if(t == 2 && (val[y] + 1) % 3 != val[x]) ans++;
            /**
            满足x吃y,在父亲相同的情况下
            1) x吃父亲,y与父亲是同类    val[x]=1,val[y]=0
            2) x被父亲吃,y吃父亲        val[x]=2,val[y]=1
            3) x与父亲是同类,y被父亲吃  val[x]=0,val[y]=2
            */
        }
        else   ///如果父亲不相同无法根据之前的条件判断x与y的关系
        {
            f[b] = a;
            if(t == 1) val[b] = (val[x] - val[y] + 3) % 3;
            /**
            x与y是同类
            1)b与a是同类,val[b] = 0--------> val[x] = val[y] (包含3种关系,不展开叙述了)
            2)a吃b val[b] = 2 -----> (x与父亲a是同类,y吃父亲b val[x] = 0,val[y] = 1)
                                      (x吃父亲a,y被父亲b吃   val[x] = 1,val[y] = 2)
                                      (x被父亲a吃,y与父亲b是同类  val[x] = 2,val[y] = 0)
            3)a被b吃 val[b] = 1 ----->(x与父亲a是同类,y被父亲b吃 val[x] = 0,val[y] = 2)
                                      (x吃父亲a,y与父亲b是同类   val[x] = 1,val[y] = 0)
                                      (x被父亲a吃,y吃父亲b  val[x] = 2,val[y] = 1)
            */
            else val[b] = (val[x] - val[y] + 2)  % 3;
            /**
            x吃y
            1)a吃b val[b] = 2--------> val[x] = val[y] (包含3种关系,不展开叙述了)
            2)b与a是同类,val[b] = 0 -----> (x与父亲a是同类,y被父亲b吃 val[x] = 0,val[y] = 2)
                                            (x吃父亲a,y与父亲b是同类   val[x] = 1,val[y] = 0)
                                            (x被父亲a吃,y吃父亲b  val[x] = 2,val[y] = 1)
            3)a被b吃 val[b] = 1     ----->  略
            */
        }
    }
    printf("%d\n",ans);
    return 0;
}

poj 1417 背包+dp

一个关于说真话假话的问题orz

#include<iostream>
#include<queue>
#include<cstring>
#include<vector>
#include<cstdio>
using namespace std;

vector<int>v[1000][2];
int f[1000],val[1000],dp[1000][1000];
int a[1000][2];
int n,x,y,p1,p2,tot;
string s;

int findz(int x)
{
    if(f[x] == x) return x;
    int tmp = findz(f[x]);
    val[x] ^= val[f[x]];
    return f[x] = tmp;
}

int main()
{
    while(~scanf("%d%d%d",&n,&p1,&p2) && n + p1 + p2)
    {
        for(int i = 1;i <= p1 + p2; ++i)
        {
            f[i] = i,val[i] = 0;
            v[i][1].clear();
            v[i][0].clear();
            a[i][0] = a[i][1] = 0;
        }
        for(int i = 1;i <= n; ++i)
        {
            scanf("%d%d",&x,&y);
            cin>>s;
            int k = (s == "no");
            int ai = findz(x);
            int bi = findz(y);
            if(ai != bi)
            {
                f[ai] = bi;
                val[ai] = val[x] ^ val[y] ^ k;
            }
        }///把他们分成多个集合,每个集合分为0/1,这里的0/1只是相对的,表示不同类
        tot = 0;
        for(int i = 1;i <= p1 + p2; ++i)
        {
            if(findz(i) == i)
            {
                tot++;
                for(int j = 1;j <= p1 + p2; ++j)
                    if(findz(j) == i) v[tot][val[j]].push_back(j);
                a[tot][1] = v[tot][1].size();
                a[tot][0] = v[tot][0].size();
            }
        }///把每个集合里的人存起来
        memset(dp,0,sizeof(dp));
        dp[0][0] = 1;
        for(int i = 1; i <= tot; i++)
            for(int j = p1; j >= 0;j--)
            {
                if(j >= a[i][0]) dp[i][j] += dp[i - 1][j - a[i][0]];
                if(j >= a[i][1]) dp[i][j] += dp[i - 1][j - a[i][1]];
            }   ///从每个集合里取出0或者1进行dp,看能凑成p1个说真话的人的方案有多少种
        if(dp[tot][p1] != 1)
        {
            printf("no\n");
            continue;
        }
        priority_queue<int,vector<int>,greater<int> >q;  ///打印分组方案
        for(int i = tot;i >= 1;i--)
        {
            if(p1 >= a[i][0] && p2 >= a[i][1] && dp[i - 1][p1 - a[i][0]])
            {
                for(int j = 0; j < v[i][0].size();j++)
                    q.push(v[i][0][j]);
                p1 -= a[i][0];
                p2 -= a[i][1];
            }
            else if(p1 >= a[i][1] && p2 >= a[i][0] && dp[i - 1][p1 - a[i][1]])
            {
                for(int j = 0; j < v[i][1].size();j++)
                    q.push(v[i][1][j]);
                p1 -= a[i][1];
                p2 -= a[i][0];
            }
        }
        while(!q.empty())
        {
            printf("%d\n",q.top());
            q.pop();
        }
        printf("end\n");
    }
    return 0;
}

POJ 1984

带权并查集

把位置坐标分为x轴和y轴,所以有两个权值,x[i]记录i的x轴到其根节点x轴的距离,y[i]记录i的y轴到其根节点y轴的距离,
(压缩路径和合并集合时权值的改变类似于向量的加减)

最后把询问离线存储,再把询问按index排序,之后在询问之前先合并集合,接着再判断就好了

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

const int maxn = 1e5 + 10;
struct node
{
    int x,y,len;
    char dir;
}a[maxn];
struct nod
{
    int x,y,id,idx,ans;
}q[maxn];
struct nodee
{
    int fa,x,y;
}f[maxn];
int n,m,k;

bool cmp1(nod k,nod l)
{
    return k.id < l.id;
}

bool cmp2(nod k,nod l)
{
    return k.idx < l.idx;
}

int findz(int x)
{
    if(f[x].fa == x) return x;
    int tmp = f[x].fa;
    f[x].fa = findz(f[x].fa);
    f[x].x += f[tmp].x;
    f[x].y += f[tmp].y;
    return f[x].fa;
}

void unionz(int x,int y,char dir,int len)
{
    int fx = findz(x);
    int fy = findz(y);
    if(fx != fy)
    {
        f[fx].fa = fy;
        if(dir == 'N')
            f[fx].x = f[y].x - f[x].x + len,f[fx].y = f[y].y - f[x].y;
        else if(dir == 'S')
            f[fx].x = f[y].x - f[x].x - len,f[fx].y = f[y].y - f[x].y;
        else if(dir == 'E')
            f[fx].x = f[y].x - f[x].x,f[fx].y = f[y].y - f[x].y - len;
        else f[fx].x = f[y].x - f[x].x,f[fx].y = f[y].y - f[x].y + len;
    }
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n; ++i)
        f[i].fa = i,f[i].x = f[i].y = 0;
    for(int i = 1;i <= m; ++i)
        scanf("%d%d%d %c",&a[i].x,&a[i].y,&a[i].len,&a[i].dir);
    scanf("%d",&k);
    for(int i = 1;i <= k; ++i)
    {
        scanf("%d%d%d",&q[i].x,&q[i].y,&q[i].id);
        q[i].idx = i;
    }
    sort(q + 1,q + k + 1,cmp1);
    int x = 0;
    for(int i = 1;i <= k; ++i)
    {
        while(++x <= q[i].id) unionz(a[x].x,a[x].y,a[x].dir,a[x].len);
        x--;
        int fx = findz(q[i].x);
        int fy = findz(q[i].y);
        //cout<<fx<<' '<<fy<<endl;
        if(fx != fy) q[i].ans = -1;
        else q[i].ans = abs(f[q[i].x].x - f[q[i].y].x) + abs(f[q[i].x].y - f[q[i].y].y);
    }
    sort(q + 1,q + k + 1,cmp2);
    for(int i = 1;i <= k; ++i) printf("%d\n",q[i].ans);
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值