Problem Set 6
Problem Set 6.1
Problem 6.1.1
对于没有特殊条件限制的图,Prim算法的时间复杂度为\(O(m\log n)\)(假设边比点多)
\((1)\)
此时有\(m\leq nk\),所以\(O(m\log n)=O(nk\log n)=O(n\log n)\)
\((2)\)
此时\(|E|=m=|V|+|F|-2=n-2+|F|\leq3n-6\)(这是由于每个面至少由 3 条边围成,且每条边被两个面共享,因此\(3|F|\leq2|E|\)),所以\(O(m\log n)=O(n\log n)\)
Problem 6.1.2
先去除\(U\)中节点(及相关的边),对剩余节点和边使用堆优化的Prim算法求出MST。时间复杂度为\(O((m+n)\log n)\)。如果在这一步中,MST不存在,那么最终的MST就不存在
再遍历所有一边。如果对于当前遍历到的边\((u,v)\),如果两个端点中的一个属于\(U\),另一个不属于\(U\),不妨设\(u∈U\),那么将\(u\)到已生成的MST的距离更新为\((u,v)\)(如果更小的话);遍历完之后,所有\(U\)中节点记录的最小值就是最终MST的边。时间复杂度为\(O(m)\)
正确性证明:首先如果最终的MST存在的话,那么去除\(U\)后,剩余子图的MST应该也会存在。如果最终的MST在去除\(U\)之后,剩余的树不是剩余子图的MST,那么我们将这颗剩余的树换成MST,再加上\(U\)权重会更少,与最终的MST矛盾,所以最终的MST去除\(U\)之后,剩余的树是剩余子图的MST;在利用贪心求解即可
import heapq
def find_light_spanning_tree(G, U):
n = G['n']
edges = G['edges']
U_set = set(U)
non_U = [v for v in range(n) if v not in U_set]
remaining_edges = []
for u, v, w in edges:
if u not in U_set and v not in U_set:
remaining_edges.append((u, v, w))
mst_weight, mst_nodes = prim(remaining_edges, non_U)
if mst_weight is None:
return None
min_edges = {u: float('inf') for u in U_set}
for u, v, w in edges:
if (u in U_set and v not in U_set) or (v in U_set and u not in U_set):
if v in U_set:
u, v = v, u
if u in U_set and v in mst_nodes:
if w < min_edges[u]:
min_edges[u] = w
total_add = 0
for u in U_set:
if min_edges[u] == float('inf'):
return None
total_add += min_edges[u]
total_weight = mst_weight + total_add
return total_weight
def prim(edges, nodes):
if not nodes:
return 0, set()
adj = {}
for u, v, w in edges:
if u not in adj:
adj[u] = []
if v not in adj:
adj[v] = []
adj[u].append((v, w))
adj[v].append((u, w))
heap = []
visited = set()
start = nodes[0]
heapq.heappush(heap, (0, start, -1)) # (weight, node, parent)
mst_weight = 0
mst_nodes = set()
while heap:
weight, u, parent = heapq.heappop(heap)
if u in visited:
continue
visited.add(u)
mst_weight += weight
if parent != -1:
pass
for v, w in adj.get(u, []):
if v not in visited:
heapq.heappush(heap, (w, v, u))
if len(visited) != len(nodes):
return None, None
return mst_weight, visited
Problem 6.1.3
首先使用Kruscal算法找出任意一个MST,设为\(T\)。如果某一条边不在\(T\)里面,那么这条边肯定不是临界边(因为\(T\)就是一个不包含这条边的MST,与临界边的定义矛盾)。所以临界边一定是\(T\)中的边。考虑一条非树边\(e\),将\(e\)加入到\(T\)中会形成一个环。根据MST性质,\(e\)是最大的边。如果存在一条环上的树边,这条树边的权重与\(e\)相同,那么这条树边一定不是临界边,因为去掉这条树边,加上\(e\),仍然是一个权重不变的生成树。我们考虑一个朴素的做法:遍历所有非树边,将每条非树边加入到树中,暴力标记形成的环上与非树边权重相同的树边;在循环过程结束之后,有标记的树边肯定不是临界边,没标记的树边如果是桥边那么就不是临界边(因为临界边要求去掉之后\(G\)仍然连通),否则的话一定是临界边(考虑去掉没标记的树边,那么\(T\)会形成两个连通块;由于这条树边不是桥边,所以一定存在连接这两个连通块的边,而且这些边是非树边,我们在前面的循环过程中可以遍历到,而这条树边既然没有被打上标记,就说明这些非树边的权重都严格大于这条树边,而去掉这条树边之后\(G^{'}\)生成的MST一定会包含至少一条这些非树边,我们将仍然一条非树边换成这条树边,就可以知道\(G\)的MST比\(G^{'}\)要小)
于是我们现在考虑优化这个暴力过程。要判断一条树边是不是临界边,就是要判断是否存在一条与其权重相同的非树边,使得加入这条非树边形成的环上包含这条树边。考虑如下事实:无向图\(G\)的一颗生成树\(T\),如果\(e=(u,v)∈T\)且存在\(e_1∈G\)但\(e_1∉T\)且在\(T\)中加入\(e_1\)后,形成的环包含\(e\),假设\(v\)是\(u\)的儿子,那么\(e_1\)的一个顶点一定是\(v\)的子树的某一点,另一个顶点一定不是\(v\)的子树的某一点
利用上述事实,我们可以按照权值遍历。假设当前遍历到的权值为\(W\),那么整体考虑\(G\)中所有权值为\(W\)的边(这在之前的Kruscal过程中也排好序了的)。创建两个大小为\(n\)的数组\(A\)和\(B\)。设这些边中的一条非树边为\(e=(u,v)\)且dfn表示节点在\(T\)上的时间戳。不妨设dfn[u]小于dfn[v],于是令A[dfn[u]]=max(A[dfn[u]],dfn[v]),B[dfn[v]]=min(B[dfn[v]],dfn[u])。对所有非树边做完这个操作之后,对\(A\)和\(B\)做ST表。然后考虑这些边中的树边。设某条树边为\(e=(u,v)\),不妨设\(u\)是\(v\)的父亲,且\(v\)的子树大小为sz[v]。查找A[dfn[v]]到A[dfn[v]+sz[v]]上的最大值,设为Max,查找B[dfn[v]]到B[dfn[v]+sz[v]]上的最小值,设为Min。如果Max大于dfn[v]+sz[v]或者Min小于dfn[v],就说明\(e\)是临界边,否则的话\(e\)就不是临界边
import sys
from collections import defaultdict
class UnionFind:
def __init__(self, size):
self.parent = list(range(size))
def find(self, x):
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, x, y):
fx, fy = self.find(x), self.find(y)
if fx == fy:
return False
self.parent[fy] = fx
return True
def find_critical_edges(n, edges):
edges = sorted(edges, key=lambda x: x[2])
m = len(edges)
uf = UnionFind(n)
mst_edges = []
edge_in_mst = [False] * m
for i, (u, v, w) in enumerate(edges):
if uf.union(u, v):
mst_edges.append((u, v, w, i))
edge_in_mst[i] = True
adj = [[] for _ in range(n)]
for u, v, w, _ in mst_edges:
adj[u].append((v, w))
adj[v].append((u, w))
dfn = [0] * n
sz = [0] * n
parent = [-1] * n
timestamp = 0
def dfs(u, fa):
nonlocal timestamp
dfn[u] = timestamp
timestamp += 1
sz[u] = 1
for v, w in adj[u]:
if v != fa:
parent[v] = u
dfs(v, u)
sz[u] += sz[v]
dfs(0, -1)
weight_map = defaultdict(list)
for i, (u, v, w) in enumerate(edges):
weight_map[w].append((u, v, i))
critical = []
for w in weight_map:
A = [-1] * (n + 1)
B = [n + 1] * (n + 1)
for u, v, i in weight_map[w]:
if not edge_in_mst[i]:
x, y = u, v
if dfn[x] > dfn[y]:
x, y = y, x
if dfn[y] >= dfn[x] and dfn[y] < dfn[x] + sz[x]:
l = dfn[y]
r = dfn[y] + sz[y] - 1
A[l] = max(A[l], r)
B[r] = min(B[r], l)
else:
pass
# 构建ST表(此处简化为前缀处理)
# 实际应使用线段树或ST表优化区间查询
prefix_max = A.copy()
suffix_min = B.copy()
for i in range(1, n+1):
prefix_max[i] = max(prefix_max[i], prefix_max[i-1])
for i in range(n-1, -1, -1):
suffix_min[i] = min(suffix_min[i], suffix_min[i+1])
for u, v, w, idx in mst_edges:
if w != w:
continue
if parent[v] == u:
child = v
else:
child = u
L = dfn[child]
R = L + sz[child] - 1
max_in = prefix_max[R]
min_out = suffix_min[L]
if max_in > R or min_out < L:
critical.append(idx)
return [edges[i][3] for i in critical]
然后证明一下答案那个做法,也挺好的
删除\(e\)相当于把\(e\)的权值变成\(\infty\),于是由黄宇书P10.10可知,我们只需要去证明在最终的生成树中不存在一条与\(e\)权值相同的边跨越\(e\)连接的两个连通块即可。形式化证明如下
在只考虑权值严格小于\(w_i\)的边的时候,图会形成若干个连通块\(c_1,c_2,...,c_k\);假设现在\(e=(c_1,c_2)\)是割边,那么在最终的生成树\(T\)中,删除\(e\)之后,会形成两个大的连通块\(C_1\)和\(C_2\),且\(c_1∈C_1,c_2∈C_2\);如果现在存在一条边并且只存在一条边(其实存在多条边也是一样的,将所有这种边全部按照\(e_1\)的处理方式处理即可)\(e_1=(c_3,c_4)\)且其边权等于\(w_i\),并且其跨越\(C_1\)和\(C_2\)(也就是\(c_3∈C_1,c_4∈C_2\)),不妨将\(e_1\)放到同样边权的最后让Kruscal处理,由于\(e_1\)不在\(T\)中,所以在Kruscal考虑到\(e_1\)的时候,\(e_1\)一定不是割边,也就是说存在一条不包含\(e_1\)的路径\(p\),这条路径的两个端点是\(c_3\)和\(c_4\);注意到我们现在是最后处理的\(e_1\),所以之前考虑的权值为\(w_i\)的边,除了\(e\)就没有跨越\(C_1\)和\(C_2\)的边,也就是说\(C_1\)和\(C_2\)的点要想互相到达,必须经过\(e\),于是可以知道\(p\)一定是\(c_3->c_1->c_2->c_4\),于是就存在一条路径\(c_1->c_3->c_4->c_2\),也就是说\(e\)不是割边,就矛盾了,所以肯定不存在这样的\(e_1\)
Problem Set 6.2
Problem 6.2.1
\((1)\)
开一个大小为\(W+1\)的链表头数组\(A\).初始的时候。将\(s\)插入到\(A[0]\)中。然后开始循环遍历\(A\)(也就是要完整遍历\(A\)多次):
- 设当前遍历到\(A[i]\)且\(A[i]\)不为空,于是遍历\(A[i]\)上的每一个节点。设当前遍历到的节点为\(u\),如果\(u\)在之前就已经确定了最短路了就跳过,继续遍历下一个节点。现在假设\(u\)没有被确定最短路
- 遍历\(u\)的邻居,设当前遍历到的为\(v\),假设\(v\)在之前还没有被确定最短路。更新\(v\)的最短路,如果能够更新,那么在\(A[(dis[u]+l)\%(W+1)]\)中插入\(v\)
- 继续下一次更新
上面的方法主要就是利用Dij的优先队列中,最长路与最短路的差不会超过\(W\),所以用链表头去代替优先队列(同时使用了同余类的思想)
注意在两个点之间最多遍历一次完整的\(A\),而每个点仍然只能入队一次,所以时间复杂度是题目所求
from collections import deque
def dijkstra_W_part1(graph, start, W):
n = len(graph)
INF = float('inf')
dist = [INF] * n
dist[start] = 0
buckets = [deque() for _ in range(W + 1)]
buckets[0].append(start)
current = 0
while True:
idx = current % (W + 1)
if not buckets[idx]:
if all(len(b) == 0 for b in buckets):
break
current += 1
continue
found_valid = False
for _ in range(len(buckets[idx])):
u = buckets[idx].popleft()
if dist[u] == current:
found_valid = True
break
buckets[idx].append(u)
if not found_valid:
current += 1
continue
for v, l in graph[u]:
if dist[v] > dist[u] + l:
if dist[v] != INF:
pass
dist[v] = dist[u] + l
new_idx = dist[v] % (W + 1)
buckets[new_idx].append(v)
current = dist[u] + 1
return dist
\((2)\)
显然就是优化上面的遍历查找。可以这么做:用一个优先队列来维护不为空的链表头,每插入一个新点,就看这个点所在的链表是否为空,如果为空的话就插入优先队列;每次查找的时候直接取出队头即可
上面这个过程也可以用二分改进
Problem 6.2.2
\((1)\)
将权重大于\(L\)的边删除。在删除之后的图上看\(s\)和\(t\)是否连通即可
from collections import deque
def is_reachable(n, edges, s, t, L):
adj = [[] for _ in range(n)]
for u, v, l in edges:
if l <= L:
adj[u].append(v)
adj[v].append(u)
visited = [False] * n
q = deque([s])
visited[s] = True
while q:
u = q.popleft()
if u == t:
return True
for v in adj[u]:
if not visited[v]:
visited[v] = True
q.append(v)
return False
\((2)\)
题目即在图中找到一条路径,这条路径的边的最大权重最小。将迪杰斯特拉算法更新时候的\(dis[v]=min(dis[v],dis[u]+l)\)改成\(dis[v]=max(dis[v],l)\)即可
正确性证明:
假设已经出队的节点的\(dis\)满足其为从\(s\)到这个节点的最小最大权重,设当前即将出队的节点为\(u\),我们要证明\(dis[u]\)满足\(dis[u]\)满足其为从\(s\)到\(u\)的最小最大权重,我们只需要证明不存在一条到\(u\)的路径,这条路径的边的最大长度严格小于\(u\)即可。假设存在这么一条路径,设为\(s->x_1->x_2->...->x_l->u\),那么\(x_i\)一定是比\(u\)先出队的,所以\(x_l\)一定会更新\(u\),于是\(dis[u]\)就应该比当前更小,这就矛盾了。所以已经出队的节点的\(dis\)满足其为从\(s\)到这个节点的最小最大权重
import heapq
def min_capacity(n, edges, s, t):
adj = [[] for _ in range(n)]
for u, v, l in edges:
adj[u].append((v, l))
adj[v].append((u, l))
dis = [float('inf')] * n
dis[s] = 0
heap = []
heapq.heappush(heap, (0, s))
while heap:
current_dis, u = heapq.heappop(heap)
if u == t:
break
if current_dis > dis[u]:
continue
for v, l in adj[u]:
candidate = max(current_dis, l)
if candidate < dis[v]:
dis[v] = candidate
heapq.heappush(heap, (dis[v], v))
return dis[t] if dis[t] != float('inf') else -1
Problem 6.2.3
假设\(x_{i+1}-x_i\leq 100\).采用一个自然的贪心策略:假设当前在\(x_i\),如果剩余电量能够行驶到\(x_{i+1}\)就不在\(x_i\)充电,否则就在\(x_i\)充满电
利用数学归纳法证明正确性:假设当前在\(x_i\),而且之前的策略是某个最优策略的一部分,现在还剩下电量\(l\).如果\(x_{i+1}-x_i>l\),那么肯定需要在\(x_i\)充电;否则的话,最优策略一定不用在\(x_i\)充电,假设最优策略一定要在\(x_i\)充电,那么我们保持这个最优策略的充电决策不变,只是不再\(x_i\)充电而是在\(x_{i+1}\)充电,不难得知这个方案仍然可以到达\(x_n\),而且仍然是最优方案,这就与之前的假设矛盾,所以数学归纳得证,贪心策略是正确的
def min_charging_stops(stations):
if len(stations) < 2:
return 0
count = 0
current_charge = 100
for i in range(1, len(stations)):
distance = stations[i] - stations[i-1]
if current_charge >= distance:
current_charge -= distance
else:
count += 1
current_charge = 100 - distance
return count
Problem 6.2.4
将所有区间按照左端点递增排序(如果左端点相同,就按照右端点递减排序)。
一个很显然的性质:如果一个区间被另一个区间完全包含,那么\(Y\)肯定不会包含这个区间
执行如下算法:首先选择\(X_1\),假设\(X_2\sim X_k\)的左端点都不超过\(X_1\)的右端点,那么在\(X_2\sim X_k\)中选择右端点最大的一个区间,设为\(X_p\),不妨设\(X_p\)的右端点大于\(X_1\)的右端点,选择\(X_p\);然后假设\(X_{k+1}\sim X_t\)的左端点都不超过\(X_p\)的右端点,那么在\(X_{k+1}\sim X_t\)中选择右端点最大的一个区间,设为\(X_q\),不妨设\(X_q\)的右端点大于\(X_p\)的右端点,选择\(X_q\).重复上述过程直到最后选不出来。然后在对剩下的没遍历的区间执行类似过程
正确性证明:显然\(X_1\)是要选择的。如果\(X_p\)的右端点大于\(X_1\)的右端点,那么我们也要在\(X_2\sim X_k\)中选择至少一个区间出来。假设不选\(X_p\),如果选择的\(X_p\)后面的区间,那么这个区间被\(X_p\)完全包含,由之前所提到的性质,是不会选择这个区间的;如果选择\(X_p\)前面的区间,设为\(X_o\),那么在最终的方案中,\(X_o\)能够覆盖的位置,要么能够被\(X_1\)覆盖,要么能够被\(X_p\)覆盖,所以将\(X_o\)换成\(X_p\)之后仍然能够覆盖所有区间。接下来的过程类似证明即可
def find_min_cover(XL, XR):
intervals = list(zip(XL, XR))
intervals.sort(key=lambda x: (x[0], -x[1]))
n = len(intervals)
if n == 0:
return []
Y = []
current_end = -float('inf')
i = 0
while i < n:
if intervals[i][0] > current_end:
Y.append(intervals[i])
current_end = intervals[i][1]
i += 1
else:
max_r = current_end
best_j = -1
j = i
while j < n and intervals[j][0] <= current_end:
if intervals[j][1] > max_r:
max_r = intervals[j][1]
best_j = j
j += 1
if best_j != -1:
Y.append(intervals[best_j])
current_end = intervals[best_j][1]
i = best_j + 1
else:
i = j
return Y
Problem Set 6.3
Problem 6.3.1
\((1)\)
直接使用迪杰斯特拉即可,正确性与PPT上的正确性证明一样
import heapq
def max_capacity_single_source(graph, n, s):
dis = [0] * n
dis[s] = float('inf')
heap = [(-dis[s], s)]
visited = [False] * n
while heap:
current_dis_u, u = heapq.heappop(heap)
current_dis_u = -current_dis_u
if visited[u]:
continue
visited[u] = True
for v, c in graph[u]:
new_cap = min(current_dis_u, c)
if new_cap > dis[v]:
dis[v] = new_cap
heapq.heappush(heap, (-dis[v], v))
return dis
\((2)\)
用Floyd求即可
def all_pairs_max_capacity(graph, n):
cap = [[0]*n for _ in range(n)]
for i in range(n):
cap[i][i] = float('inf')
for v, c in graph[i]:
if c > cap[i][v]:
cap[i][v] = c
for k in range(n):
for i in range(n):
for j in range(n):
cap[i][j] = max(cap[i][j], min(cap[i][k], cap[k][j]))
return cap
Problem 6.3.2
使用Bellman-Ford即可。设一个点的最短路边数为\(k\),那么最多经过\(k\)次循环,就可以得到这个点的最短路。于是可以知道时间复杂度为\(O(km)\)
下面的代码实现是按照SPFA(队列优化的Bellman-Ford算法写的)
from collections import deque
def spfa_k_edges(graph, n, u, v, k):
dist = [float('inf')] * n
dist[u] = 0
enqueue_count = [0] * n
queue = deque([u])
in_queue = [False] * n
in_queue[u] = True
while queue:
current = queue.popleft()
in_queue[current] = False
for neighbor, weight in graph[current]:
if dist[current] + weight < dist[neighbor]:
dist[neighbor] = dist[current] + weight
if not in_queue[neighbor]:
enqueue_count[neighbor] += 1
queue.append(neighbor)
in_queue[neighbor] = True
return dist[v]

浙公网安备 33010602011771号