|
@@ -1,27 +1,28 @@
|
1
|
1
|
import strscans
|
2
|
2
|
|
|
3
|
+# For performance, we only use pointers to Marbles (and manually allocate)
|
|
4
|
+# memory required for them. This stops the GC tracking them, saving a fair
|
|
5
|
+# chunk of time. Removed marbles will leak, but it doesn't really matter for
|
|
6
|
+# the scope of this program.
|
|
7
|
+
|
3
|
8
|
type
|
4
|
|
- Marble = object
|
5
|
|
- next, prev: ptr Marble
|
|
9
|
+ Marble = ptr object
|
|
10
|
+ next: Marble
|
6
|
11
|
value: int32
|
7
|
12
|
|
8
|
|
-proc insertAfter(node: ptr Marble, value: int) {.inline.} =
|
9
|
|
- var newNode = cast[ptr Marble](alloc0(sizeof(Marble)))
|
|
13
|
+proc insertAfter(node: Marble, value: int) {.inline.} =
|
|
14
|
+ var newNode = cast[Marble](alloc0(sizeof(Marble)))
|
10
|
15
|
newNode.value = cast[int32](value)
|
11
|
16
|
newNode.next = node.next
|
12
|
|
- newNode.prev = node
|
13
|
|
- newNode.next.prev = newNode
|
14
|
|
- newNode.prev.next = newNode
|
|
17
|
+ node.next = newNode
|
15
|
18
|
|
16
|
|
-proc remove(node: ptr Marble) {.inline.} =
|
17
|
|
- node.prev.next = node.next
|
18
|
|
- node.next.prev = node.prev
|
|
19
|
+proc removeNext(node: Marble) {.inline.} =
|
|
20
|
+ node.next = node.next.next
|
19
|
21
|
|
20
|
|
-proc newSingleNode(value: int): ptr Marble =
|
21
|
|
- result = cast[ptr Marble](alloc0(sizeof(Marble)))
|
|
22
|
+proc newSingleNode(value: int): Marble =
|
|
23
|
+ result = cast[Marble](alloc0(sizeof(Marble)))
|
22
|
24
|
result.value = cast[int32](value)
|
23
|
25
|
result.next = result
|
24
|
|
- result.prev = result
|
25
|
26
|
|
26
|
27
|
var
|
27
|
28
|
input = readFile("data/09.txt")
|
|
@@ -31,25 +32,43 @@ var
|
31
|
32
|
if not input.scanf("$i players; last marble is worth $i points", players, marbles):
|
32
|
33
|
raise newException(Defect, "Invalid input line: " & input)
|
33
|
34
|
|
|
35
|
+# Instead of using a doubly-linked list, we keep a current pointer and a
|
|
36
|
+# separate one trailing behind it. The trail will expand to 8 marbles and is
|
|
37
|
+# used when a multiple of 23 is played (so we can remove the N-7th marble). The
|
|
38
|
+# trail then catches up to the current pointer and drifts back to 8 again over
|
|
39
|
+# the next few moves. This saves a 64 bit memory allocation per marble, which
|
|
40
|
+# gives a small but noticable speed bump.
|
|
41
|
+
|
34
|
42
|
var
|
35
|
43
|
player = 0
|
36
|
44
|
scores = newSeq[int](players)
|
37
|
45
|
current = newSingleNode(0)
|
|
46
|
+ currentTrail = current
|
|
47
|
+ currentTrailDrift = 0
|
38
|
48
|
specialCountdown = 23
|
39
|
49
|
hundredMarbles = marbles * 100
|
40
|
50
|
|
41
|
51
|
for i in 1 .. hundredMarbles:
|
|
52
|
+ # Instead of testing each marble number to see if it's a multiple of 23, we
|
|
53
|
+ # keep a counter that we just loop over and over again.
|
42
|
54
|
specialCountdown.dec
|
43
|
55
|
if specialCountdown == 0:
|
44
|
|
- specialCountdown = 23
|
|
56
|
+ # The current player is only relevant when a 23nth marble is played, so
|
|
57
|
+ # we can just update the player here instead of every turn.
|
45
|
58
|
player = (player + 23) mod players
|
46
|
|
- current = current.prev.prev.prev.prev.prev.prev.prev
|
47
|
|
- scores[player] += i + current.value
|
48
|
|
- current.remove
|
49
|
|
- current = current.next
|
|
59
|
+ scores[player] += i + currentTrail.next.value
|
|
60
|
+ currentTrail.removeNext
|
|
61
|
+ current = currentTrail.next
|
|
62
|
+ currentTrail = current
|
|
63
|
+ currentTrailDrift = 0
|
|
64
|
+ specialCountdown = 23
|
50
|
65
|
else:
|
51
|
66
|
current.next.insertAfter(i)
|
52
|
67
|
current = current.next.next
|
|
68
|
+ if currentTrailDrift == 8:
|
|
69
|
+ currentTrail = currentTrail.next.next
|
|
70
|
+ else:
|
|
71
|
+ currentTrailDrift += 2
|
53
|
72
|
|
54
|
73
|
if i == marbles or i == hundredMarbles:
|
55
|
74
|
echo scores.max
|