文章目录
    
    
    
    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;
			}
		}
	}
    
    
    二分图判定
   
将两个罪犯分开看作一种对立关系,在他们之间连不带权的边,然后每次分开跑一次二分图判定,直至判定失败时那对罪犯之间的仇恨值即为答案。
    
    
    二分答案策略
   
    
    
    二分图判定
   
二分最大的仇恨值,同理将两个罪犯分开看作对立关系,将所有权值小于二分答案的边所对应的罪犯连边,跑二分图判定,如果判定成功则尝试令答案更小,否则令二分的答案增大继续尝试。
    
    
    小结
   
本题一题多解,灵活变通,是锻炼思维的好题目。从中我们也可以学习到多种多样的知识点。
 
