1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//! Submodule providing the `CactusDetection` trait and its blanket
//! implementation for undirected graphs.
use alloc::vec::Vec;
use num_traits::AsPrimitive;
use crate::traits::{
PositiveInteger, SparseMatrix2D, UndirectedMonopartiteEdges, UndirectedMonopartiteMonoplexGraph,
};
type NeighborIterator<'graph, G> = <<<G as UndirectedMonopartiteMonoplexGraph>::UndirectedMonopartiteEdges as UndirectedMonopartiteEdges>::SymmetricSquaredMatrix as SparseMatrix2D>::SparseRow<'graph>;
struct DfsFrame<'graph, G: UndirectedMonopartiteMonoplexGraph + ?Sized>
where
<<G as UndirectedMonopartiteMonoplexGraph>::UndirectedMonopartiteEdges as UndirectedMonopartiteEdges>::SymmetricSquaredMatrix:
'graph,
{
/// Current node.
node: G::NodeId,
/// Parent in the DFS tree.
parent: Option<G::NodeId>,
/// Iterator over the node neighbors.
neighbors: NeighborIterator<'graph, G>,
}
#[inline]
fn normalize_edge<NodeId: PositiveInteger>(left: NodeId, right: NodeId) -> [NodeId; 2] {
if left <= right { [left, right] } else { [right, left] }
}
#[inline]
fn validate_component<NodeId: PositiveInteger>(
edge_stack: &mut Vec<[NodeId; 2]>,
stop_edge: Option<[NodeId; 2]>,
touched: &mut [bool],
touched_vertices: &mut Vec<NodeId>,
) -> bool {
let mut edge_count = 0usize;
while let Some(edge) = edge_stack.pop() {
edge_count += 1;
for vertex in edge {
let vertex_index = vertex.as_();
if !touched[vertex_index] {
touched[vertex_index] = true;
touched_vertices.push(vertex);
}
}
if Some(edge) == stop_edge {
break;
}
}
let vertex_count = touched_vertices.len();
for vertex in touched_vertices.drain(..) {
touched[vertex.as_()] = false;
}
edge_count == 1 || edge_count == vertex_count
}
/// Trait providing cactus-graph recognition for undirected graphs.
///
/// A graph is a cactus when every edge belongs to at most one simple cycle.
/// Equivalently, each edge-based biconnected component is either a single
/// edge or a simple cycle.
///
/// The implementation performs a depth-first traversal with Tarjan low-link
/// bookkeeping and validates each edge-biconnected component as soon as it
/// closes. This avoids materializing the full decomposition and rejects early
/// on the first non-cactus block.
///
/// # Examples
///
/// ```
/// use geometric_traits::{
/// impls::{SortedVec, SymmetricCSR2D},
/// prelude::*,
/// traits::{CactusDetection, EdgesBuilder, VocabularyBuilder},
/// };
///
/// let nodes: Vec<usize> = vec![0, 1, 2, 3, 4];
/// let edges: Vec<(usize, usize)> = vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)];
/// let nodes: SortedVec<usize> = GenericVocabularyBuilder::default()
/// .expected_number_of_symbols(nodes.len())
/// .symbols(nodes.into_iter().enumerate())
/// .build()
/// .unwrap();
/// let edges: SymmetricCSR2D<_> = UndiEdgesBuilder::default()
/// .expected_number_of_edges(edges.len())
/// .expected_shape(nodes.len())
/// .edges(edges.into_iter())
/// .build()
/// .unwrap();
/// let graph: UndiGraph<usize> = UndiGraph::from((nodes, edges));
///
/// assert!(graph.is_cactus());
/// ```
pub trait CactusDetection: UndirectedMonopartiteMonoplexGraph {
/// Returns whether the graph is a cactus graph.
///
/// Disconnected graphs are accepted when each connected component is a
/// cactus. Graphs with self-loops are rejected.
///
/// # Complexity
///
/// O(V + E) time and O(V + E) space.
///
/// # Examples
///
/// ```
/// use geometric_traits::{
/// impls::{SortedVec, SymmetricCSR2D},
/// prelude::*,
/// traits::{CactusDetection, EdgesBuilder, VocabularyBuilder},
/// };
///
/// let nodes: Vec<usize> = vec![0, 1, 2, 3];
/// let edges: Vec<(usize, usize)> = vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)];
/// let nodes: SortedVec<usize> = GenericVocabularyBuilder::default()
/// .expected_number_of_symbols(nodes.len())
/// .symbols(nodes.into_iter().enumerate())
/// .build()
/// .unwrap();
/// let edges: SymmetricCSR2D<_> = UndiEdgesBuilder::default()
/// .expected_number_of_edges(edges.len())
/// .expected_shape(nodes.len())
/// .edges(edges.into_iter())
/// .build()
/// .unwrap();
/// let graph: UndiGraph<usize> = UndiGraph::from((nodes, edges));
///
/// assert!(!graph.is_cactus());
/// ```
#[inline]
fn is_cactus(&self) -> bool
where
Self: Sized,
{
if self.has_self_loops() {
return false;
}
let number_of_nodes = self.number_of_nodes().as_();
let mut discovery_time = vec![usize::MAX; number_of_nodes];
let mut low = vec![0usize; number_of_nodes];
let mut dfs_time = 0usize;
let mut edge_stack: Vec<[Self::NodeId; 2]> = Vec::new();
let mut dfs_stack: Vec<DfsFrame<'_, Self>> = Vec::new();
let mut touched = vec![false; number_of_nodes];
let mut touched_vertices = Vec::new();
for root in self.node_ids() {
if discovery_time[root.as_()] != usize::MAX {
continue;
}
discovery_time[root.as_()] = dfs_time;
low[root.as_()] = dfs_time;
dfs_time += 1;
dfs_stack.push(DfsFrame { node: root, parent: None, neighbors: self.neighbors(root) });
while !dfs_stack.is_empty() {
let maybe_neighbor = {
let frame = dfs_stack.last_mut().unwrap();
frame.neighbors.next()
};
if let Some(neighbor) = maybe_neighbor {
let node = dfs_stack.last().unwrap().node;
let parent = dfs_stack.last().unwrap().parent;
if discovery_time[neighbor.as_()] == usize::MAX {
discovery_time[neighbor.as_()] = dfs_time;
low[neighbor.as_()] = dfs_time;
dfs_time += 1;
edge_stack.push(normalize_edge(node, neighbor));
dfs_stack.push(DfsFrame {
node: neighbor,
parent: Some(node),
neighbors: self.neighbors(neighbor),
});
continue;
}
if Some(neighbor) != parent
&& discovery_time[neighbor.as_()] <= discovery_time[node.as_()]
{
low[node.as_()] = low[node.as_()].min(discovery_time[neighbor.as_()]);
edge_stack.push(normalize_edge(node, neighbor));
}
continue;
}
let frame = dfs_stack.pop().unwrap();
let node = frame.node;
if let Some(parent) = frame.parent {
let parent_index = parent.as_();
let node_index = node.as_();
low[parent_index] = low[parent_index].min(low[node_index]);
if low[node_index] >= discovery_time[parent_index]
&& !validate_component(
&mut edge_stack,
Some(normalize_edge(parent, node)),
&mut touched,
&mut touched_vertices,
)
{
return false;
}
}
}
if !validate_component(&mut edge_stack, None, &mut touched, &mut touched_vertices) {
return false;
}
}
true
}
}
impl<G: ?Sized + UndirectedMonopartiteMonoplexGraph> CactusDetection for G {}