洛谷P1525 关押罪犯(并查集、二分图判定)

时间:2022-08-07 06:14:53

本人蒟蒻,只能靠题解AC,看到大佬们的解题思路,%%%%%%

https://www.luogu.org/problemnew/show/P1525

题目描述

S城现有两座*,一共关押着N名罪犯,编号分别为1−N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。

我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为c 的罪犯被关押在同一*,

他们俩之间会发生摩擦,并造成影响力为c的冲突事件。每年年末,警察局会将本年内*中的所有冲突事件按影响力从大到小排成一个列表,然后上报到S城Z市长那里。

公务繁忙的Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。在详细考察了N名罪犯间的矛盾关系后,警察局长觉得压力巨大。

他准备将罪犯们在两座*内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一*内的某两个罪犯间有仇恨,

那么他们一定会在每年的某个时候发生摩擦。那么,应如何分配罪犯,才能使Z市长看到的那个冲突事件的影响力最小?这个最小值是多少?

输入输出格式

输入格式:

每行中两个数之间用一个空格隔开。第一行为两个正整数N,M,分别表示罪犯的数目以及存在仇恨的罪犯对数。接下来的M行每行为三个正整数aj​,bj​,cj​,表示aj​ 号和bj​号罪犯之间存在仇恨,其怨气值为cj​。数据保证1<aj≤bj≤N,0<cj≤1,000,000,000,且每对罪犯组合只出现一次。

输出格式:

共1 行,为Z市长看到的那个冲突事件的影响力。如果本年内*中未发生任何冲突事件,请输出0。

输入输出样例



说明

【输入输出样例说明】罪犯之间的怨气值如下面左图所示,右图所示为罪犯的分配方法,市长看到的冲突事件影响力是3512(由2 号和3 号罪犯引发)。其他任何分法都不会比这个分法更优。

洛谷P1525 关押罪犯(并查集、二分图判定)

洛谷P1525 关押罪犯(并查集、二分图判定)

【数据范围】

对于30%的数据有N≤15。

对于70%的数据有N≤2000,M≤50000。

对于100%的数据有N≤20000,M≤100000。


这是一道鬼知道是什么算法一眼就能看出来是用二分图或并查集的题

下面给出个人认为最易懂的一种方法:

如果要确保在同一个*的罪犯之间的仇恨值最小,就要对仇恨值进行从大到小排序。

确保仇恨值大的两个人不在同一个牢房。当读入二人数据时发现他们已经在同一牢房,则直接输出他们的仇恨值大小。

当我们需要把仇恨值大的人分开始时,就应该采取敌人的敌人就是朋友的原则,开一个e数组维护该点的敌人,他的敌人们可以进入到一个牢房

下面摘自洛谷的几篇题解,仅供学习


作者: ___new2zy___ (推荐)

附语:真是一道好题!!!

希望对并查集和二分图匹配想进一步了解的童鞋好好理解本题

=================================================

题面大意:(人性翻译)

给你m对矛盾关系,每对关系分别涉及到x,y两人,矛盾值为w

请你判断分配x和y到两个集合中,能否避免冲突

如能避免请输出0,如果冲突不可避免,请输出最小的矛盾值

以上是本人自己的“翻译”理解,接下来请看算法分析

=================================================

算法分析:

先来一些没用的~~~

本人在做本题时,注意到题目中的两个*(即两个集合),

就很顺理成章的想到最近看到的图论问题----二分图,

所以本人最开始用的是二分图判定(染色法)A掉了本题

但是我还是太蒟了。。。在某Wang姓巨佬的指导下

("%4^89&8$^%&&*#@!$%^&~~~?')

此处省去一万字大佬的话得知还有一种东西叫做并查集

(还是大佬厉害,我太蒟了= =)调试了半天,终于两种方法都过了,接下来我就讲一下这两种方法 (辣鸡BB结束,接下来是正题)

1.并查集

本题,因为说了有“边权值”(我理解为矛盾值),所以要求出现矛盾情况下的最小边权值显然是需要排序的

那么问题又来了,我们要按照什么方法进行分配呢?

我们不妨这样想:两个人a,b有仇,那么把他们放在一起显然会打起来,那么我们还不如把a与b的其他敌人放在一起,

因为这样可能会出现“敌人的敌人就是朋友”的情况,恰好a与b的其他敌人之间没有矛盾,那么他们就可以放在同一个集合中,反之b对a亦然。

那么我们不妨这样实现: 首先需要并查集初始化

(1)先把所有的矛盾关系按照矛盾值从大到小排一遍序,

(2)接下来每次操作取出一个关系,看矛盾的两个人x和y是否已经分配到同一个集合中(并查集找父亲即可),那么还分如下两种情况:

如果在一起那么显然会打起来(会出现矛盾),那么直接输出当前的边权(矛盾值)即可(此时保证是最小矛盾值,因为已经排序了)

如果不在同一组,则按照“敌人的敌人就是朋友”的原则,把x与y的其他敌人分在同一组,y与x的其他敌人分在同一组

不断进行以上操作最终可以得到答案

以上是第一种dalao做法= =

2.二分图判定

其实对于二分图判定的做法还是比较好想的,(因为我太蒻想不到并查集还要有边权)也易于实现

由于本题要求把罪犯划分到两个*中(我理解为划分到两个不同的集合中)那么我不禁想到图论的二分图

首先,抛来一个二分图的定义:

如果一张无向图的n个节点(n>=2)可以分为A,B两个集合,

且满足A ∩ B = ∅ ,而且在同一集合内的点之间都没有边相连,那么这张无向图被称为二分图,其中A和B分别叫做二分图的左部和右部

那么对于本题,我们就是要把所有人分为两个部分,其间不出现矛盾,显然很符合二分图的要求

别太高兴,问题来了:如何判定这个“矛盾图”是不是二分图

在这里,本人抛出一个二分图判定定理:

一张无向图是二分图:

当且仅当图中不存在奇环(奇环是指长度为奇数的环)

对于该定理的证明,本人就不再赘述(百度吧年轻人)

既然有了判定定理,我们就可以使用染色法进行二分图判定

染色法基本实现如下:

1.大多数情况基于dfs(深度优先搜索)

但本题本人用了bfs实现(害怕爆系统栈)

2.我们尝试用黑和白两种颜色标记图中的点,当一个节点被标记了,那么所有与它相连的点应全部标记为相反的颜色

如果在标记过程中出现冲突,那么算法结束,证明本图中存在奇环,即本图不为二分图;反之,如果算法正常结束,那么证明本图是二分图

那好啦,现在有了染色法判定二分图,那么我们还需要考虑一件事:这个最小矛盾值怎么求?

首先,我们考虑这样一个判定问题:是否存在一种分配方案,使得最小的矛盾值不超过mid。显然,当mid较小时可行的方案对于mid较大时依然可行。换言之,本题的答案具有单调性,可以采用二分的方法求解,将求最小值问题转换为判定问题

策略如下:我们二分答案,设当前二分的值为mid,此时任意两个矛盾双方x和y必须被分在两个不同集合中,将罪犯们作为节点,在矛盾值大于等于mid的罪犯之间连一条边,我们得到一张无向图。此时我们只需判定这张无向图是否为二分图即可(因为要分为两部分),如果是二分图,令二分右端点R=mid,否则令L=mid即可

以上就是第二种思路~~~

=================================================

总结:本人做完本题之后,对比了一下两种做法,发现并查集的效率略高(也许是它有路径压缩,均摊下接近常数级别)

但二分图判定做法也毫不逊色,差不了多少,而且好实现

二者效率比较:

并查集:128ms
二分图判定:168ms

接下来放代码= =两个一起放了~~~

PS:代码中也有解释哦,希望能帮到大家

=================================================

做法一:并查集(自己修改过的)

 #include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
#define MAXN 20010
#define MAXM 100010 struct Edge//定义边:起点u,终点v,边权c
{
int u;
int v;
int c;
}E[MAXM*];
//fa[i]是i的父亲(并查集),Enemy[i]是i的敌人(不能在同一组)
int Enemy[MAXN];
int fa[MAXN];
int n,m;
int MAX; bool cmp(Edge a,Edge b)
{
return a.c>b.c;
} int Find(int n)//并查集找父亲
{
return n==fa[n]? fa[n]:fa[n]=Find(fa[n]);
} int main()
{
freopen("sample.txt","r",stdin);
scanf("%d %d",&n,&m);
memset(Enemy,,sizeof(Enemy));
for(int i=;i<=m;i++)//加边
{
scanf("%d %d %d",&E[i].u,&E[i].v,&E[i].c);
}
sort(E+,E++m,cmp);//按怒气值从大到小排序
//接下来开始合并罪犯
for(int i=;i<=n;i++)
{
fa[i]=i;
}
MAX=;
for(int i=;i<=m;i++)
{
int t1,t2;
t1=Find(E[i].u);
t2=Find(E[i].v);
if(t1==t2)//出现矛盾:直接结束(if(Find(E[i].u)==Find(E[i].v))这种写法是错的)
{
MAX=E[i].c;
break;
}
//其余的就把敌人的敌人与自己分到一组
if(!Enemy[E[i].u])
Enemy[E[i].u]=E[i].v;
else
fa[Find(Enemy[E[i].u])]=Find(E[i].v);
//同上
if(!Enemy[E[i].v])
Enemy[E[i].v]=E[i].u;
else
fa[Find(Enemy[E[i].v])]=Find(E[i].u);
}
printf("%d\n",MAX);
return ;
}

=================================================

做法2:二分图判定(有点难理解)

 #include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
typedef long long ll;
const int inf=1e9+;
inline int read()
{
int p=,f=;char c=getchar();
while(c<''||c>''){if(c=='-')f=-;c=getchar();}
while(c>=''&&c<=''){p=p*+c-'';c=getchar();}
return f*p;}
struct Edge
{
int from,to,w;
}p[];
int n,m,L,R,cnt,head[];
void add_edge(int x,int y,int W)
{
cnt++;
p[cnt].from=head[x];
head[x]=cnt;
p[cnt].to=y;
p[cnt].w=W;
}
bool work(int mid)
//判断以mid为仇恨值是否能形成二分图
{
queue <int> q;
int color[]={};
//以下是染色法判断二分图
for(int i=;i<=n;i++)
if(!color[i])
{
q.push(i);
color[i]=;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=p[i].from)
if(p[i].w>=mid)
{
if(!color[p[i].to])//没染过色
{
q.push(p[i].to);
if(color[x]==)
color[p[i].to]=;
else color[p[i].to]=;
//涂上相反颜色入队
}
else if(color[p[i].to]==color[x])
return false;
//如果出现矛盾直接返回(不为二分图)
}
}
}
return true;//正常结束,证明是二分图
}
int main()
{
n=read(),m=read();
for(int i=;i<=m;i++)//加边(无向图)
{
int x=read(),y=read(),w=read();
R=max(R,w);//二分右端点
add_edge(x,y,w);
add_edge(y,x,w);
}
R++;//别忘了加1(右端点R),左端点L开始为0
while(R>L+)//开始二分判断二分图(答案单调)
{
int mid=(L+R)>>;
if(work(mid))
//染色法判断二分图如果可行就缩小范围
R=mid;
else L=mid;
}
printf("%d",L);//最后左端点即为答案
return ;
}

作者: 星星之火

不同的人有不同的做法,看见题解里一群并查集,说实在并查集做法我是不会的。我的做法是老师随口提到到,二分+判断是否构成二分图

首先我们二分枚举最大的影响力(由于是求最小的最大,满足二分的性质,很容易想到二分),那么很显然,影响力大于我们所枚举的midd的罪犯就必须拆开,那么现在我们为他们之间连出一条边。每条边都连上之后,我们得到了几个连通图(注意是几个,不是只有一个),下面我们要对这些连通图判断它们能否拆成两边,这就是所谓的判断二分图。

只要会二分图判定,这题就已经解决的吧

下面我来介绍一下二分图判定的方法(我只介绍一种,染色法)

首先我们枚举每一个点,如果这个点没有染色,那么我们开始我们的算法。首先把这个点标记成黑色,然后开始bfs,枚举所有与它相连的点,将它们标记成白色(在我的程序里用1和2表示颜色),不断遍历整张图,如果你发现下一个染色的点已经有了颜色,判断是否与此时你在的点颜色一致。若是颜色一致,说明染色存在冲突,无法拆成二分图,直接return false。如果无误,直到队列为空退出(注意只有没染色的点才需要加入队列)。之后我们枚举下一个点,若已染色则跳过,没有的话说明我们找到了一张新的图,接下来我们只需要重复上面的步骤就行了。

很好理解的染色法吧

除了这个我想还有一点需要注意,关于二分时如何改变midd。这样想,如果当前的midd值足够让我拆成二分图,那么我就继续把它再变小(二分的贪心性质嘛),然后就。。。。这题就能A了,知道了原理,不难吧,代码实现也很容易。

下面我附上代码:

 #include<bits/stdc++.h>
using namespace std; const int maxn=+;
int n,m,sum;
int head[maxn];
struct node{int to;int z;int next;}edge[maxn*];
inline int read()
{
char ch;
int fu=,x=;
for (ch=getchar(); ch<=; ch=getchar());
if (ch=='-') fu=-,ch=getchar();
for (x=; ch>; ch=getchar()) x=x*+ch-;
return x*fu;
}
void add(int x,int y,int z)
{
edge[++sum].next=head[x];
edge[sum].to=y;
edge[sum].z=z;
head[x]=sum;
}
bool bfs(int midd)
{
int color[maxn];
memset(color,,sizeof(color));
queue <int> q;
for (int j=;j<=n;j++)
{
if (color[j]) continue;
q.push(j);color[j]=;
do
{
int k=q.front();q.pop();
for (int i=head[k];i;i=edge[i].next)
if (edge[i].z>=midd)
{
if (color[edge[i].to]==) {
color[edge[i].to]=color[k]==?:;
q.push(edge[i].to);
}
else if (color[edge[i].to]==color[k]) return false;
}
}while (!q.empty());
}
return true;
}
int main()
{
int maxx;
n=read();m=read();
for (int i=;i<=m;i++)
{
int a,b,c;
a=read();b=read();c=read();
maxx=max(maxx,c);
add(a,b,c);
add(b,a,c);
}
int l=,r=maxx+,midd;
while (l+<r)
{
midd=(l+r)>>;
if (bfs(midd)) r=midd;
else l=midd;
}
printf("%d",l);
return ;
}

题解 P1525 【关押罪犯】:种类并查集

前言:

在数据结构并查集中,种类并查集属于扩展域并查集一类。

比较典型的题目就是:食物链(比本题难一些,有三个种类存在)

首先讲一下本题的贪心,这个是必须要懂的。我们假设最后Z 市长看到的那个冲突事件的影响力为 x (也就是有一对仇恨值为 x 的罪犯在同一*)那么比 x 仇恨值更高的每一对罪犯必须分配到不同的*(不然,最终答案就不是 x ,而是这一对罪犯的仇恨值了);

所以本题是存在单调性的,只需要从大到小枚举仇恨值,到那一对与前面出现矛盾了,直接输出即可;

思路:

种类并查集中“种类”这个词也不是绝对的,它也可以说是一种关系,而本题的关系就在于要将罪犯分配到的两个*;我们可以将数组开到两倍来模拟这两个*(用A,B表示),每个罪犯在*中都有一个位置。

假设现在要把两个有仇的罪犯分别放到 A 或 B 中,我们发现如果要满足这一对的要求(即分到的*不同),那么如果第一个罪犯在 A *,第二个罪犯必须在 B *,反之也一样。

所以我们可以将 A *中第一个罪犯的位置与 B *中第二个罪犯的位置用并查集维护,即这样合并才能保证分到的*不一样。但第一个罪犯不一定只能在 A *,所以我们将 B *中 第一个罪犯的位置与 A *中第二个罪犯的位置维护。

而出现矛盾的情况,举个例子: a 和 c 有仇,b 和 c 有仇,那么此时 a 和 c 在不同*,b 和 c 也在不同*,也就是说 a 和 b 一定在一个*。可一旦此时 a 和 b 有仇那么就矛盾了,因为a 和 b 要在不同*不然会有矛盾,可 a 和 b 已经在之前判定为必须在同一*,所会矛盾,此时就可以直接输出 a 和 b 的仇恨值(原理参见前言的贪心)

代码实现:

 #include<iostream>
#include<cstdio>
#include<iomanip>
#include<algorithm>
#define rg register int//卡常 using namespace std; struct su{
int f,t,v;
}a[]; int n,m,l,r,mid,f;
int s[<<]; //不要单独开两数组! inline int qr(){ char ch; //快读
while((ch=getchar())<''||ch>'');
int res=ch^;
while((ch=getchar())>=''&&ch<='')
res=res*+(ch^);
return res;
} inline bool cmp(su x,su y){
return x.v>y.v; //从大到小排序
} inline int get(int x){
if(x==s[x])return x;
return s[x]=get(s[x]);//路径压缩
} int main(){
n=qr(),m=qr();
for(rg i=;i<=m;++i)
a[i].f=qr(),a[i].t=qr(),a[i].v=qr();
sort(a+,a+m+,cmp); //贪心
for(rg i=;i<=n;++i)
s[i]=i,s[i+n]=i+n; //初始化不能忘!!!
for(rg i=;i<=m;++i){
rg x=a[i].f,y=a[i].t;
if(get(x)==get(y)||get(x+n)==get(y+n)){
f=a[i].v;break;//不能在同一*的仇人撞上了
}
s[s[x+n]]=s[y];
s[s[x]]=s[y+n];//维护两罪犯在不同*的关系
}printf("%d\n",f);
return ;
}

种类并查集其实你理解了,码量不高(如果算上快读等预处理的话......

一些对种类并查集的理解(血泪史)

代码中开数组是讲了不要开两个单独数组,因为两个独立的数组初始化时,赋的值都是 从 1 开始的,返回的是无区别的11到nn的下标,丢失了“A*”和“B*”的关系。将两数组并查集维护(不好实现)而且会因为有些值重复而让你 WA 到怀疑人生

其次,我们每次判断矛盾时,两个罪犯在A*和B*的位置都要判,而且是每个*单独判!这保证了算法的正确性!