3

Advent of Code 2023: Day5

 9 months ago
source link: https://scottyeung.top/2023/advent-of-code-day-5/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
 2023.12.9 2023.12.9  Posts  

题目具体可以看 这里

简单概括描述就是: 给定一些整数输入 seeds,并且给出一些 mapping (list list (dst, src, length)),如果 seed 落在 [src, src + length] 这个区间里面,就 map 成 (dst + seed - src)。求经过多次 mapping 之后,seeds 当中的最小值。

暴力 with F#

看到题目很直观的一个想法就是对于输入的每个 seed 都扫一遍所有的 mapping,得到一个最终的 seed,并从中求最小值。part1 很轻松就过掉,但是到了 part2 的时候,每两个 seed 表示输入的是 [seed1, seed1 + seed2] 区间内的所有 seeds,加起来超过 198 亿个 seeds。

最开始直接用 F# 写了一个暴力法模拟,但是跑了一段时间后可能因为数量太多直接给我中断了。于是手动把输入分成了两半,然后跑两次,一次大概要跑 8 分钟,不过能跑到正确的答案。

既然这么耗时,第一时间想到的就是用多线程并行来跑。在 F# 当中,可以很简单地通过 async block + Async.Parallel 实现并行计算,看起来能跑满七八个核。

seeds
|> Seq.map (fun (a, b) ->
	async {
		let allSeeds =
			[| for i in a .. (a + b - (int64 1)) do
				   yield i |]

		return maps |> Array.fold (fun seeds map -> seed2Map seeds map) allSeeds |> Seq.min

	})
|> Async.Parallel
|> Async.RunSynchronously
|> Array.min

在 F# 当中使用多线程跑,5 分钟多点就能跑完。

既然跑得慢,那么切换到一个跑得更快的语言,用 Rust 来写,应该能提升不少。

暴力 with Rust

先用 Rust 写了个逻辑一样的暴力模拟,基本上就是把 F# 写的逻辑搬了过来,4 分钟左右能跑完。这个速度比我在 F# 用多线程来跑还快!Rust 的性能果然名不虚传。

然后继续用多线程的方法又写了一版,每两个 seed 作为一组,开一个线程来逐个 seed 处理。然后一分钟左右就能跑完,确实快,Rust 实在是太强辣

let mut handlers = vec![];
for (start, cnt) in seeds {
	let maps_clone = maps.clone();
	let handler = thread::spawn(move || {
		(start..start + cnt)
			.map(|seed| {
				maps_clone
					.iter()
					.fold(seed, |seed, map| seed_2_map(seed, &map))
			})
			.min()
			.unwrap()
	});
	handlers.push(handler);
}

handlers
	.into_iter()
	.map(|handler| handler.join().unwrap())
	.min()
	.unwrap()

真正的解法

之前暴力的解法是逐个 seed 做多次 mapping 得到结果再找最小值,但是这个数量太多,需要跑的次数太多。但实际也不需要每个 seed 做 mapping,完全可以根据区间来做 mapping.

对于一个 seeds 的区间 range,经过一轮 mapping 后,

  • 如果跟其中的一个 map 有交集,可以取这个交集出来做 mapping
  • 交集部分两个端点进行 mapping 可以得到新 range
  • 非交集部分取出来得到新 range,继续和其他的 map 进行处理
  • 如果都没有交集,那么直接进入下一轮 mapping

具体代码如下

#[derive(Copy, Clone)]
struct Range {
    start: u64,
    end: u64,
}

impl Range {
    fn new(start: u64, end: u64) -> Range {
        Range { start, end }
    }

    fn is_empty(&self) -> bool {
        self.start >= self.end
    }

    fn intersect(&self, other: Range) -> Range {
        Range {
            start: self.start.max(other.start),
            end: self.end.min(other.end),
        }
    }

    fn difference(&self, other: Range) -> (Range, Range) {
        let left_diff = Range::new(self.start, self.end.min(other.start));
        let right_diff = Range::new(self.start.max(other.end), self.end);
        (left_diff, right_diff)
    }
}

let mut ranges = seeds;
for map in maps {
	let mut mapped_ranges = vec![];
	while let Some(range) = ranges.pop() {
		let mut has_intersect = false;
		for (target, source, len) in map.iter() {
			let mapped_range = Range {
				start: *source,
				end: *source + len,
			};

			let intersection = range.intersect(mapped_range);
			if intersection.is_empty() {
				continue;
			}

			has_intersect = true;
			mapped_ranges.push(Range {
				start: intersection.start - *source + *target,
				end: intersection.end - *source + *target,
			});

			let (left_diff, right_diff) = range.difference(mapped_range);
			if !left_diff.is_empty() {
				ranges.push(left_diff);
			}
			if !right_diff.is_empty() {
				ranges.push(right_diff);
			}

			break;
		}

		if !has_intersect {
			mapped_ranges.push(range);
		}
	}
	ranges = mapped_ranges;
}

ranges.iter().map(|range| range.start).min().unwrap()

切换到这个解法后,直接 0.05s 秒了!

一个小问题:对于这种需要三个嵌套循环的代码逻辑,如果转成使用函数式编程的思路,比如用 F# 来实现,该怎么办呢?具体请看下回分解。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK