题目具体可以看 这里
简单概括描述就是:
给定一些整数输入 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 实现并行计算,看起来能跑满七八个核。
1
2
3
4
5
6
7
8
9
10
11
12
13
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 实在是太强辣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 具体代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#[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# 来实现,该怎么办呢?具体请看下回分解。