1use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum FvsAlgorithm {
20 Greedy,
23}
24
25pub fn feedback_vertex_set(
57 graph: &Graph,
58 weights: Option<&[f64]>,
59 _algo: FvsAlgorithm,
60) -> IgraphResult<Vec<VertexId>> {
61 let n = graph.vcount();
62
63 if let Some(w) = weights {
64 if w.len() != n as usize {
65 return Err(IgraphError::InvalidArgument(format!(
66 "weights length {} does not match vcount {}",
67 w.len(),
68 n
69 )));
70 }
71 if !w.iter().all(|x| x.is_finite()) {
72 return Err(IgraphError::InvalidArgument(
73 "weights must be finite".into(),
74 ));
75 }
76 }
77
78 if graph.ecount() == 0 {
79 return Ok(Vec::new());
80 }
81
82 fvs_greedy(graph, weights)
83}
84
85fn find_cycle_avoiding(
86 graph: &Graph,
87 removed: &[bool],
88 directed: bool,
89) -> IgraphResult<Vec<VertexId>> {
90 let n = graph.vcount();
91 let mut seen = vec![0u8; n as usize]; let mut stack: Vec<(VertexId, usize)> = Vec::new();
94 let mut path: Vec<VertexId> = Vec::new();
95
96 for start in 0..n {
97 if removed[start as usize] || seen[start as usize] != 0 {
98 continue;
99 }
100
101 stack.clear();
102 stack.push((start, 0));
103 path.clear();
104
105 while let Some(&mut (v, ref mut idx)) = stack.last_mut() {
106 if *idx == 0 {
107 if seen[v as usize] == 1 {
108 let mut cycle = Vec::new();
110 let pos = path.iter().rposition(|&x| x == v).unwrap_or(0);
111 for &cv in &path[pos..] {
112 cycle.push(cv);
113 }
114 cycle.push(v);
115 return Ok(cycle);
116 }
117 if seen[v as usize] == 2 {
118 stack.pop();
119 continue;
120 }
121 seen[v as usize] = 1;
122 path.push(v);
123 }
124
125 let neighbors = if directed {
126 graph.out_neighbors_vec(v)?
127 } else {
128 graph.neighbors(v)?
129 };
130
131 let mut found_next = false;
132 while *idx < neighbors.len() {
133 let w = neighbors[*idx];
134 *idx += 1;
135
136 if removed[w as usize] {
137 continue;
138 }
139
140 if !directed && path.len() >= 2 && w == path[path.len() - 2] {
141 continue;
142 }
143
144 if seen[w as usize] == 1 {
145 let mut cycle = Vec::new();
147 let pos = path.iter().rposition(|&x| x == w).unwrap_or(0);
148 for &cv in &path[pos..] {
149 cycle.push(cv);
150 }
151 return Ok(cycle);
152 }
153
154 if seen[w as usize] == 0 {
155 stack.push((w, 0));
156 found_next = true;
157 break;
158 }
159 }
160
161 if !found_next {
162 seen[v as usize] = 2;
163 path.pop();
164 stack.pop();
165 }
166 }
167 }
168
169 Ok(Vec::new())
170}
171
172#[allow(clippy::cast_possible_truncation)] fn effective_degree(
174 graph: &Graph,
175 v: VertexId,
176 removed: &[bool],
177 directed: bool,
178) -> IgraphResult<u32> {
179 if directed {
180 let out_n = graph.out_neighbors_vec(v)?;
181 let in_n = graph.in_neighbors_vec(v)?;
182 let out_deg = out_n.iter().filter(|&&w| !removed[w as usize]).count() as u32;
183 let in_deg = in_n.iter().filter(|&&w| !removed[w as usize]).count() as u32;
184 Ok(out_deg.saturating_add(in_deg))
185 } else {
186 let n = graph.neighbors(v)?;
187 Ok(n.iter().filter(|&&w| !removed[w as usize]).count() as u32)
188 }
189}
190
191fn pick_best_vertex(
192 graph: &Graph,
193 cycle: &[VertexId],
194 weights: Option<&[f64]>,
195 removed: &[bool],
196 directed: bool,
197) -> IgraphResult<VertexId> {
198 let mut best_v = cycle[0];
199 if let Some(w) = weights {
200 let mut best_w = w[cycle[0] as usize];
201 for &v in &cycle[1..] {
202 let vw = w[v as usize];
203 if vw > best_w || (vw > best_w - f64::EPSILON && v < best_v) {
204 best_w = vw;
205 best_v = v;
206 }
207 }
208 } else {
209 let mut best_deg = effective_degree(graph, cycle[0], removed, directed)?;
210 for &v in &cycle[1..] {
211 let deg = effective_degree(graph, v, removed, directed)?;
212 if deg > best_deg || (deg == best_deg && v < best_v) {
213 best_deg = deg;
214 best_v = v;
215 }
216 }
217 }
218 Ok(best_v)
219}
220
221fn fvs_greedy(graph: &Graph, weights: Option<&[f64]>) -> IgraphResult<Vec<VertexId>> {
222 let n = graph.vcount();
223 let directed = graph.is_directed();
224 let mut removed = vec![false; n as usize];
225 let mut result = Vec::new();
226
227 loop {
228 let cycle = find_cycle_avoiding(graph, &removed, directed)?;
229 if cycle.is_empty() {
230 break;
231 }
232
233 let best_v = pick_best_vertex(graph, &cycle, weights, &removed, directed)?;
234 removed[best_v as usize] = true;
235 result.push(best_v);
236 }
237
238 result.sort_unstable();
239 Ok(result)
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn graph_is_acyclic_without(graph: &Graph, fvs: &[VertexId]) -> bool {
247 let n = graph.vcount();
248 let mut removed = vec![false; n as usize];
249 for &v in fvs {
250 removed[v as usize] = true;
251 }
252 let cycle = find_cycle_avoiding(graph, &removed, graph.is_directed()).unwrap();
253 cycle.is_empty()
254 }
255
256 #[test]
257 fn empty_graph() {
258 let g = Graph::with_vertices(0);
259 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
260 assert!(fvs.is_empty());
261 }
262
263 #[test]
264 fn no_edges() {
265 let g = Graph::with_vertices(5);
266 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
267 assert!(fvs.is_empty());
268 }
269
270 #[test]
271 fn dag_no_feedback() {
272 let mut g = Graph::new(4, true).unwrap();
273 g.add_edge(0, 1).unwrap();
274 g.add_edge(1, 2).unwrap();
275 g.add_edge(2, 3).unwrap();
276 g.add_edge(0, 3).unwrap();
277 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
278 assert!(fvs.is_empty());
279 }
280
281 #[test]
282 fn simple_directed_cycle() {
283 let mut g = Graph::new(3, true).unwrap();
284 g.add_edge(0, 1).unwrap();
285 g.add_edge(1, 2).unwrap();
286 g.add_edge(2, 0).unwrap();
287 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
288 assert_eq!(fvs.len(), 1);
289 assert!(graph_is_acyclic_without(&g, &fvs));
290 }
291
292 #[test]
293 fn directed_cycle_4() {
294 let mut g = Graph::new(4, true).unwrap();
295 g.add_edge(0, 1).unwrap();
296 g.add_edge(1, 2).unwrap();
297 g.add_edge(2, 3).unwrap();
298 g.add_edge(3, 0).unwrap();
299 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
300 assert_eq!(fvs.len(), 1);
301 assert!(graph_is_acyclic_without(&g, &fvs));
302 }
303
304 #[test]
305 fn undirected_tree_no_feedback() {
306 let mut g = Graph::with_vertices(5);
307 g.add_edge(0, 1).unwrap();
308 g.add_edge(1, 2).unwrap();
309 g.add_edge(1, 3).unwrap();
310 g.add_edge(3, 4).unwrap();
311 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
312 assert!(fvs.is_empty());
313 }
314
315 #[test]
316 fn undirected_triangle() {
317 let mut g = Graph::with_vertices(3);
318 g.add_edge(0, 1).unwrap();
319 g.add_edge(1, 2).unwrap();
320 g.add_edge(2, 0).unwrap();
321 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
322 assert_eq!(fvs.len(), 1);
323 assert!(graph_is_acyclic_without(&g, &fvs));
324 }
325
326 #[test]
327 fn two_directed_cycles_sharing_vertex() {
328 let mut g = Graph::new(5, true).unwrap();
330 g.add_edge(0, 1).unwrap();
331 g.add_edge(1, 2).unwrap();
332 g.add_edge(2, 0).unwrap();
333 g.add_edge(0, 3).unwrap();
334 g.add_edge(3, 4).unwrap();
335 g.add_edge(4, 0).unwrap();
336 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
337 assert!(fvs.len() <= 2);
339 assert!(graph_is_acyclic_without(&g, &fvs));
340 }
341
342 #[test]
343 fn bidirectional_edges() {
344 let mut g = Graph::new(2, true).unwrap();
345 g.add_edge(0, 1).unwrap();
346 g.add_edge(1, 0).unwrap();
347 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
348 assert_eq!(fvs.len(), 1);
349 assert!(graph_is_acyclic_without(&g, &fvs));
350 }
351
352 #[test]
353 fn self_loop_directed() {
354 let mut g = Graph::new(3, true).unwrap();
355 g.add_edge(0, 0).unwrap();
356 g.add_edge(0, 1).unwrap();
357 g.add_edge(1, 2).unwrap();
358 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
359 assert_eq!(fvs.len(), 1);
360 assert!(fvs.contains(&0));
361 assert!(graph_is_acyclic_without(&g, &fvs));
362 }
363
364 #[test]
365 fn self_loop_undirected() {
366 let mut g = Graph::with_vertices(3);
367 g.add_edge(0, 0).unwrap();
368 g.add_edge(0, 1).unwrap();
369 g.add_edge(1, 2).unwrap();
370 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
371 assert_eq!(fvs.len(), 1);
372 assert!(fvs.contains(&0));
373 }
374
375 #[test]
376 fn undirected_k4() {
377 let mut g = Graph::with_vertices(4);
378 for u in 0..4u32 {
379 for v in (u + 1)..4 {
380 g.add_edge(u, v).unwrap();
381 }
382 }
383 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
384 assert!(fvs.len() >= 2);
386 assert!(graph_is_acyclic_without(&g, &fvs));
387 }
388
389 #[test]
390 fn weighted_picks_heaviest() {
391 let mut g = Graph::new(3, true).unwrap();
392 g.add_edge(0, 1).unwrap();
393 g.add_edge(1, 2).unwrap();
394 g.add_edge(2, 0).unwrap();
395 let weights = vec![1.0, 10.0, 1.0];
397 let fvs = feedback_vertex_set(&g, Some(&weights), FvsAlgorithm::Greedy).unwrap();
398 assert_eq!(fvs.len(), 1);
399 assert_eq!(fvs[0], 1);
400 }
401
402 #[test]
403 fn invalid_weights_length() {
404 let g = Graph::with_vertices(3);
405 let weights = vec![1.0]; assert!(feedback_vertex_set(&g, Some(&weights), FvsAlgorithm::Greedy).is_err());
407 }
408
409 #[test]
410 fn nan_weights_rejected() {
411 let mut g = Graph::with_vertices(2);
412 g.add_edge(0, 1).unwrap();
413 let weights = vec![f64::NAN, 1.0];
414 assert!(feedback_vertex_set(&g, Some(&weights), FvsAlgorithm::Greedy).is_err());
415 }
416
417 #[test]
418 fn isolated_vertices() {
419 let g = Graph::new(5, true).unwrap();
420 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
421 assert!(fvs.is_empty());
422 }
423
424 #[test]
425 fn complex_directed() {
426 let mut g = Graph::new(5, true).unwrap();
428 g.add_edge(0, 1).unwrap();
429 g.add_edge(1, 2).unwrap();
430 g.add_edge(2, 0).unwrap();
431 g.add_edge(2, 3).unwrap();
432 g.add_edge(3, 4).unwrap();
433 g.add_edge(4, 2).unwrap();
434 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
435 assert!(fvs.len() <= 2);
437 assert!(graph_is_acyclic_without(&g, &fvs));
438 }
439
440 #[test]
441 fn sorted_output() {
442 let mut g = Graph::new(6, true).unwrap();
443 g.add_edge(0, 1).unwrap();
444 g.add_edge(1, 0).unwrap();
445 g.add_edge(2, 3).unwrap();
446 g.add_edge(3, 2).unwrap();
447 g.add_edge(4, 5).unwrap();
448 g.add_edge(5, 4).unwrap();
449 let fvs = feedback_vertex_set(&g, None, FvsAlgorithm::Greedy).unwrap();
450 assert_eq!(fvs.len(), 3);
451 for i in 1..fvs.len() {
453 assert!(fvs[i] > fvs[i - 1]);
454 }
455 assert!(graph_is_acyclic_without(&g, &fvs));
456 }
457}