Skip to content

Commit 2f74f9b

Browse files
authored
Merge pull request #8 from SebVde/bafna-alt
Bafna alt branch merge
2 parents 796c0cc + f184261 commit 2f74f9b

2 files changed

Lines changed: 203 additions & 1 deletion

File tree

bafna_fvs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def get_fvs(og_G):
7272
node = stack.pop()
7373
sg = nx.subgraph(og_G, og_G.nodes - (F - {node}))
7474
# With this check, it should ensure that each call for nx.is_forest is in O(|V|) time since |E| < |V|
75-
if G.number_of_edges() <= G.number_of_nodes() - 1 and nx.is_forest(sg):
75+
if sg.number_of_edges() <= sg.number_of_nodes() - 1 and nx.is_forest(sg):
7676
F.remove(node)
7777

7878
return F

bafna_fvs_alt.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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

Comments
 (0)