并查集
- 将两个集合合并
- 询问两个元素是否在一个集合里面
o(1)
基本原理:
- 用树的形式来维护集合,根节点的编号就是集合的编号
- 每个节点都存储父节点,p[x]表示父节点
问题:
- 如何判断树根:if(p[x] == x)
- 如何求x的集合编号:while(p[x] != x) x = p[x];
- 如何合并两个集合:p[x]为x的集合编号,p[y]为y的集合编号,p[x] = y
优化:
- 找某个节点的根节点的时候,顺手把整条路径上的点的父节点都标记为根节点(路径压缩)
注意:
- 对于一些要读入单个字母的情况,我们可以声明字符数组然后读入字符串,取字符数组的0位置的元素
- 当要用到x的祖宗节点时,不要直接使用p[x], 要用find(x)状态压缩更新一下祖宗节点
朴素并查集算法模板:
int p[N]; //存储每个点的祖宗节点
// p[x] == x代表这个点是树根
// 返回x的祖宗节点 + 路径压缩
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
维护size(每个集合的元素个数)的并查集算法模板:
int p[N], siz[N];
// p[]存储每个点的祖宗节点, siz[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n, siz[]也要初始化为1
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
siz[i] = 1;
}
// 合并a和b所在的两个集合:
// 有的情况下要判断a和b是否在一个集合里面,假如在一个集合里面还要操作的话集合元素就会翻倍
if(find(a) == find(b)) continue;
else
{
siz[find(b)] += siz[find(a)]; // 注意这里是将a集合合并到b集合
p[find(a)] = find(b);
}
// 查询所在集合元素数量
siz[find(a)]
维护到祖宗节点距离的并查集算法模板:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
食物链
A 吃 B,B 吃 C,C 吃 A。现有 N 个动物,以 1∼N 编号。每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。
第一种说法是
1 X Y
,表示 X 和 Y 是同类。第二种说法是2 X Y
,表示 X 吃 Y。此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X 或 Y 比 N 大,就是假话;
- 当前的话表示 X 吃 X,就是假话。
输出假话的总数
用每个点到根节点之间的距离来表示和根节点的关系
模3余1:可以吃根节点
模3余2:可以被根节点吃
模3余0:和根节点是同类
#include <iostream>
using namespace std;
const int N = 50010;
int n, m;
int p[N], d[N];
// p[]表示元素的根节点,d[]表示到父节点的距离
int find(int x) // 询问根节点
{
if (p[x] != x) // 若自身不是根节点
{
int t = find(p[x]);
// 首先通过find(p[x])来找到父节点的根节点t
d[x] += d[p[x]];
// d[x]表示x到父节点p[x]的距离,d[p[x]]通过前面一步已经更新为p[x]到根节点的距离
// 在通过这一步将d[x]更新为到根节点的距离
p[x] = t;
// 更新父节点为根节点
}
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) p[i] = i;
int res = 0;
while (m -- )
{
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
if (x > n || y > n) res ++ ;
else
{
int px = find(x), py = find(y);
if (t == 1) // x和y是同类
{
if (px == py && (d[x] - d[y]) % 3) res ++ ;
// 如果x和y在同一集合内,且(d[x] - d[y]) % 3 == 0,那么已经是同一类了
else if (px != py) // 不在同一集合内就需要更新到同一集合里面
{
p[px] = py; // 将x所在集合合并到y所在集合
d[px] = d[y] - d[x];
// p[x]是x的根节点,p[y]是y的根节点,d[x]是x到p[x]的距离,d[y]是y到p[y]的距离
// d[px]是x所在集合到y所在集合的根节点py的距离
// x与y同类,那么合并后(d[x] - d[y]) % 3 == 0
// 然而合并的时候d[x] = d[x] + d[px]
// 所以(d[x] + d[px] - d[y]) % 3 == 0
// d[px] = d[y] - d[x]
}
}
else // x吃y
{
if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
// 如果x和y在同一集合内,且(d[x] - d[y] - 1) % 3 == 0,那么可以表示x吃y
else if (px != py)
{
p[px] = py;
d[px] = d[y] + 1 - d[x];
// 合并后(d[x] - d[y] - 1) % 3 == 0
// 合并的时候d[x] = d[x] + d[px]
// 所以(d[x] + d[px] - d[y] - 1) % 3 == 0
// d[px] = d[y] - d[x] + 1
}
}
}
}
printf("%d\n", res);
return 0;
}