From f64d0c3c981b2a8fcf319705438f4c34770a2428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Sejkora?= Date: Sat, 9 Jan 2021 22:13:21 +0100 Subject: [PATCH] Dual move optimization (WIP, useless?) --- Cargo.lock | 16 +++++ Cargo.toml | 1 + src/city.rs | 15 ++-- src/main.rs | 6 ++ src/optimization.rs | 166 ++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 192 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd5d90c..506f124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -57,6 +63,15 @@ dependencies = [ "regex", ] +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -87,6 +102,7 @@ version = "0.1.0" dependencies = [ "byteorder", "indicatif", + "itertools", "rand", ] diff --git a/Cargo.toml b/Cargo.toml index a747125..f3460a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2018" byteorder = "1.3.4" rand = "0.8.0" indicatif = "0.15.0" +itertools = "0.10.0" [[bin]] name = "prague" diff --git a/src/city.rs b/src/city.rs index e7e8ee4..ad723bc 100644 --- a/src/city.rs +++ b/src/city.rs @@ -34,7 +34,7 @@ impl City { City { prices, buyable_house_count } } - pub fn get_price(&self, house: &House) -> u16 { + pub fn get_price(&self, house: House) -> u16 { self.prices[house.y * SIZE + house.x] } @@ -42,8 +42,8 @@ impl City { self.prices[y * SIZE + x] } - pub fn is_house(&self, house: &House) -> bool { - self.get_price(&house) > 0 + pub fn is_house(&self, house: House) -> bool { + self.get_price(house) > 0 } pub fn is_house_xy(&self, x: usize, y: usize) -> bool { @@ -76,6 +76,7 @@ impl House { } /// Rectangle - a 2D range with inclusive bounds +#[derive(Clone, Copy)] pub struct Rectangle { /// The smaller x coordinate. pub left: usize, @@ -166,7 +167,7 @@ impl<'a> HouseLayout<'a> { } pub fn is_valid(&self) -> bool { - self.reachable_houses == self.city.buyable_house_count + self.reachable_houses == self.city.get_house_count() } pub fn price(&self) -> u32 { @@ -180,8 +181,8 @@ impl<'a> HouseLayout<'a> { fn get_price(city: &City, houses: &Vec) -> u32 { let mut price = 0u32; - for house in houses { - price += city.get_price(&house) as u32; + for &house in houses { + price += city.get_price(house) as u32; } price @@ -200,7 +201,7 @@ fn is_valid(city: &City, houses: &Vec) -> Option { reachable[y as usize * SIZE + x as usize] = true; } } - price += city.get_price(&house) as u32; + price += city.get_price(*house) as u32; } for y in 0..SIZE { diff --git a/src/main.rs b/src/main.rs index 547cf94..53ce4b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,12 @@ fn main() { improved = true; } eprintln!("Finished pairwise house merge"); + //eprintln!("Starting pairwise house move..."); + //if optimization::improve_move_houses_pairwise(&mut layout) { + // dump_layout(&layout, &mut best_price, seed); + // improved = true; + //} + //eprintln!("Finished pairwise house move"); if !improved { break; } diff --git a/src/optimization.rs b/src/optimization.rs index 9496de2..a464216 100644 --- a/src/optimization.rs +++ b/src/optimization.rs @@ -1,13 +1,14 @@ use std::collections::HashSet; use rand::prelude::{SliceRandom, StdRng}; use crate::city::{Rectangle, HOUSE_RANGE, SIZE, House, HouseLayout}; +use itertools::iproduct; pub enum RectangleSearchError { Useless, Unsatisfiable } -pub fn get_valid_move_rectangle_multiple(layout: &HouseLayout, houses: &Vec) -> Result { +fn get_valid_move_rectangle_multiple(layout: &HouseLayout, houses: &Vec) -> Result { // This is a generalization of get_valid_move_rectangle, it's basically the same thing, // just with a dynamic rectangles_containing_count @@ -67,7 +68,7 @@ pub fn get_valid_move_rectangle_multiple(layout: &HouseLayout, houses: &Vec Result { +fn get_valid_move_rectangle(layout: &HouseLayout, house: House) -> Result { // We first establish a bounding box for an that has to be covered if the house is removed. let mut covered_rect: Option = None; @@ -143,13 +144,13 @@ pub fn improve_move_individual_houses(layout: &mut HouseLayout, mut rng: &mut St let mut new_candidates = Vec::new(); for new_y in move_rectangle.top..=move_rectangle.bottom { for new_x in move_rectangle.left..=move_rectangle.right { - if layout.city.is_house_xy(new_x, new_y) && layout.city.get_price_xy(new_x, new_y) < layout.city.get_price(&house) { + if layout.city.is_house_xy(new_x, new_y) && layout.city.get_price_xy(new_x, new_y) < layout.city.get_price(house) { new_candidates.push(House::new(new_x, new_y)); } } } - new_candidates.sort_by(|a, b| layout.city.get_price(&a).cmp(&layout.city.get_price(&b))); + new_candidates.sort_by(|a, b| layout.city.get_price(*a).cmp(&layout.city.get_price(*b))); if new_candidates.len() == 0 { //eprintln!("Did not find candidate"); } else { @@ -222,7 +223,7 @@ pub fn improve_merge_pairwise(layout: &mut HouseLayout) -> bool { } } if let Some((x, y, price)) = cheapest { - if price >= layout.city.get_price(&house1) + layout.city.get_price(&house2) { + if price >= layout.city.get_price(house1) + layout.city.get_price(house2) { // Merging not worth //eprintln!("Merging not worth!"); } else { @@ -262,3 +263,158 @@ pub fn improve_merge_pairwise(layout: &mut HouseLayout) -> bool { improved } + +pub fn improve_move_houses_pairwise(layout: &mut HouseLayout) -> bool { + // TODO: Implement layout move house to avoid changing indices + for i in 0..layout.houses().len() { + for j in i + 1..layout.houses().len() { + let house1 = layout.houses()[i]; + let house2 = layout.houses()[j]; + let x_dist = (house1.x as i32 - house2.x as i32).abs() as usize; + let y_dist = (house1.y as i32 - house2.y as i32).abs() as usize; + if x_dist > 2 * HOUSE_RANGE + 1 || y_dist > 2 * HOUSE_RANGE + 1 { + // Not close enough to overlap or touch areas (can move on its own) + continue; + } + + if let Some(distances) = get_dual_move_distances(&layout, i, j) { + eprintln!("Houses {} {} move distances: L{} R{} T{} B{}", i, j, distances.left, distances.right, distances.up, distances.down); + let mut best_move = None; + + let old_price = layout.city.get_price(house1) + layout.city.get_price(house2); + + let left_deltas = (1..distances.left).map(|x| (-(x as i32), 0)); + let right_deltas = (1..distances.right).map(|x| (x as i32, 0)); + let up_deltas = (1..distances.up).map(|x| (0, -(x as i32))); + let down_deltas = (1..distances.down).map(|x| (0, x as i32)); + let move_deltas = left_deltas.chain(right_deltas).chain(up_deltas).chain(down_deltas); + + for (x_delta, y_delta) in move_deltas { + let new_house1 = House::new( + (house1.x as i32 + x_delta) as usize, + (house1.y as i32 + y_delta) as usize, + ); + let new_house2 = House::new( + (house2.x as i32 + x_delta) as usize, + (house2.y as i32 + y_delta) as usize, + ); + + if !layout.city.is_house(new_house1) || !layout.city.is_house(new_house2) { + continue; + } + + let new_price = layout.city.get_price(new_house1) + layout.city.get_price(new_house2); + eprintln!("Move x{} y{}, diff {} (price {}->{})", x_delta, y_delta, new_price as i32 - old_price as i32, old_price, new_price); + + if new_price < old_price { + match best_move { + None => best_move = Some((x_delta, y_delta, new_price)), + Some((_, _, best_price)) if new_price < best_price => { + best_move = Some((x_delta, y_delta, new_price)); + } + _ => {} + } + } + } + + if let Some((x_delta, y_delta, best_price)) = best_move { + eprintln!("Best move x{} y{}, diff {} (price {}->{})", x_delta, y_delta, best_price as i32 - old_price as i32, old_price, best_price); + // TODO: IMPLEMENT MOVE + } else { + eprintln!("No move worth it."); + } + } + } + } + + unimplemented!() +} + +struct MoveDistances { + left: usize, + right: usize, + up: usize, + down: usize, +} + +fn get_dual_move_distances(layout: &HouseLayout, house1_index: usize, house2_index: usize) -> Option { + let house1 = layout.houses()[house1_index]; + let house2 = layout.houses()[house2_index]; + + let rect1 = house1.range_rectangle(); + let rect2 = house2.range_rectangle(); + + let top1 = rect1.top.min(rect2.top); + let top2 = rect1.top.max(rect2.top); + let bottom1 = rect1.bottom.min(rect2.bottom); + let bottom2 = rect1.bottom.max(rect2.bottom); + let left1 = rect1.left.min(rect2.left); + let left2 = rect1.left.max(rect2.left); + let right1 = rect1.right.min(rect2.right); + let right2 = rect1.right.max(rect2.right); + + let left_rect = if house1.x <= house2.x { rect1 } else { rect2 }; + let right_rect = if house1.x <= house2.x { rect2 } else { rect1 }; + let top_rect = if house1.y <= house2.y { rect1 } else { rect2 }; + let bottom_rect = if house1.y <= house2.y { rect2 } else { rect1 }; + + let margin_left = left1..left2; + let shared_x = left2..=right1; + let margin_right = right1 + 1..=right2; + let margin_top = top1..top2; + let shared_y = top2..=bottom1; + let margin_bottom = bottom1 + 1..=bottom2; + + let mut top_distance = SIZE; + let mut bottom_distance = SIZE; + let mut right_distance = SIZE; + let mut left_distance = SIZE; + + // We check the same tile twice if it's in both rectangles (and both shared_x and shared_y), + // this could be made more efficient by dividing the rectangles into 5 zones. + let rect1 = iproduct!(rect1.top..=rect1.bottom, rect1.left..=rect1.right); + let rect2 = iproduct!(rect2.top..=rect2.bottom, rect2.left..=rect2.right); + for (y, x) in rect1.chain(rect2) { + let shared_both = shared_x.contains(&x) && shared_y.contains(&y); + let min_cover_count = if shared_both { 2 } else { 1 }; + if !layout.city.is_house_xy(x, y) && layout.cover_count_xy(x, y) != min_cover_count { + continue; + } + + if margin_left.contains(&x) { + top_distance = top_distance.min(y - left_rect.top); + bottom_distance = bottom_distance.min(left_rect.bottom - y); + } else if shared_x.contains(&x) { + top_distance = top_distance.min(y - left_rect.top.min(right_rect.top)); + bottom_distance = bottom_distance.min(left_rect.bottom.max(right_rect.bottom) - y); + } else if margin_right.contains(&x) { + top_distance = top_distance.min(y - right_rect.top); + bottom_distance = bottom_distance.min(right_rect.bottom - y); + } else { + unreachable!(); + } + + if margin_top.contains(&y) { + left_distance = left_distance.min(x - top_rect.left); + right_distance = right_distance.min(top_rect.right - x); + } else if shared_y.contains(&y) { + left_distance = left_distance.min(x - bottom_rect.left.min(top_rect.left)); + right_distance = right_distance.min(bottom_rect.right.max(top_rect.right) - x); + } else if margin_bottom.contains(&y) { + left_distance = left_distance.min(x - bottom_rect.left); + right_distance = right_distance.min(bottom_rect.right - x); + } else { + unreachable!() + } + } + + // TODO: Handle properly + assert_ne!(top_distance, SIZE); + + Some(MoveDistances { + left: left_distance, + right: right_distance, + up: top_distance, + down: bottom_distance + }) +} \ No newline at end of file