Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
"SequencingWithReleaseTimesAndDeadlines": [Sequencing with Release Times and Deadlines],
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"MultipleCopyFileAllocation": [Multiple Copy File Allocation],
"SteinerTree": [Steiner Tree],
"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],
"SubgraphIsomorphism": [Subgraph Isomorphism],
Expand Down Expand Up @@ -1557,6 +1558,45 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("MultipleCopyFileAllocation")
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
let K = x.instance.bound
let sol = (config: x.optimal_config, metric: x.optimal_value)
let copies = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
[
#problem-def("MultipleCopyFileAllocation")[
Given a graph $G = (V, E)$, usage values $u: V -> ZZ_(> 0)$, storage costs $s: V -> ZZ_(> 0)$, and a positive integer $K$, determine whether there exists a subset $V' subset.eq V$ such that
$sum_(v in V') s(v) + sum_(v in V) u(v) dot d(v, V') <= K,$
where $d(v, V') = min_(w in V') d_G(v, w)$ is the shortest-path distance from $v$ to the nearest copy vertex.
][
Multiple Copy File Allocation appears in the storage-and-retrieval section of Garey and Johnson (SR6) @garey1979. The model combines two competing costs: each chosen copy vertex incurs a storage charge, while every vertex pays an access cost weighted by its demand and graph distance to the nearest copy. Garey and Johnson record the problem as NP-complete in the strong sense, even when usage and storage costs are uniform @garey1979.

*Example.* Consider the 6-cycle $C_6$ with uniform usage $u(v) = 10$, uniform storage $s(v) = 1$, and bound $K = #K$. Placing copies at $V' = {#copies.map(i => $v_#i$).join(", ")}$ gives storage cost $1 + 1 + 1 = 3$. The remaining vertices $v_0, v_2, v_4$ are each at distance 1 from the nearest copy, so the access cost is $10 + 10 + 10 = 30$. Thus the total cost is $3 + 30 = 33 <= #K$, so this placement is satisfying. The alternating placement shown below is one symmetric witness.

#figure({
let blue = graph-colors.at(0)
let gray = luma(200)
canvas(length: 1cm, {
import draw: *
let verts = ((0, 1.6), (1.35, 0.8), (1.35, -0.8), (0, -1.6), (-1.35, -0.8), (-1.35, 0.8))
for (u, v) in edges {
g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray)
}
for (k, pos) in verts.enumerate() {
let has-copy = copies.any(c => c == k)
g-node(pos, name: "v" + str(k),
fill: if has-copy { blue } else { white },
label: if has-copy { text(fill: white)[$v_#k$] } else { [$v_#k$] })
}
})
},
caption: [Multiple Copy File Allocation on a 6-cycle. Copy vertices $v_1$, $v_3$, and $v_5$ are shown in blue; every white vertex is one hop from the nearest copy, so the total cost is $33$.],
) <fig:multiple-copy-file-allocation>
]
]
}

== Set Problems

#{
Expand Down
9 changes: 8 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ Flags by problem type:
BMF --matrix (0/1), --rank
ConsecutiveOnesSubmatrix --matrix (0/1), --k
SteinerTree --graph, --edge-weights, --terminals
MultipleCopyFileAllocation --graph, --usage, --storage, --bound
CVP --basis, --target-vec [--bounds]
MultiprocessorScheduling --lengths, --num-processors, --deadline
SequencingWithinIntervals --release-times, --deadlines, --lengths
Expand Down Expand Up @@ -470,7 +471,7 @@ pub struct CreateArgs {
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
#[arg(long)]
pub required_edges: Option<String>,
/// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
/// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
#[arg(long, allow_hyphen_values = true)]
pub bound: Option<i64>,
/// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0)
Expand Down Expand Up @@ -509,6 +510,12 @@ pub struct CreateArgs {
/// Candidate weighted arcs for StrongConnectivityAugmentation (e.g., 2>0:1,2>1:3)
#[arg(long)]
pub candidate_arcs: Option<String>,
/// Usage frequencies for MultipleCopyFileAllocation (comma-separated, e.g., "5,4,3,2")
#[arg(long)]
pub usage: Option<String>,
/// Storage costs for MultipleCopyFileAllocation (comma-separated, e.g., "1,1,1,1")
#[arg(long)]
pub storage: Option<String>,
/// Deadlines for scheduling problems (comma-separated, e.g., "5,5,5,3,3")
#[arg(long)]
pub deadlines: Option<String>,
Expand Down
67 changes: 67 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ use problemreductions::topology::{
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};

const MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS: &str =
"--graph 0-1,1-2,2-3 --usage 5,4,3,2 --storage 1,1,1,1 --bound 8";
const MULTIPLE_COPY_FILE_ALLOCATION_USAGE: &str =
"Usage: pred create MultipleCopyFileAllocation --graph 0-1,1-2,2-3 --usage 5,4,3,2 --storage 1,1,1,1 --bound 8";

/// Check if all data flags are None (no problem-specific input provided).
fn all_data_flags_empty(args: &CreateArgs) -> bool {
args.graph.is_none()
Expand Down Expand Up @@ -89,6 +94,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.strings.is_none()
&& args.costs.is_none()
&& args.arcs.is_none()
&& args.usage.is_none()
&& args.storage.is_none()
&& args.source.is_none()
&& args.sink.is_none()
&& args.size_bound.is_none()
Expand All @@ -99,6 +106,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.candidate_arcs.is_none()
&& args.potential_edges.is_none()
&& args.budget.is_none()
&& args.deadlines.is_none()
&& args.precedence_pairs.is_none()
&& args.resource_bounds.is_none()
&& args.resource_requirements.is_none()
Expand Down Expand Up @@ -402,6 +410,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--schedules \"1,1,1,1,1,0,0;0,1,1,1,1,1,0;0,0,1,1,1,1,1;1,0,0,1,1,1,1;1,1,0,0,1,1,1\" --requirements 2,2,2,3,3,2,1 --num-workers 4 --k 5"
}
"SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4",
"MultipleCopyFileAllocation" => {
MULTIPLE_COPY_FILE_ALLOCATION_EXAMPLE_ARGS
}
"OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5",
"DirectedTwoCommodityIntegralFlow" => {
"--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1"
Expand Down Expand Up @@ -957,6 +968,37 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(ser(HamiltonianPath::new(graph))?, resolved_variant.clone())
}

// MultipleCopyFileAllocation (graph + usage + storage + bound)
"MultipleCopyFileAllocation" => {
let (graph, num_vertices) = parse_graph(args)
.map_err(|e| anyhow::anyhow!("{e}\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"))?;
let usage = parse_vertex_i64_values(
args.usage.as_deref(),
"usage",
num_vertices,
"MultipleCopyFileAllocation",
MULTIPLE_COPY_FILE_ALLOCATION_USAGE,
)?;
let storage = parse_vertex_i64_values(
args.storage.as_deref(),
"storage",
num_vertices,
"MultipleCopyFileAllocation",
MULTIPLE_COPY_FILE_ALLOCATION_USAGE,
)?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!(
"MultipleCopyFileAllocation requires --bound\n\n{MULTIPLE_COPY_FILE_ALLOCATION_USAGE}"
)
})?;
(
ser(MultipleCopyFileAllocation::new(
graph, usage, storage, bound,
))?,
resolved_variant.clone(),
)
}

// UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements)
"UndirectedTwoCommodityIntegralFlow" => {
let usage = "Usage: pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1";
Expand Down Expand Up @@ -3105,6 +3147,29 @@ fn parse_vertex_weights(args: &CreateArgs, num_vertices: usize) -> Result<Vec<i3
}
}

fn parse_vertex_i64_values(
raw: Option<&str>,
field_name: &str,
num_vertices: usize,
problem_name: &str,
usage: &str,
) -> Result<Vec<i64>> {
let raw =
raw.ok_or_else(|| anyhow::anyhow!("{problem_name} requires --{field_name}\n\n{usage}"))?;
let values: Vec<i64> = util::parse_comma_list(raw)
.map_err(|e| anyhow::anyhow!("invalid {field_name} list: {e}\n\n{usage}"))?;
if values.len() != num_vertices {
bail!(
"Expected {} {} values but got {}\n\n{}",
num_vertices,
field_name,
values.len(),
usage
);
}
Ok(values)
}

/// Parse `--terminals` as comma-separated vertex indices.
fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result<Vec<usize>> {
let s = args
Expand Down Expand Up @@ -4554,6 +4619,8 @@ mod tests {
costs: None,
cut_bound: None,
size_bound: None,
usage: None,
storage: None,
}
}

Expand Down
Loading
Loading