题解 P1525 【关押罪犯】
题目大意
有n个罪犯,要将它们分成两个集合。给出m对关系(x,y,val),表示若将x,y分到同一集合中,会产生val的怨气值;若未给出关系,则说明(x,y)分到同一集合怨气值为0。询问最小化最大怨气值。n<=20000,m<=100000
思考方向1
首先想到将最优性问题转化为判断性问题,简化求解。
即:枚举最大怨气值maxa,令val小于等于maxa的边(包括边权为0的边)才能存在,判定是否可行。问题转化成如何判定1个图能否被划分成两个完全图。
如果正向判断,会因为各边错综复杂,限制不明确而难以求解。
尝试反向判断,考虑存在合法方案时,补图的相关限制。由于“完全图”的限制,这些实际不存在的边的顶点必须分属两个集合,因此补图为二分图为必要条件。尝试证明充分性,若补图为二分图,则不存在 同一集合的两点间没有连边 的情况,则两集合一定均为完全图。故补图为二分图为充要条件,证毕。
问题转化成如何判定图是否为二分图。
实现方式1
DFS染色判定二分图。单次判定时间复杂度O(N+M)
由于答案具有单调性,二分最大怨气值优化。
综上,二分最大怨气值 + 二分图染色 即可。时间复杂度O((N+M)logM)
实现方式2
二分图判定其实是矛盾关系的判定,考虑并查集。
传统的并查集只能维护点的连通性问题。当点之间存在多种关系,且关系具有传递性时,就要使用 带权并查集 或者 扩展域并查集。
时间复杂度O(MlogM)
关于 带权并查集 与 扩展域并查集
带权并查集在并查集的基础上,通过边权维护各点间的关系。点与根的关系需要通过点与父亲、父亲与根的关系推算;两点间的实际关系需要通过两点与根的关系推算;合并两个集合时需要推算出两根的关系。难点主要在于如何将关系及其传递性用数值和相应的算式体现,以实现查询与合并。
扩展域并查集则另辟蹊径,将1个点拆分成若干虚点,通过合并虚点维护各点间的关系。相比起带权并查集,优点在于仅用到传统的并查集;缺点在于 空间复杂度 为 点数*状态数,而且需要全面考虑体现在扩展域上的所有等价关系。 _插一句题外话:这种“拆点”思想是非常美妙且常用的思想,广泛应用于各种算法/习题。
实际上,这两种应用最经典的体现应该是食物链
回归本题
下文用“敌对”“友好”代替点对在二分图中的关系。
1.带权并查集 解法
设边权1表示敌对,边权0表示友好。关系的传递性利用%的性质体现。
已知x与fa的关系,fa与根的关系,求x与根关系:Rel[x] = (Rel[x] + Rel[Fa[x]])%2;
x,y同属1集合,已知x,y与根的关系,求x与y的关系:rel(x,y) = (Rel[x] + Rel[y])%2
x,y不属1集合,已知x与y的关系,已知x与rootx,y与rooty的关系,求rootx与rooty的关系:Rel[rootx] = (Rel[x] + rel(x,y) + Rel[y])%2
主体代码如下:
int findA(int x)
{
if(Fa[x] == x) return x;
int temp = findA(Fa[x]);
Rel[x] = (Rel[x] + Rel[Fa[x]]) % 2;
return Fa[x] = temp;
}
void Merge(int x,int y,int rootx,int rooty)
{
int rel = (Rel[x] + 1 + Rel[y]) % 2;
Fa[rootx] = rooty;
Rel[rootx] = rel;
}
void Calc()
{
int ans = 0;
for(int i = 1 ; i <= m ; i ++)
{
int x = Edge[i].x , y = Edge[i].y , w = Edge[i].w;
int rootx = findA(x) , rooty = findA(y);
if(rootx != rooty) Merge(x,y,rootx,rooty);
else
{
int rel = (Rel[x] + Rel[y]) % 2;
if(rel == 0)
{
ans = w;
break;
}
}
}
printf("%d\n",ans);
}
2.扩展域并查集 解法
x的敌对域/友好域:x , x + n
询问x,y关系:询问x,y的敌对域x,y是否为同1集合,若是则x,y为同类,否则反之。即if(rootx == rooty) x,y为同类。
维护x,y敌对关系:合并x的敌对域与y的友好域、x的友好域与y的敌对域,即Merge(x,y+n),Merge(x+n,y);
主体代码如下:
int findA(int x)
{
if(Fa[x] == x) return x;
return Fa[x] = findA(Fa[x]);
}
void Merge(int x,int y)
{
int rootx = findA(x) , rooty = findA(y);
Fa[rootx] = rooty;
}
void Calc()
{
int ans = 0;
for(int i = 1 ; i <= m ; i ++)
{
int x = Edge[i].x , y = Edge[i].y , w = Edge[i].w;
if(findA(x) == findA(y))//两者的敌对域属于同1集合,则两者为同类
{
ans = w;
break;
}
else
{
//将x的敌对域与y的友好域合并,将x的友好域与y的敌对域合并。
Merge(x,y + n);
Merge(x + n ,y);
}
}
printf("%d\n",ans);
}
思考方向二
最小化最大边权,则考虑贪心。 为了最大边权尽可能小,尽可能删除边权较大的边,因此按照边权从大到小排序,依次取出试图将其顶点染成不同的颜色。最终做法与上文相同,不过不失为另一种思考的好方向。