1850H. The Third Letter
dfs and similar, dsu, graphs, greedy, implementation, 1700*, https://codeforces.com/contest/1850/problem/H
In order to win his toughest battle, Mircea came up with a great strategy for his army. He has 𝑛 soldiers and decided to arrange them in a certain way in camps. Each soldier has to belong to exactly one camp, and there is one camp at each integer point on the 𝑥-axis (at points ⋯,−2,−1,0,1,2,⋯).
The strategy consists of 𝑚 conditions. Condition 𝑖 tells that soldier 𝑎𝑖 should belong to a camp that is situated 𝑑𝑖 meters in front of the camp that person 𝑏𝑖 belongs to. (If 𝑑𝑖<0, then 𝑎𝑖's camp should be −𝑑𝑖 meters behind 𝑏𝑖's camp.)
Now, Mircea wonders if there exists a partition of soldiers that respects the condition and he asks for your help! Answer "YES" if there is a partition of the 𝑛 soldiers that satisfies all of the 𝑚 conditions and "NO" otherwise.
Note that two different soldiers may be placed in the same camp.
Input
The first line contains a single integer 𝑡 (1≤𝑡≤100) — the number of test cases.
The first line of each test case contains two positive integers 𝑛 and 𝑚 (2≤𝑛≤2⋅10^5; 1≤𝑚≤𝑛) — the number of soldiers, and the number of conditions respectively.
Then 𝑚 lines follow, each of them containing 3 integers: 𝑎𝑖, 𝑏𝑖, 𝑑𝑖 (𝑎𝑖≠𝑏𝑖; 1≤𝑎𝑖,𝑏𝑖≤𝑛; −10^9 ≤ 𝑑𝑖 ≤ 10^9) — denoting the conditions explained in the statement. Note that if 𝑑𝑖 is positive, 𝑎𝑖 should be 𝑑𝑖 meters in front of 𝑏𝑖 and if it is negative, 𝑎𝑖 should be −𝑑𝑖 meters behind 𝑏𝑖.
Note that the sum of 𝑛 over all test cases doesn't exceed 2⋅105.
Output
For each test case, output "YES" if there is an arrangement of the 𝑛 soldiers that satisfies all of the 𝑚 conditions and "NO" otherwise.
Example
input
4
5 3
1 2 2
2 3 4
4 2 -6
6 5
1 2 2
2 3 4
4 2 -6
5 4 4
3 5 100
2 2
1 2 5
1 2 4
4 1
1 2 3output
YES
NO
NO
YESNote
For the first test case, we can partition the soldiers into camps in the following way: soldier:
- Soldier 1 in the camp with the coordinate 𝑥=3.
- Soldier 2 in the camp with the coordinate 𝑥=5.
- Soldier 3 in the camp with the coordinate 𝑥=9.
- Soldier 4 in the camp with the coordinate 𝑥=11.
For the second test case, there is no partition that can satisfy all the constraints at the same time.
For the third test case, there is no partition that satisfies all the constraints since we get contradictory information about the same pair.
For the fourth test case, in order to satisfy the only condition, a possible partition is:
- Soldier 1 in the camp with the coordinate 𝑥=10.
- Soldier 2 in the camp with the coordinate 𝑥=13.
- Soldier 3 in the camp with the coordinate 𝑥=−2023.
- Soldier 4 in the camp with the coordinate 𝑥=−2023.
这道题可以使用 图的遍历(BFS/DFS) 或者 带权并查集(Weighted DSU) 来解决。
题目分析
每个条件指出:士兵
我们可以将这
- 从
指向 的边,权值为 ,表示 - 从
指向 的边,权值为 ,表示
对于图中的每一个连通分量,我们可以任选一个起点,将其坐标设为
- 当遍历到未确定的节点时,根据边权计算并固定其坐标。
- 当遍历到已确定坐标的节点时,校验当前的计算坐标是否与已有坐标一致。若不一致,说明存在冲突,无法满足所有条件,输出
NO。 - 如果所有连通分量都遍历完毕且没有冲突,则输出
YES。
由于 Python 的默认递归深度限制,使用迭代式的 BFS 可以避免递归栈溢出的问题。
BFS 实现
import sys
from collections import deque
def solve():
# 使用快速输入
input_data = sys.stdin.read().split()
if not input_data:
return
t = int(input_data[0])
idx = 1
out = []
for _ in range(t):
n = int(input_data[idx])
m = int(input_data[idx+1])
idx += 2
# 构建邻接表
adj = [[] for _ in range(n + 1)]
for _ in range(m):
u = int(input_data[idx])
v = int(input_data[idx+1])
d = int(input_data[idx+2])
idx += 3
# x_u = x_v + d
adj[v].append((u, d))
adj[u].append((v, -d))
pos = [None] * (n + 1)
possible = True
# 遍历所有连通分量
for i in range(1, n + 1):
if pos[i] is not None:
continue
# BFS 队列
queue = deque([i])
pos[i] = 0
while queue:
curr = queue.popleft()
for neighbor, weight in adj[curr]:
expected_pos = pos[curr] + weight
if pos[neighbor] is None:
pos[neighbor] = expected_pos
queue.append(neighbor)
elif pos[neighbor] != expected_pos:
possible = False
break
if not possible:
break
if not possible:
break
if possible:
out.append("YES")
else:
out.append("NO")
print('\n'.join(out))
if __name__ == '__main__':
solve()复杂度分析
- 时间复杂度:
。每个节点和每条边在 BFS 过程中最多被访问两次,能够在线性时间内解决。 - 空间复杂度:
。主要消耗在存储图的邻接表 adj和记录坐标的数组pos。
解题思路
- 模型转换:每个士兵看作图中的一个节点,条件
表示 。这意味着如果我们将 设为相对坐标 ,那么 的相对坐标就是 。 - 逻辑判断:这是一个典型的“相对位置约束”问题。如果图中有环,我们需要检查环上的约束是否自洽。
- 例如,如果
到 是 米, 到 是 米, 到 应该是 米。如果给出的条件是 到 为 米,则矛盾。
- 例如,如果
- 实现方法:
- 使用 DFS:对于每个尚未访问的连通分量,将其中的一个节点设为相对坐标
,然后通过 DFS 遍历所有连接的节点,计算它们的相对坐标。 - 如果遇到一个已经访问过的节点,检查其当前的相对坐标是否与计算出的坐标一致。如果不一致,则说明存在矛盾,输出 "NO"。
- 如果所有连通分量都处理完毕且没有矛盾,输出 "YES"。
- 使用 DFS:对于每个尚未访问的连通分量,将其中的一个节点设为相对坐标
DFS 实现
import sys
# 增加递归深度,防止深度较大的图引发溢出
sys.setrecursionlimit(300000)
def solve():
# 读取 n 和 m
try:
line = sys.stdin.readline().split()
if not line: return
n, m = map(int, line)
except ValueError: return
adj = [[] for _ in range(n + 1)]
for _ in range(m):
u, v, d = map(int, sys.stdin.readline().split())
# u = v + d => pos[u] - pos[v] = d
adj[u].append((v, d))
adj[v].append((u, -d))
pos = {} # 存储每个节点的相对坐标
def dfs(u, current_pos):
pos[u] = current_pos
for v, d in adj[u]:
if v in pos:
# 检查是否矛盾
if pos[v] != pos[u] + d:
return False
else:
if not dfs(v, pos[u] + d):
return False
return True
# 处理每个连通分量
for i in range(1, n + 1):
if i not in pos:
if not dfs(i, 0):
print("NO")
return
print("YES")
def main():
line = sys.stdin.readline()
if not line: return
t = int(line)
for _ in range(t):
solve()
if __name__ == '__main__':
main()代码要点解释:
- 邻接表构建:对于条件
,我们建立两条边: 的权重为 的权重为 这样可以保证无论从哪个节点开始遍历,都可以推导出其他节点的相对位置。
pos字典:用于记录访问过的节点及其坐标。如果节点已经在pos中,说明它是第二次被访问,此时必须校验:当前计算的坐标 == 已存在的坐标。- 连通分量:输入图可能不是连通的,因此循环检查
1到n的所有节点,确保所有独立的群组都被检查过。 - 复杂度:时间复杂度为
,空间复杂度为 ,符合题目的数据规模要求 ( )。
带权并查集(Weighted DSU)
是解决此类“相对关系/差分约束”问题的经典且高效的方法。
在带权并查集中,我们不仅维护节点的父亲节点 parent[i],还要维护一个权重数组 weight[i],表示当前节点与其父亲节点之间的相对距离(差值)。
1. 核心原理
我们定义:
即:
路径压缩(Find 操作)
当我们在寻找根节点并进行路径压缩时,需要更新节点到新父亲(根节点)的权重。 假设路径为
- 已知
- 已知
- 压缩后
的直接父亲变为 ,则新权重为:
合并集合(Union 操作)
当有新条件“
- 找到
的根节点 和 的根节点 。 - 如果
(已经在同一集合中),检查已有关系是否冲突: 我们需要验证 是否成立。如果不等,说明冲突。 - 如果
,我们需要将这两个集合合并。不妨让 成为 的父亲(即 parent[r_a] = r_b)。 此时我们需要计算到 的新权重 : - 因为
- 因为
- 代入
,可得:
- 因为
2. Python 实现代码
由于数据范围较大,Python 的默认递归深度较小,我们需要使用 sys.setrecursionlimit 扩大递归栈。
import sys
# 设置递归深度以防栈溢出
sys.setrecursionlimit(300000)
def solve():
input_data = sys.stdin.read().split()
if not input_data:
return
t = int(input_data[0])
idx = 1
out = []
for _ in range(t):
n = int(input_data[idx])
m = int(input_data[idx+1])
idx += 2
# 并查集初始化
parent = list(range(n + 1))
# weight[i] 表示 x_i - x_parent[i]
weight = [0] * (n + 1)
def find(i):
if parent[i] == i:
return i
# 递归找到根节点
root = find(parent[i])
# 路径压缩:累加路径上的权值
weight[i] += weight[parent[i]]
parent[i] = root
return root
def union(u, v, d):
root_u = find(u)
root_v = find(v)
if root_u == root_v:
# 在同一个集合中,检查是否冲突
return weight[u] - weight[v] == d
else:
# 合并集合,令 root_v 成为 root_u 的父亲
parent[root_u] = root_v
weight[root_u] = d - weight[u] + weight[v]
return True
possible = True
for _ in range(m):
u = int(input_data[idx])
v = int(input_data[idx+1])
d = int(input_data[idx+2])
idx += 3
if possible:
# u 在 v 前方 d 米 => x_u - x_v = d
if not union(u, v, d):
possible = False
if possible:
out.append("YES")
else:
out.append("NO")
print('\n'.join(out))
if __name__ == '__main__':
solve()3. 带权并查集与 BFS/DFS 解法的对比
- 时间复杂度:带权并查集单次操作的复杂度接近常数级
,整体复杂度约为 。由于不需要像 BFS 那样先完整建图,它可以在线(Online)处理边,在某些需要动态加边并查询的场景中更有优势。 - 空间复杂度:
,只需要维护 parent和weight数组,比 BFS/DFS 邻接表所使用的空间略小。