From 573788fbfc8a1e04f2ff4e1c6f9dd13442f9b600 Mon Sep 17 00:00:00 2001 From: Arne Keller Date: Sun, 31 Mar 2019 17:47:46 +0200 Subject: [PATCH] Edge cases, more comments and formatting --- src/display.rs | 41 ++++++++++++++---- src/input.rs | 32 ++------------ src/main.rs | 112 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 115 insertions(+), 70 deletions(-) diff --git a/src/display.rs b/src/display.rs index 3e61dd8..ebe0f45 100644 --- a/src/display.rs +++ b/src/display.rs @@ -82,7 +82,13 @@ pub(crate) fn dump_route(opt: Opt, house: Point, polys: &[Polygon], route: &[Run ); } -pub(crate) fn save_svg(opt: Opt, filename: &str, house: Point, polys: &[Polygon], route: &[RunState]) { +pub(crate) fn save_svg( + opt: Opt, + filename: &str, + house: Point, + polys: &[Polygon], + route: &[RunState], +) { let (points, lines, route1, route2_start) = gen_params(opt, house, route); fs::write( filename, @@ -103,9 +109,7 @@ fn gen_params( Point, ) { let last = route.last().unwrap(); - let points = vec![ - (house, "red"), - ]; + let points = vec![(house, "red")]; let lines = route .iter() .map(|x| x.pos) @@ -115,7 +119,9 @@ fn gen_params( .collect::>(); let route1 = route.iter().map(|x| x.pos).collect::>(); - let route2_start = last.pos.translate(0.0, -last.total_distance * (opt.bus/opt.lisa)); + let route2_start = last + .pos + .translate(0.0, -last.total_distance * (opt.bus / opt.lisa)); (points, lines, route1, route2_start) } @@ -127,8 +133,25 @@ pub(crate) fn generate_svg( route2_start: Point, ) -> String { let dot_radius = 25.2; - let width = route1.iter().map(|p| p.x()).chain(polys.iter().map(|x| x.exterior().0.iter().map(|p| p.x)).flatten()).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap() + dot_radius * 2.0; - let upper_y = polys.iter().map(|x| x.exterior().0.iter().map(|p| p.y)).flatten().chain(Some(route1.last().unwrap().y())).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); + let width = route1 + .iter() + .map(|p| p.x()) + .chain( + polys + .iter() + .map(|x| x.exterior().0.iter().map(|p| p.x)) + .flatten(), + ) + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap() + + dot_radius * 2.0; + let upper_y = polys + .iter() + .map(|x| x.exterior().0.iter().map(|p| p.y)) + .flatten() + .chain(Some(route1.last().unwrap().y())) + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap(); let mut document = Document::new() // view box at (x,y), (w,h) .set( @@ -137,8 +160,8 @@ pub(crate) fn generate_svg( -dot_radius, -upper_y - dot_radius, width, - -route2_start.y() + upper_y + dot_radius * 2.0 - ) + -route2_start.y() + upper_y + dot_radius * 2.0, + ), ) .set("xmlns:xlink", "http://www.w3.org/1999/xlink"); diff --git a/src/input.rs b/src/input.rs index d0cb02e..287b48a 100644 --- a/src/input.rs +++ b/src/input.rs @@ -60,43 +60,19 @@ fn read_stdin() -> InputData { fn _custom_input() -> InputData { let mut polys = vec![]; polys.push(Polygon::new( - vec![ - [2.0, 0.0], - [6.0, 0.0], - [6.0, 6.0], - [2.0, 5.0], - ] - .into(), + vec![[2.0, 0.0], [6.0, 0.0], [6.0, 6.0], [2.0, 5.0]].into(), vec![], )); polys.push(Polygon::new( - vec![ - [1.0, 20.0], - [2.0, 20.0], - [2.0, 23.0], - [1.0, 23.0], - ] - .into(), + vec![[1.0, 20.0], [2.0, 20.0], [2.0, 23.0], [1.0, 23.0]].into(), vec![], )); polys.push(Polygon::new( - vec![ - [1.0, 25.0], - [2.0, 25.0], - [2.0, 28.0], - [1.0, 28.0], - ] - .into(), + vec![[1.0, 25.0], [2.0, 25.0], [2.0, 28.0], [1.0, 28.0]].into(), vec![], )); let mut stick = Polygon::new( - vec![ - [1.0, 115.0], - [2.0, 115.0], - [2.0, 130.0], - [1.0, 130.0], - ] - .into(), + vec![[1.0, 115.0], [2.0, 115.0], [2.0, 130.0], [1.0, 130.0]].into(), vec![], ); polys.push(stick.clone()); diff --git a/src/main.rs b/src/main.rs index b376da2..7527ab4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use structopt::StructOpt; use std::cmp; use std::collections::BinaryHeap; use std::f64; +use std::process::exit; mod display; mod input; @@ -16,39 +17,42 @@ type Line = geo::Line; type LineString = geo::LineString; type Polygon = geo::Polygon; +/// Datenstruktur für Kommandozeilen-Argumente #[derive(StructOpt, Clone, Copy)] #[structopt(name = "aufgabe1", about = "Implementierung für Aufgabe 1")] struct Opt { - /// Debug-Modus aktivieren - #[structopt(short = "d", long = "debug")] - debug: bool, + /// Debug-Modus aktivieren + #[structopt(short = "d", long = "debug")] + debug: bool, /// Zwischenlösungen speichern #[structopt(short = "s", long = "save")] save_intermediates: bool, /// Beste Lösung als tkz-Code ausgeben #[structopt(short = "t", long = "tkz")] tkz: bool, - /// Geschwindigkeit von Lisa (in km/h) - #[structopt(short = "l", long = "lisa", default_value = "15")] - lisa: f64, + /// Geschwindigkeit von Lisa (in km/h) + #[structopt(short = "l", long = "lisa", default_value = "15")] + lisa: f64, /// Geschwindigkeit vom Bus (in km/h) - #[structopt(short = "b", long = "bus", default_value = "30")] - bus: f64, + #[structopt(short = "b", long = "bus", default_value = "30")] + bus: f64, } +/// Möglicher Zustand von Lisas Rennen #[derive(Debug, Clone, Copy, PartialEq)] struct RunState { - /// Theoretisch mögliche Wartezeit - delay: f64, + /// Theoretisch mögliche Startposition des Busses + bus: f64, /// Lisas Position pos: Point, // Bisher zurückgelegte Strecke total_distance: f64, } +/// Trait-Implementierung, damit die Struktur in BinaryHeaps passt impl cmp::Eq for RunState {} impl cmp::Ord for RunState { fn cmp(&self, other: &Self) -> cmp::Ordering { - self.delay.partial_cmp(&other.delay).unwrap() + self.bus.partial_cmp(&other.bus).unwrap() } } impl cmp::PartialOrd for RunState { @@ -60,13 +64,30 @@ impl cmp::PartialOrd for RunState { fn main() { // Kommandozeilen-Argumente einlesen let opt = Opt::from_args(); + // (unwahrscheinlich) + if opt.lisa >= opt.bus { + eprintln!("Lisa sollte lieber zur Schule rennen, als den Bus zu nehmen."); + exit(1); + } + // (noch unwahrscheinlicher) + if opt.bus <= 0.0 { + if opt.lisa > 0.0 { + eprintln!("Lisa sollte lieber zur Schule rennen, da der Bus vermutlich eine Panne hat oder im Rückwärtsgang ist."); + } else { + eprintln!("Lisa kann nicht laufen."); + } + exit(1); + } else if opt.lisa <= 0.0 { + eprintln!("Lisa muss den Busfahrer bitten, zu ihrem Haus zu fahren."); + exit(1); + } // Geschwindigkeiten in m/s umrechnen let bus_speed = opt.bus / 3.6; let lisa_speed = opt.lisa / 3.6; // Startzeitpunkt berechnen let calc_time = |total_distance, y| { NaiveTime::from_hms(7, 30, 0) - - Duration::seconds(((total_distance * (opt.bus/opt.lisa) - y) / bus_speed) as i64) + - Duration::seconds(((total_distance * (opt.bus / opt.lisa) - y) / bus_speed) as i64) }; // Ankuftszeit berechnen let calc_end_time = |total_distance, y| { @@ -88,12 +109,15 @@ fn main() { // Startzustand: Lisa ist an ihrem Haus, zurückgelegte Distanz ist Null let start = RunState { pos: house, - delay: max_possible_delay(&opt, 0.0, house), + bus: best_bus_pos(&opt, 0.0, house), total_distance: 0.0, }; // bestmöglicher Fall: Lisa geht direkt zum Bus - let meeting_point = Point::new(0.0, house.y() + (opt.lisa/opt.bus).asin().tan() * house.x()); + let meeting_point = Point::new( + 0.0, + house.y() + (opt.lisa / opt.bus).asin().tan() * house.x(), + ); eprintln!( "Theoretisches Maximum: {:?}", calc_time(distance(meeting_point, house), meeting_point.y()) @@ -104,14 +128,14 @@ fn main() { states.push(vec![start]); // beste Lösung speichern - let mut best_delay = f64::NEG_INFINITY; + let mut best_bus = f64::NEG_INFINITY; let mut best = vec![]; // Zwischenlösungen speichern - let save_prefix = "tmp_"; + let save_prefix = "route_"; let mut save_counter = 0; // weitersuchen, bis kein besserer Zustand mehr vorhanden ist - while states.peek().map(|x| x[0].delay > best_delay) == Some(true) { + while states.peek().map(|x| x[0].bus > best_bus) == Some(true) { // besten Zustand einlesen let state = states.pop().unwrap(); let last = &state[0]; @@ -132,14 +156,15 @@ fn main() { { // Lisa könnte zu dieser Ecke rennen let total_distance = last.total_distance + distance(last.pos, next); - let delay = max_possible_delay(&opt, total_distance, next); - if delay > best_delay { + let bus = best_bus_pos(&opt, total_distance, next); + // theoretisch möglicher Startzeitpunkt ist nach bestem gefundenem Startzeitpunkt + if bus > best_bus { let mut route = state.clone(); route.insert( 0, RunState { pos: next, - delay, + bus, total_distance, }, ); @@ -147,29 +172,29 @@ fn main() { } } // versuche, direkt zum Bus zu gehen - // Lisa trifft Bus mit 60°-Winkel + // Lisa trifft Bus in einem bestimmten Winkel (meist 60°) let next = Point::new( 0.0, - last.pos.y() + (opt.lisa/opt.bus).asin().tan() * last.pos.x(), + last.pos.y() + (opt.lisa / opt.bus).asin().tan() * last.pos.x(), ); let line = Line::new(last.pos, next); // freier Weg? if none_intersect(&polys, &line) { let line_length = line.end_point().euclidean_distance(&line.start_point()); let total_distance = last.total_distance + line_length; - let delay = max_possible_delay(&opt, total_distance, line.end_point()); - if delay > best_delay { + let bus = best_bus_pos(&opt, total_distance, line.end_point()); + if bus > best_bus { // neue beste Wartezeit let mut route = state.clone(); route.insert( 0, RunState { pos: next, - delay, + bus, total_distance, }, ); - // Verbesserung anzeigen und speichern + // Verbesserung anzeigen und evtl. speichern eprintln!( "Verbesserung: {:?} ({:?}+ Möglichkeiten verbleiben)", calc_time(total_distance, line.end.y), @@ -177,7 +202,7 @@ fn main() { ); best = route; best.reverse(); - best_delay = delay; + best_bus = bus; if opt.save_intermediates { display::save_svg( opt, @@ -233,12 +258,15 @@ fn main() { } } +/// Schneidet die Linie keines der Polygone? +/// (Berührungen erlaubt) fn none_intersect(polys: &[Polygon], line: &Line) -> bool { let mut middle = line.start; middle.x += line.dx() / 2.0; middle.y += line.dy() / 2.0; let middle: Point = middle.into(); 'poly: for p in polys { + // Alle Seitenlinien des Polygons for l in p.exterior().lines().chain(vec![Line::new( p.exterior().0[0], *p.exterior().0.last().unwrap(), @@ -246,7 +274,7 @@ fn none_intersect(polys: &[Polygon], line: &Line) -> bool { if (l.start == line.start && l.end == line.end) || (l.start == line.end && l.end == line.start) { - // point is on polygon border + // Linie liegt auf Kante des Polygons continue 'poly; } if l.start == line.start @@ -254,25 +282,43 @@ fn none_intersect(polys: &[Polygon], line: &Line) -> bool { || l.start == line.end || l.end == line.start { - continue; // would always intersect with itself + continue; // Linie schneidet sich immer selbst } if l.intersects(line) { return false; } } + // falls Mittelpunkt in Polygon: if p.euclidean_distance(&middle) == 0.0 { + // schneidet Polygon return false; } + // (dieser Fall tritt auf, wenn die Linie von einer Polygon-Ecke zu einer anderen Ecke verläuft) } true } +/// Euklidische Distanz zweier Punkte fn distance(a: Point, b: Point) -> f64 { ((a.x() - b.x()).powi(2) + (a.y() - b.y()).powi(2)).sqrt() } -fn max_possible_delay(opt: &Opt, total_distance: f64, lisa: Point) -> f64 { - let x_l = lisa.x(); - let y_l = lisa.y(); - y_l - (((opt.bus/opt.lisa).powi(2) - 1.0) * x_l.powi(2)).sqrt() - total_distance * (opt.bus/opt.lisa) +/// Mit dieser Funktion kann man bestimmen, wo der Bus zu Lisas Startzeit war. +/// Die Funktion nimmt an, dass Lisa, von ihrer aktuellen Position aus, den Bus ohne Umwege erreichen kann. +/// Sehr hilfreich als Heuristik, da sie nie schlechter als das eigentliche Ergebnis sein kann. +fn best_bus_pos(opt: &Opt, total_distance: f64, lisa: Point) -> f64 { + let x_l = lisa.x(); // a + let y_l = lisa.y(); // b + // y-Koordinate des Busses: d + // Länge, die Lisa noch laufen muss: l + // Verhältnis beider Geschwindigkeiten: x = bus / lisa + // l^2 = sqrt(a^2 + (b - xl - d)^2) + // die quadratische Gleichung hat zwei Lösungen (siehe Dok.) + // nach der ABC-Formel muss b^2 - 4ac = 0 sein, damit die Gleichung nur eine Lösung hat: + // (2xb-2xd)^2 - 4*((1-x^2)*(-a^2-b^2-d^2+2bd)) = 0 + // (eine WolframAlpha-Eingabe später..) + // d = sqrt(a^2 * (x^2 - 1)) + b + // in dieser Berechnung wird auch die bereits zurückgelegte Strecke berücksichtigt: + y_l - (((opt.bus / opt.lisa).powi(2) - 1.0) * x_l.powi(2)).sqrt() + - total_distance * (opt.bus / opt.lisa) }