并查集 + Tarjan算法

并查集 + Tarjan 算法

并查集是一种用于找出一个森林(图)中树(连通分支)的个数的算法,也可用于判断两个节点是否在同一棵树上。它在每一棵树(连通分支)上选择一个节点作为本棵树(连通分支)的代表。对于给定两个节点,如果他们具有相同的代表节点,则说明两个节点在同一个树(连通分支)上。

一、并查集的简单应用

例题 1:城市群的数量

题目描述:

魔法大陆上有 n 个城市,编号为 1 到 n。城市与城市之间的道路均为双向道路,共有 m 条双向道路,并非任意两个城市之间都有双向道路。问,魔法大陆上有多少个城市群?
若两个城市之间存在一条双向道路,则两个城市属于同一个城市群。任意两个城市之间最多只有一条双向道路。

输入格式:

第一行包含两个整数 n,m,含义与问题描述中相同。接下来 m 行,每行包含两个整数 u,v,表示城市 u 和城市 v 之间存在一条双向道路。

输出格式:

输出共一行,包含一个整数,表示城市群的数量。

代码示例:

# DFS 暴力做法
from sys import setrecursionlimit
setrecursionlimit(1000000)
def dfs(cur):
    # traversal from the city whose index is cur
    global n,g,visited
    visited[cur] = True
    # search next city
    for i in g[cur]:
        if visited[i]:
            continue
        dfs(i) 

def counter():
    # traversal all if the city,everytime it uses the dfs() function,add one to the answer 
    global n,visited
    ans = 0
 
    for i in range(1,n+1):
        if visited[i]:
            continue
        # hasn't been visited yet
        ans += 1
        # mark all of the cities that belong to the same group 
        dfs(i)
    return ans

# main part
n,m = map(int,input().split())
# create a list to record the roads
g = [[] for i in range(n+1)]
for _ in range(m):
    u,v = map(int,input().split())
    g[u].append(v)
    g[v].append(u)
# create a visited list to record whether the city has been visited or not 
visited = [False for i in range(n+1)]
visited[0] = True
print(counter())
# 并查集模板题
# 找出x所在树的根节点
def find(x):
    if pre[x] != x:
        return find(pre[x])
    return x
# 判断两个节点是否在同一个树(城市群)上,如果不在,则合并两个城市群并计数
def join(x,y):
    global n
    x_root = find(x) # 找出x所在树的根节点
    y_root = find(y) # 找出y所在树的根节点
    if x_root != y_root:
        # 两个节点不在同一个树上(城市群),将两个城市群合并,以后再碰到这两个树的节点就不会重复计数了,保证每一颗树只计数一次
        pre[x_root] = y_root # 将x_root变成y_root的子节点,合并两树
        # 初始时有n个节点,彼此没有路径关系,视为n个城市群
        # 随着道路关系的引入,城市群不断合并,n就是城市群的数量
        n -= 1  

# 主程序
n,m = map(int, input().split())
# 注意序号从1开始
pre = [i for i in range(n+1)]
for _ in range(m):
    u,v = map(int,input().split())
    join(u,v)
print(n)
# 优化后的并查集,在找到x的根节点后直接将x的前驱节点改为根节点,缩短x的子节点查找根节点的路径长度
n, m = map(int, input().split())
p = list(range(n + 1))
def find_root(x):
    if p[x] == x:
        return x
    p[x] = find_root(p[x])
    return pre[x]
for i in range(m):
    u, v = map(int, input().split())
    u_root = find_root(u)
    v_root = find_root(v)
    if u_root != v_root:
        p[u_root] = v_root
        n-=1
print(n)

例题 2:修改数组(第10届蓝桥杯省赛真题)

题目描述:

给定一个长度为 N N N 的数组 A = [ A 1 , A 2 , ⋅ ⋅ ⋅ , A N ] A = [A_1, A_2, · · ·, A_N] A=[A1,A2,⋅⋅⋅,AN],数组中有可能有重复出现的整数。
现在小明要按以下方法将其修改为没有重复整数的数组。小明会依次修改 A 2 , A 3 , ⋅ ⋅ ⋅ , A N A_2, A_3, · · ·, A_N A2,A3,⋅⋅⋅,AN
当修改 A i A_i Ai 时,小明会检查 A i A_i Ai 是否在 A 1 ∼ A i − 1 A_1 ∼ A_{i−1} A1Ai1 中出现过。如果出现过,则小明会给 A i A_i Ai 加上 1 ;如果新的 A i A_i Ai 仍在之前出现过,小明会持续给 A i A_i Ai 加 1 ,直到 A i A_i Ai 没有在 A 1 ∼ A i − 1 A_1 ∼ A_{i-1} A1Ai1 中出现过。
A N A_N AN 也经过上述修改之后,显然 A A A 数组中就没有重复的整数了。现在给定初始的 A A A 数组,请你计算出最终的 A A A 数组。

输入格式:

第一行包含一个整数 N。
第二行包含 N 个整数 A 1 , A 2 , ⋅ ⋅ ⋅ , A N A_1, A_2, · · ·, A_N A1,A2,⋅⋅⋅,AN

输出格式:

输出 N 个整数,依次是最终的 A 1 , A 2 , ⋅ ⋅ ⋅ , A N A_1, A_2, · · ·, A_N A1,A2,⋅⋅⋅,AN

代码示例:

# 并查集
def find(x):
    if x == f[x]:
        # 找到还没有出现过的元素
        return x
    p = x
    while p != f[p]:
        p = f[p]
    f[x] = p
    return p
 
n = int(input())
a = [int(i) for i in input().split()]
f = [i for i in range(1000001)]  # 使用a[]中元素的最大值作为并查集数组容量
for i in range(n):
    # 更新a[]
    a[i] = find(a[i])
    # 更新并查集
    f[a[i]] = find(a[i]+1) 
print(' '.join(list(map(str, a))))

二、Tarjan 算法

(1)算法作用

T a r j a n Tarjan Tarjan 算法是DFS序和并查集的结合应用,可以高效地求出树上两点的最近公共祖先( L C A LCA LCA),求出的 L C A LCA LCA 可以用于求树上两点之间的最短距离、树上差分等问题。

(2)算法思路

  1. 由于 T a r j a n Tarjan Tarjan 算法是一种离线算法,所以要先将所有的查询操作存储起来,等待一并处理。
  2. 以树的根节点作为入口进行DFS遍历,同时利用并查集维护当前节点的父节点
  3. 在遍历当前节点时,标记当前节点
  4. 先遍历当前节点的所有孩子节点,如果未被访问,DFS这个孩子,然后调用并查集将这个孩子的父节点标记为当前节点
  5. 遍历以当前节点为主节点的所有询问请求,如果当前节点的询问请求的另一个节点已经有标记了,那么这个询问的答案就是另一个节点此时的父节点,记录这个答案

(3)算法模板

def find(x):
    if x == fa[x]:
        return x
    p = x
    while p != fa[p]:
        p = fa[p]
    # 合并路径版的并查集
    fa[x] = p
    return p

def tarjan(x):
    visited[x] = True
    # 遍历所有子节点
    for i in e[x]:
        if visited[i]:
            continue
        tarjan(i)
        fa[i] = x
    # 检查以x为主元素的查询
    for t in query[x]:
        if visited[t[0]]:
            ans[t[1]] = find(t[0])

n,m,s = map(int,input().split())
e = [[] for _ in range(n+1)] # 存储边的关系
visited = [False]*(n+1)
# 并查集
fa = [i for i in range(n+1)]
# 存储查询,元素类型是二元组
query = [[] for _ in range(n+1)]
# 存储结果
ans = [-1]*m
# 接收输入,构造树
for _ in range(n-1):
    x,y = map(int,input().split())
    e[x].append(y)
    e[y].append(x)
# 接收查询信息
for i in range(m):
    x,y = map(int,input().split())
    # 在记录每一组查询的同时记录查询的次序
    query[x].append((y,i))
    query[y].append((x,i))

tarjan(s)
# 输出结果
for i in ans:
    if i == -1:
        continue
    print(i)

扩展应用:计算树上两点之间的最短距离

L C A LCA LCA 的最基本应用是求树上两点之间的最短距离,公式如下:

d i s t ( x , y ) = d e e p [ x ] + d e e p [ y ] − 2 ∗ d e e p [ L C A ( x , y ) ] dist(x,y) = deep[x] + deep[y] - 2*deep[LCA(x,y)] dist(x,y)=deep[x]+deep[y]2deep[LCA(x,y)]

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值