前置知识
- 并查集
- 路径压缩与按秩合并
“边带权”并查集
并查集实际上是由若干棵树构成的森林,我们可以在树中的每条边上记录一个权值,即维护一个数组d,用d[x]保存节点x到父节点fa[x]之前的边权。在每次路径压缩后,每个访问过的节点都会直接指向树根,如果我们同时更新这些节点的d值,就可以利用路径压缩的过程来统计每个节点到树根之间的路径上的一些信息。这就是所谓“边带权” 的并查集。
例题:
POJ1773
题目描述:见《算法竞赛进阶指南》P190
解题思路1:边带权
本题可作为“边带权”和“扩展域”模板题整理。与一般的并查集不同的是,本题传递关系不止一种:
- 若x1和x2奇偶性相同,x2与x3奇偶性也相同,则x1与x3奇偶性相同。
- 若x1和x2奇偶性相同,x2与x3奇偶性不同,则x1与x3奇偶性不同。
- 若x1和x2奇偶性不同,x2与x3奇偶性不同,则x1与x3奇偶性相同。
另外,本题的N太大,而M却很小,可以使用离散化方法,所以顺便复习一下离散化。
为了处理本题的多种传递关系,我们可以采用一种“边带权”的并查集。具体操作是用边权d[x] = 0 表示 x 与 par[x] 奇偶性相同;为1表示 x 与 par[x] 奇偶性不同。于是在路径压缩时,就可以通过对路径上边权做异或运算,即可得到 x 与树根的奇偶性关系。
于是对于每一个询问的(l , r , ans),我们假设回答"odd"时,ans = 1,“even”时ans = 0,我们令 x 、 y 分别代表l-1和r离散化后的序号,如果x和y在同一个集合,则判断d[x] ^ d[y] 是否等于 ans,若不等则冲突;如果x和y不在同一个集合内,则合并 x 和 y ,此时需要注意如何更新d数组。(代码 code - 1 详解)
解题思路2:扩展域
本题还可以使用“扩展域”的并查集。
我们把每个变量x拆分成两个节点x_odd和x_even。其中用 x_odd 表示1~x有奇数个1,x_even表示有偶数个。我们把这两个节点称为x的“奇数域”与“偶数域”。
对于每个问题,假设在离散化后的 l-1 与 r 的值分别是x和y,设ans表示该问题的回答(0代表回答偶数个,1代表回答奇数个)。
- 若ans = 0,x_odd 与 y_even在同一个集合,则与答案矛盾;否则合并x_odd与y_odd,x_even与y_even,这表示[l , r]中有偶数个1。
- 若ans = 1,x_odd与y_odd在同一个集合,则与答案矛盾;否则合并x_odd与y_even,x_even与y_odd。
代码见code - 2.
参考书目
- 《算法竞赛进阶指南》,李煜东,P190.
代码示例:code - 1
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 21000;
struct Query{
int l,r,ans;
} q[MAXN];
int a[MAXN],par[MAXN],d[MAXN],cnt = 0;
/*
a数组用来离散化,par[x]代表x的父节点编号,d[x]代表
从x到根节点路径和,cnt也是用于离散化。
这里根节点即集合的代表节点。
*/
int n,m;
int Find(int x){
if(par[x] == x) return x; //找到根节点,返回
int root = Find(par[x]); //临时存根节点
d[x] ^= d[par[x]]; //路径压缩,距离数组d也应该更新
return par[x] = root; //路径压缩
}
int main(){
scanf("%d%d",&n,&m);
char rs[10];
for(int i = 1;i <= m;i++){
scanf("%d%d%s",&q[i].l,&q[i].r,rs);
if(rs[0] == 'o') q[i].ans = 1;
else q[i].ans = 0;
a[cnt++] = q[i].l-1; //这里需要存l-1,为了使l也包含在内
a[cnt++] = q[i].r;
}
//以下两行代码离散化用
sort(a,a+cnt);
int len = unique(a,a+cnt)-a;
for(int i = 0;i <= len;i++) par[i] = i; //初始化爸爸数组
for(int i = 1;i <= m;i++){
//左右边界在离散化数组中的编号:
int x = lower_bound(a,a+len,q[i].l-1)-a;
int y = lower_bound(a,a+len,q[i].r)-a;
//左右边界的祖先分别为ta和tb
int ta = Find(x),tb = Find(y);
if(ta == tb){ //如果已经在同一个集合内,则判断是否矛盾
if((d[x]^d[y]) != q[i].ans){//矛盾的条件就是事实和结果不同
printf("%d\n",i-1);
return 0;
}
}else{
par[ta] = tb;d[ta] = d[x]^d[y]^q[i].ans;
//这里d[ta]的值需要推导一下。
/*
我们将x所在的集合和y所在的集合合并,那么就需要更新d数组
这里d[x]存放的是x到par[x]的距离,所以合并两个集合对于
非根节点来说并无影响,关键是对于两个根d[ta]和d[tb]如何处理
如果我们将ta代表的集合归于tb下,那么就需要更新d[ta],而已知
ans = d[x]^d[y]^d[ta],即x到y的总路径等于x到ta XOR y到tb XOR
ta到tb;对上式变形,得:d[ta] = ans^d[x]^d[y],而后三者是已知,
故可以求出d[ta]。
*/
}
}
printf("%d\n",m);
return 0;
}
代码示例:code - 2
#include<cstdio>
#include<algorithm>
using namespace std;
int n,m,t = 0;
const int N = 2*21000;
struct Query{
int l,r,ans;
}Q[N];
int a[N],par[N]; //a用于离散化,par是爸爸数组
int Find(int x){
if(par[x] == x) return x;
return par[x] = Find(par[x]);
}
int main(){
char str[10];
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
scanf("%d%d%s",&Q[i].l,&Q[i].r,str);
Q[i].ans = str[0] == 'o'?1:0;
a[++t] = Q[i].l-1;
a[++t] = Q[i].r;
}
sort(a+1,a+1+t);
n = unique(a+1,a+1+t) - a- 1;
for(int i = 1;i <= 2*n;i++) par[i] = i;
//因为每个节点有两个扩展域,所以n需要*2
for(int i = 1;i <= m;i++){
int x = lower_bound(a+1,a+1+n,Q[i].l-1)-a;
int y = lower_bound(a+1,a+1+n,Q[i].r)-a;
int x_odd = x,x_even = x+n;
int y_odd = y,y_even = y+n;
if(Q[i].ans == 0){
if(Find(x_odd) == Find(y_even)){ //答案与事实不符
printf("%d\n",i-1);
return 0;
}
par[Find(x_odd)] = Find(y_odd);
par[Find(x_even)] = Find(y_even);
}else{
if(Find(x_odd) == Find(y_odd)){ //答案与事实不符
printf("%d\n",i-1);
return 0;
}
par[Find(x_odd)] = Find(y_even); //合并x为奇,y为偶
par[Find(x_even)] = Find(y_odd); //合并x为偶和y为奇
}
}
printf("%d\n",m);
return 0;
}