Skip to content

Commit

Permalink
Merge pull request #22 from vcoppe/lcs
Browse files Browse the repository at this point in the history
  • Loading branch information
xgillard committed Jun 9, 2023
2 parents a37a722 + 135cd5c commit 1edfa59
Show file tree
Hide file tree
Showing 36 changed files with 1,120 additions and 28 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
**/*.rs.bk
*.iml
.DS_Store
.idea/
/target
/examples/target
Expand Down
44 changes: 44 additions & 0 deletions ddo/examples/lcs/dominance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

use ddo::Dominance;

use crate::model::LcsState;

pub struct LcsDominance;
impl Dominance for LcsDominance {
type State = LcsState;
type Key = usize;

fn get_key(&self, state: std::sync::Arc<Self::State>) -> Option<Self::Key> {
Some(state.position[0])
}

fn nb_dimensions(&self, state: &Self::State) -> usize {
state.position.len()
}

fn get_coordinate(&self, state: &Self::State, i: usize) -> isize {
- (state.position[i] as isize)
}

fn use_value(&self) -> bool {
true
}
}
52 changes: 52 additions & 0 deletions ddo/examples/lcs/dp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

/// Naive DP solver the 2-strings longest common subsequence problem, used
/// to build a heuristic for the m-strings problem
pub struct LcsDp<'a> {
pub n_chars: usize,
pub a: &'a Vec<usize>,
pub b: &'a Vec<usize>,
}

impl<'a> LcsDp<'a> {
pub fn solve(&self) -> Vec<Vec<isize>> {
let mut table = vec![vec![-1; self.b.len() + 1]; self.a.len() + 1];
for i in 0..=self.a.len() {

Check warning on line 31 in ddo/examples/lcs/dp.rs

View workflow job for this annotation

GitHub Actions / clippy

the loop variable `i` is only used to index `table`

warning: the loop variable `i` is only used to index `table` --> ddo/examples/lcs/dp.rs:31:18 | 31 | for i in 0..=self.a.len() { | ^^^^^^^^^^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_range_loop help: consider using an iterator | 31 | for <item> in table.iter_mut().take(self.a.len() + 1) { | ~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
table[i][self.b.len()] = 0;
}
for j in 0..=self.b.len() {
table[self.a.len()][j] = 0;
}
self._solve(&mut table, 0, 0);
table
}

fn _solve(&self, table: &mut Vec<Vec<isize>>, i: usize, j: usize) -> isize {
if table[i][j] != -1 {
return table[i][j];
}

table[i][j] = self._solve(table, i + 1, j)
.max(self._solve(table, i, j + 1))
.max(self._solve(table, i + 1, j + 1) + (self.a[i] == self.b[j]) as isize);

table[i][j]
}
}
133 changes: 133 additions & 0 deletions ddo/examples/lcs/io_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

use std::{num::ParseIntError, path::Path, fs::File, io::{BufReader, BufRead}, collections::{BTreeSet, BTreeMap}};

use crate::model::Lcs;

/// This enumeration simply groups the kind of errors that might occur when parsing a
/// psp instance from file. There can be io errors (file unavailable ?), format error
/// (e.g. the file is not an instance but contains the text of your next paper),
/// or parse int errors (which are actually a variant of the format errror since it tells
/// you that the parser expected an integer number but got ... something else).
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// There was an io related error
#[error("io error {0}")]
Io(#[from] std::io::Error),
/// The parser expected to read somehting that was an integer but got some garbage
#[error("parse int {0}")]
ParseInt(#[from] ParseIntError),
/// The file was not properly formatted.
#[error("ill formed instance")]
Format,
}

/// This function is used to read a lcs instance from file. It returns either a
/// lcs instance if everything went on well or an error describing the problem.
pub fn read_instance<P: AsRef<Path>>(fname: P) -> Result<Lcs, Error> {
let f = File::open(fname)?;
let f = BufReader::new(f);

let mut lines = f.lines();

let params = lines.next().ok_or(Error::Format)??.split_whitespace()
.map(|x| x.parse::<usize>().unwrap())
.collect::<Vec<_>>();

if params.len() != 2 {
return Err(Error::Format);
}

let n_strings = params[0];
let n_chars = params[1];

let mut strings = vec![];
let mut alphabet = BTreeSet::default();

for (i, line) in (&mut lines).enumerate() {
let line = line?;
let mut data = line.split_ascii_whitespace();

data.next().ok_or(Error::Format)?; // string length
strings.push(data.next().ok_or(Error::Format)?.to_string());

for char in strings[i].chars() {
alphabet.insert(char);
}
}

let mut mapping = BTreeMap::default();
let mut inverse_mapping = BTreeMap::default();
for (i, char) in alphabet.iter().enumerate() {
mapping.insert(i, *char);
inverse_mapping.insert(*char, i);
}

let mut strings = strings.drain(..)
.map(|s| s.chars().map(|c| *inverse_mapping.get(&c).unwrap()).collect::<Vec<usize>>())
.collect::<Vec<Vec<usize>>>();

// take shortest string as reference in DP model to have less layers
strings.sort_unstable_by_key(|s| s.len());

let mut string_length = vec![];
let mut next = vec![];
let mut rem = vec![];

for i in 0..n_strings {
string_length.push(strings[i].len());

let mut next_for_string = vec![];
let mut rem_for_string = vec![];
for j in 0..n_chars {
let mut next_for_char = vec![0; strings[i].len() + 1];
let mut rem_for_char = vec![0; strings[i].len() + 1];

next_for_char[strings[i].len()] = strings[i].len();
rem_for_char[strings[i].len()] = 0;

for (k, char) in strings[i].iter().enumerate().rev() {
if *char == j {
next_for_char[k] = k;
rem_for_char[k] = rem_for_char[k + 1] + 1;
} else {
next_for_char[k] = next_for_char[k + 1];
rem_for_char[k] = rem_for_char[k + 1];
}
}

next_for_string.push(next_for_char);
rem_for_string.push(rem_for_char);
}

next.push(next_for_string);
rem.push(rem_for_string);
}

Ok(Lcs::new(
strings,
n_strings,
n_chars,
string_length,
next,
rem,
mapping,
))
}
128 changes: 128 additions & 0 deletions ddo/examples/lcs/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//! This example uses ddo to solve the Multiple Longest Common Subsequence problem
//! Instances can be downloaded from https://github.com/milanagrbic/LCSonNuD/tree/main/Instances

use std::{time::{Duration, Instant}};

use clap::Parser;
use ddo::*;

use crate::{io_utils::read_instance, model::{LcsRelax, LcsRanking, GO_TO_END_OF_STRINGS}, dominance::LcsDominance};

mod model;
mod dp;
mod dominance;
mod io_utils;

#[cfg(test)]
mod tests;

/// This structure uses `clap-derive` annotations and define the arguments that can
/// be passed on to the executable solver.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The path to the instance file
fname: String,
/// The number of concurrent threads
#[clap(short, long, default_value = "8")]
threads: usize,
/// The maximum amount of time you would like this solver to run
#[clap(short, long)]
duration: Option<u64>,
/// The maximum number of nodes per layer
#[clap(short, long)]
width: Option<usize>,
}

/// An utility function to return an max width heuristic that can either be a fixed width
/// policy (if w is fixed) or an adaptative policy returning the number of unassigned variables
/// in the overall problem.
fn max_width<P: Problem>(p: &P, w: Option<usize>) -> Box<dyn WidthHeuristic<P::State> + Send + Sync> {
if let Some(w) = w {
Box::new(FixedWidth(w))
} else {
Box::new(NbUnassignedWitdh(p.nb_variables()))
}
}
/// An utility function to return a cutoff heuristic that can either be a time budget policy
/// (if timeout is fixed) or no cutoff policy.
fn cutoff(timeout: Option<u64>) -> Box<dyn Cutoff + Send + Sync> {
if let Some(t) = timeout {
Box::new(TimeBudget::new(Duration::from_secs(t)))
} else {
Box::new(NoCutoff)
}
}

/// This is your executable's entry point. It is the place where all the pieces are put together
/// to create a fast an effective solver for the knapsack problem.
fn main() {
let args = Args::parse();
let fname = &args.fname;
let problem = read_instance(fname).unwrap();
let relaxation = LcsRelax::new(&problem);
let ranking = LcsRanking;

let width = max_width(&problem, args.width);
let dominance = SimpleDominanceChecker::new(LcsDominance);
let cutoff = cutoff(args.duration);
let mut fringe = NoDupFringe::new(MaxUB::new(&ranking));

// This solver compile DD that allow the definition of long arcs spanning over several layers.
let mut solver = ParBarrierSolverPooled::custom(
&problem,
&relaxation,
&ranking,
width.as_ref(),
&dominance,
cutoff.as_ref(),
&mut fringe,
args.threads,
);

let start = Instant::now();
let Completion{ is_exact, best_value } = solver.maximize();

let duration = start.elapsed();
let upper_bound = solver.best_upper_bound();
let lower_bound = solver.best_lower_bound();
let gap = solver.gap();
let best_solution: Option<Vec<_>> = solver.best_solution()
.map(|mut decisions|{
decisions.sort_unstable_by_key(|d| d.variable.id());
decisions.iter()
.map(|d| d.value)
.collect()
});
let best_solution = best_solution.unwrap_or_default().iter()
.filter(|char| **char != GO_TO_END_OF_STRINGS)
.map(|char| *problem.chars.get(&(*char as usize)).unwrap()).collect::<Vec<char>>()
.iter().collect::<String>();

println!("Duration: {:.3} seconds", duration.as_secs_f32());
println!("Objective: {}", best_value.unwrap_or(-1));
println!("Upper Bnd: {}", upper_bound);
println!("Lower Bnd: {}", lower_bound);
println!("Gap: {:.3}", gap);
println!("Aborted: {}", !is_exact);
println!("Solution: {}", best_solution);
}
Loading

0 comments on commit 1edfa59

Please sign in to comment.