小欧皇
小欧皇
OPPO 笔试真题 · 2024 秋招 · 算法岗
判题请到牛客原题提交。
题目描述
小欧正在扮演一个中世纪的皇帝。地图上有 个城市,其中有 条道路,每条道路连接了两个城市。
小欧占领了其中一些城市。如果两个城市可以通过若干条道路互相到达,且这些道路经过的城市都是小欧占领的,那么这两个城市之间就可以通过经商获得收益 。请注意,每两个城市之间的收益只会被计算一次。
现在,小欧准备占领一个未被占领的城市,使得总收益最大化。你能帮帮她吗?
输入格式
第一行输入两个正整数 ,代表城市数量和道路数量。
第二行输入一个长度为 的 01 串。第 个字符为 '0' 代表小欧未占领该城市,'1' 代表小欧已经占领了该城市。
接下来的 行,每行输入两个正整数 ,代表城市 和城市 有一条道路连接。
输出格式
输出一行两个空格隔开的整数,第一个整数代表占领的城市编号,第二个整数代表占领后的收益。
请保证收益的最大化。如果有多种方案收益最大,小欧会优先占领编号最小的城市。
示例
示例 1
输入:
5 5
01010
1 2
1 3
1 4
4 5
1 5输出:
1 3说明:
占领 1 号城市后,总收益为 3。
1 号城市和 2 号城市经商,1 号城市和 4 号城市经商,2 号城市和 4 号城市经商。
解题思路
第一步:收益到底是什么
题目说「两个城市可以通过若干条道路互相到达,且这些道路经过的城市都是
小欧占领的」,就产生 1 点收益,且每对城市只算一次。
这等价于:只保留已占领的城市(和两端都已占领的道路),得到若干个连通块。
一个连通块里有 个城市,块内任意两点都能互相到达,于是贡献
点收益。总收益就是所有块的这个值之和。
第二步:用并查集维护当前连通块
读入每条边时,只有当两端都已被占领才合并。这样并查集里每个集合恰好
就是一个「已占领的连通块」,集合大小就是 。
当前总收益
第三步:占领一个城市带来的变化
设要占领的城市是 ,它通过道路直接连着的已占领连通块(去重后)大小是
。占领 之后,这些块和 自己会连成一个大块:
新收益 = 老收益 − 原来那些块各自的贡献 + 新大块的贡献:
去重很关键:同一个连通块可能通过多条边连到 ,如果重复累加,
会被算大。
第四步:枚举并处理并列
对每个未占领的城市算一次 ,取最大值。题目要求收益相同时占领
编号最小的城市,所以更新答案的条件写成严格大于:先枚举到的编号更小,
并列时不会被覆盖。
完整代码实现
import sys
class DSU:
"""路径压缩 + 按大小合并的并查集"""
def __init__(self, n: int) -> None:
self.parent = list(range(n))
self.size = [1] * n
def find(self, x: int) -> int:
root = x
while self.parent[root] != root:
root = self.parent[root]
while self.parent[x] != root: # 路径压缩
self.parent[x], x = root, self.parent[x]
return root
def union(self, a: int, b: int) -> None:
ra, rb = self.find(a), self.find(b)
if ra == rb:
return
if self.size[ra] < self.size[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
self.size[ra] += self.size[rb]
def solve() -> None:
data = sys.stdin.buffer.read().split()
if not data:
return
n = int(data[0])
m = int(data[1])
occupied = [c == 49 for c in data[2]] # 49 是字符 '1' 的字节值
edges = []
pos = 3
for _ in range(m):
u = int(data[pos]) - 1
v = int(data[pos + 1]) - 1
pos += 2
edges.append((u, v))
graph = [[] for _ in range(n)]
dsu = DSU(n)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
if occupied[u] and occupied[v]: # 只有两端都被占领才连通
dsu.union(u, v)
# 当前收益:每个已占领连通块贡献 C(s, 2)
base = 0
for i in range(n):
if occupied[i] and dsu.find(i) == i:
s = dsu.size[i]
base += s * (s - 1) // 2
best_city = -1
best_total = -1
for u in range(n):
if occupied[u]:
continue
roots = set()
for v in graph[u]:
if occupied[v]:
roots.add(dsu.find(v))
joined = 1
lost = 0
for r in roots:
s = dsu.size[r]
joined += s
lost += s * (s - 1) // 2
total = base + joined * (joined - 1) // 2 - lost
if total > best_total: # 严格大于,天然保留最小编号
best_total = total
best_city = u
if best_city == -1: # 题目保证至少有一个未占领城市
best_city, best_total = 0, base
sys.stdout.write(f"{best_city + 1} {best_total}\n")
if __name__ == "__main__":
solve()示例推演
输入中 01010 表示只有 2 号、4 号城市已被占领。
当前并查集:2 号、4 号各自成块,大小都是 1,收益
逐个尝试未占领的城市:
| 占领 | 相邻的已占领块 | 新增收益 | 总收益 | |
|---|---|---|---|---|
| 1 | {2}、{4} | 3 | ||
| 3 | 无(只连着 1 号,而 1 号未占领) | 1 | 0 | 0 |
| 5 | {4} | 1 |
最大是 3,对应的城市是 1 号。占领后 {1,2,4} 三城两两可经商,
恰好 3 对,与题面说明一致。输出 1 3。
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 每次枚举新占领城市后重新求连通块 | ,不可行 | ||
| 并查集 + 枚举 | 推荐,每个点/边只处理常数次 |
易错点总结
1. 把收益当成「有序对」
题面说「每两个城市之间的收益只会被计算一次」,所以是
,不是 。样例中三城连通输出 3(而不是 6),
可以验证这一点。
2. 相邻块没有去重
一个未占领城市可能有多条边连到同一个连通块(例如它和块内两个不同城市
都有路)。这时该块只能算一次。用 set 存根结点即可。
3. 忘记减去原块的贡献
新收益不是简单的 base + C(S,2),还要减掉合并前那些块各自的
,否则会把重复的配对算两遍。
4. 并列时没有取最小编号
只有在 total > best_total 时更新,才能保证编号小的优先。
写成 >= 会让编号大的覆盖编号小的。
5. 字符串读入
01 串是一整个不含空格的 token,data[2] 直接就是它。第 个字符对应
号城市,注意代码里把它转成 0 起点下标。
扩展思考
- 如果允许占领两个城市,就需要考虑「两个新城市各自连接同一块」的
组合情况,枚举量变成 ,需要按块的收益排序后优化。 - 如果把「收益」改成「新增加的收益」(即输出 而不是
base + Δ),base那一项就可以去掉,其余逻辑不变。 - 并查集只支持合并、不支持拆分,所以这类「加一个点后合并若干块」的题
特别适合它;如果要求「删掉一个点」,往往要改成离线倒序处理。