Day 10: Knot Hash

If you were hoping for some slightly more hardcore challenges from the AoC, then Day 10 should be right up your street. There’s hexadecimal, bitwise XOR, ASCII and all sorts of other fun things going on. As always, let’s take a look at the challenge text and see where to begin!

Solving Part 1

To achieve this, begin with a list of numbers from 0 to 255, a current position which begins at 0 (the first element in the list), a skip size (which starts at 0), and a sequence of lengths (your puzzle input). Then, for each length:

  • Reverse the order of that length of elements in the list, starting with the element at the current position.
  • Move the current position forward by that length plus the skip size.
  • Increase the skip size by one.

The list is circular; if the current position and the length try to reverse elements beyond the end of the list, the operation reverses using as many extra elements as it needs from the front of the list. If the current position moves past the end of the list, it wraps around to the front. Lengths larger than the size of the list are invalid.

This doesn’t actually sound too bad. We can leverage some tricks we learnt on Day 6 to handle a circular list. Before we try to tackle the list of 256 numbers, we can work through the example for today. Let’s fire up a Q session and get started.

We need a few variables to keep track of the position and skip size, we can call these p and s respectively and initialise them both to zero

q)p:s:0 / assign both p and s to zero as a one-liner

And let’s call our list 0 1 2 3 4, l

q)til 5 / til gives us the range from 0 up to the input
0 1 2 3 4
q)l:til 5 / saves writing a few characters

Variable initialisation done, next stop: the tricky bit.

We need to index into our list l at position p, and extract the next 3 (first item in our input list i) items (wrapping around if need be), reverse them and put them back in place.

From Day 6 we saw we could use til and mod in order to generate a range of indexes (indices) that wrapped around a circular list:

(offset + til range) mod (count list)

Replacing the placeholders with our variables, and the first length, 3, gives us

q)(p + til 3) mod count l
0 1 2

And sure enough, these are the indices we want, using them to index back into l gives us 0 1 2 as expected

q)l (p + til 3) mod count l
0 1 2

We need to reverse these values, and then put them back into our list l. For that we can use the amend operator that we also used on Day 6.

For today’s challenge we will be using it with 4 parameters:

@[variable;indices;function;arguments]

The variable is our list, l, the indices are given by (p + til 3) mod count l, the function is assignment, :, and the arguments are the reverse of l (p + til 3) mod count l.

q)@[l;(p + til 3) mod count l;:;reverse l (p + til 3) mod count l]
2 1 0 3 4

Note: This does not save the updated the value in l unless we use `l or assign the result back into l as follows:

q)l:@[l;(p + til 3) mod count l;:;reverse l (p + til 3) mod count l]
q)l
2 1 0 3 4
q)@[`l;(p + til 3) mod count l;:;reverse l (p + til 3) mod count l]  / backticks only works on globals
`l
q)l / we performed reverse twice on the same indices, thus we are back to the start!
0 1 2 3 4
q)l:@[l;(p + til 3) mod count l;:;reverse l (p + til 3) mod count l] / get back to 2 1 0 3 4
q)l
2 1 0 3 4

We should now increment the position, p by that length (3) plus the skip size (s), wrapping around with mod

q)p+:(3 + s) mod count l
q)p / check the value of p
3

And now increase the skip size by one

q)s+:1

Let’s perform the apply again with the next input length, 4

q)l:@[l;(p + til 4) mod count l;:;reverse l (p + til 4) mod count l]
q)l
4 3 0 1 2

…and then update p and s

q)p+:(4 + s) mod count l
q)s+:1

Next input is 1 which has no impact on our list, but we still need to update p and s

q)l:@[l;(p + til 1) mod count l;:;reverse l (p + til 1) mod count l]
q)l
4 3 0 1 2
q)p+:(1 + s) mod count l
q)s+:1

.. and now with the final input, 5 gives us the following list:

q)l:@[l;(p + til 5) mod count l;:;reverse l (p + til 5) mod count l]
q)l
3 4 2 1 0

In order to multiply the first two numbers together we can use take, # and prd which returns us the product of the inputs:

q)2#l     / take 2 from list l
3 4
q)prd 2#l / multiply together
12

Great. We have some code that works for the example, albeit with a lot of keyboard work. Let’s put the 3 steps (updating list l, updating p and updating s) together into a function.

This is the first function we’ve created that will have two arguments. We will be passing in (1) the list to be modified, and (2) a number to modify the list.

Q etiquette dictates we should use x and y as parameters, but for starters we can use list for the list parameter, and length for the input length parameter:

{[list;length]
  list:@[list;(p + til length) mod count list;:;reverse list (p + til length) mod count list];
  p+:(length + s) mod count list; / update position p
  s+:1;                           / add 1 to s
  list                            / return the list from the function (don't add a semicolon here!)
  }

We are performing (p + til length) mod count list twice. Instead we can perform it once, save the result to a variable and then use this the next time we need it. Let’s assign the first occurrence to variable i for indices, and then use that as the 2nd parameter of the apply function:

{[list;length]
  list:@[list;i;:;reverse list i:(p + til length) mod count list]; / 'left of right' evaluation :)
  p+:(length + s) mod count list;                                  / update position p
  s+:1;                                                            / add 1 to s
  list                                                             / return list
  }

If we want to adhere to etiquette then the function becomes, which we can call knot:

knot:{[x;y]                                       / we don't actually need the [x;y]
  x:@[x;i;:;reverse x i:(p + til y) mod count x]; / amend list
  p+:(y + s) mod count x;                         / update position p
  s+:1;                                           / add 1 to s
  x                                               / return list
  }

Let’s reset p and s, and run through the example again, using knot

q)p:s:0 / reset position and skip size
q)knot[til 5;3]
2 1 0 3 4
q)knot[2 1 0 3 4;4]
4 3 0 1 2
q)knot[4 3 0 1 2;1]
4 3 0 1 2
q)knot[4 3 0 1 2;5]
3 4 2 1 0

It would be great if we could feed the result of knot back into knot for each of the input lengths… and we can.

Q gives us the adverb over to perform a function over a list.

Generally over will feed 2 items from a list into a function, and then take the result and continue feeding it and each additional item from the list into that function. This can be demonstrated by combining over with add, +, on the numbers 1 to 10. Note that because + is a built-in that is normally used infix (e.g. 1 + 3), we need to wrap it in brackets:

q)1 + til 10 / numbers 1 through 10
1 2 3 4 5 6 7 8 9 10
q)(+) over 1 + til 10
55

The sister function to over is scan, which outputs each result along the way, and can be very useful in seeing what is going on:

q)(+) scan 1 + til 10
1 3 6 10 15 21 28 36 45 55

Unfortunately for today’s challenge, the syntax is not that easy to understand. We also want to avoid using global variables (p and s). Therefore there are 4 arguments to pass into our function:

  • The input list, e.g. 0 1 2 3 4
  • The initial value of pointer p: 0
  • The initial value of skip size, also 0
  • The list operations to perform, e.g. 3 4 1 5

We wish to iterate over the sequence of lengths whilst keeping track of our input list, p and s.

Until I figure out a better method, the easiest way to do this appear to be grouping together our input, p and s as a 3-item list:

q)(0 1 2 3 4;0;0)
0 1 2 3 4
0
0

The first (or 0th) item in this list is our input list, we can use 0 to index to fetch it:

q)(0 1 2 3 4;0;0) 0
0 1 2 3 4

We are keeping track of p as the second item (index 1), and s as third item (index 2):

q)(0 1 2 3 4;0;0) 1 / this is the 2nd item in the list, p
0
q)(0 1 2 3 4;0;0) 2 / this is the 3rd item in the list, s
0

If we were to rewrite our earlier knot function to account for the fact that we are passing in a 3-item list rather than leaning on global variables to track p and s, it might look like this:

knot:{[x;y]
  l:x 0;                                          / list is item 0
  p:x 1;                                          / p is item 1
  s:x 2;                                          / s is item 2
  l:@[l;i;:;reverse l i:(p + til y) mod count l]; / amend list
  p+:(y + s) mod count l;                         / update position p
  s+:1;                                           / add 1 to s
  (x;p;s)                                         / return a 3-item list with the updated values
  };

We can test this with the first length:

q)knot[(til 5;0;0);3]
2 1 0 3 4
3
1

If we feed in the next length using the returned values:

q)knot[(2 1 0 3 4;3;1);4]
4 3 0 1 2
3
2

Combining this new function with scan \ (spitting out each interim step) gives:

q)knot\[(til 5;0;0);3 4 1 5]
2 1 0 3 4 3 1
4 3 0 1 2 3 2
4 3 0 1 2 6 3
3 4 2 1 0 9 4

We only care about the final result, so we should use over /:

q)knot/[(til 5;0;0);3 4 1 5]
3 4 2 1 0
9
4

More accurately, we only care about the first item of the final result:

q)first knot/[(til 5;0;0);3 4 1 5]
3 4 2 1 0

So we can rewrite the knot function as returning the first item from the result of performing an over. We will pass p and s with starting values of 0:

knot:{[x;y]
  first {
    l:x 0;                                          / list is item 0
    p:x 1;                                          / p is item 1
    s:x 2;                                          / s is item 2
    l:@[l;i;:;reverse l i:(p + til y) mod count l]; / amend list
    p+:(y + s) mod count l;                         / update position p
    s+:1;                                           / add 1 to s
    (l;p;s)                                         / return a 3-item list with the updated values
    }/[(x;0;0);y]
  };

Our knot function can now be called with 2 arguments: the input list, and the list of lengths:

q)knot[til 5;3 4 1 5]
3 4 2 1 0
q)prd 2#knot[til 5;3 4 1 5]
12

OK! We can handle the example as a one-liner. Let’s take a look at the puzzle input for today.

q)read0 `:input/10.txt
"97,167,54,178,2,11,209,174,119,248,254,0,255,1,64,190"

Ah, a list of numbers separated by commas. We’ve not had this one yet, but it’s almost the same as Day 6, just separated by "," instead of "\t".

q)first read0 `:input/10.txt / take the first line
"97,167,54,178,2,11,209,174,119,248,254,0,255,1,64,190"
q)"," vs first read0 `:input/10.txt / split on ","
"97"
"167"
"54"
"178"
,"2"
"11"
"209"
"174"
"119"
"248"
"254"
,"0"
"255"
,"1"
"64"
"190"
q)"J"$"," vs first read0 `:input/10.txt / and use big-J casting to cast to longs
97 167 54 178 2 11 209 174 119 248 254 0 255 1 64 190

The other difference between solving the example and solving Part 1 is that our list needs to contain 256 items (0..255) rather than just 5. This is easy to generate with til

q)til 256
0 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 71 72 73 74 75 76 77 78 79 80 81 8..

So, lets feed our input list of 0..255, and puzzle input into knot, take the first 2 elements and calculate the product:

q)prd 2#knot[til 256;"J"$"," vs first read0 `:input/10.txt]
8536

Bingo. That’s today’s first star!

Solving Part 2

The logic you’ve constructed forms a single round of the Knot Hash algorithm; running the full thing requires many of these rounds. Some input and output processing is also required… instead of merely running one round like you did above, run a total of 64 rounds, using the same length sequence in each round. The current **position and skip size should be preserved between rounds.

The first thing to note is that we need to convert our puzzle input ASCII characters and then append 17 31 73 47 23.

We then need to perform the knot function 64 times.

Once that’s done we need to xor together each of the 16, 16-item blocks, before finally converting to hexadecimal.

Thankfully we can re-use our knot function as-is, we just need to add bit more code around it to get our second star.

Let’s crack on…

We know from Day 1 that using the small-j casting on a string will return us the ASCII values:

q)"j"$first read0 `:input/10.txt
57 55 44 49 54 55 44 53 52 44 49 55 56 44 50 44 49 49 44 50 48 57 44 49 55 52 44 49 49 57 44 50 52 56 44 50 53 52 44 48 44 50 53 53 44 49 44 54 52 44 49 57 48

We need to append 17 31 73 47 23 to this (scroll right to see that it’s worked!)

q)("j"$first read0 `:input/10.txt),17 31 73 47 23 / note brackets
57 55 44 49 54 55 44 53 52 44 49 55 56 44 50 44 49 49 44 50 48 57 44 49 55 52 44 49 49 57 44 50 52 56 44 50 53 52 44 48 44 50 53 53 44 49 44 54 52 44 49 57 48 17 31 73 47 23

If we want to perform our knot function 64 times with this input list we can just take 64 copies of this list, and feed them into knot.

In order to take (#) the whole list 64 times we need to enlist it. Here’s a quick example showing the difference:

q)10#1 2 3 / take 10 from the list '1 2 3'
1 2 3 1 2 3 1 2 3 1
q)10#enlist 1 2 3 / take 10 of the list '1 2 3'
1 2 3
1 2 3
1 2 3
1 2 3
1 2 3
1 2 3
1 2 3
1 2 3
1 2 3
1 2 3

We then need to flatten this down again, which we can do with raze:

q)raze 10#enlist 1 2 3
1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3

Let’s do the same with our puzzle input:

q)raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23
57 55 44 49 54 55 44 53 52 44 49 55 56 44 50 44 49 49 44 50 48 57 44 49 55 52 44 49 49 57 44 50 52 56 44 50 53 52 44 48 44 50 53 53 44 49 44 54 52 44 49 57 48 17 31 73 47 23 57 55 44 49 54 55 44 53 52 44 49 55 56 44 50 44 49 49 44 50 48 ..

It’s a long list…

q)count raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23
3712

Let’s feed this list into knot along with til 256:

q)knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
128 8 0 225 59 13 127 253 100 224 188 218 153 239 58 220 159 163 111 2 23 237 54 182 228 56 50 61 75 99 34 7 76 168 53 192 216 57 176 204 105 21 52 132 198 244 62 223 19 90 67 136 174 33 246 72 141 139 249 133 179 121 35 89 155 4 241 3 1..

In order to cut this into chunks of 16, we can use cut. This operator takes 2 parameters, a list (or matrix) to operate on, and the size of the chunks to cut the input into.

q)4 cut "Hello, World!"
"Hell"
"o, W"
"orld"
,"!"

Let’s use this on the result of our knot

q)16 cut knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
128 8   0   225 59  13  127 253 100 224 188 218 153 239 58  220
159 163 111 2   23  237 54  182 228 56  50  61  75  99  34  7  
76  168 53  192 216 57  176 204 105 21  52  132 198 244 62  223
19  90  67  136 174 33  246 72  141 139 249 133 179 121 35  89
155 4   241 3   181 187 146 254 27  173 236 81  66  71  135 255
217 91  211 191 30  183 193 154 202 87  138 80  250 114 15  85
64  206 190 203 208 120 170 109 164 129 26  107 98  110 102 124
77  25  9   147 51  166 86  240 39  45  185 160 92  189 74  32
145 143 126 200 55  222 73  36  63  10  103 214 245 95  226 212
178 49  83  231 151 65  169 12  161 196 162 101 156 149 41  88
180 195 96  20  78  140 118 108 199 122 205 31  5   201 68  194
37  233 40  157 116 172 43  113 123 106 79  115 6   84  232 125
221 148 235 29  213 119 18  234 134 44  242 177 17  215 16  97
150 243 70  167 171 48  14  130 112 117 165 94  142 209 158 227
38  131 144 238 230 42  252 11  104 47  248 210 69  219 82  152
207 28  251 93  186 184 22  60  137 229 1   247 24  175 46  197

The next step is to xor each of these lines together to create a ‘dense hash’ from this ‘sparse hash’. There is no bitwise xor built-in to Q. Instead we have to convert our inputs to boolean lists, perform xor on them, and then convert the result back.

In order to convert a number to it’s bit representation we need to use vs as an encoder.

q)0b vs 123 / this is the syntax to convert to a list of bits
0000000000000000000000000000000000000000000000000000000001111011b

In order to convert a list of bits back to a number, we need to use sv as a decoder

q)0b sv 0000000000000000000000000000000000000000000000000000000001111011b / converting back
123

We an use the <> comparison as xor. The xor of 65 and 27 is shown below

q)(0b vs 65)<>0b vs 27 / <> is not-equal, ie xor
0000000000000000000000000000000000000000000000000000000001011010b
q)0b sv (0b vs 65)<>0b vs 27
90

If we convert this to a lambda we get:

{ 0b sv (0b vs x)<>(0b vs y) }

Which we can call to xor and try out:

q)xor:{ 0b sv (0b vs x)<>(0b vs y) }
q)xor[65;27]
90

In order to apply this over a list, we can use the over adverb we used in Part 1.

The example states that the xor of 65 27 9 1 4 3 40 50 91 7 6 0 2 5 68 22 is 64, let’s make sure we agree:

q)xor over 65 27 9 1 4 3 40 50 91 7 6 0 2 5 68 22
64

Nice. Let’s apply xor over to each of our 16 lists

q){ xor over x } each 16 cut knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
175 245 147 121 121 137 214 101 52 158 254 17 187 79 217 155

We can remove the lambda by wrapping the xor over in brackets:

q)(xor over) each 16 cut knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
175 245 147 121 121 137 214 101 52 158 254 17 187 79 217 155

Now we need to convert this to hexadecimal. We can do this by casting to bytes with "x"$:

q)"x"$(xor over) each 16 cut knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
0xaff593797989d665349efe11bb4fd99b

For completeness we should convert this to a string:

q)string "x"$(xor over) each 16 cut knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
"af"
"f5"
"93"
"79"
"79"
"89"
"d6"
"65"
"34"
"9e"
"fe"
"11"
"bb"
"4f"
"d9"
"9b"

… which we need to flatten with raze

q)raze string "x"$(xor over) each 16 cut knot[til 256;raze 64#enlist ("j"$first read0 `:input/10.txt),17 31 73 47 23]
"aff593797989d665349efe11bb4fd99b"

… and today’s second star is ours!

Complete Solution To Day 10

My full solution for Day 10 is below.

I have simplified the code inside the knot function to save some of the assignments, but ultimately it’s doing the same job as the code posted above.