1414//! Experimental evaluation of a branch and bound algorithm for computing pathwidth.
1515//! <https://doi.org/10.1007/978-3-319-07959-2_5>
1616
17+ use rand:: rngs:: SmallRng ;
1718use rand:: seq:: IndexedRandom ;
18- use std:: collections:: { HashMap , HashSet } ;
19+ use rand:: SeedableRng ;
20+ use std:: collections:: { BTreeSet , HashMap , HashSet } ;
21+
22+ /// Default seed used when no explicit seed is passed to [`pathwidth`].
23+ ///
24+ /// Keeping a fixed default makes [`pathwidth`] and [`greedy_decompose`] deterministic
25+ /// across repeated invocations with the same input, which reductions and downstream
26+ /// benchmarks rely on for reproducibility. Callers that want diverse layouts can use
27+ /// [`pathwidth_with_seed`] instead.
28+ pub const DEFAULT_PATHWIDTH_SEED : u64 = 0 ;
1929
2030/// Adjacency list representation built once from an edge list.
21- type AdjList = Vec < HashSet < usize > > ;
31+ ///
32+ /// Uses `BTreeSet` rather than `HashSet` so iteration order is deterministic
33+ /// (sorted by vertex index). Several places in this module push from `adj[v]`
34+ /// into the layout's neighbor list; HashSet iteration order leaked into the
35+ /// vertex ordering and caused non-reproducible path decompositions.
36+ type AdjList = Vec < BTreeSet < usize > > ;
2237
2338/// Build an adjacency list from an edge list.
2439fn build_adj ( num_vertices : usize , edges : & [ ( usize , usize ) ] ) -> AdjList {
25- let mut adj: Vec < HashSet < usize > > = vec ! [ HashSet :: new( ) ; num_vertices] ;
40+ let mut adj: AdjList = vec ! [ BTreeSet :: new( ) ; num_vertices] ;
2641 for & ( u, v) in edges {
2742 adj[ u] . insert ( v) ;
2843 adj[ v] . insert ( u) ;
@@ -258,8 +273,14 @@ fn greedy_exact(adj: &AdjList, mut layout: Layout) -> Layout {
258273
259274/// Perform one greedy step by choosing the best vertex from a list.
260275///
261- /// Selects randomly among vertices that minimize the new vsep.
262- fn greedy_step ( adj : & AdjList , layout : & Layout , list : & [ usize ] ) -> Layout {
276+ /// Selects among vertices that minimize the new vsep, breaking ties using the
277+ /// provided RNG. Passing an explicit RNG makes tie-breaking deterministic given a seed.
278+ fn greedy_step < R : rand:: Rng + ?Sized > (
279+ adj : & AdjList ,
280+ layout : & Layout ,
281+ list : & [ usize ] ,
282+ rng : & mut R ,
283+ ) -> Layout {
263284 let layouts: Vec < Layout > = list. iter ( ) . map ( |& v| extend ( adj, layout, v) ) . collect ( ) ;
264285
265286 let costs: Vec < usize > = layouts. iter ( ) . map ( |l| l. vsep ( ) ) . collect ( ) ;
@@ -272,27 +293,43 @@ fn greedy_step(adj: &AdjList, layout: &Layout, list: &[usize]) -> Layout {
272293 . map ( |( i, _) | i)
273294 . collect ( ) ;
274295
275- let mut rng = rand:: rng ( ) ;
276- let & chosen_idx = best_indices. as_slice ( ) . choose ( & mut rng) . unwrap ( ) ;
296+ let & chosen_idx = best_indices. as_slice ( ) . choose ( rng) . unwrap ( ) ;
277297
278298 layouts. into_iter ( ) . nth ( chosen_idx) . unwrap ( )
279299}
280300
281301/// Compute a path decomposition using the greedy algorithm.
282302///
283- /// This combines exact rules (that don't increase pathwidth) with
284- /// greedy choices when exact rules don't apply.
303+ /// Uses a fixed default seed (see [`DEFAULT_PATHWIDTH_SEED`]) so repeated calls on
304+ /// the same input produce the same layout. Use [`pathwidth_with_seed`] for variation.
305+ ///
306+ /// Only exposed for unit tests; production code reaches the greedy path through
307+ /// [`pathwidth`] or [`pathwidth_with_seed`].
308+ #[ cfg( test) ]
285309pub fn greedy_decompose ( num_vertices : usize , edges : & [ ( usize , usize ) ] ) -> Layout {
310+ let mut rng = SmallRng :: seed_from_u64 ( DEFAULT_PATHWIDTH_SEED ) ;
311+ greedy_decompose_with_rng ( num_vertices, edges, & mut rng)
312+ }
313+
314+ /// Compute a path decomposition using the greedy algorithm with a caller-supplied RNG.
315+ ///
316+ /// This combines exact rules (that don't increase pathwidth) with greedy choices
317+ /// when exact rules don't apply. Random tie-breaking draws from `rng`.
318+ fn greedy_decompose_with_rng < R : rand:: Rng + ?Sized > (
319+ num_vertices : usize ,
320+ edges : & [ ( usize , usize ) ] ,
321+ rng : & mut R ,
322+ ) -> Layout {
286323 let adj = build_adj ( num_vertices, edges) ;
287324 let mut layout = Layout :: empty ( num_vertices) ;
288325
289326 loop {
290327 layout = greedy_exact ( & adj, layout) ;
291328
292329 if !layout. neighbors . is_empty ( ) {
293- layout = greedy_step ( & adj, & layout, & layout. neighbors . clone ( ) ) ;
330+ layout = greedy_step ( & adj, & layout, & layout. neighbors . clone ( ) , rng ) ;
294331 } else if !layout. disconnected . is_empty ( ) {
295- layout = greedy_step ( & adj, & layout, & layout. disconnected . clone ( ) ) ;
332+ layout = greedy_step ( & adj, & layout, & layout. disconnected . clone ( ) , rng ) ;
296333 } else {
297334 break ;
298335 }
@@ -423,6 +460,23 @@ pub fn pathwidth(
423460 num_vertices : usize ,
424461 edges : & [ ( usize , usize ) ] ,
425462 method : PathDecompositionMethod ,
463+ ) -> Layout {
464+ pathwidth_with_seed ( num_vertices, edges, method, DEFAULT_PATHWIDTH_SEED )
465+ }
466+
467+ /// Like [`pathwidth`], but with a caller-chosen RNG seed for greedy tie-breaking.
468+ ///
469+ /// The greedy path-decomposition algorithm uses random choices to break ties
470+ /// between candidate vertices with the same vertex separation. A single `SmallRng`
471+ /// seeded with `seed` is threaded through all restarts, so restarts remain diverse
472+ /// (each advances the RNG state) while the overall output is reproducible.
473+ ///
474+ /// `MinhThiTrick` and `Auto` on small graphs are deterministic and ignore the seed.
475+ pub fn pathwidth_with_seed (
476+ num_vertices : usize ,
477+ edges : & [ ( usize , usize ) ] ,
478+ method : PathDecompositionMethod ,
479+ seed : u64 ,
426480) -> Layout {
427481 let method = match method {
428482 PathDecompositionMethod :: Auto => {
@@ -438,9 +492,10 @@ pub fn pathwidth(
438492 PathDecompositionMethod :: Greedy { nrepeat } => {
439493 // Defend against direct enum construction with nrepeat = 0.
440494 let nrepeat = nrepeat. max ( 1 ) ;
495+ let mut rng = SmallRng :: seed_from_u64 ( seed) ;
441496 let mut best: Option < Layout > = None ;
442497 for _ in 0 ..nrepeat {
443- let layout = greedy_decompose ( num_vertices, edges) ;
498+ let layout = greedy_decompose_with_rng ( num_vertices, edges, & mut rng ) ;
444499 if best. is_none ( ) || layout. vsep ( ) < best. as_ref ( ) . unwrap ( ) . vsep ( ) {
445500 best = Some ( layout) ;
446501 }
0 commit comments