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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
use crate::sparse::csc::CscPattern;
/// Elimination tree of a symmetric matrix.
///
/// For a symmetric matrix A, the elimination tree has the property:
/// parent[j] = min { i > j : L(i,j) ≠ 0 } where L is the Cholesky factor.
/// For indefinite matrices, the same structure applies to the fill pattern.
///
/// Constructed from the symmetric sparsity pattern using union-find with
/// path compression (George & Liu 1981, Chapter 4).
#[derive(Debug, Clone)]
pub struct EliminationTree {
/// parent[j] = Some(i) where i > j, or None if j is a root.
pub parent: Vec<Option<usize>>,
pub n: usize,
}
impl EliminationTree {
/// Build the elimination tree from a symmetric sparsity pattern.
///
/// Uses the column-by-column algorithm with path compression
/// (Liu 1990, based on George & Liu 1981):
///
/// For each column j (in order 0..n), examine all rows i < j in column j
/// (the upper triangle entries). Walk from i up the partially built tree
/// using path compression until finding a root or reaching j. Make j the
/// parent of that root. This produces parent[j] = min { i > j : L(i,j) ≠ 0 }.
pub fn from_pattern(pattern: &CscPattern) -> Self {
let n = pattern.n;
let mut parent: Vec<Option<usize>> = vec![None; n];
let mut ancestor = vec![0usize; n]; // union-find forest
for j in 0..n {
ancestor[j] = j; // j is its own root initially
for k in pattern.col_ptr[j]..pattern.col_ptr[j + 1] {
let i = pattern.row_idx[k];
if i >= j {
continue; // only process entries with i < j
}
// Find the root of i's subtree (with path compression)
let mut r = i;
while ancestor[r] != r {
r = ancestor[r];
}
// Path compression: make all nodes on the path point to r
let mut node = i;
while node != r {
let next = ancestor[node];
ancestor[node] = r;
node = next;
}
// If r != j, make j the parent of r
if r != j {
parent[r] = Some(j);
ancestor[r] = j; // union: attach r's tree under j
}
}
}
EliminationTree { parent, n }
}
/// Compute children lists from parent pointers.
pub fn children(&self) -> Vec<Vec<usize>> {
let mut ch = vec![Vec::new(); self.n];
for j in 0..self.n {
if let Some(p) = self.parent[j] {
ch[p].push(j);
}
}
ch
}
/// Return root nodes (nodes with no parent).
pub fn roots(&self) -> Vec<usize> {
(0..self.n).filter(|&j| self.parent[j].is_none()).collect()
}
/// Compute subtree sizes (number of nodes in each subtree, including self).
pub fn subtree_sizes(&self) -> Vec<usize> {
let mut sizes = vec![1usize; self.n];
// Process in reverse topological order (children before parents)
// Since parent[j] > j always, processing 0..n in order is fine
// if we accumulate into parents.
for j in 0..self.n {
if let Some(p) = self.parent[j] {
sizes[p] += sizes[j];
}
}
sizes
}
/// Postorder traversal of the etree forest. Returns a Vec of
/// node indices in postorder (each subtree's children listed
/// before the subtree's root; roots of the forest come last).
///
/// Iterative DFS using an explicit stack so deep trees don't
/// blow the call stack.
pub fn postorder(&self) -> Vec<usize> {
let n = self.n;
let mut out = Vec::with_capacity(n);
let children = self.children();
let mut next_child = vec![0usize; n];
let mut stack: Vec<usize> = Vec::with_capacity(n);
for root in self.roots() {
stack.push(root);
while let Some(&node) = stack.last() {
let k = next_child[node];
if k < children[node].len() {
next_child[node] = k + 1;
stack.push(children[node][k]);
} else {
out.push(node);
stack.pop();
}
}
}
out
}
/// First-descendant numbering used by the Gilbert-Ng-Peyton
/// column-count algorithm.
///
/// Given a postorder `post` (as returned by [`postorder`]), the
/// result `first[i]` is the smallest postorder number taken by
/// any descendant of node i (including i itself). Leaves have
/// `first[i]` equal to their own postorder index.
pub fn first_descendants(&self, post: &[usize]) -> Vec<usize> {
let n = self.n;
debug_assert_eq!(post.len(), n);
let mut post_of = vec![0usize; n];
for (pnum, &node) in post.iter().enumerate() {
post_of[node] = pnum;
}
// Initialize first[i] = its own postorder index. Walking
// the tree in postorder guarantees every child finalizes
// before its parent, so parent's `first` folds in children.
let mut first = post_of.clone();
for &node in post {
if let Some(p) = self.parent[node] {
if first[node] < first[p] {
first[p] = first[node];
}
}
}
first
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::csc::CscMatrix;
#[test]
fn test_etree_tridiagonal() {
// Tridiagonal 5x5: elimination tree is a path 0→1→2→3→4
let m = CscMatrix::from_triplets(
5,
&[0, 1, 1, 2, 2, 3, 3, 4, 4],
&[0, 0, 1, 1, 2, 2, 3, 3, 4],
&[1.0; 9],
)
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
assert_eq!(etree.parent[0], Some(1));
assert_eq!(etree.parent[1], Some(2));
assert_eq!(etree.parent[2], Some(3));
assert_eq!(etree.parent[3], Some(4));
assert_eq!(etree.parent[4], None); // root
}
#[test]
fn test_etree_arrow() {
// Arrow matrix: node 0 is connected to all others
// After natural ordering, etree should have 0 as root
// with nodes 1,2,3,4 filling through 0
let m = CscMatrix::from_triplets(
5,
&[0, 1, 2, 3, 4, 1, 2, 3, 4],
&[0, 0, 0, 0, 0, 1, 2, 3, 4],
&[1.0; 9],
)
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
// With natural ordering on arrow matrix:
// All nodes 1-4 connect to 0, and eliminating 0 creates a clique
// among 1-4. So the etree should be 0→1→2→3→4 (chain from fill).
// Actually: parent[j] = min { i > j : L(i,j) != 0 }
// For column 0: rows 1,2,3,4 all have entries → parent[0] = 1 (not a root!)
// Wait — arrow has column 0 connected to rows 1,2,3,4
// Column 0: entries at rows 1,2,3,4 → parent[0] = min(1,2,3,4) = 1
// Column 1: entry at row 0 (but 0 < 1, skip). Fill from eliminating 0: rows 2,3,4
// → parent[1] = 2
// etc. So etree is a chain 0→1→2→3→4, root = 4
assert_eq!(etree.parent[4], None);
assert_eq!(etree.roots(), vec![4]);
}
fn chain_etree(n: usize) -> EliminationTree {
// Build a chain 0→1→2→...→(n-1) directly.
let mut parent = vec![None; n];
for j in 0..n.saturating_sub(1) {
parent[j] = Some(j + 1);
}
EliminationTree { parent, n }
}
#[test]
fn test_postorder_chain() {
let et = chain_etree(5);
let post = et.postorder();
assert_eq!(post, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_postorder_star() {
// 4 leaves (0, 1, 2, 3) all parented to root 4.
let parent = vec![Some(4), Some(4), Some(4), Some(4), None];
let et = EliminationTree { parent, n: 5 };
let post = et.postorder();
// Root must come last; leaves visited before root.
assert_eq!(*post.last().unwrap(), 4);
assert_eq!(post.len(), 5);
let mut leaves: Vec<_> = post[..4].to_vec();
leaves.sort_unstable();
assert_eq!(leaves, vec![0, 1, 2, 3]);
}
#[test]
fn test_postorder_two_roots() {
// Two disjoint chains: 0→1 (root 1) and 2→3 (root 3).
let parent = vec![Some(1), None, Some(3), None];
let et = EliminationTree { parent, n: 4 };
let post = et.postorder();
assert_eq!(post.len(), 4);
// Each child precedes its root. Both roots come at positions 1 and 3.
let p0 = post.iter().position(|&x| x == 0).unwrap();
let p1 = post.iter().position(|&x| x == 1).unwrap();
let p2 = post.iter().position(|&x| x == 2).unwrap();
let p3 = post.iter().position(|&x| x == 3).unwrap();
assert!(p0 < p1);
assert!(p2 < p3);
}
#[test]
fn test_first_descendants_chain() {
// Chain 0→1→2→3→4. Postorder is [0,1,2,3,4].
// Subtree of i is {0, 1, ..., i}. First descendant is 0 for all.
let et = chain_etree(5);
let post = et.postorder();
let first = et.first_descendants(&post);
assert_eq!(first, vec![0, 0, 0, 0, 0]);
}
#[test]
fn test_first_descendants_leaves() {
// For a leaf, first[leaf] = its own postorder number.
let parent = vec![Some(4), Some(4), Some(4), Some(4), None];
let et = EliminationTree { parent, n: 5 };
let post = et.postorder();
let first = et.first_descendants(&post);
// Each leaf's first is its own postorder index.
for leaf in 0..4 {
let ppos = post.iter().position(|&x| x == leaf).unwrap();
assert_eq!(first[leaf], ppos);
}
// Root's first = min of the 4 leaf postorder numbers = 0.
assert_eq!(first[4], 0);
}
#[test]
fn test_etree_diagonal() {
// Diagonal: no off-diagonal entries → forest of singletons
let m = CscMatrix::from_triplets(4, &[0, 1, 2, 3], &[0, 1, 2, 3], &[1.0; 4]).unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
for j in 0..4 {
assert_eq!(etree.parent[j], None);
}
assert_eq!(etree.roots().len(), 4);
}
#[test]
fn test_etree_children() {
// Tridiagonal: children of node k = [k-1] (except 0)
let m =
CscMatrix::from_triplets(4, &[0, 1, 1, 2, 2, 3, 3], &[0, 0, 1, 1, 2, 2, 3], &[1.0; 7])
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let ch = etree.children();
assert_eq!(ch[0], Vec::<usize>::new());
assert_eq!(ch[1], vec![0]);
assert_eq!(ch[2], vec![1]);
assert_eq!(ch[3], vec![2]);
}
#[test]
fn test_subtree_sizes() {
// Tridiagonal 4x4: chain 0→1→2→3
let m =
CscMatrix::from_triplets(4, &[0, 1, 1, 2, 2, 3, 3], &[0, 0, 1, 1, 2, 2, 3], &[1.0; 7])
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let sizes = etree.subtree_sizes();
assert_eq!(sizes[0], 1);
assert_eq!(sizes[1], 2);
assert_eq!(sizes[2], 3);
assert_eq!(sizes[3], 4);
}
}