P1525 [NOIP2010 提高组] 关押罪犯

  • Post author:
  • Post category:其他




P1525 [NOIP2010提高组] 关押罪犯


P1525 [NOIP2010 提高组] 关押罪犯


题意:

题意是有



N

N






N





个罪犯,将他们划分成两个集合,有一些罪犯之间有带权边(一共有



M

M






M





条),如果他们在同一集合中,此边就存在,求如何划分,使得存在的最大的边权最小。

对于



100

%

100\%






1


0


0


%





的数据有



N

20000

,

M

100000

N\leq 20000,M\leq 100000






N













2


0


0


0


0


,




M













1


0


0


0


0


0





关键字眼:划分为

两个集合



最大边权最小



贪心策略

策略:尽可能让仇恨值最大的罪犯在两个不同的监狱中

将罪犯划分入一个集合中,可能会同时产生几条新边,按某种规则将罪犯逐一划入两个监狱,可能不经意间就将仇恨值最大的罪犯划分在一起,不好确定如何划分,所以考虑将罪犯分开。

将罪犯之间的边按边权由大到小排序。



朴素并查集

枚举当前最大的仇恨值的边,让当前仇恨值最大的罪犯在两个不同的监狱中,根据“朋友的朋友是朋友,敌人的敌人是朋友”的原则,将这两名罪犯划分入不同的监狱中,如果当前仇恨值最大的罪犯已经在同一个监狱中,那么当前最大仇恨值就是答案。

for(int i = 1;i <= m;i ++)
{
	if(judge(e[i].from,e[i].to))
	{
		ans = e[i].val;
		break;
	}
	if(ag[e[i].from] > 0) join(ag[e[i].from],e[i].to);//ag[i] 表示 i 的敌人,即不连边的两个罪犯
	else ag[e[i].from] = e[i].to;
	if(ag[e[i].to] > 0) join(ag[e[i].to],e[i].from);
	else ag[e[i].to] = e[i].from;
}



种类并查集(扩展域并查集)

继续考虑“敌人的敌人是朋友”这一原则,根据以上的做法可以引申出种类并查集(又称扩展并查集)的做法。

考虑“敌人的敌人是朋友”这一原则,用一个并查集维护罪犯之间的关系,称之为原域,为了记录敌人,再开一个并查集,称之为扩展域。

在这里插入图片描述

现在假设



i

i






i









j

j






j





敌对,那么根据上述定义,令原域的



i

i






i





与扩展域的



j

j






j





连边,再令原域的



j

j






j





与扩展域的



i

i






i





连边,表达敌对关系。若此时有另一敌对关系



(

j

,

k

)

(j,k)






(


j


,




k


)





,那么完成连边后,可以得到下图:

在这里插入图片描述

这时我们可以发现,如果我们在原域查询



i

,

k

i,k






i


,




k





,可以得到友好关系,完成了“敌人的敌人是朋友”的表达。

由此可知:种类并查集可以维护对立关系,而且可以维护多对等价双向的对立关系,可以维护“敌人的敌人是朋友”这一原则。

int main()
{
	scanf("%d%d",&N,&M);
	for(int i = 1;i <= M;i++)
		scanf("%d%d%d",&r[i].a,&r[i].b,&r[i].c);
	sort(r + 1,r + 1 + M,cmp);
	for(int i = 1;i <= N*2/*增设扩展域*/;i ++) fa[i] = i;
	for(int i = 1;i <= M;i ++)
	{
		if(judge(r[i].a,r[i].b))
		{
			printf("%d",r[i].c);
			return 0;
		}
		merge(r[i].a,r[i].b + N);//由于对立关系有对称性
		merge(r[i].b,r[i].a + N);//所以要录入两次
	}
	printf("0");
	return 0;
}



带权并查集

我们可以考虑只用一倍空间的并查集,但是运用完全不一样的思路。

对于“敌人的敌人是朋友”这一原则,我们考虑用模 2 运算表达,那么每次连边合并,我们就要赋予边权。

现在考虑实现以下要求:

  • 敌人之间的路径权值模 2 为 1
  • 朋友之间的路径权值模 2 为 0

容易想到以下构造:

在这里插入图片描述

黑色标号为加边的顺序,红色标号为边的权值,那么这个关系是



r

t

rt






r


t









f

x

f_x







f










x





















敌对,



f

x

f_x







f










x

























x

x






x





敌对,而



r

t

rt






r


t









x

x






x





友好,同时它们之间的路径权值和模 2 为 0.

现在我们定义



v

a

l

(

x

)

val(x)






v


a


l


(


x


)





表示



x

x






x





到父亲结点的路径上的权值和。



考虑能否在带权并查集上实现路径压缩

可以实现路径压缩,但是路径压缩的过程中,被压缩掉的边的权值和要记录下来,并与结点到父结点的边权一同组成新边权,这条新边从这个结点到根结点。





v

a

l

(

x

)

=

v

a

l

(

x

)

+

S

val'(x) = val(x) + S






v


a



l






















(


x


)




=








v


a


l


(


x


)




+








S







其中



S

S






S





可以在递归的过程中计算出,并最后赋值在



v

a

l

(

f

x

)

val'(f_x)






v


a



l






















(



f










x


















)






代码如下:

int findx(int x)
{
	int tmp = fa[x];
	if(tmp != x)
	{
		fa[x] = findx(fa[x]);
		val[x] = (val[x] + val[tmp])%2'
		return fa[x];
	}
	else return tmp;
}


考虑如何合并两点所在的两个不同的集合

合并两点所在的两个不同集合,关键在于修改这两个集合的根之间的边的权值,即知道



x

x






x









f

x

f_x







f










x





















之间的关系,知道



y

y






y









f

y

f_y







f










y





















之间的关系,知道



x

x






x









y

y






y





的关系,求



f

x

f_x







f










x

























f

y

f_y







f










y





















之间的关系。

现在我们压缩路径之后,得到



v

a

l

(

x

)

val(x)






v


a


l


(


x


)





表示



x

x






x





到根结点的边权和。

在这里插入图片描述

所以在合并两个结合的时候,要修改



x

x






x





的根结点



f

x

f_x







f










x

























f

y

f_y







f










y





















的权值,令权值



v

a

l

(

f

x

)

=

v

a

l

(

y

)

v

a

l

(

x

)

+

r

val(f_x) = val(y) – val(x) + r






v


a


l


(



f










x


















)




=








v


a


l


(


y


)













v


a


l


(


x


)




+








r




void join(int x,int y)
{
	int rx = findx(x);int ry = findx(y);
	fa[rx] = ry;
	val[rx] = (((val[y] - val[x])%2 + 2)%2 + 1) % 2;
	return ;
}


考虑如何计算两点间的权值

一般来说,只有当两点在同一连通块中才可以计算两点间的权值,那么令



x

x






x





到根结点的权值和为



v

a

l

(

x

)

val(x)






v


a


l


(


x


)





,令



y

y






y





到根结点的权值和为



v

a

l

(

y

)

val(y)






v


a


l


(


y


)





,同样观察上图的情况,可以得到



r

=

v

a

l

(

x

)

v

a

l

(

y

)

r = val(x) – val(y)






r




=








v


a


l


(


x


)













v


a


l


(


y


)




综上,我们得到了带权并查集的基本操作,于是就可以运用它来解题。

int findx(int x)//以后还是严格按照这个格式写,否则容易出错
{
	int tmp = fa[x];
	if(tmp != x)
	{
		fa[x] = findx(tmp);
		val[x] = (val[x] + val[tmp])%2;//压缩权值
		return fa[x];
	}
	else return tmp;
}

void join(int x,int y)
{
	int rx = findx(x);int ry = findx(y);
	fa[rx] = ry;
	val[rx] = (((val[y] - val[x])%2 + 2)%2 + 1) % 2;//更新其中一个根结点与它的新父亲的边的权值
	return ;
}

int main()
{
	scanf("%d%d",&N,&M);
	for(int i = 1;i <= M;i ++)
		scanf("%d%d%d",&r[i].a,&r[i].b,&r[i].c);
	sort(r + 1,r + 1 + M,cmp);
	for(int i = 1;i <= N;i ++) fa[i] = i,val[i] = 0;
	for(int i = 1;i <= M;i ++)
	{
		int ra = findx(r[i].a);int rb = findx(r[i].b);
		if(ra != rb) join(r[i].a,r[i].b);
		else
		{
			int newrel = ((val[r[i].a] - val[r[i].b])%2 + 2)%2;//计算两点间的权值
			if(newrel == 0)
			{
				printf("%d",r[i].c);
				return 0;
			}
		}
	}



二分图判定

将两个罪犯分开看作一种对立关系,在他们之间连不带权的边,然后每次分开跑一次二分图判定,直至判定失败时那对罪犯之间的仇恨值即为答案。



二分答案策略



二分图判定

二分最大的仇恨值,同理将两个罪犯分开看作对立关系,将所有权值小于二分答案的边所对应的罪犯连边,跑二分图判定,如果判定成功则尝试令答案更小,否则令二分的答案增大继续尝试。



小结

本题一题多解,灵活变通,是锻炼思维的好题目。从中我们也可以学习到多种多样的知识点。



版权声明:本文为bell041030原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。