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] 
posted @ 2025-05-08 10:42  最爱丁珰  阅读(36)  评论(0)    收藏  举报