You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
21 KiB
487 lines
21 KiB
#+title: Solution to p10 |
|
|
|
This problem was quite hard. I think that probably I was hit hard by |
|
the inefficiencies of elisp. Anyways. here it is: |
|
|
|
#+begin_src emacs-lisp :results none |
|
(with-temp-buffer |
|
(insert-file-contents "input") |
|
(advent/replace-multiple-regex-buffer |
|
'(("," . " ") |
|
("^" . "(") |
|
("$" . ")") |
|
("\\[" . "\"") |
|
("\\]" . "\"") |
|
("{" . "(") |
|
("}" . ")") |
|
)) |
|
(goto-char (point-min)) |
|
(insert "(setq data '(") |
|
(goto-char (point-max)) |
|
(insert "))") |
|
(eval-buffer)) |
|
(setq machines (--map (-rotate 1 (cdr it)) data)) |
|
#+end_src |
|
|
|
For part 1 we do not need the last item This is a linear algebra |
|
problem in characteristic 2; we are essentially bruteforcing the |
|
vector space; we easily succeed. |
|
#+begin_src emacs-lisp :results none |
|
(defun zero-one (n) |
|
(if (eq n ?#) 1 0)) |
|
|
|
(setq cleanedup-data (--map (cons (-map #'zero-one (advent/split-string-into-char-list (car it))) (cdr it)) data)) |
|
#+end_src |
|
|
|
#+begin_src emacs-lisp |
|
(setq machines (--map (-drop-last 1 it) cleanedup-data)) |
|
|
|
(defun to-bin (l) |
|
(-sum (--map-indexed (* it (expt 2 it-index)) l))) |
|
|
|
(defun to-mask (l) |
|
(-sum (--map (expt 2 it) l))) |
|
|
|
(setq mask-machines (--map (cons (to-bin (car it)) (-map #'to-mask (cdr it))) machines)) |
|
|
|
(-sum |
|
(-map (lambda (machine) |
|
(-min (-map 'length (--filter (= (car machine) (apply 'logxor it)) |
|
(-powerset (cdr machine)))))) |
|
mask-machines)) |
|
#+end_src |
|
|
|
For part 2, the same approach would blow the stack even for the test |
|
input. As a first attempt, go depth first and memoize for the win… |
|
This works for the test input, but appears to take forever for the |
|
true input. |
|
#+begin_src emacs-lisp |
|
(setq machines (--map (-rotate 1 (cdr it)) data) |
|
machines (--map (cons (car it) (--sort (> (length it) (length other)) (cdr it))) machines)) |
|
|
|
(defun apply-button (joltage button) |
|
(--map-indexed (if (-contains-p button it-index) (- it 1) it) joltage)) |
|
|
|
(defun good-buttons (machine) |
|
(-filter (lambda (button) (--every (< 0 (nth it (car machine))) button)) (cdr machine))) |
|
|
|
(defun or-min (l) |
|
(when l (-min l))) |
|
|
|
(defun nil-1+ (l) |
|
(when l (1+ l))) |
|
|
|
(defun solve-machine (machine) |
|
(when machine |
|
(if (= 0 (-sum (car machine))) 0 |
|
(nil-1+ (solve-machine (-first 'solve-machine (--map (cons it (cdr machine)) (--map (apply-button (car machine) it) (good-buttons machine))))))))) |
|
|
|
(memoize 'solve-machine) |
|
(-sum (-map 'solve-machine machines)) |
|
#+end_src |
|
|
|
#+RESULTS: |
|
: 33 |
|
|
|
So we need to stop being a brute and realize that this is a linear |
|
algebra problem. Gauss elimination could work, but in general leads |
|
to matrices with negative entries. |
|
|
|
These are some auxiliary functions to create and deal with matrices |
|
I leave here a row-reducing routine for posterity, but I am not using |
|
it in the actual solution |
|
#+begin_src emacs-lisp :results none |
|
(defun matrix-buttons (machine) |
|
"Takes MACHINE and returns the corresponding augmented matrix of the |
|
linear system. The vector of constants is the first column vector |
|
instead of the last column as usual(it is more idiomatic this way)" |
|
(--map-indexed (cons it (--map (if (-contains-p it it-index) 1 0) |
|
(cdr machine))) |
|
(car machine))) |
|
|
|
;; These are convenience functions that we will use for row-reducing |
|
(defun find-pivot (row index) |
|
(let ((p (--find-index (not (= it 0)) (-drop index row)))) |
|
(when p (+ p index)))) |
|
|
|
(defun swap-indices (i j list) |
|
(if (= i j) list |
|
(let ((el-i (nth i list)) |
|
(el-j (nth j list))) |
|
(-replace-at j el-i (-replace-at i el-j list))))) |
|
|
|
(defun subtract-indices (λ i j list) |
|
"Subtracts λ× element i from element j" |
|
(let ((el-i (nth i list)) |
|
(el-j (nth j list))) |
|
(-replace-at j (- el-j (* λ el-i)) list))) |
|
|
|
(defun flip-index (i list) |
|
"Flip the sign of element i" |
|
(let ((el-i (nth i list))) |
|
(-replace-at i (* -1 el-i) list))) |
|
|
|
(defun subtract-composite (lambdas i list) |
|
(--each (-iota (length lambdas)) |
|
(setq list (subtract-indices (nth it lambdas) i it list))) |
|
list) |
|
|
|
; This is a routine for row-reducing the augmented matrix |
|
(defun row-reduce (matrix) |
|
(let* ((rMt (apply '-zip-lists matrix)) ;transpose |
|
(base-index 0)) |
|
; here we cannot use -map, since we are changing the matrix as we |
|
; go |
|
(--each (-iota (1- (length rMt)) 1) ;skip over the constant |
|
(let* ((original-row (nth it rMt)) |
|
(pivot-index (find-pivot original-row base-index))) |
|
(when pivot-index |
|
(setq rMt (--map (swap-indices base-index pivot-index it) rMt)) |
|
(let* ((pivot-coeff (nth pivot-index original-row))) |
|
(when (< pivot-coeff 0) |
|
(setq rMt (--map (flip-index base-index it) rMt))) |
|
(if (not (= 1 (abs pivot-coeff))) (setq pivot-coeff (* 1.0 pivot-coeff))) |
|
(let* ((lambdas (append (-repeat (1+ base-index) 0) |
|
(-drop (1+ base-index) (nth it rMt)))) |
|
(lambdas-corrected (--map (/ it (abs pivot-coeff)) lambdas))) |
|
(setq rMt (--map (subtract-composite lambdas-corrected base-index it) rMt) |
|
base-index (1+ base-index))))))) |
|
(apply '-zip-lists rMt))) |
|
#+end_src |
|
#+begin_src emacs-lisp |
|
(defun find-possible-indices (matrix i) |
|
(let* ((row (nth i matrix)) |
|
(next-row (nth (1+ i) matrix)) |
|
(i ) |
|
(j (if next-row (--find-index (not (= 0 it)) (cdr next-row)) |
|
(1- length row)))) |
|
(-iota (- j i) i))) |
|
|
|
|
|
(defun solve-row-reduced (matrix) |
|
;; we start from the last row |
|
(let* ((soln (list (-repeat (1- (length (car matrix))) 0))) |
|
(last-used-button (length (car soln))) |
|
(current-row (1- (length matrix)))) |
|
(while (<= 0 current-row) |
|
(let* ((row (nth current-row matrix)) |
|
(a (car row)) |
|
(i (--find-index (not (= 0 it)) (cdr row)))) |
|
(if i |
|
(let ((possible-indices (--filter (not (= 0 (nth it (cdr row)))) |
|
(-iota (- last-used-button i) i)))) |
|
(if (= 1 (length possible-indices)) ;no choices here, easy |
|
(setq soln (-non-nil (--map (let* ((correction (advent/dot it (-replace-at i 0 (cdr row)))) |
|
(corrected-a (- a correction)) |
|
(pushes (/ corrected-a (nth i (cdr row))))) |
|
(unless (< pushes 0) (-replace-at i pushes it))) |
|
soln)) |
|
last-used-button i |
|
current-row (1- current-row)) |
|
;;otherwise, we create a number of solutions |
|
(let* ((button (-last-item possible-indices))) |
|
(setq soln (--mapcat (let* ((correction (advent/dot it (-replace-at i 0 (cdr row)))) |
|
(corrected-a (- a correction)) |
|
(max-soln (/ corrected-a (nth button (cdr row))))) |
|
(if (< max-soln 0) (list it) |
|
(-map (lambda (candidate) (-replace-at button candidate it)) (-iota (1+ (round max-soln)))))) |
|
soln) |
|
last-used-button button)))) |
|
(setq current-row (1- current-row))))) |
|
soln)) |
|
#+end_src |
|
|
|
|
|
Instead of using row-reduction we sort the buttons and the list of |
|
buttons in a (arbitrary) way that makes the matrix "triangular". The |
|
sorting is what I thought would be a good idea, but possibly it is |
|
inefficient in the long run. In any case, it does what it needs to, |
|
which is present the matrix in a way that has zeros in the bottom left |
|
corner. |
|
#+begin_src emacs-lisp |
|
(defun set-distance (a b) |
|
(length (-difference b a))) |
|
|
|
(defun -min-or-0 (list) |
|
(if (not list) 0 |
|
(-min list))) |
|
|
|
(defun trip-value (list &optional base) |
|
(let ((n (* 1.0 (length list)))) |
|
(--map (+ (set-distance base it) |
|
(/ (-min-or-0 (--remove (= 0 it) (-map (lambda (new) |
|
(set-distance (-union base it) new)) |
|
list))) |
|
n)) |
|
list))) |
|
|
|
(defun sort-re-trip-value (list &optional base) |
|
(-map 'cdr (--sort (< (car it) (car other)) (-zip-pair (trip-value list base) list)))) |
|
|
|
(defun sort-recursively (list &optional base) |
|
(when list |
|
(let* ((step (sort-re-trip-value list base)) |
|
(top (car step)) |
|
(bottom (cdr step))) |
|
(cons top |
|
(sort-recursively bottom (-union top base)))))) |
|
|
|
(defun fix-machine (machine) |
|
(let* ((machine-1 (cons (car machine) (sort-recursively (cdr machine)))) |
|
(sorted (-reduce '-union (cdr machine-1))) |
|
(permutation (-grade-up '< sorted))) |
|
(cons (-select-by-indices sorted (car machine-1)) |
|
(--map (--map (nth it permutation) it) (cdr machine-1))))) |
|
#+end_src |
|
|
|
This is the tricky part; we want solve the row-reduced form, but we |
|
need to be careful with our choices if we have more than one |
|
possibility. The following implementation works, but it may blow the |
|
stack if the matrix is particularly degenerate; this is a breadth |
|
first implementation that I use to check other solutions |
|
|
|
#+begin_src emacs-lisp |
|
(defun test-soln (matrix soln) |
|
(equal (-map 'car matrix) (-map (lambda (row) (advent/dot (cdr row) soln)) matrix))) |
|
|
|
(defun solve-well-ordered (matrix) |
|
;; we start from the last row |
|
(let* ((number-of-buttons (1- (length (car matrix)))) |
|
(soln (list (-repeat number-of-buttons 0))) |
|
(last-used-button number-of-buttons) |
|
(current-row (1- (length matrix)))) |
|
(while (>= current-row 0) |
|
(message (format "%d %d" last-used-button (length soln))) |
|
;; (setq soln (--map (-min-by (lambda (a b) (< (-sum a) (-sum b))) (cdr it)) (--group-by (test-soln matrix it) soln))) |
|
(let* ((row (nth current-row matrix)) |
|
(a (car row)) |
|
(rrow (cdr row)) |
|
(i (--find-index (not (zerop it)) (-take last-used-button rrow)))) |
|
(if i |
|
(let ((possible-indices (--filter (not (zerop (nth it rrow))) |
|
(-iota (- last-used-button i) i)))) |
|
(if (= 1 (length possible-indices)) ;no choices here, easy |
|
(setq soln (-non-nil (--map (let* ((correction (advent/dot it rrow)) |
|
(corrected-a (- a correction))) |
|
(unless (< corrected-a 0) |
|
(-replace-at i corrected-a it))) |
|
soln)) |
|
|
|
last-used-button i |
|
current-row (1- current-row)) ; this needs to change |
|
;;otherwise, we create a number of solutions |
|
(let* ((button (-last-item possible-indices))) |
|
(setq soln (--mapcat (let* ((max-soln (-min (-non-nil (-map (lambda (row) |
|
(when (= 1 (nth button (cdr row))) |
|
(- (car row) (advent/dot it (cdr row))))) |
|
matrix))))) |
|
(unless (< max-soln 0) |
|
(-map (lambda (candidate) (-replace-at button candidate it)) (-iota (1+ max-soln) max-soln -1)))) |
|
soln) |
|
last-used-button button)))) |
|
(setq soln (--filter (= a (advent/dot it rrow)) soln) |
|
current-row (1- current-row))) |
|
(push soln solutions-tree) |
|
)) |
|
soln)) |
|
#+end_src |
|
|
|
#+RESULTS: |
|
: solve-well-ordered |
|
|
|
Breadth first may yield too many vectors to check. Subdivide into |
|
chunks if this happens and go breadth first on each chunk |
|
separately. The size at which we chunkize is arbitrary |
|
|
|
#+begin_src emacs-lisp |
|
(defun create-chunks (n list) |
|
(let ((result nil)) |
|
(while list |
|
(push (-take n list) result) |
|
(setq list (-drop n list))) |
|
result)) |
|
|
|
(defun solve-well-ordered-chunks (matrix) |
|
;; we start from the last row |
|
(let* ((soln-acc nil) |
|
(number-of-buttons (1- (length (car matrix)))) |
|
(soln (list (-repeat number-of-buttons 0))) |
|
(last-used-button number-of-buttons) |
|
(current-row (1- (length matrix))) |
|
(soln-chunks nil)) |
|
(while (or (>= current-row 0) soln-chunks) |
|
(message (format "%d %d - %d" last-used-button (length soln) (length soln-chunks))) |
|
(when (< current-row 0) |
|
(let ((chunk (pop soln-chunks))) |
|
(push soln soln-acc) |
|
(setq current-row (pop chunk) |
|
last-used-button (pop chunk) |
|
soln (pop chunk)))) |
|
;; chunkize here |
|
(when (> (length soln) 50000) |
|
(let* ((chunks (create-chunks 8000 soln)) |
|
(new-soln (car chunks)) |
|
(chunks-to-store (--map (list current-row last-used-button it) (cdr chunks)))) |
|
(setq soln new-soln |
|
soln-chunks (append chunks-to-store soln-chunks)))) |
|
(let* ((row (nth current-row matrix)) |
|
(a (car row)) |
|
(rrow (cdr row)) |
|
(i (--find-index (not (zerop it)) (-take last-used-button rrow)))) |
|
(if i |
|
(let ((possible-indices (--filter (not (zerop (nth it rrow))) |
|
(-iota (- last-used-button i) i)))) |
|
(if (= 1 (length possible-indices)) ;no choices here, easy |
|
(setq soln (-non-nil (--map (let* ((correction (advent/dot it rrow)) |
|
(corrected-a (- a correction))) |
|
(unless (< corrected-a 0) |
|
(-replace-at i corrected-a it))) |
|
soln)) |
|
|
|
last-used-button i |
|
current-row (1- current-row)) ; this needs to change |
|
;;otherwise, we create a number of solutions |
|
(let* ((button (-last-item possible-indices))) |
|
(setq soln (--mapcat (let* ((max-soln (-min (-non-nil (-map (lambda (row) |
|
(when (= 1 (nth button (cdr row))) |
|
(- (car row) (advent/dot it (cdr row))))) |
|
matrix))))) |
|
(unless (< max-soln 0) |
|
(-map (lambda (candidate) (-replace-at button candidate it)) (-iota (1+ max-soln) max-soln -1)))) |
|
soln) |
|
last-used-button button)))) |
|
(setq soln (--filter (= a (advent/dot it rrow)) soln) |
|
current-row (1- current-row))))) |
|
(push soln soln-acc) |
|
(apply #'append soln-acc))) |
|
#+end_src |
|
|
|
Even with the chunks, the bruteforcing was not going through. I now |
|
solve mod a prime for a few primes and then use the Chinese Remainder |
|
Theorem to go back to ℤ. This routine solves the matrix over ℤ_prime |
|
#+begin_src emacs-lisp |
|
(defun solve-well-ordered-chunks-mod (matrix prime) |
|
;; we start from the last row |
|
(let* ((soln-acc nil) |
|
(number-of-buttons (1- (length (car matrix)))) |
|
(soln (list (-repeat number-of-buttons 0))) |
|
(last-used-button number-of-buttons) |
|
(current-row (1- (length matrix))) |
|
(soln-chunks nil)) |
|
(while (or (>= current-row 0) soln-chunks) |
|
(message (format "%d %d - %d" last-used-button (length soln) (length soln-chunks))) |
|
(when (< current-row 0) |
|
(let ((chunk (pop soln-chunks))) |
|
(push soln soln-acc) |
|
(setq current-row (pop chunk) |
|
last-used-button (pop chunk) |
|
soln (pop chunk)))) |
|
;; chunkize here |
|
(when (> (length soln) 50000) |
|
(let* ((chunks (create-chunks 8000 soln)) |
|
(new-soln (car chunks)) |
|
(chunks-to-store (--map (list current-row last-used-button it) (cdr chunks)))) |
|
(setq soln new-soln |
|
soln-chunks (append chunks-to-store soln-chunks)))) |
|
(let* ((row (nth current-row matrix)) |
|
(a (mod (car row) prime)) |
|
(rrow (cdr row)) |
|
(i (--find-index (not (zerop it)) (-take last-used-button rrow)))) |
|
(if i |
|
(let ((possible-indices (--filter (not (zerop (nth it rrow))) |
|
(-iota (- last-used-button i) i)))) |
|
(if (= 1 (length possible-indices)) ;no choices here, easy |
|
(setq soln (-non-nil (--map (let* ((correction (advent/dot it rrow)) |
|
(corrected-a (mod (- a correction) prime))) |
|
(-replace-at i corrected-a it)) |
|
soln)) |
|
last-used-button i |
|
current-row (1- current-row)) ; this needs to change |
|
;;otherwise, we create a number of solutions |
|
(let* ((button (-last-item possible-indices))) |
|
(setq soln (--mapcat (-map (lambda (candidate) |
|
(-replace-at button candidate it)) |
|
(-iota prime)) |
|
soln) |
|
last-used-button button)))) |
|
(setq soln (--filter (= (mod a prime) (mod (advent/dot it rrow) prime)) soln) |
|
current-row (1- current-row))))) |
|
(push soln soln-acc) |
|
(apply #'append soln-acc))) |
|
#+end_src |
|
|
|
#+RESULTS: |
|
: solve-well-ordered-chunks-mod |
|
|
|
This is a recursive implementation, but it is super-slow and I ended |
|
up not using it |
|
#+begin_src emacs-lisp |
|
(defun solve-well-ordered-recursively (matrix) |
|
(if (not matrix) t |
|
(setq matrix (-distinct matrix)) |
|
(let* ((number-of-buttons (1- (length (car matrix)))) |
|
(row (-last-item matrix)) |
|
(a (car row)) |
|
(rrow (cdr row)) |
|
(possible-indices (--find-indices (not (zerop it)) rrow))) |
|
(if (not possible-indices) |
|
(when (zerop a) |
|
(solve-well-ordered-recursively (-butlast matrix))) |
|
(let ((possible-solutions |
|
(if (= 1 (length possible-indices)) (list a) |
|
(let* ((button (-last-item possible-indices)) |
|
(max-soln (-min (-non-nil (-map (lambda (row) |
|
(when (= 1 (nth button (cdr row))) (car row))) |
|
|
|
matrix))))) |
|
(when (>= max-soln 0) (-iota (1+ max-soln))))))) |
|
(-non-nil (-mapcat (lambda (a) |
|
(let* ((new-car (--map (if (= 1 (-last-item it)) (- (car it) a) (car it)) matrix))) |
|
(when (--every (>= it 0) new-car) |
|
(let* ((new-matrix (-map '-butlast (--map-indexed (cons (nth it-index new-car) (cdr it)) matrix))) ; remove one column |
|
(next (solve-well-ordered-recursively new-matrix))) |
|
(when next (if (listp next) |
|
(--map (cons a it) next) |
|
(list (list a)))))))) |
|
possible-solutions))))))) |
|
|
|
(--map (cons 'a it) '((1) (1 2) (1 3))) |
|
#+end_src |
|
|
|
Here we compute solutions modulo 2 3 5 and 7, then try to check |
|
possible solutions in ℤ by hand. Computing solutions modulo a prime |
|
is far cheaper bruteforcing. |
|
|
|
#+begin_src emacs-lisp |
|
(setq rainbow (-annotate (lambda (n) (--map (mod n it) '(2 3 5 7))) (-iota (* 2 3 5 7)))) |
|
|
|
|
|
(defun process-machine (machine) |
|
(let* ((matrix (-distinct (matrix-buttons (fix-machine machine)))) |
|
(solmod (--map (solve-well-ordered-chunks-mod matrix it) '(2 3 5 7))) |
|
(solns nil) |
|
(numcand (apply '* (-map 'length solmod))) |
|
(count 0)) |
|
;; Oh Programming Gods, have mercy of me for I have sinned |
|
(-each (car solmod) |
|
(lambda (a) |
|
(-each (cadr solmod) |
|
(lambda (b) |
|
(-each (caddr solmod) |
|
(lambda (c) |
|
(-each (cadddr solmod) |
|
(lambda (d) |
|
(let ((cand (--map (cdr (assoc it rainbow)) (-zip-lists a b c d)))) |
|
(message (format "machine %d. Verifying %d / %d - found %d" machine-id (setq count (1+ count)) numcand (length solns))) |
|
(when (test-soln matrix cand) (push cand solns))))))))))) |
|
solns)) |
|
|
|
(setq machine-id -1) |
|
(-sum (--map (progn |
|
(setq machine-id (1+ machine-id)) |
|
(-min (-map '-sum (process-machine it)))) |
|
machines)) |
|
#+end_src |
|
|
|
#+RESULTS: |
|
: 16474
|
|
|