|
| 1 | +import networkx as nx |
| 2 | +import heapq |
| 3 | +import itertools |
| 4 | +from collections import deque |
| 5 | + |
| 6 | + |
| 7 | +class PriorityQueue: |
| 8 | + def __init__(self): |
| 9 | + self.pq = [] |
| 10 | + self.entry_finder = {} |
| 11 | + self.counter = itertools.count() |
| 12 | + self.REMOVED = "<removed-task>" |
| 13 | + |
| 14 | + def insert(self, item, priority): |
| 15 | + if item in self.entry_finder: |
| 16 | + self.delete(item) |
| 17 | + count = next(self.counter) |
| 18 | + entry = [priority, count, item] |
| 19 | + self.entry_finder[item] = entry |
| 20 | + heapq.heappush(self.pq, entry) |
| 21 | + |
| 22 | + def delete(self, item): |
| 23 | + entry = self.entry_finder.pop(item, None) |
| 24 | + if entry: |
| 25 | + entry[-1] = self.REMOVED |
| 26 | + |
| 27 | + def extract_min(self): |
| 28 | + while self.pq: |
| 29 | + priority, count, item = heapq.heappop(self.pq) |
| 30 | + if item is not self.REMOVED: |
| 31 | + del self.entry_finder[item] |
| 32 | + return item, priority |
| 33 | + raise KeyError("pop from an empty priority queue") |
| 34 | + |
| 35 | + def __len__(self): |
| 36 | + return len(self.entry_finder) |
| 37 | + |
| 38 | + |
| 39 | +def cleanup(G): |
| 40 | + while True: |
| 41 | + to_remove = [node for node in G.nodes if G.degree(node) <= 1] |
| 42 | + if len(to_remove) == 0: |
| 43 | + break |
| 44 | + G.remove_nodes_from(to_remove) |
| 45 | + |
| 46 | + return G |
| 47 | + |
| 48 | + |
| 49 | +def find_semidisjoint_cycle(G): |
| 50 | + degree_2 = {n for n, d in G.degree() if d == 2} |
| 51 | + if len(degree_2) == 0: |
| 52 | + return None |
| 53 | + |
| 54 | + sg_d2 = nx.subgraph(G, degree_2) |
| 55 | + visited = set() |
| 56 | + |
| 57 | + for node in degree_2: |
| 58 | + if node in visited: |
| 59 | + continue |
| 60 | + |
| 61 | + component = list(nx.node_connected_component(sg_d2, node)) |
| 62 | + visited.update(component) |
| 63 | + sg_comp = nx.subgraph(G, component) |
| 64 | + |
| 65 | + # If in the component the number of edges equals the number of nodes, then it's a cycle |
| 66 | + if 0 < len(component) == sg_comp.number_of_edges(): |
| 67 | + return component |
| 68 | + |
| 69 | + # Else, we need to check if it's a path with endpoints connected to a common junction node |
| 70 | + endpoints = [n for n in component if sg_comp.degree(n) <= 1] |
| 71 | + if len(endpoints) == 2: |
| 72 | + ep1, ep2 = endpoints |
| 73 | + nb_1 = {n for n in G.neighbors(ep1) if n not in component} |
| 74 | + nb_2 = {n for n in G.neighbors(ep2) if n not in component} |
| 75 | + common_junctions = {n for n in nb_1 & nb_2 if G.degree(n) > 2} |
| 76 | + |
| 77 | + if len(common_junctions) > 0: |
| 78 | + return component + [list(common_junctions)[0]] |
| 79 | + |
| 80 | + return None |
| 81 | + |
| 82 | + |
| 83 | +def get_current_weight(G, n, node_data, total_gamma_clean): |
| 84 | + base_w = node_data[n]["weight"] |
| 85 | + last_g = node_data[n]["last_seen_gamma"] |
| 86 | + deg = G.degree(n) |
| 87 | + # w_actuel = w_base - (gamma accumulé - gamma au dernier update) * (degré actuel - 1) |
| 88 | + return base_w - (total_gamma_clean - last_g) * (deg - 1) |
| 89 | + |
| 90 | + |
| 91 | +# Fonction pour "fixer" le poids actuel (commit) avant un changement de degré |
| 92 | +def commit_weight(G, n, node_data, total_gamma_clean): |
| 93 | + curr_w = get_current_weight(G, n, node_data, total_gamma_clean) |
| 94 | + node_data[n]["weight"] = curr_w |
| 95 | + node_data[n]["last_seen_gamma"] = total_gamma_clean |
| 96 | + return curr_w |
| 97 | + |
| 98 | + |
| 99 | +def get_fvs(og_G): |
| 100 | + F = set() |
| 101 | + G = og_G.copy() |
| 102 | + stack = [] |
| 103 | + |
| 104 | + # pour accumuler les gamma soustraits lorsque le graphe n'a pas de cycle semi-disjoint |
| 105 | + total_gamma_clean = 0.0 |
| 106 | + |
| 107 | + # stocker la valeur qu'avait total_gamma_clean la dernière fois qu'on a touché au noeud n |
| 108 | + node_data = {} |
| 109 | + for n in G.nodes: |
| 110 | + node_data[n] = {"weight": 1.0, "last_seen_gamma": 0.0} |
| 111 | + |
| 112 | + pq = PriorityQueue() |
| 113 | + for n in G.nodes: |
| 114 | + if G.degree(n) > 1: |
| 115 | + # clé = w(u)/(d(u)-1) + total_gamma_clean (0) |
| 116 | + ratio = node_data[n]["weight"] / (G.degree(n) - 1) |
| 117 | + pq.insert(n, ratio) |
| 118 | + |
| 119 | + while len(G.nodes) > 0: |
| 120 | + to_remove = [] |
| 121 | + sd_cycle = find_semidisjoint_cycle(G) |
| 122 | + if sd_cycle is not None: |
| 123 | + min_w = 2 |
| 124 | + for n in sd_cycle: |
| 125 | + w = get_current_weight(G, n, node_data, total_gamma_clean) |
| 126 | + if w < min_w: |
| 127 | + min_w = w |
| 128 | + |
| 129 | + gamma = min_w |
| 130 | + for n in sd_cycle: |
| 131 | + commit_weight(G, n, node_data, total_gamma_clean) |
| 132 | + node_data[n]["weight"] -= gamma |
| 133 | + |
| 134 | + if node_data[n]["weight"] <= 1e-9: |
| 135 | + to_remove.append(n) |
| 136 | + else: |
| 137 | + new_ratio = node_data[n]["weight"] / (G.degree(n) - 1) |
| 138 | + pq.insert(n, new_ratio + total_gamma_clean) |
| 139 | + |
| 140 | + else: |
| 141 | + if len(pq) == 0: |
| 142 | + break |
| 143 | + |
| 144 | + try: |
| 145 | + u, ratio = pq.extract_min() |
| 146 | + except KeyError: |
| 147 | + break |
| 148 | + |
| 149 | + current_ratio = ratio - total_gamma_clean |
| 150 | + gamma = current_ratio |
| 151 | + total_gamma_clean += gamma |
| 152 | + |
| 153 | + node_data[u][ |
| 154 | + "weight" |
| 155 | + ] = 0 # car pour le sommet à min ratio, w(u) = w(u) - (w(u)/(d(u)-1)) * (d(u)-1) = w(u)-w(u) |
| 156 | + node_data[u]["last_seen_gamma"] = total_gamma_clean |
| 157 | + to_remove.append(u) |
| 158 | + |
| 159 | + queue = deque(to_remove) |
| 160 | + while queue: |
| 161 | + node = queue.popleft() |
| 162 | + if not G.has_node(node): |
| 163 | + continue |
| 164 | + |
| 165 | + nb = set(G.neighbors(node)) |
| 166 | + F.add(node) |
| 167 | + stack.append(node) |
| 168 | + pq.delete(node) |
| 169 | + G.remove_node(node) |
| 170 | + |
| 171 | + for u in nb: |
| 172 | + u_curr_weight = node_data[u]["weight"] - ( |
| 173 | + total_gamma_clean - node_data[u]["last_seen_gamma"] |
| 174 | + ) * G.degree( |
| 175 | + u |
| 176 | + ) # degré+1 (anncien degré) - 1 |
| 177 | + |
| 178 | + node_data[u]["weight"] = u_curr_weight |
| 179 | + node_data[u]["last_seen_gamma"] = total_gamma_clean |
| 180 | + |
| 181 | + if G.degree(u) <= 1: |
| 182 | + if u not in queue: |
| 183 | + queue.append(u) |
| 184 | + else: |
| 185 | + # ratio = poids actuel / (degré actuel - 1) |
| 186 | + new_ratio = u_curr_weight / (G.degree(u) - 1) |
| 187 | + pq.insert(u, new_ratio + total_gamma_clean) |
| 188 | + |
| 189 | + while len(stack) > 0: |
| 190 | + node = stack.pop() |
| 191 | + sg = nx.subgraph_view(og_G, filter_node=lambda n: n not in F or n == node) |
| 192 | + # With this check, it should ensure that each call for nx.is_forest is in O(|V|) time since |E| < |V| |
| 193 | + if sg.number_of_edges() <= sg.number_of_nodes() - 1 and nx.is_forest(sg): |
| 194 | + F.remove(node) |
| 195 | + |
| 196 | + return F |
| 197 | + |
| 198 | + |
| 199 | +def get_decycling_number_2_approx_alt(G): |
| 200 | + clean_G = cleanup(G.copy()) |
| 201 | + fvs = get_fvs(clean_G) |
| 202 | + return len(fvs) |
0 commit comments