Skip to content

Commit 1590ea6

Browse files
GiggleLiuisPANNclaude
authored
Fix #409: [Model] RootedTreeStorageAssignment (#749)
* Add plan for #409: [Model] RootedTreeStorageAssignment * Implement #409: [Model] RootedTreeStorageAssignment * chore: remove plan file after implementation * chore: fix formatting after merge with main Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e8fbdbe commit 1590ea6

8 files changed

Lines changed: 455 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"QuantifiedBooleanFormulas": [Quantified Boolean Formulas (QBF)],
159159
"RectilinearPictureCompression": [Rectilinear Picture Compression],
160160
"ResourceConstrainedScheduling": [Resource Constrained Scheduling],
161+
"RootedTreeStorageAssignment": [Rooted Tree Storage Assignment],
161162
"SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines],
162163
"SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost],
163164
"SequencingToMinimizeWeightedCompletionTime": [Sequencing to Minimize Weighted Completion Time],
@@ -2912,6 +2913,70 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
29122913
]
29132914
}
29142915

2916+
#{
2917+
let x = load-model-example("RootedTreeStorageAssignment")
2918+
let n = x.instance.universe_size
2919+
let subsets = x.instance.subsets
2920+
let m = subsets.len()
2921+
let K = x.instance.bound
2922+
let config = x.optimal_config
2923+
let edges = config.enumerate().filter(((v, p)) => v != p).map(((v, p)) => (p, v))
2924+
let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$"
2925+
let highlight-nodes = (0, 2, 4)
2926+
let highlight-edges = ((0, 2), (2, 4))
2927+
[
2928+
#problem-def("RootedTreeStorageAssignment")[
2929+
Given a finite set $X = {0, 1, dots, #(n - 1)}$, a collection $cal(C) = {X_1, dots, X_m}$ of subsets of $X$, and a nonnegative integer $K$, find a directed rooted tree $T = (X, A)$ and supersets $X_i' supset.eq X_i$ such that every $X_i'$ forms a directed path in $T$ and $sum_(i = 1)^m |X_i' backslash X_i| <= K$.
2930+
][
2931+
Rooted Tree Storage Assignment is the storage-and-retrieval problem SR5 in Garey and Johnson @garey1979. Their catalog credits a reduction from Rooted Tree Arrangement, framing the problem as hierarchical file organization: pick a rooted tree on the records so every request set can be completed to a single root-to-leaf path using only a limited number of extra records. The implementation here uses one parent variable per element of $X$, so the direct exhaustive bound is $|X|^(|X|)$ candidate parent arrays, filtered down to valid rooted trees#footnote[No exact algorithm improving on the direct parent-array search bound is claimed here for the general formulation.].
2932+
2933+
*Example.* Let $X = {0, 1, dots, #(n - 1)}$, $K = #K$, and $cal(C) = {#range(m).map(i => $X_#(i + 1)$).join(", ")}$ with #subsets.enumerate().map(((i, s)) => $X_#(i + 1) = #fmt-set(s)$).join(", "). The satisfying parent array $p = (#config.map(str).join(", "))$ encodes the rooted tree with arcs #edges.map(((u, v)) => $(#u, #v)$).join(", "). In this tree, $X_1 = {0, 2}$, $X_2 = {1, 3}$, and $X_4 = {2, 4}$ are already directed paths. The only extension is $X_3 = {0, 4}$, which becomes $X_3' = {0, 2, 4}$ along the path $0 -> 2 -> 4$, so the total extension cost is exactly $1 = K$.
2934+
2935+
#pred-commands(
2936+
"pred create --example " + problem-spec(x) + " -o rooted-tree-storage-assignment.json",
2937+
"pred solve rooted-tree-storage-assignment.json --solver brute-force",
2938+
"pred evaluate rooted-tree-storage-assignment.json --config " + x.optimal_config.map(str).join(","),
2939+
)
2940+
2941+
#figure(
2942+
canvas(length: 1cm, {
2943+
import draw: *
2944+
2945+
let positions = (
2946+
(1.5, 1.8),
2947+
(0.6, 0.9),
2948+
(2.4, 0.9),
2949+
(0.6, 0.0),
2950+
(2.4, 0.0),
2951+
)
2952+
2953+
for (u, v) in edges {
2954+
let highlighted = highlight-edges.contains((u, v))
2955+
line(
2956+
positions.at(u),
2957+
positions.at(v),
2958+
stroke: if highlighted { 1.2pt + graph-colors.at(0) } else { 0.8pt + luma(140) },
2959+
mark: (end: "straight", scale: 0.45),
2960+
)
2961+
}
2962+
2963+
for (vertex, pos) in positions.enumerate() {
2964+
let highlighted = highlight-nodes.contains(vertex)
2965+
circle(
2966+
pos,
2967+
radius: 0.2,
2968+
fill: if highlighted { graph-colors.at(0) } else { white },
2969+
stroke: 0.6pt + black,
2970+
)
2971+
content(pos, if highlighted { text(fill: white)[$#vertex$] } else { [$#vertex$] })
2972+
}
2973+
}),
2974+
caption: [Rooted Tree Storage Assignment example. The rooted tree encoded by $p = (#config.map(str).join(", "))$ is shown; the blue path $0 -> 2 -> 4$ is the unique extension needed to realize $X_3 = {0, 4}$ within total cost $K = #K$.],
2975+
) <fig:rooted-tree-storage-assignment>
2976+
]
2977+
]
2978+
}
2979+
29152980
#{
29162981
let x = load-model-example("TwoDimensionalConsecutiveSets")
29172982
let n = x.instance.alphabet_size

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ Flags by problem type:
260260
SetBasis --universe, --sets, --k
261261
MinimumCardinalityKey --num-attributes, --dependencies, --k
262262
PrimeAttributeName --universe, --deps, --query
263+
RootedTreeStorageAssignment --universe, --sets, --bound
263264
TwoDimensionalConsecutiveSets --alphabet-size, --sets
264265
BicliqueCover --left, --right, --biedges, --k
265266
BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k

problemreductions-cli/src/commands/create.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
598598
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
599599
"HamiltonianCircuit" => "--graph 0-1,1-2,2-3,3-0",
600600
"EnsembleComputation" => "--universe 4 --sets \"0,1,2;0,1,3\" --budget 4",
601+
"RootedTreeStorageAssignment" => "--universe 5 --sets \"0,2;1,3;0,4;2,4\" --bound 1",
601602
"MinMaxMulticenter" => {
602603
"--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2 --bound 2"
603604
}
@@ -2668,6 +2669,31 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
26682669
)
26692670
}
26702671

2672+
// RootedTreeStorageAssignment
2673+
"RootedTreeStorageAssignment" => {
2674+
let usage =
2675+
"Usage: pred create RootedTreeStorageAssignment --universe 5 --sets \"0,2;1,3;0,4;2,4\" --bound 1";
2676+
let universe_size = args.universe.ok_or_else(|| {
2677+
anyhow::anyhow!("RootedTreeStorageAssignment requires --universe\n\n{usage}")
2678+
})?;
2679+
let subsets = parse_sets(args)?;
2680+
let bound = args.bound.ok_or_else(|| {
2681+
anyhow::anyhow!("RootedTreeStorageAssignment requires --bound\n\n{usage}")
2682+
})?;
2683+
let bound = parse_nonnegative_usize_bound(bound, "RootedTreeStorageAssignment", usage)?;
2684+
(
2685+
ser(
2686+
problemreductions::models::set::RootedTreeStorageAssignment::try_new(
2687+
universe_size,
2688+
subsets,
2689+
bound,
2690+
)
2691+
.map_err(anyhow::Error::msg)?,
2692+
)?,
2693+
resolved_variant.clone(),
2694+
)
2695+
}
2696+
26712697
// BicliqueCover
26722698
"BicliqueCover" => {
26732699
let usage = "pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2";
@@ -7495,6 +7521,38 @@ mod tests {
74957521
assert!(err.contains("ExpectedRetrievalCost requires --latency-bound"));
74967522
}
74977523

7524+
#[test]
7525+
fn test_create_rooted_tree_storage_assignment_json() {
7526+
let mut args = empty_args();
7527+
args.problem = Some("RootedTreeStorageAssignment".to_string());
7528+
args.universe = Some(5);
7529+
args.sets = Some("0,2;1,3;0,4;2,4".to_string());
7530+
args.bound = Some(1);
7531+
7532+
let output_path =
7533+
std::env::temp_dir().join("pred_test_create_rooted_tree_storage_assignment.json");
7534+
let out = OutputConfig {
7535+
output: Some(output_path.clone()),
7536+
quiet: true,
7537+
json: false,
7538+
auto_json: false,
7539+
};
7540+
7541+
create(&args, &out).unwrap();
7542+
7543+
let content = std::fs::read_to_string(&output_path).unwrap();
7544+
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
7545+
assert_eq!(json["type"], "RootedTreeStorageAssignment");
7546+
assert_eq!(json["data"]["universe_size"], 5);
7547+
assert_eq!(
7548+
json["data"]["subsets"],
7549+
serde_json::json!([[0, 2], [1, 3], [0, 4], [2, 4]])
7550+
);
7551+
assert_eq!(json["data"]["bound"], 1);
7552+
7553+
std::fs::remove_file(output_path).ok();
7554+
}
7555+
74987556
#[test]
74997557
fn test_create_stacker_crane_json() {
75007558
let mut args = empty_args();

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ pub mod prelude {
8080
};
8181
pub use crate::models::set::{
8282
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
83-
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis,
83+
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName,
84+
RootedTreeStorageAssignment, SetBasis,
8485
};
8586

8687
// Core traits

src/models/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ pub use misc::{
4848
};
4949
pub use set::{
5050
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
51-
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName, SetBasis,
52-
TwoDimensionalConsecutiveSets,
51+
MinimumCardinalityKey, MinimumHittingSet, MinimumSetCovering, PrimeAttributeName,
52+
RootedTreeStorageAssignment, SetBasis, TwoDimensionalConsecutiveSets,
5353
};

src/models/set/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! - [`MinimumHittingSet`]: Minimum-size universe subset hitting every set
99
//! - [`MinimumSetCovering`]: Minimum weight set cover
1010
//! - [`PrimeAttributeName`]: Determine if an attribute belongs to any candidate key
11+
//! - [`RootedTreeStorageAssignment`]: Extend subsets to directed tree paths within a total-cost bound
1112
1213
pub(crate) mod comparative_containment;
1314
pub(crate) mod consecutive_sets;
@@ -17,6 +18,7 @@ pub(crate) mod minimum_cardinality_key;
1718
pub(crate) mod minimum_hitting_set;
1819
pub(crate) mod minimum_set_covering;
1920
pub(crate) mod prime_attribute_name;
21+
pub(crate) mod rooted_tree_storage_assignment;
2022
pub(crate) mod set_basis;
2123
pub(crate) mod two_dimensional_consecutive_sets;
2224

@@ -28,6 +30,7 @@ pub use minimum_cardinality_key::MinimumCardinalityKey;
2830
pub use minimum_hitting_set::MinimumHittingSet;
2931
pub use minimum_set_covering::MinimumSetCovering;
3032
pub use prime_attribute_name::PrimeAttributeName;
33+
pub use rooted_tree_storage_assignment::RootedTreeStorageAssignment;
3134
pub use set_basis::SetBasis;
3235
pub use two_dimensional_consecutive_sets::TwoDimensionalConsecutiveSets;
3336

@@ -42,6 +45,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
4245
specs.extend(minimum_hitting_set::canonical_model_example_specs());
4346
specs.extend(minimum_set_covering::canonical_model_example_specs());
4447
specs.extend(prime_attribute_name::canonical_model_example_specs());
48+
specs.extend(rooted_tree_storage_assignment::canonical_model_example_specs());
4549
specs.extend(set_basis::canonical_model_example_specs());
4650
specs.extend(two_dimensional_consecutive_sets::canonical_model_example_specs());
4751
specs

0 commit comments

Comments
 (0)