Skip to content

Commit 0846570

Browse files
authored
add solver for Road Planning (#263)
1 parent bb84255 commit 0846570

6 files changed

Lines changed: 303 additions & 0 deletions

File tree

cspuz_rs/src/solver/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,23 @@ impl<'a> Solver<'a> {
220220
}
221221
}
222222

223+
pub fn int_var_2d_from_domains(
224+
&mut self,
225+
shape: (usize, usize),
226+
domains: &[Vec<Vec<i32>>],
227+
) -> IntVarArray2D {
228+
let (h, w) = shape;
229+
NdArray {
230+
shape,
231+
data: (0..(h * w))
232+
.map(|i| {
233+
let domain = &domains[i / w][i % w];
234+
self.solver.new_int_var_from_list(domain.clone())
235+
})
236+
.collect(),
237+
}
238+
}
239+
223240
/// Adds a constraint that the specified boolean expression(s) is true.
224241
///
225242
/// You can pass multiple boolean expressions to this method, and the solver will add a constraint that all of them are true.

cspuz_rs_puzzles/src/penpa_editor.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub struct PenpaEditorSquare {
2929
height: usize,
3030
width: usize,
3131
cells: Vec<Vec<Vec<Item>>>,
32+
vertices: Vec<Vec<Vec<Item>>>,
3233
}
3334

3435
impl PenpaEditorSquare {
@@ -37,6 +38,7 @@ impl PenpaEditorSquare {
3738
height,
3839
width,
3940
cells: vec![vec![vec![]; width]; height],
41+
vertices: vec![vec![vec![]; width + 1]; height + 1],
4042
}
4143
}
4244

@@ -55,6 +57,14 @@ impl PenpaEditorSquare {
5557
pub fn add_cell_item(&mut self, y: usize, x: usize, item: Item) {
5658
self.cells[y][x].push(item);
5759
}
60+
61+
pub fn get_vertex(&self, y: usize, x: usize) -> &[Item] {
62+
&self.vertices[y][x]
63+
}
64+
65+
pub fn add_vertex_item(&mut self, y: usize, x: usize, item: Item) {
66+
self.vertices[y][x].push(item);
67+
}
5868
}
5969

6070
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -247,6 +257,20 @@ fn decode_penpa_editor_data_square(
247257
}
248258
};
249259

260+
let vertex_position = |ki: usize| -> Option<(usize, usize)> {
261+
let lo = 4 * height + (5 + height) * width + 21;
262+
if ki < lo {
263+
return None;
264+
}
265+
let y = (ki - lo) / (width + 4);
266+
let x = (ki - lo) % (width + 4);
267+
if y <= height && x <= width {
268+
Some((y, x))
269+
} else {
270+
None
271+
}
272+
};
273+
250274
{
251275
// fills
252276
let fill_data = &body["zS"];
@@ -317,6 +341,20 @@ fn decode_penpa_editor_data_square(
317341
style_id,
318342
}),
319343
);
344+
} else if let Some((y, x)) = vertex_position(ki) {
345+
let color_id = v[0].as_i32().ok_or("Invalid color_id")?;
346+
let name = v[1].as_str().ok_or("Invalid symbol_name")?.to_string();
347+
let style_id = v[2].as_i32().ok_or("Invalid style_id")?;
348+
349+
ret.add_vertex_item(
350+
y,
351+
x,
352+
Item::Symbol(Symbol {
353+
color_id,
354+
name,
355+
style_id,
356+
}),
357+
);
320358
}
321359
}
322360
}

cspuz_rs_puzzles/src/puzzles/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pub mod railpool;
118118
pub mod reflect;
119119
pub mod ringring;
120120
pub mod ripple;
121+
pub mod road_planning;
121122
pub mod roma;
122123
pub mod sansaroad;
123124
pub mod sasahigane;
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use crate::penpa_editor::{decode_penpa_editor_url, Item, PenpaEditorPuzzle};
2+
use cspuz_rs::graph;
3+
use cspuz_rs::solver::{count_true, Solver};
4+
5+
pub fn solve_road_planning(
6+
clues: &[Vec<bool>],
7+
) -> Option<graph::BoolInnerGridEdgesIrrefutableFacts> {
8+
let h = clues.len() - 1;
9+
let w = clues[0].len() - 1;
10+
11+
let mut solver = Solver::new();
12+
let is_border = graph::BoolInnerGridEdges::new(&mut solver, (h, w));
13+
solver.add_answer_key_bool(&is_border.horizontal);
14+
solver.add_answer_key_bool(&is_border.vertical);
15+
16+
let mut domain = vec![];
17+
for i in 1..=(h * w) {
18+
if h * w % i == 0 {
19+
domain.push(i as i32);
20+
}
21+
}
22+
23+
let global_num = &solver.int_var_from_domain(domain.clone());
24+
let num = &solver.int_var_2d_from_domains((h, w), &vec![vec![domain; w]; h]);
25+
solver.add_expr(num.eq(global_num));
26+
27+
graph::graph_division_2d(&mut solver, num, &is_border);
28+
29+
for y in 0..=h {
30+
for x in 0..=w {
31+
if (y == 0 || y == h) && (x == 0 || x == w) {
32+
if clues[y][x] {
33+
return None;
34+
}
35+
continue;
36+
}
37+
38+
if y == 0 {
39+
solver.add_expr(is_border.vertical.at((y, x - 1)).iff(clues[y][x]));
40+
} else if y == h {
41+
solver.add_expr(is_border.vertical.at((y - 1, x - 1)).iff(clues[y][x]));
42+
} else if x == 0 {
43+
solver.add_expr(is_border.horizontal.at((y - 1, x)).iff(clues[y][x]));
44+
} else if x == w {
45+
solver.add_expr(is_border.horizontal.at((y - 1, x - 1)).iff(clues[y][x]));
46+
} else {
47+
let adj = [
48+
is_border.horizontal.at((y - 1, x)),
49+
is_border.horizontal.at((y - 1, x - 1)),
50+
is_border.vertical.at((y, x - 1)),
51+
is_border.vertical.at((y - 1, x - 1)),
52+
];
53+
if clues[y][x] {
54+
solver.add_expr(count_true(adj).ge(3));
55+
} else {
56+
solver.add_expr(count_true(adj).le(2));
57+
}
58+
}
59+
}
60+
}
61+
62+
solver.irrefutable_facts().map(|f| f.get(&is_border))
63+
}
64+
65+
type Problem = Vec<Vec<bool>>;
66+
67+
pub fn deserialize_problem(url: &str) -> Option<Problem> {
68+
let decoded = decode_penpa_editor_url(url).ok()?;
69+
#[allow(unreachable_patterns)]
70+
let decoded = match decoded {
71+
PenpaEditorPuzzle::Square(s) => s,
72+
_ => return None,
73+
};
74+
75+
let mut ret = vec![vec![false; decoded.width() + 1]; decoded.height() + 1];
76+
for y in 0..=decoded.height() {
77+
for x in 0..=decoded.width() {
78+
for item in decoded.get_vertex(y, x) {
79+
if let Item::Symbol(symbol) = item {
80+
if symbol.name.starts_with("circle_") {
81+
ret[y][x] = true;
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
Some(ret)
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use super::*;
94+
95+
fn problem_for_tests() -> Problem {
96+
crate::util::tests::to_bool_2d([
97+
[0, 1, 1, 0, 0, 0],
98+
[0, 0, 0, 0, 0, 0],
99+
[0, 0, 1, 1, 1, 1],
100+
[1, 0, 0, 0, 0, 0],
101+
[0, 0, 0, 1, 0, 0],
102+
])
103+
}
104+
105+
#[test]
106+
fn test_road_planning_problem() {
107+
let problem = problem_for_tests();
108+
let ans = solve_road_planning(&problem);
109+
110+
assert!(ans.is_some());
111+
let ans = ans.unwrap();
112+
113+
let expected = graph::InnerGridEdges {
114+
horizontal: crate::util::tests::to_option_bool_2d([
115+
[0, 0, 1, 1, 0],
116+
[0, 1, 1, 1, 1],
117+
[1, 1, 0, 0, 0],
118+
]),
119+
vertical: crate::util::tests::to_option_bool_2d([
120+
[1, 1, 0, 0],
121+
[1, 0, 0, 1],
122+
[0, 1, 1, 0],
123+
[0, 0, 1, 0],
124+
]),
125+
};
126+
assert_eq!(ans, expected);
127+
}
128+
129+
#[test]
130+
fn test_road_planning_serializer() {
131+
let problem = problem_for_tests();
132+
let url = "https://opt-pan.github.io/penpa-edit/#m=solve&p=tVRRb9owEH7Pr5j8fA+xgRb81nXrXjq2DqYKWREKkJaoAXdOslZG9Lf3fIkGJuZl2hT505fPF/vsuy/lrzo1GQygD70hxMDxEWIIfIT8ckAjbp9pXhWZ/ABXdbXWBgnAtzE8pEWZRaoNSiLFBAManCVvdvKmGAOeRDv7Q+7sXKpkD/bngQ4PdCJ3iGNCTjiTOzbsMak4sGVulkU2n0wYiATYsB+UeSzO6OFleHxuncEZfRTWBQ/oeIQbOoggnOI5wfYIPxHGhAPCW4r5THhPeE3YJ7ygmEt3U1GkRFMq92CRzjOsB149K3UxL2vzkC4zJqliQNq23iwy40mF1s9FvvXj8setNllwyonZ6jEUv9BmdbL6S1oUntA0oCc1N+hJlcm999QY/eIpm7Rae8IirbBdy3X+7K+UbSs/gSr1U0yf0pPdNocz7yP2ymgoAeICBHX2SNo7sF+k1/tg77C1v0o7c53duMAVWeGkaxms9B96T/OOXTcij5GPW450htTvOPtdKjsF5jb6SJ87yjb6N+ZK39H7Um8WeBrFju6jmSnrlX6q21juWvWqyXcSyLd3yNfRJl/HAvm65I7yvW0W+qfpjpJ9U4n4r/8r/8mar63btAkaDuWA51ANeqvVO/ZCvWMkt2HXS6gG7ITqqaNQ6poKxY6vUDtjLbfqqbtcVqcGc1t1POa2OrYZ/raIvQM=";
133+
assert_eq!(deserialize_problem(url).unwrap(), problem);
134+
}
135+
}

cspuz_solver_backend/src/puzzle/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ puzzle_list!(penpa_edit,
317317
(exercise, ["exercise"], "Exercise", "Exercise"),
318318
(pyramid, ["pyramid"], "Pyramid", "ピラミッド"),
319319
(pyramid_climbers, ["pyramid_climbers"], "Pyramid Climbers", "Pyramid Climbers"),
320+
(road_planning, ["road_planning"], "Road Planning", "道路計画"),
320321
);
321322

322323
pub mod double_lits;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use crate::board::{Board, BoardKind, Item, ItemKind};
2+
use crate::uniqueness::check_uniqueness;
3+
use cspuz_rs_puzzles::puzzles::road_planning;
4+
5+
pub fn solve(url: &str) -> Result<Board, &'static str> {
6+
let problem = road_planning::deserialize_problem(url).ok_or("invalid url")?;
7+
let ans = road_planning::solve_road_planning(&problem);
8+
9+
let height = problem.len() - 1;
10+
let width = problem[0].len() - 1;
11+
let mut board = Board::new(
12+
BoardKind::ColoredGrid("#cccccc"),
13+
height,
14+
width,
15+
check_uniqueness(&ans),
16+
);
17+
18+
board.add_borders_as_answer(ans.as_ref());
19+
20+
for y in 0..=height {
21+
for x in 0..=width {
22+
if problem[y][x] {
23+
board.push(Item {
24+
y: y * 2,
25+
x: x * 2,
26+
color: "white",
27+
kind: ItemKind::SmallFilledCircle,
28+
});
29+
board.push(Item {
30+
y: y * 2,
31+
x: x * 2,
32+
color: "black",
33+
kind: ItemKind::SmallCircle,
34+
});
35+
}
36+
}
37+
}
38+
39+
Ok(board)
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use super::solve;
45+
use crate::board::*;
46+
use crate::compare_board_and_check_no_solution_case;
47+
use crate::uniqueness::Uniqueness;
48+
49+
#[test]
50+
#[rustfmt::skip]
51+
fn test_solve() {
52+
compare_board_and_check_no_solution_case!(
53+
solve("https://opt-pan.github.io/penpa-edit/#m=solve&p=tVRRb9owEH7Pr5j8fA+xgRb81nXrXjq2DqYKWREKkJaoAXdOslZG9Lf3fIkGJuZl2hT505fPF/vsuy/lrzo1GQygD70hxMDxEWIIfIT8ckAjbp9pXhWZ/ABXdbXWBgnAtzE8pEWZRaoNSiLFBAManCVvdvKmGAOeRDv7Q+7sXKpkD/bngQ4PdCJ3iGNCTjiTOzbsMak4sGVulkU2n0wYiATYsB+UeSzO6OFleHxuncEZfRTWBQ/oeIQbOoggnOI5wfYIPxHGhAPCW4r5THhPeE3YJ7ygmEt3U1GkRFMq92CRzjOsB149K3UxL2vzkC4zJqliQNq23iwy40mF1s9FvvXj8setNllwyonZ6jEUv9BmdbL6S1oUntA0oCc1N+hJlcm999QY/eIpm7Rae8IirbBdy3X+7K+UbSs/gSr1U0yf0pPdNocz7yP2ymgoAeICBHX2SNo7sF+k1/tg77C1v0o7c53duMAVWeGkaxms9B96T/OOXTcij5GPW450htTvOPtdKjsF5jb6SJ87yjb6N+ZK39H7Um8WeBrFju6jmSnrlX6q21juWvWqyXcSyLd3yNfRJl/HAvm65I7yvW0W+qfpjpJ9U4n4r/8r/8mar63btAkaDuWA51ANeqvVO/ZCvWMkt2HXS6gG7ITqqaNQ6poKxY6vUDtjLbfqqbtcVqcGc1t1POa2OrYZ/raIvQM="),
54+
Board {
55+
kind: BoardKind::ColoredGrid("#cccccc"),
56+
height: 4,
57+
width: 5,
58+
data: vec![
59+
Item { y: 2, x: 1, color: "green", kind: ItemKind::Cross },
60+
Item { y: 1, x: 2, color: "green", kind: ItemKind::BoldWall },
61+
Item { y: 2, x: 3, color: "green", kind: ItemKind::Cross },
62+
Item { y: 1, x: 4, color: "green", kind: ItemKind::BoldWall },
63+
Item { y: 2, x: 5, color: "green", kind: ItemKind::BoldWall },
64+
Item { y: 1, x: 6, color: "green", kind: ItemKind::Cross },
65+
Item { y: 2, x: 7, color: "green", kind: ItemKind::BoldWall },
66+
Item { y: 1, x: 8, color: "green", kind: ItemKind::Cross },
67+
Item { y: 2, x: 9, color: "green", kind: ItemKind::Cross },
68+
Item { y: 4, x: 1, color: "green", kind: ItemKind::Cross },
69+
Item { y: 3, x: 2, color: "green", kind: ItemKind::BoldWall },
70+
Item { y: 4, x: 3, color: "green", kind: ItemKind::BoldWall },
71+
Item { y: 3, x: 4, color: "green", kind: ItemKind::Cross },
72+
Item { y: 4, x: 5, color: "green", kind: ItemKind::BoldWall },
73+
Item { y: 3, x: 6, color: "green", kind: ItemKind::Cross },
74+
Item { y: 4, x: 7, color: "green", kind: ItemKind::BoldWall },
75+
Item { y: 3, x: 8, color: "green", kind: ItemKind::BoldWall },
76+
Item { y: 4, x: 9, color: "green", kind: ItemKind::BoldWall },
77+
Item { y: 6, x: 1, color: "green", kind: ItemKind::BoldWall },
78+
Item { y: 5, x: 2, color: "green", kind: ItemKind::Cross },
79+
Item { y: 6, x: 3, color: "green", kind: ItemKind::BoldWall },
80+
Item { y: 5, x: 4, color: "green", kind: ItemKind::BoldWall },
81+
Item { y: 6, x: 5, color: "green", kind: ItemKind::Cross },
82+
Item { y: 5, x: 6, color: "green", kind: ItemKind::BoldWall },
83+
Item { y: 6, x: 7, color: "green", kind: ItemKind::Cross },
84+
Item { y: 5, x: 8, color: "green", kind: ItemKind::Cross },
85+
Item { y: 6, x: 9, color: "green", kind: ItemKind::Cross },
86+
Item { y: 7, x: 2, color: "green", kind: ItemKind::Cross },
87+
Item { y: 7, x: 4, color: "green", kind: ItemKind::Cross },
88+
Item { y: 7, x: 6, color: "green", kind: ItemKind::BoldWall },
89+
Item { y: 7, x: 8, color: "green", kind: ItemKind::Cross },
90+
Item { y: 0, x: 2, color: "white", kind: ItemKind::SmallFilledCircle },
91+
Item { y: 0, x: 2, color: "black", kind: ItemKind::SmallCircle },
92+
Item { y: 0, x: 4, color: "white", kind: ItemKind::SmallFilledCircle },
93+
Item { y: 0, x: 4, color: "black", kind: ItemKind::SmallCircle },
94+
Item { y: 4, x: 4, color: "white", kind: ItemKind::SmallFilledCircle },
95+
Item { y: 4, x: 4, color: "black", kind: ItemKind::SmallCircle },
96+
Item { y: 4, x: 6, color: "white", kind: ItemKind::SmallFilledCircle },
97+
Item { y: 4, x: 6, color: "black", kind: ItemKind::SmallCircle },
98+
Item { y: 4, x: 8, color: "white", kind: ItemKind::SmallFilledCircle },
99+
Item { y: 4, x: 8, color: "black", kind: ItemKind::SmallCircle },
100+
Item { y: 4, x: 10, color: "white", kind: ItemKind::SmallFilledCircle },
101+
Item { y: 4, x: 10, color: "black", kind: ItemKind::SmallCircle },
102+
Item { y: 6, x: 0, color: "white", kind: ItemKind::SmallFilledCircle },
103+
Item { y: 6, x: 0, color: "black", kind: ItemKind::SmallCircle },
104+
Item { y: 8, x: 6, color: "white", kind: ItemKind::SmallFilledCircle },
105+
Item { y: 8, x: 6, color: "black", kind: ItemKind::SmallCircle },
106+
],
107+
uniqueness: Uniqueness::Unique,
108+
},
109+
);
110+
}
111+
}

0 commit comments

Comments
 (0)