Compare commits

..

170 Commits

Author SHA1 Message Date
Mikaël Capelle
b9341bdecc 2024 day 22.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-22 14:07:40 +01:00
Mikaël Capelle
ae5527b72d 2024 day 21 part 1.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-21 13:13:31 +01:00
Mikaël Capelle
96f139fe10 2024 day 20.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-20 09:19:12 +01:00
Mikael CAPELLE
683cac334c 2024 day 19.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-19 12:13:00 +01:00
Mikael CAPELLE
146d025d41 Add generic simple dijkstra method.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-18 10:41:01 +01:00
Mikael CAPELLE
954ef1e6ce 2024 day 18.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-18 08:54:06 +01:00
Mikael CAPELLE
24580fdfd8 Add Thomas input for 2024 day 17.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-17 15:27:21 +01:00
Mikael CAPELLE
7c0a124a5d 2024 day 17 kind-of-generic. 2024-12-17 15:20:30 +01:00
Mikael CAPELLE
11e32ddfda 2024 day 17 specific to me.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-17 14:28:37 +01:00
Mikael CAPELLE
4dcdab9931 2024 day 17 WIP.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-17 09:45:27 +01:00
Mikael CAPELLE
f965eea33a 2024 day 16 no networkx.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-16 16:48:45 +01:00
Mikael CAPELLE
c3c73ee517 2024 day 16, networkx version.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-16 08:19:42 +01:00
Mikaël Capelle
8308940674 Allow creation of avi video.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 14:07:32 +01:00
Mikaël Capelle
bc06f86fdc 2024 day 15 colored gif output.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 11:46:07 +01:00
Mikaël Capelle
2c25b33bcc Optimize generated gifs.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 11:30:23 +01:00
Mikaël Capelle
8651884ca6 2024 day 15 gif output.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 11:21:01 +01:00
Mikaël Capelle
7447c7b536 2024 day 15.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 10:56:28 +01:00
Mikaël Capelle
3e8d796b2e 2024 day 15.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 10:44:47 +01:00
Mikaël Capelle
fcd4b47951 Use imageio instead of matplotlib to generate image.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-15 10:13:42 +01:00
Mikaël Capelle
b15131bf1e Add dot file to 2021 day 12.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 22:35:18 +01:00
Mikaël Capelle
2c5c51e05f 2021 day 12.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 22:29:20 +01:00
Mikaël Capelle
51275dd539 2021 day 11.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 22:07:05 +01:00
Mikaël Capelle
f1ae1c598f 2021 day 10. 2024-12-14 21:49:05 +01:00
Mikaël Capelle
91ba8ec86f 2015 day 25.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 21:02:23 +01:00
Mikaël Capelle
323f810fcd 2015 day 24.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 15:21:19 +01:00
Mikaël Capelle
8969ea895f Handle PNG file generation.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 11:52:32 +01:00
Mikaël Capelle
67f7eef636 Update poetry dependencies.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 11:10:46 +01:00
Mikaël Capelle
4f8b50577a 2024 day 14.
Some checks failed
continuous-integration/drone/push Build is failing
2024-12-14 10:23:00 +01:00
Mikaël Capelle
67e41503c9 2024 day 13.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-13 09:15:23 +01:00
Mikael CAPELLE
30e0bb3665 2015 day 23.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-12 17:19:25 +01:00
Mikael CAPELLE
291c627c79 2024 day 12.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-12 17:19:00 +01:00
Mikaël Capelle
89306f4a04 UGLY, 2024 day 12.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-12 07:21:12 +01:00
Mikael CAPELLE
721d69e766 Remove unused functions from previous years.
All checks were successful
continuous-integration/drone/push Build is passing
2024-12-11 15:48:38 +01:00
Mikael CAPELLE
356fd35b08 2024 day 11 without str. 2024-12-11 09:57:47 +01:00
Mikaël Capelle
92bd85e1dd 2024 day 11. 2024-12-11 07:13:28 +01:00
Mikael CAPELLE
22129048e7 Fix 2023 day 20 for API. 2024-12-10 16:54:18 +01:00
Mikael CAPELLE
781e4cd6e1 Remove bad print in 2023 day 21. 2024-12-10 16:22:03 +01:00
Mikael CAPELLE
46558672e8 File handling for API. 2024-12-10 15:39:00 +01:00
Mikael CAPELLE
3c544c559b Clean 2024 day 10. 2024-12-10 08:46:44 +01:00
Mikaël Capelle
4367a5183a 2024 day 10. 2024-12-10 07:02:39 +01:00
Mikael CAPELLE
03e4e75978 Add TQDM to 2024 day 9. 2024-12-09 10:33:28 +01:00
Mikaël Capelle
98eb515c19 2024 day 9. 2024-12-09 06:55:27 +01:00
Mikaël Capelle
dd3f332870 Fix 2022 day 16 for progress API. 2024-12-08 19:25:21 +01:00
Mikaël Capelle
9d7ef94fa6 Force string type for answer value. 2024-12-08 14:34:57 +01:00
ce315b8778 Refactor code for API (#3)
Co-authored-by: Mikael CAPELLE <mikael.capelle@thalesaleniaspace.com>
Co-authored-by: Mikaël Capelle <capelle.mikael@gmail.com>
Reviewed-on: #3
2024-12-08 13:06:41 +00:00
Mikael CAPELLE
ab4e3e199c Refactor 2024 day 6 to be a little bit faster. 2024-12-06 14:10:51 +01:00
Mikaël Capelle
2c1a0b919b 2024 day 6, brute force. 2024-12-06 06:50:35 +01:00
Mikael CAPELLE
cd6f97cd7e Refactor 2024 day 5 without networkx. 2024-12-05 08:28:55 +01:00
Mikaël Capelle
5312755f32 2024 day 5. 2024-12-05 07:25:55 +01:00
Mikael CAPELLE
55cb5ed745 Refactor 2024 day 4. 2024-12-04 09:31:47 +01:00
Mikaël Capelle
f0d8e156a9 2024 day 4. 2024-12-04 07:35:22 +01:00
Mikael CAPELLE
c19279fad3 Refactor 2024 day 3. 2024-12-03 15:22:54 +01:00
0d50b44c37 Add .drone.yml for CI. (#2) 2024-12-03 13:38:03 +00:00
Mikael CAPELLE
b32d46b641 Fix linting. 2024-12-03 14:11:29 +01:00
Mikael CAPELLE
5c43eb2c73 2024 day 3. 2024-12-03 08:29:25 +01:00
Mikaël Capelle
540fe37b9d Fix 2024 day 2. 2024-12-02 18:44:50 +01:00
Mikael CAPELLE
2a4f923552 Update Python dependencies. 2024-12-02 17:08:50 +01:00
Mikael CAPELLE
4821db89cc 2024 day 2. 2024-12-02 15:42:56 +01:00
Mikaël Capelle
d1733a5888 2024 day 1. 2024-12-01 10:26:02 +01:00
Mikaël Capelle
dd8458fa96 2015 day 22, part 1. 2024-12-01 10:25:49 +01:00
Mikaël Capelle
850c66cd8d 2015 day 21. 2024-01-20 17:57:37 +01:00
Mikaël Capelle
2597235d0c 2015 day 20. 2024-01-06 22:07:34 +01:00
Mikaël Capelle
db9a3b3ed3 2015 day 19. 2024-01-06 21:35:48 +01:00
Mikaël Capelle
31b0e9f195 2015 day 18. 2024-01-06 16:43:35 +01:00
Mikaël Capelle
5b07e73382 2015 day 17. 2024-01-06 15:46:43 +01:00
Mikaël Capelle
c1732baa0d 2015 day 16. 2024-01-06 15:27:45 +01:00
Mikaël Capelle
de96ab0e25 2015 day 15. 2024-01-06 15:11:47 +01:00
Mikaël Capelle
cd58b7861b 2015 day 12, 13 & 14. 2024-01-06 14:56:30 +01:00
Mikaël Capelle
8d2f61fa65 2015 day 11. 2024-01-06 11:46:59 +01:00
Mikaël Capelle
3d7dd37c11 2015 day 10. 2024-01-06 11:30:10 +01:00
Mikael CAPELLE
f373528b06 2015 day 9. 2024-01-05 14:46:05 +01:00
Mikael CAPELLE
3fe9555cb1 2015 day 8. 2024-01-05 10:01:02 +01:00
Mikaël Capelle
f94e2bd831 2015 day 4, 5, 6, 7. 2024-01-04 21:05:42 +01:00
Mikael CAPELLE
685f1e56d7 2015 day 3. 2024-01-04 18:36:30 +01:00
Mikael CAPELLE
ea0c9e7812 2015 day 1 & 2. 2024-01-04 18:27:17 +01:00
Mikaël Capelle
52cb793d06 Faster 2023 day 24 (part 1). 2024-01-01 18:44:13 +01:00
Mikaël Capelle
4a2a63b0b0 Minor cleaning 2023. 2023-12-30 19:35:06 +01:00
Mikaël Capelle
9f96abbd43 2023 day 25. 2023-12-25 10:36:36 +01:00
Mikaël Capelle
57bf025622 2023 day 24. 2023-12-25 10:36:29 +01:00
Mikaël Capelle
bcadb68189 2023 day 23. 2023-12-23 11:05:35 +01:00
Mikaël Capelle
d7d7837c1f 2021 day 9. 2023-12-22 21:26:13 +01:00
Mikaël Capelle
82fab771ab 2023 day 22. 2023-12-22 14:49:31 +01:00
Mikaël Capelle
85fff24cc1 2023 day 21, version 2. 2023-12-21 21:56:38 +01:00
Mikael CAPELLE
9326d6c76c 2023 day 21. 2023-12-21 16:47:43 +01:00
Mikael CAPELLE
eefb3ceb44 2023 day 20. 2023-12-20 14:27:25 +01:00
Mikael CAPELLE
2959387bcd Poetry stuff. 2023-12-19 17:45:33 +01:00
Mikaël Capelle
41b07cfe83 2023 day 19. 2023-12-19 10:41:53 +01:00
Mikael CAPELLE
981e745eb0 2023 day 18. 2023-12-18 11:40:32 +01:00
Mikaël Capelle
8760e47283 2023 day 17. 2023-12-17 18:19:49 +01:00
Mikaël Capelle
1db3ab9090 2023 day 16. 2023-12-16 18:33:11 +01:00
Mikaël Capelle
9a1769e200 2023 day 15. 2023-12-15 16:18:21 +01:00
Mikaël Capelle
8c150c0bb1 2023 day 14. 2023-12-14 19:34:38 +01:00
Mikaël Capelle
69929b28dd 2023 day 13. 2023-12-13 20:05:05 +01:00
f41b50c9e0 2023 day 12. 2023-12-12 20:20:26 +00:00
Mikaël Capelle
d6b99454d2 2023 day 11. 2023-12-11 10:25:56 +01:00
Mikaël Capelle
1fc3c1632d 2021 day 8. 2023-12-10 20:23:22 +01:00
Mikaël Capelle
9141029557 2023 day 10. 2023-12-10 10:09:12 +01:00
Mikaël Capelle
9d5b57fd56 2021 day 7. 2023-12-09 12:52:46 +01:00
Mikaël Capelle
0756cf74c4 2021 day 6. 2023-12-09 12:36:10 +01:00
Mikaël Capelle
bd5727c758 2021 day 1-5. 2023-12-09 11:01:28 +01:00
Mikaël Capelle
0ebd823656 Clean 2022. 2023-12-09 09:54:53 +01:00
Mikaël Capelle
7c6c9e5995 2023 day 9. 2023-12-09 08:08:46 +01:00
Mikaël Capelle
f4cd8318b0 2023 day 8. 2023-12-08 08:44:03 +01:00
Mikaël Capelle
e06f3da2bd 2023 day 7. 2023-12-08 08:38:08 +01:00
Mikael CAPELLE
fb8a911d4d Clean 2023. 2023-12-06 08:47:59 +01:00
Mikaël Capelle
4e1c71b221 2023 day 6. 2023-12-06 07:14:26 +01:00
Mikael CAPELLE
0f8a272b71 2023 day 5. 2023-12-05 13:41:17 +01:00
Mikaël Capelle
508c8cdc42 2023 day 5 part 1. 2023-12-05 07:22:56 +01:00
Mikaël Capelle
ee55c807ef Prepare 2023 days. 2023-12-04 19:32:41 +01:00
Mikaël Capelle
10a5b92740 2023 day 4. 2023-12-04 19:30:44 +01:00
Mikaël Capelle
d3eacd48aa 2023 day 3. 2023-12-03 09:09:25 +01:00
Mikaël Capelle
53f05058f3 2023 day 2. 2023-12-02 09:47:52 +01:00
Mikaël Capelle
c0ea724d4c Fix 2023 day 1. 2023-12-01 20:27:42 +01:00
Mikael CAPELLE
57c15270dc 2023: Day 1. 2023-12-01 10:41:13 +01:00
Mikael CAPELLE
7533dd0b11 Post-christmas cleaning. 2023-01-06 13:48:18 +01:00
Mikaël Capelle
dd80b30e26 Day 25. 2022-12-25 11:34:49 +01:00
Mikaël Capelle
0508d95e33 Faster day 24. 2022-12-24 23:00:14 +01:00
Mikaël Capelle
f2cc3e4d16 Day 24. 2022-12-24 20:50:37 +01:00
Mikael CAPELLE
9526c383f3 Add day 23. 2022-12-23 13:25:22 +01:00
Mikaël Capelle
dd88a1838d Ugly day 22. 2022-12-22 23:00:36 +01:00
Mikaël Capelle
72ffd399b3 Update day 22. 2022-12-22 18:46:59 +01:00
Mikael CAPELLE
020ad7c6d1 Update day 22. 2022-12-22 17:22:56 +01:00
Mikael CAPELLE
1fc4d6531d Update day 22. 2022-12-22 17:00:09 +01:00
Mikael CAPELLE
d8bb659e78 Day 22 part 1. 2022-12-22 14:10:30 +01:00
Mikael CAPELLE
6ade69ac35 Start day 22. 2022-12-22 08:20:09 +01:00
Mikael CAPELLE
ed7149e5f4 Remove tqdm from day 19. 2022-12-21 17:39:43 +01:00
Mikael CAPELLE
e2df3c4825 Day 21. 2022-12-21 09:44:05 +01:00
Mikaël Capelle
266703cdc0 Clean day 20. 2022-12-20 23:39:26 +01:00
Mikaël Capelle
f635ea3c97 Add empty files for following days. 2022-12-20 21:51:38 +01:00
Mikael CAPELLE
304aeb16bd Faster day 20. 2022-12-20 18:27:09 +01:00
Mikael CAPELLE
ed00c72e59 Day 20, slow. 2022-12-20 13:39:36 +01:00
Mikael CAPELLE
835458e0f3 Start day 20. 2022-12-20 12:35:01 +01:00
Mikaël Capelle
3b9e9a2c8f Add all tests from previous days. 2022-12-19 22:32:15 +01:00
Mikaël Capelle
fadb2a71c2 Day 19. 2022-12-19 22:09:20 +01:00
Mikael CAPELLE
91434051c6 Tmp 2022-12-19 18:46:16 +01:00
Mikael CAPELLE
c1161f6c1f Start working on day 19. 2022-12-19 13:51:32 +01:00
Mikaël Capelle
3b4efce02f Clean day 17. 2022-12-18 16:46:00 +01:00
Mikaël Capelle
2aaf55b72f Day 18. 2022-12-18 09:57:35 +01:00
Mikaël Capelle
d493856d20 Day 17. 2022-12-17 12:27:05 +01:00
Mikaël Capelle
67647c7923 Day 17. 2022-12-17 12:25:48 +01:00
Mikaël Capelle
3c61e5cb7f Less BFS for day 16. 2022-12-16 22:56:34 +01:00
Mikaël Capelle
37e0e1ef06 Better day 16. 2022-12-16 22:52:03 +01:00
Mikael CAPELLE
725e18d480 Update ugly day 16. 2022-12-16 18:40:21 +01:00
Mikael CAPELLE
3d383527e4 Add ugly day 16, to be improved... 2022-12-16 09:21:54 +01:00
Mikael CAPELLE
48e8dff52b Update day 15. 2022-12-15 14:17:04 +01:00
Mikael CAPELLE
0b53406b52 Add day 15. 2022-12-15 09:36:19 +01:00
Mikael CAPELLE
232d019b40 Add day 14. 2022-12-14 09:29:56 +01:00
Mikael CAPELLE
35190adcdf Clean day 9. 2022-12-13 11:18:31 +01:00
Mikael CAPELLE
d4199b2810 Clean day 13. 2022-12-13 08:59:53 +01:00
Mikael CAPELLE
971c1b0dda Add day 13. 2022-12-13 08:54:15 +01:00
Mikael CAPELLE
4fe1f521b7 Clean day 12. 2022-12-12 17:55:24 +01:00
Mikael CAPELLE
38b1d86514 One to many Dijkstra. 2022-12-12 17:50:48 +01:00
Mikael CAPELLE
4899583b15 Generic Dijkstra for day 12. 2022-12-12 15:59:19 +01:00
Mikael CAPELLE
792951afa8 Clean day 12. 2022-12-12 10:48:37 +01:00
Mikael CAPELLE
5004aa3376 Add day 12. 2022-12-12 09:35:12 +01:00
Mikaël Capelle
9ba338a4f1 Clean monkey code. 2022-12-11 11:50:23 +01:00
Mikaël Capelle
8795d7a276 Add day 11. 2022-12-11 11:42:47 +01:00
Mikaël Capelle
89adfb151a Add day 10. 2022-12-10 10:24:23 +01:00
Mikaël Capelle
9fd8a5866a Add day 9. 2022-12-09 10:45:00 +01:00
Mikael CAPELLE
7564fac345 Add 2022 day 8. 2022-12-08 08:59:25 +01:00
Mikael CAPELLE
6bd27e4bca Add 2021 day 5. 2022-12-07 18:41:39 +01:00
Mikael CAPELLE
26cfe60f16 Add day 7. 2022-12-07 09:28:06 +01:00
Mikael CAPELLE
32a1220072 Cleaning. 2022-12-06 15:28:46 +01:00
Mikael CAPELLE
9852aea94b Add day 6. 2022-12-06 09:03:22 +01:00
Mikaël Capelle
f76767ca6d Add day 4. 2022-12-05 19:09:55 +01:00
Mikael CAPELLE
1b60725e9c Add day 5. 2022-12-05 08:58:25 +01:00
Mikaël Capelle
b8cb2cb0b9 Day 3. 2022-12-03 11:42:09 +01:00
Mikael CAPELLE
228f7501bb More comments. 2022-12-02 15:06:37 +01:00
Mikael CAPELLE
f4ef0a2666 Comments. 2022-12-02 09:21:38 +01:00
Mikael CAPELLE
60e68ed31c Day 2. 2022-12-02 09:12:49 +01:00
193 changed files with 17476 additions and 3507 deletions

518
poetry.lock generated
View File

@ -1,4 +1,15 @@
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "absl-py"
version = "2.1.0"
description = "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py."
optional = false
python-versions = ">=3.7"
files = [
{file = "absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff"},
{file = "absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308"},
]
[[package]] [[package]]
name = "appnope" name = "appnope"
@ -133,6 +144,30 @@ traitlets = ">=4"
[package.extras] [package.extras]
test = ["pytest"] test = ["pytest"]
[[package]]
name = "cplex"
version = "22.1.1.2"
description = "A Python interface to the CPLEX Callable Library, Community Edition."
optional = false
python-versions = "*"
files = [
{file = "cplex-22.1.1.2-cp310-cp310-macosx_10_6_x86_64.whl", hash = "sha256:d74a91f6e9c6f4ad4c7c69f7ab937ea9e91178a556f6f105c87eef9e434ea42e"},
{file = "cplex-22.1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b066b5aa01fcf7cb471ad41920b3fecdd87dc95686e9a7031ff470873f0db10f"},
{file = "cplex-22.1.1.2-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:a7230032928b1a657c384d3f49c04d8b80d0ab8134a2f4c0b26ff50e71ec767a"},
{file = "cplex-22.1.1.2-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:dd81e8ee7a7f1fb5769bcbb1349b084b37c495cbd0db1a095d774f97d790ee3c"},
{file = "cplex-22.1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:68b33bb9ff84be3442a6f71000e7214781c6aa8674143a9aa79cb9a84e697dfd"},
{file = "cplex-22.1.1.2-cp311-cp311-macosx_10_6_x86_64.whl", hash = "sha256:cda2f59af50d6c3d6476b2e38aba1e947f9bd72591d71961a9d53b5582062ba9"},
{file = "cplex-22.1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0f69a539ed50994e26e32c3d2b203eb5f112d1ba64400241614e1a91c0933974"},
{file = "cplex-22.1.1.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:2a0f6984980779e6878a6cded52ee08806bae49af6bd209c7740549417e69e96"},
{file = "cplex-22.1.1.2-cp311-cp311-manylinux2014_ppc64le.whl", hash = "sha256:0ac0005414a09facbeaa976a89b3153d4ed15f23a89bf5d283f65d4e951f63be"},
{file = "cplex-22.1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:46550cac476d74cc95dc3abf6f9bfe08c9fd61d889e20f2028f754b9fe503b88"},
{file = "cplex-22.1.1.2-cp39-cp39-macosx_10_6_x86_64.whl", hash = "sha256:21f6fd1ad4876a7775e64fe8a1fb43f6bb7a010c5e099abdb8c01887a8cc1d84"},
{file = "cplex-22.1.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6acbf74c3fe32f2138a98730d1ebe3fa275c8c3fdcd8b1f68d312bfe9ef6899"},
{file = "cplex-22.1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3211f9c84f44c6317ea1e83a6c2ff1cfdc08532f421987b7faa8c07a018dfae5"},
{file = "cplex-22.1.1.2-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:648ad8c1c83ea30b0802c571a0dbf7fac23dcb9dc121ea0768c1794313aec2a7"},
{file = "cplex-22.1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:285b26008a77942c6c9c29bff91e1658c1beed2aa520e1a8b26137d81abf81dc"},
]
[[package]] [[package]]
name = "debugpy" name = "debugpy"
version = "1.8.9" version = "1.8.9"
@ -179,6 +214,19 @@ files = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
] ]
[[package]]
name = "docplex"
version = "2.28.240"
description = "The IBM Decision Optimization CPLEX Modeling for Python"
optional = false
python-versions = "*"
files = [
{file = "docplex-2.28.240.tar.gz", hash = "sha256:c0de407e33f8709bb4cd91b6efeb96fd88bfecbdce2caec51afb79253bde6ff5"},
]
[package.dependencies]
six = "*"
[[package]] [[package]]
name = "exceptiongroup" name = "exceptiongroup"
version = "1.2.2" version = "1.2.2"
@ -207,6 +255,50 @@ files = [
[package.extras] [package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
[[package]]
name = "imageio"
version = "2.36.1"
description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats."
optional = false
python-versions = ">=3.9"
files = [
{file = "imageio-2.36.1-py3-none-any.whl", hash = "sha256:20abd2cae58e55ca1af8a8dcf43293336a59adf0391f1917bf8518633cfc2cdf"},
{file = "imageio-2.36.1.tar.gz", hash = "sha256:e4e1d231f47f9a9e16100b0f7ce1a86e8856fb4d1c0fa2c4365a316f1746be62"},
]
[package.dependencies]
numpy = "*"
pillow = ">=8.3.2"
[package.extras]
all-plugins = ["astropy", "av", "imageio-ffmpeg", "numpy (>2)", "pillow-heif", "psutil", "rawpy", "tifffile"]
all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"]
build = ["wheel"]
dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"]
docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"]
ffmpeg = ["imageio-ffmpeg", "psutil"]
fits = ["astropy"]
full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpy (>2)", "numpydoc", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "rawpy", "sphinx (<6)", "tifffile", "wheel"]
gdal = ["gdal"]
itk = ["itk"]
linting = ["black", "flake8"]
pillow-heif = ["pillow-heif"]
pyav = ["av"]
rawpy = ["numpy (>2)", "rawpy"]
test = ["fsspec[github]", "pytest", "pytest-cov"]
tifffile = ["tifffile"]
[[package]]
name = "immutabledict"
version = "4.2.1"
description = "Immutable wrapper around dictionaries (a fork of frozendict)"
optional = false
python-versions = ">=3.8"
files = [
{file = "immutabledict-4.2.1-py3-none-any.whl", hash = "sha256:c56a26ced38c236f79e74af3ccce53772827cef5c3bce7cab33ff2060f756373"},
{file = "immutabledict-4.2.1.tar.gz", hash = "sha256:d91017248981c72eb66c8ff9834e99c2f53562346f23e7f51e7a5ebcf66a3bcc"},
]
[[package]] [[package]]
name = "ipykernel" name = "ipykernel"
version = "6.29.5" version = "6.29.5"
@ -430,68 +522,133 @@ files = [
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.1.3" version = "2.2.0"
description = "Fundamental package for array computing in Python" description = "Fundamental package for array computing in Python"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
files = [ files = [
{file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"},
{file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"},
{file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"},
{file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"},
{file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"},
{file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"},
{file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"},
{file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"},
{file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, {file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"},
{file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, {file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"},
{file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, {file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"},
{file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, {file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"},
{file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"},
{file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"},
{file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"},
{file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"},
{file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"},
{file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"},
{file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, {file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"},
{file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, {file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"},
{file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, {file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"},
{file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, {file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"},
{file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"},
{file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"},
{file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"},
{file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"},
{file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"},
{file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"},
{file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, {file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"},
{file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, {file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"},
{file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, {file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"},
{file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, {file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"},
{file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"},
{file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"},
{file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"},
{file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"},
{file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"},
{file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"},
{file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, {file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"},
{file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, {file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, {file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, {file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"},
{file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"},
{file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"},
{file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"},
{file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"},
{file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"},
{file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, {file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"},
{file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, {file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, {file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"},
{file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, {file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"},
{file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"},
] ]
[[package]]
name = "opencv-python"
version = "4.10.0.84"
description = "Wrapper package for OpenCV python bindings."
optional = false
python-versions = ">=3.6"
files = [
{file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"},
{file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"},
{file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"},
{file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"},
{file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"},
{file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"},
{file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"},
]
[package.dependencies]
numpy = [
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
]
[[package]]
name = "ortools"
version = "9.11.4210"
description = "Google OR-Tools python libraries and modules"
optional = false
python-versions = ">=3.8"
files = [
{file = "ortools-9.11.4210-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:127f20f03ce04c28f979eac635d1cacdc01597c9e035a1981070506294d7db9c"},
{file = "ortools-9.11.4210-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:250c62ba9e5fcaf18ada449bc0128c71bb0dbea83ddec5559cc506cff920235c"},
{file = "ortools-9.11.4210-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c15cefc7bc71aa2f70bee157aaa7e51ed8cb74c3edd499d15b6f5cd79cdf5b"},
{file = "ortools-9.11.4210-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c3693f0cb07ee0b5d36c4817bcfe05a745e3a613798a2ed62eb998d7fff979c"},
{file = "ortools-9.11.4210-cp310-cp310-win_amd64.whl", hash = "sha256:6b9d4ae6c7e9efac7cbef8a6289e97e238c2f0a8ef587b3e56b71af14c2f04e6"},
{file = "ortools-9.11.4210-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:0f902caa1576d737714f6a4fa165db62469bce82115e250409607197b3b6b434"},
{file = "ortools-9.11.4210-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6f3e2869396dc6d8ee2d11b65d6f88f6386bb3ad64212c0ad7a6d32ddcb48ca"},
{file = "ortools-9.11.4210-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a56c5844ff927ce3c5428159cdd01b7fdbe243e8062bf1dfdb2e0eb305a55a30"},
{file = "ortools-9.11.4210-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5a17a37aeaa7d149e2fe8c8dfd5f09630ae28ad734a109ad55536605f8059df"},
{file = "ortools-9.11.4210-cp311-cp311-win_amd64.whl", hash = "sha256:d9b858f0273e19f81555428d54d407428d0a70a8cb5df2c320935bb735f2c6bb"},
{file = "ortools-9.11.4210-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:079bea08c6341dcfe3fb9586eb6edec6ae80f4ed16ed366fd7a46ef4b5709009"},
{file = "ortools-9.11.4210-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f55124f9d1afa6434d0d6de07c6a4eb836f29b00b3413d27138634d5d79b606"},
{file = "ortools-9.11.4210-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f48e3d4053a169440608d881c1abd2a706db885d9b0af85bf45b444a1fec244"},
{file = "ortools-9.11.4210-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03ecd32e5d760d48e59832ef6bf724f8cac95e4e40db72a7fb912abf7adcf931"},
{file = "ortools-9.11.4210-cp312-cp312-win_amd64.whl", hash = "sha256:bc1b6e4cc0a121ef888481a99194765e6df72d4d3da81f928543171a2bac8cbb"},
{file = "ortools-9.11.4210-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:a503291dc12dc44da48567c5e1f79c77cd054fb86176f2c99d2860bc5a57e03d"},
{file = "ortools-9.11.4210-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fd0aa0b4edfb3088f086bc05d42a381cc3d03da4b8ad18ce18ba213ab2c719f"},
{file = "ortools-9.11.4210-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c242738a49279c3a58b4611a64dd24634f1638f4dbb435163e3f9308e7c84c9"},
{file = "ortools-9.11.4210-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9186bbc7a30538f277e704e9e77ff52bc75aa2c17095a125b2dc212a75cf8e"},
{file = "ortools-9.11.4210-cp38-cp38-win_amd64.whl", hash = "sha256:afcca4919e1095a79af0375276c5377a2d75794ebd592c4cc841d9979e0526e7"},
{file = "ortools-9.11.4210-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:a2828e91960c4e4fcf27bc6e200e2cd61ce1c42d4a3b95481a842a58c8315345"},
{file = "ortools-9.11.4210-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8d7d1a105f105502cf2f785816b09b796e5845fd47efac0dc0e3c0476b4c961a"},
{file = "ortools-9.11.4210-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f250641d7b822be25237fb78aed0878b07e8afdefddb700bafcc52f32ad520a"},
{file = "ortools-9.11.4210-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd2c0b319f4c0999360ab45a85d5764838c9dd0fd33437d12e32b2c07cbe04e4"},
{file = "ortools-9.11.4210-cp39-cp39-win_amd64.whl", hash = "sha256:219ffa56e8e4f52561586cea3dd55eb0f5d174a84c83a819d71debad766338e3"},
]
[package.dependencies]
absl-py = ">=2.0.0"
immutabledict = ">=3.0.0"
numpy = ">=1.13.3"
pandas = ">=2.0.0"
protobuf = ">=5.26.1,<5.27"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" version = "24.2"
@ -556,9 +713,9 @@ files = [
[package.dependencies] [package.dependencies]
numpy = [ numpy = [
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
{version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
] ]
python-dateutil = ">=2.8.2" python-dateutil = ">=2.8.2"
pytz = ">=2020.1" pytz = ">=2020.1"
@ -640,6 +797,98 @@ files = [
[package.dependencies] [package.dependencies]
ptyprocess = ">=0.5" ptyprocess = ">=0.5"
[[package]]
name = "pillow"
version = "11.0.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.9"
files = [
{file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"},
{file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"},
{file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"},
{file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"},
{file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"},
{file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"},
{file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"},
{file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"},
{file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"},
{file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"},
{file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"},
{file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"},
{file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"},
{file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"},
{file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"},
{file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"},
{file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"},
{file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"},
{file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"},
{file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"},
{file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"},
{file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"},
{file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"},
{file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"},
{file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"},
{file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"},
{file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"},
{file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"},
{file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"},
{file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"},
{file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"},
{file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"},
{file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"},
{file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"},
{file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"},
{file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"},
{file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"},
{file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"},
{file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"},
{file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"},
{file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"},
{file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"},
{file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"},
{file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"},
{file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"},
{file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"},
{file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"},
{file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"},
{file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"},
{file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"},
{file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.3.6" version = "4.3.6"
@ -689,6 +938,26 @@ files = [
[package.dependencies] [package.dependencies]
wcwidth = "*" wcwidth = "*"
[[package]]
name = "protobuf"
version = "5.26.1"
description = ""
optional = false
python-versions = ">=3.8"
files = [
{file = "protobuf-5.26.1-cp310-abi3-win32.whl", hash = "sha256:3c388ea6ddfe735f8cf69e3f7dc7611e73107b60bdfcf5d0f024c3ccd3794e23"},
{file = "protobuf-5.26.1-cp310-abi3-win_amd64.whl", hash = "sha256:e6039957449cb918f331d32ffafa8eb9255769c96aa0560d9a5bf0b4e00a2a33"},
{file = "protobuf-5.26.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:38aa5f535721d5bb99861166c445c4105c4e285c765fbb2ac10f116e32dcd46d"},
{file = "protobuf-5.26.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:fbfe61e7ee8c1860855696e3ac6cfd1b01af5498facc6834fcc345c9684fb2ca"},
{file = "protobuf-5.26.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:f7417703f841167e5a27d48be13389d52ad705ec09eade63dfc3180a959215d7"},
{file = "protobuf-5.26.1-cp38-cp38-win32.whl", hash = "sha256:d693d2504ca96750d92d9de8a103102dd648fda04540495535f0fec7577ed8fc"},
{file = "protobuf-5.26.1-cp38-cp38-win_amd64.whl", hash = "sha256:9b557c317ebe6836835ec4ef74ec3e994ad0894ea424314ad3552bc6e8835b4e"},
{file = "protobuf-5.26.1-cp39-cp39-win32.whl", hash = "sha256:b9ba3ca83c2e31219ffbeb9d76b63aad35a3eb1544170c55336993d7a18ae72c"},
{file = "protobuf-5.26.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ee014c2c87582e101d6b54260af03b6596728505c79f17c8586e7523aaa8f8c"},
{file = "protobuf-5.26.1-py3-none-any.whl", hash = "sha256:da612f2720c0183417194eeaa2523215c4fcc1a1949772dc65f05047e08d5932"},
{file = "protobuf-5.26.1.tar.gz", hash = "sha256:8ca2a1d97c290ec7b16e4e5dff2e5ae150cc1582f55b5ab300d45cb0dfa90e51"},
]
[[package]] [[package]]
name = "psutil" name = "psutil"
version = "6.1.0" version = "6.1.0"
@ -755,6 +1024,19 @@ files = [
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
] ]
[[package]]
name = "pygifsicle"
version = "1.1.0"
description = "Python package wrapping the gifsicle library for editing and optimizing gifs."
optional = false
python-versions = "*"
files = [
{file = "pygifsicle-1.1.0.tar.gz", hash = "sha256:dcef433520ace4c1136dfc7060e77042142a3dbd6bdb6a19bd9149ef5cbe7441"},
]
[package.extras]
test = ["pytest", "pytest-cov", "touch", "validate_version_code"]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.18.0" version = "2.18.0"
@ -771,13 +1053,13 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyright" name = "pyright"
version = "1.1.389" version = "1.1.390"
description = "Command line wrapper for pyright" description = "Command line wrapper for pyright"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60"}, {file = "pyright-1.1.390-py3-none-any.whl", hash = "sha256:ecebfba5b6b50af7c1a44c2ba144ba2ab542c227eb49bc1f16984ff714e0e110"},
{file = "pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220"}, {file = "pyright-1.1.390.tar.gz", hash = "sha256:aad7f160c49e0fbf8209507a15e17b781f63a86a1facb69ca877c71ef2e9538d"},
] ]
[package.dependencies] [package.dependencies]
@ -1026,90 +1308,40 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.8.1" version = "0.8.3"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5"}, {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
{file = "ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087"}, {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
{file = "ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209"}, {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
{file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"}, {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa"}, {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540"}, {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9"}, {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
{file = "ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5"}, {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
{file = "ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790"}, {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
{file = "ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6"}, {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
{file = "ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737"}, {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
{file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"}, {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
] ]
[[package]]
name = "scipy"
version = "1.14.1"
description = "Fundamental algorithms for scientific computing in Python"
optional = false
python-versions = ">=3.10"
files = [
{file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"},
{file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"},
{file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"},
{file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"},
{file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"},
{file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"},
{file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"},
{file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"},
{file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"},
{file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"},
{file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"},
{file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"},
{file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"},
{file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"},
{file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"},
{file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"},
{file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"},
{file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"},
{file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"},
{file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"},
{file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"},
{file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"},
{file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"},
{file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"},
{file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"},
{file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"},
{file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"},
{file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"},
{file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"},
{file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"},
{file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"},
{file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"},
{file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"},
]
[package.dependencies]
numpy = ">=1.23.5,<2.3"
[package.extras]
dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"]
doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"]
test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.17.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [ files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
] ]
[[package]] [[package]]
@ -1245,6 +1477,20 @@ files = [
docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"]
[[package]]
name = "types-networkx"
version = "3.4.2.20241115"
description = "Typing stubs for networkx"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-networkx-3.4.2.20241115.tar.gz", hash = "sha256:d669b650cf6c6c9ec879a825449eb04a5c10742f3109177e1683f57ee49e0f59"},
{file = "types_networkx-3.4.2.20241115-py3-none-any.whl", hash = "sha256:f0c382924d6614e06bf0b1ca0b837b8f33faa58982bc086ea762efaf39aa98dd"},
]
[package.dependencies]
numpy = ">=1.20"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.12.2" version = "4.12.2"
@ -1281,4 +1527,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "b643261f91a781d77735e05f6d2ac1002867600c2df6393a9d1a15f5e1189109" content-hash = "5b57bccd8dc65a9acecbe187939bae625ef6a259f4188c6587907245bcfa604f"

View File

@ -12,10 +12,12 @@ python = "^3.10"
numpy = "^2.1.3" numpy = "^2.1.3"
tqdm = "^4.67.1" tqdm = "^4.67.1"
parse = "^1.20.2" parse = "^1.20.2"
scipy = "^1.14.1"
sympy = "^1.13.3" sympy = "^1.13.3"
networkx = "^3.4.2" networkx = "^3.4.2"
pandas = "^2.2.3" pillow = "^11.0.0"
imageio = "^2.36.1"
pygifsicle = "^1.1.0"
opencv-python = "^4.10.0.84"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pyright = "^1.1.389" pyright = "^1.1.389"
@ -23,6 +25,14 @@ ruff = "^0.8.1"
poethepoet = "^0.31.1" poethepoet = "^0.31.1"
ipykernel = "^6.29.5" ipykernel = "^6.29.5"
networkx-stubs = "^0.0.1" networkx-stubs = "^0.0.1"
types-networkx = "^3.4.2.20241115"
[tool.poetry.group.cplex.dependencies]
docplex = "^2.28.240"
cplex = "^22.1.1.2"
[tool.poetry.group.ortools.dependencies]
ortools = "^9.11.4210"
[tool.poetry.scripts] [tool.poetry.scripts]
holt59-aoc = "holt59.aoc.__main__:main" holt59-aoc = "holt59.aoc.__main__:main"

View File

@ -1,10 +1,12 @@
import sys from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
floor = 0
floors = [(floor := floor + (1 if c == "(" else -1)) for c in line]
print(f"answer 1 is {floors[-1]}") class Solver(BaseSolver):
print(f"answer 2 is {floors.index(-1)}") def solve(self, input: str) -> Iterator[Any]:
floor = 0
floors = [(floor := floor + (1 if c == "(" else -1)) for c in input]
yield floors[-1]
yield floors.index(-1)

View File

@ -1,7 +1,7 @@
import itertools import itertools
import sys from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
# see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements) # see http://www.se16.info/js/lands2.htm for the explanation of 'atoms' (or elements)
# #
@ -9,7 +9,7 @@ line = sys.stdin.read().strip()
# CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148 # CodeGolf answer https://codegolf.stackexchange.com/a/8479/42148
# fmt: off # fmt: off
atoms = [ ATOMS: list[tuple[str, tuple[int, ...]]] = [
("22", (0, )), # 0 ("22", (0, )), # 0
("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1 ("13112221133211322112211213322112", (71, 90, 0, 19, 2, )), # 1
("312211322212221121123222112", (1, )), # 2 ("312211322212221121123222112", (1, )), # 2
@ -105,7 +105,7 @@ atoms = [
] ]
# fmt: on # fmt: on
starters = [ STARTERS = [
"1", "1",
"11", "11",
"21", "21",
@ -122,27 +122,26 @@ def look_and_say_length(s: str, n: int) -> int:
if n == 0: if n == 0:
return len(s) return len(s)
if s in starters: if s in STARTERS:
return look_and_say_length( return look_and_say_length(
"".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1 "".join(f"{len(list(g))}{k}" for k, g in itertools.groupby(s)), n - 1
) )
counts = {i: 0 for i in range(len(atoms))} counts = {i: 0 for i in range(len(ATOMS))}
idx = next(i for i, (a, _) in enumerate(atoms) if s == a) idx = next(i for i, (a, _) in enumerate(ATOMS) if s == a)
counts[idx] = 1 counts[idx] = 1
for _ in range(n): for _ in range(n):
c2 = {i: 0 for i in range(len(atoms))} c2 = {i: 0 for i in range(len(ATOMS))}
for i in counts: for i in counts:
for j in atoms[i][1]: for j in ATOMS[i][1]:
c2[j] += counts[i] c2[j] += counts[i]
counts = c2 counts = c2
return sum(counts[i] * len(a[0]) for i, a in enumerate(atoms)) return sum(counts[i] * len(a[0]) for i, a in enumerate(ATOMS))
answer_1 = look_and_say_length(line, 40) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any] | None:
yield look_and_say_length(input, 40)
answer_2 = look_and_say_length(line, 50) yield look_and_say_length(input, 50)
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import itertools import itertools
import sys from typing import Any, Iterator
from ..base import BaseSolver
def is_valid(p: str) -> bool: def is_valid(p: str) -> bool:
@ -40,10 +42,8 @@ def find_next_password(p: str) -> str:
return p return p
line = sys.stdin.read().strip() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
answer_1 = find_next_password(line) answer_1 = find_next_password(input)
print(f"answer 1 is {answer_1}") yield answer_1
yield find_next_password(increment(answer_1))
answer_2 = find_next_password(increment(answer_1))
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,7 @@
import json import json
import sys from typing import Any, Iterator, TypeAlias
from typing import TypeAlias
from ..base import BaseSolver
JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str JsonObject: TypeAlias = dict[str, "JsonObject"] | list["JsonObject"] | int | str
@ -18,10 +19,9 @@ def json_sum(value: JsonObject, ignore: str | None = None) -> int:
return 0 return 0
data: JsonObject = json.load(sys.stdin) class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
data: JsonObject = json.loads(input)
answer_1 = json_sum(data) yield json_sum(data)
print(f"answer 1 is {answer_1}") yield json_sum(data, "red")
answer_2 = json_sum(data, "red")
print(f"answer 2 is {answer_2}")

View File

@ -1,10 +1,11 @@
import itertools import itertools
import sys
from collections import defaultdict from collections import defaultdict
from typing import Literal, cast from typing import Any, Iterator, Literal, cast
import parse # type: ignore import parse # type: ignore
from ..base import BaseSolver
def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int: def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int:
guests = list(happiness) guests = list(happiness)
@ -17,25 +18,23 @@ def max_change_in_happiness(happiness: dict[str, dict[str, int]]) -> int:
) )
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
happiness: dict[str, dict[str, int]] = defaultdict(dict) happiness: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines: for line in lines:
u1, gain_or_loose, hap, u2 = cast( u1, gain_or_loose, hap, u2 = cast(
tuple[str, Literal["gain", "lose"], int, str], tuple[str, Literal["gain", "lose"], int, str],
parse.parse( # type: ignore parse.parse( # type: ignore
"{} would {} {:d} happiness units by sitting next to {}.", line "{} would {} {:d} happiness units by sitting next to {}.", line
), ),
) )
happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap happiness[u1][u2] = hap if gain_or_loose == "gain" else -hap
yield max_change_in_happiness(happiness)
for guest in list(happiness):
happiness["me"][guest] = 0
happiness[guest]["me"] = 0
answer_1 = max_change_in_happiness(happiness) yield max_change_in_happiness(happiness)
print(f"answer 1 is {answer_1}")
for guest in list(happiness):
happiness["me"][guest] = 0
happiness[guest]["me"] = 0
answer_2 = max_change_in_happiness(happiness)
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,10 @@
import sys
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal, cast from typing import Any, Iterator, Literal, cast
import parse # type: ignore import parse # type: ignore
from ..base import BaseSolver
@dataclass(frozen=True) @dataclass(frozen=True)
class Reindeer: class Reindeer:
@ -13,50 +14,50 @@ class Reindeer:
rest_time: int rest_time: int
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
reindeers: list[Reindeer] = [] reindeers: list[Reindeer] = []
for line in lines: for line in lines:
reindeer, speed, speed_time, rest_time = cast( reindeer, speed, speed_time, rest_time = cast(
tuple[str, int, int, int], tuple[str, int, int, int],
parse.parse( # type: ignore parse.parse( # type: ignore
"{} can fly {:d} km/s for {:d} seconds, " "{} can fly {:d} km/s for {:d} seconds, "
"but then must rest for {:d} seconds.", "but then must rest for {:d} seconds.",
line, line,
), ),
) )
reindeers.append( reindeers.append(
Reindeer(name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time) Reindeer(
) name=reindeer, speed=speed, fly_time=speed_time, rest_time=rest_time
)
)
target = 1000 if len(reindeers) <= 2 else 2503 target = 1000 if len(reindeers) <= 2 else 2503
states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = { states: dict[Reindeer, tuple[Literal["resting", "flying"], int]] = {
reindeer: ("resting", 0) for reindeer in reindeers reindeer: ("resting", 0) for reindeer in reindeers
} }
distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} distances: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers} points: dict[Reindeer, int] = {reindeer: 0 for reindeer in reindeers}
for time in range(target): for time in self.progress.wrap(range(target)):
for reindeer in reindeers: for reindeer in reindeers:
if states[reindeer][0] == "flying": if states[reindeer][0] == "flying":
distances[reindeer] += reindeer.speed distances[reindeer] += reindeer.speed
top_distance = max(distances.values()) top_distance = max(distances.values())
for reindeer in reindeers: for reindeer in reindeers:
if distances[reindeer] == top_distance: if distances[reindeer] == top_distance:
points[reindeer] += 1 points[reindeer] += 1
for reindeer in reindeers: for reindeer in reindeers:
if states[reindeer][1] == time: if states[reindeer][1] == time:
if states[reindeer][0] == "resting": if states[reindeer][0] == "resting":
states[reindeer] = ("flying", time + reindeer.fly_time) states[reindeer] = ("flying", time + reindeer.fly_time)
else: else:
states[reindeer] = ("resting", time + reindeer.rest_time) states[reindeer] = ("resting", time + reindeer.rest_time)
yield max(distances.values())
answer_1 = max(distances.values()) yield max(points.values()) - 1
print(f"answer 1 is {answer_1}")
answer_2 = max(points.values()) - 1
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,10 @@
import math import math
import sys from typing import Any, Iterator, Sequence, cast
from typing import Sequence, cast
import parse # type: ignore import parse # type: ignore
from ..base import BaseSolver
def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int: def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
return math.prod( return math.prod(
@ -18,40 +19,38 @@ def score(ingredients: list[list[int]], teaspoons: Sequence[int]) -> int:
) )
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
ingredients: list[list[int]] = [] ingredients: list[list[int]] = []
for line in lines: for line in lines:
_, *scores = cast( _, *scores = cast(
tuple[str, int, int, int, int, int], tuple[str, int, int, int, int, int],
parse.parse( # type: ignore parse.parse( # type: ignore
"{}: capacity {:d}, durability {:d}, flavor {:d}, " "{}: capacity {:d}, durability {:d}, flavor {:d}, "
"texture {:d}, calories {:d}", "texture {:d}, calories {:d}",
line, line,
), ),
)
ingredients.append(scores)
total_teaspoons = 100
calories: list[int] = []
scores: list[int] = []
for a in range(total_teaspoons + 1):
for b in range(total_teaspoons + 1 - a):
for c in range(total_teaspoons + 1 - a - b):
teaspoons = (a, b, c, total_teaspoons - a - b - c)
scores.append(score(ingredients, teaspoons))
calories.append(
sum(
ingredient[-1] * teaspoon
for ingredient, teaspoon in zip(ingredients, teaspoons)
)
) )
ingredients.append(scores)
total_teaspoons = 100
calories: list[int] = []
scores: list[int] = []
answer_1 = max(scores) for a in range(total_teaspoons + 1):
print(f"answer 1 is {answer_1}") for b in range(total_teaspoons + 1 - a):
for c in range(total_teaspoons + 1 - a - b):
teaspoons = (a, b, c, total_teaspoons - a - b - c)
answer_2 = max(score for score, calory in zip(scores, calories) if calory == 500) scores.append(score(ingredients, teaspoons))
print(f"answer 2 is {answer_2}") calories.append(
sum(
ingredient[-1] * teaspoon
for ingredient, teaspoon in zip(ingredients, teaspoons)
)
)
yield max(scores)
yield max(score for score, calory in zip(scores, calories) if calory == 500)

View File

@ -1,8 +1,9 @@
import operator as op import operator as op
import re import re
import sys
from collections import defaultdict from collections import defaultdict
from typing import Callable from typing import Any, Callable, Iterator
from ..base import BaseSolver
MFCSAM: dict[str, int] = { MFCSAM: dict[str, int] = {
"children": 3, "children": 3,
@ -17,18 +18,10 @@ MFCSAM: dict[str, int] = {
"perfumes": 1, "perfumes": 1,
} }
lines = sys.stdin.readlines()
aunts: list[dict[str, int]] = [ def match(
{ aunts: list[dict[str, int]], operators: dict[str, Callable[[int, int], bool]]
match[1]: int(match[2]) ) -> int:
for match in re.findall(R"((?P<compound>[^:, ]+): (?P<quantity>\d+))", line)
}
for line in lines
]
def match(operators: dict[str, Callable[[int, int], bool]]) -> int:
return next( return next(
i i
for i, aunt in enumerate(aunts, start=1) for i, aunt in enumerate(aunts, start=1)
@ -36,16 +29,29 @@ def match(operators: dict[str, Callable[[int, int], bool]]) -> int:
) )
answer_1 = match(defaultdict(lambda: op.eq)) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
answer_2 = match( aunts: list[dict[str, int]] = [
defaultdict( {
lambda: op.eq, match[1]: int(match[2])
trees=op.gt, for match in re.findall(
cats=op.gt, R"((?P<compound>[^:, ]+): (?P<quantity>\d+))", line
pomeranians=op.lt, )
goldfish=op.lt, }
) for line in lines
) ]
print(f"answer 2 is {answer_2}")
yield match(aunts, defaultdict(lambda: op.eq))
yield match(
aunts,
defaultdict(
lambda: op.eq,
trees=op.gt,
cats=op.gt,
pomeranians=op.lt,
goldfish=op.lt,
),
)

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Iterator
from typing import Iterator
from ..base import BaseSolver
def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]: def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int, ...]]:
@ -16,15 +17,18 @@ def iter_combinations(value: int, containers: list[int]) -> Iterator[tuple[int,
yield (containers[i],) + combination yield (containers[i],) + combination
containers = [int(c) for c in sys.stdin.read().split()] class Solver(BaseSolver):
total = 25 if len(containers) <= 5 else 150 def solve(self, input: str) -> Iterator[Any]:
containers = [int(c) for c in input.split()]
total = 25 if len(containers) <= 5 else 150
combinations = [combination for combination in iter_combinations(total, containers)] combinations = [
combination for combination in iter_combinations(total, containers)
]
answer_1 = len(combinations) yield len(combinations)
print(f"answer 1 is {answer_1}")
min_containers = min(len(combination) for combination in combinations) min_containers = min(len(combination) for combination in combinations)
yield sum(
answer_2 = sum(1 for combination in combinations if len(combination) == min_containers) 1 for combination in combinations if len(combination) == min_containers
print(f"answer 2 is {answer_2}") )

View File

@ -1,66 +1,66 @@
import itertools import itertools
import sys from typing import Any, Iterator
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
grid0 = np.array([[c == "#" for c in line] for line in sys.stdin.read().splitlines()]) from ..base import BaseSolver
# add an always off circle around
grid0 = np.concatenate( class Solver(BaseSolver):
[ def solve(self, input: str) -> Iterator[Any]:
np.zeros((grid0.shape[0] + 2, 1), dtype=bool), grid0 = np.array([[c == "#" for c in line] for line in input.splitlines()])
np.concatenate(
# add an always off circle around
grid0 = np.concatenate(
[ [
np.zeros((1, grid0.shape[1]), dtype=bool), np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
grid0, np.concatenate(
np.zeros((1, grid0.shape[1]), dtype=bool), [
] np.zeros((1, grid0.shape[1]), dtype=bool),
), grid0,
np.zeros((grid0.shape[0] + 2, 1), dtype=bool), np.zeros((1, grid0.shape[1]), dtype=bool),
], ]
axis=1, ),
) np.zeros((grid0.shape[0] + 2, 1), dtype=bool),
],
axis=1,
)
moves = list(itertools.product([-1, 0, 1], repeat=2)) moves = list(itertools.product([-1, 0, 1], repeat=2))
moves.remove((0, 0)) moves.remove((0, 0))
jjs, iis = np.meshgrid( jjs, iis = np.meshgrid(
np.arange(1, grid0.shape[0] - 1, dtype=int), np.arange(1, grid0.shape[0] - 1, dtype=int),
np.arange(1, grid0.shape[1] - 1, dtype=int), np.arange(1, grid0.shape[1] - 1, dtype=int),
) )
iis, jjs = iis.flatten(), jjs.flatten() iis, jjs = iis.flatten(), jjs.flatten()
ins = iis[:, None] + np.array(moves)[:, 0] ins = iis[:, None] + np.array(moves)[:, 0]
jns = jjs[:, None] + np.array(moves)[:, 1] jns = jjs[:, None] + np.array(moves)[:, 1]
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]:
neighbors_on = grid[ins, jns].sum(axis=1)
cells_on = grid[iis, jjs]
def game_of_life(grid: NDArray[np.bool_]) -> NDArray[np.bool_]: grid = np.zeros_like(grid)
neighbors_on = grid[ins, jns].sum(axis=1) grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2))
cells_on = grid[iis, jjs]
grid = np.zeros_like(grid) return grid
grid[iis, jjs] = (neighbors_on == 3) | (cells_on & (neighbors_on == 2))
return grid grid = grid0
n_steps = 4 if len(grid) < 10 else 100
for _ in range(n_steps):
grid = game_of_life(grid)
yield grid.sum()
grid = grid0 n_steps = 5 if len(grid) < 10 else 100
n_steps = 4 if len(grid) < 10 else 100 grid = grid0
for _ in range(n_steps): for _ in range(n_steps):
grid = game_of_life(grid) grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
grid = game_of_life(grid)
answer_1 = grid.sum() grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
print(f"answer 1 is {answer_1}")
yield sum(cell for line in grid for cell in line)
n_steps = 5 if len(grid) < 10 else 100
grid = grid0
for _ in range(n_steps):
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
grid = game_of_life(grid)
grid[[1, 1, -2, -2], [1, -2, 1, -2]] = True
answer_2 = sum(cell for line in grid for cell in line)
print(f"answer 2 is {answer_2}")

View File

@ -1,56 +1,57 @@
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
replacements_s, molecule = sys.stdin.read().split("\n\n") from ..base import BaseSolver
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
for replacement_s in replacements_s.splitlines():
p = replacement_s.split(" => ")
REPLACEMENTS[p[0]].append(p[1])
molecule = molecule.strip()
generated = [
molecule[:i] + replacement + molecule[i + len(symbol) :]
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
for i in range(len(molecule))
if molecule[i:].startswith(symbol)
]
answer_1 = len(set(generated))
print(f"answer 1 is {answer_1}")
inversion: dict[str, str] = {
replacement: symbol
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
}
# there is actually only one way to create the molecule, and we can greedily replace
# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly
# without thinking
count = 0
while molecule != "e":
i = 0
m2 = ""
while i < len(molecule):
found = False
for replacement in inversion:
if molecule[i:].startswith(replacement):
m2 += inversion[replacement]
i += len(replacement)
count += 1
found = True
break
if not found:
m2 += molecule[i]
i += 1
# print(m2)
molecule = m2
answer_2 = count class Solver(BaseSolver):
print(f"answer 2 is {count}") def solve(self, input: str) -> Iterator[Any]:
replacements_s, molecule = input.split("\n\n")
REPLACEMENTS: dict[str, list[str]] = defaultdict(list)
for replacement_s in replacements_s.splitlines():
p = replacement_s.split(" => ")
REPLACEMENTS[p[0]].append(p[1])
molecule = molecule.strip()
generated = [
molecule[:i] + replacement + molecule[i + len(symbol) :]
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
for i in range(len(molecule))
if molecule[i:].startswith(symbol)
]
yield len(set(generated))
inversion: dict[str, str] = {
replacement: symbol
for symbol, replacements in REPLACEMENTS.items()
for replacement in replacements
}
# there is actually only one way to create the molecule, and we can greedily replace
# tokens with their replacements, e.g., if H => OH then we can replace OH by H directly
# without thinking
count = 0
while molecule != "e":
i = 0
m2 = ""
while i < len(molecule):
found = False
for replacement in inversion:
if molecule[i:].startswith(replacement):
m2 += inversion[replacement]
i += len(replacement)
count += 1
found = True
break
if not found:
m2 += molecule[i]
i += 1
molecule = m2
yield count

View File

@ -1,20 +1,24 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
length, width, height = np.array(
[[int(c) for c in line.split("x")] for line in lines]
).T
lw, wh, hl = (length * width, width * height, height * length) class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
length, width, height = np.array(
[[int(c) for c in line.split("x")] for line in input.splitlines()]
).T
answer_1 = np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0)) lw, wh, hl = (length * width, width * height, height * length)
print(f"answer 1 is {answer_1}")
answer_2 = np.sum( yield np.sum(2 * (lw + wh + hl) + np.min(np.stack([lw, wh, hl]), axis=0))
length * width * height
+ 2 * np.min(np.stack([length + width, length + height, height + width]), axis=0) yield np.sum(
) length * width * height
print(f"answer 2 is {answer_2}") + 2
* np.min(
np.stack([length + width, length + height, height + width]), axis=0
)
)

View File

@ -1,10 +1,10 @@
import itertools import itertools
import sys from typing import Any, Iterator
target = int(sys.stdin.read()) from ..base import BaseSolver
def presents(n: int, elf: int, max: int = target) -> int: def presents(n: int, elf: int, max: int) -> int:
count = 0 count = 0
k = 1 k = 1
while k * k < n: while k * k < n:
@ -21,8 +21,9 @@ def presents(n: int, elf: int, max: int = target) -> int:
return count return count
answer_1 = next(n for n in itertools.count(1) if presents(n, 10) >= target) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
target = int(input)
answer_2 = next(n for n in itertools.count(1) if presents(n, 11, 50) >= target) yield next(n for n in itertools.count(1) if presents(n, 10, target) >= target)
print(f"answer 2 is {answer_2}") yield next(n for n in itertools.count(1) if presents(n, 11, 50) >= target)

View File

@ -1,7 +1,8 @@
import itertools import itertools
import sys
from math import ceil from math import ceil
from typing import TypeAlias from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
Modifier: TypeAlias = tuple[str, int, int, int] Modifier: TypeAlias = tuple[str, int, int, int]
@ -33,34 +34,31 @@ RINGS: list[Modifier] = [
] ]
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
player_hp = 100 player_hp = 100
boss_attack = int(lines[1].split(":")[1].strip()) boss_attack = int(lines[1].split(":")[1].strip())
boss_armor = int(lines[2].split(":")[1].strip()) boss_armor = int(lines[2].split(":")[1].strip())
boss_hp = int(lines[0].split(":")[1].strip()) boss_hp = int(lines[0].split(":")[1].strip())
min_cost, max_cost = 1_000_000, 0
for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS):
if equipments[-1][0] != "" and equipments[-2] == equipments[-1]:
continue
min_cost, max_cost = 1_000_000, 0 cost, player_attack, player_armor = (
for equipments in itertools.product(WEAPONS, ARMORS, RINGS, RINGS): sum(equipment[1:][k] for equipment in equipments) for k in range(3)
if equipments[-1][0] != "" and equipments[-2] == equipments[-1]: )
continue
cost, player_attack, player_armor = ( if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil(
sum(equipment[1:][k] for equipment in equipments) for k in range(3) player_hp / max(1, boss_attack - player_armor)
) ):
min_cost = min(cost, min_cost)
else:
max_cost = max(cost, max_cost)
if ceil(boss_hp / max(1, player_attack - boss_armor)) <= ceil( yield min_cost
player_hp / max(1, boss_attack - player_armor) yield max_cost
):
min_cost = min(cost, min_cost)
else:
max_cost = max(cost, max_cost)
answer_1 = min_cost
print(f"answer 1 is {answer_1}")
answer_2 = max_cost
print(f"answer 2 is {answer_2}")

View File

@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
import heapq import heapq
import sys from typing import Any, Iterator, Literal, TypeAlias, cast
from typing import Literal, TypeAlias, cast
from ..base import BaseSolver
PlayerType: TypeAlias = Literal["player", "boss"] PlayerType: TypeAlias = Literal["player", "boss"]
SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"] SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"]
@ -62,17 +63,6 @@ def play(
continue continue
visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs)) visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs))
if hard_mode and player == "player":
player_hp = max(0, player_hp - 1)
if player_hp == 0:
continue
if boss_hp == 0:
winning_node = spells
continue
new_buffs: list[tuple[BuffType, int]] = [] new_buffs: list[tuple[BuffType, int]] = []
for buff, length in buffs: for buff, length in buffs:
length = length - 1 length = length - 1
@ -88,6 +78,16 @@ def play(
if length > 0: if length > 0:
new_buffs.append((buff, length)) new_buffs.append((buff, length))
if hard_mode and player == "player":
player_hp = player_hp - 1
if player_hp <= 0:
continue
if boss_hp <= 0:
winning_node = spells
continue
buffs = tuple(new_buffs) buffs = tuple(new_buffs)
if player == "boss": if player == "boss":
@ -155,23 +155,27 @@ def play(
return winning_node return winning_node
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
player_hp = 50 player_hp = 50
player_mana = 500 player_mana = 500
player_armor = 0 player_armor = 0
boss_hp = int(lines[0].split(":")[1].strip()) boss_hp = int(lines[0].split(":")[1].strip())
boss_attack = int(lines[1].split(":")[1].strip()) boss_attack = int(lines[1].split(":")[1].strip())
answer_1 = sum( yield sum(
c c
for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, False) for _, c in play(
) player_hp, player_mana, player_armor, boss_hp, boss_attack, False
print(f"answer 1 is {answer_1}") )
)
# 1242 (not working) yield sum(
answer_2 = sum( c
c for _, c in play(player_hp, player_mana, player_armor, boss_hp, boss_attack, True) for _, c in play(
) player_hp, player_mana, player_armor, boss_hp, boss_attack, True
print(f"answer 2 is {answer_2}") )
)

View File

@ -0,0 +1,107 @@
import inspect
from typing import Any, Callable, Final, Iterator, Mapping
from ..base import BaseSolver
class Instruction:
def __init__(self, fn: Callable[..., None]):
self._fn = fn
args = inspect.getfullargspec(fn)
self._argtypes = [args.annotations[arg] for arg in args.args[1:]]
def __call__(self, args: tuple[str, ...]):
self._fn(
*(argtype(arg) for arg, argtype in zip(args, self._argtypes, strict=True))
)
class Machine:
def __init__(
self, instructions: list[str], registers: dict[str, int] = {"a": 0, "b": 1}
):
self.instructions: Final = [
(part[0], tuple(arg.strip() for arg in " ".join(part[1:]).split(",")))
for instruction in instructions
if (part := instruction.split())
]
self._fns = {
name: Instruction(getattr(self, name))
for name in ("hlf", "tpl", "inc", "jmp", "jie", "jio")
}
self._registers = registers.copy()
self._ip = 0
@property
def registers(self) -> Mapping[str, int]:
return self._registers
@property
def ip(self) -> int:
return self._ip
def reset(self, registers: dict[str, int] = {"a": 0, "b": 0}):
self._registers = registers.copy()
self._ip = 0
def hlf(self, register: str):
self._registers[register] //= 2
self._ip += 1
def tpl(self, register: str):
self._registers[register] *= 3
self._ip += 1
def inc(self, register: str):
self._registers[register] += 1
self._ip += 1
def jmp(self, offset: int):
self._ip += offset
assert 0 <= self._ip < len(self.instructions)
def jie(self, register: str, offset: int):
if self._registers[register] % 2 == 0:
self._ip += offset
else:
self._ip += 1
def jio(self, register: str, offset: int):
if self._registers[register] == 1:
self._ip += offset
else:
self._ip += 1
def _exec(self) -> bool:
# execute next instruction
if self._ip >= len(self.instructions):
return False
ins, args = self.instructions[self._ip]
if ins not in self._fns:
return False
self._fns[ins](args)
return True
def run(self):
while self._exec():
...
return self.registers
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
machine = Machine(input.splitlines())
registers = machine.run()
yield registers["b"]
machine.reset({"a": 1, "b": 0})
registers = machine.run()
yield registers["b"]

View File

@ -0,0 +1,88 @@
from typing import Any, Iterator, TypeAlias
from ..base import BaseSolver
TupleOfInts: TypeAlias = tuple[int, ...]
def check_n_groups(
target: int, groups: tuple[TupleOfInts, ...], numbers: TupleOfInts
) -> bool:
n_groups = len(groups)
groups_s = tuple(sum(group) for group in groups)
if all(target == group_s for group_s in groups_s):
return not numbers
if not numbers:
return False
head, *tail_l = numbers
tail, tail_s = tuple(tail_l), sum(tail_l)
return any(
groups_s[i] + head <= target
and sum(groups_s[j] for j in range(len(groups)) if i != j) + tail_s
>= (n_groups - 1) * target
and check_n_groups(
target, groups[:i] + ((groups[i] + (head,)),) + groups[i + 1 :], tail
)
for i in range(len(groups))
)
def enumerate_single_subset(
target: int, numbers: TupleOfInts
) -> Iterator[tuple[int, TupleOfInts, TupleOfInts]]:
"""
Enumerate subset of numbers whose sum equals target.
Subset are enumerated in increasing order of length, then product (quantum value).
Args:
target: Target for the sum of the subset.
numbers: Tuple of integers to find the subset from.
Returns:
A generator (quantum, subset, remaining) where subset if the subset of numbers
whose sum equals target, quantum the product of the subset, and remaining the
remaining numbers.
"""
groups: list[tuple[int, TupleOfInts, TupleOfInts]] = [(1, (), numbers)]
for _ in range(len(numbers)):
new_groups: list[tuple[int, TupleOfInts, TupleOfInts]] = []
for g_quantum, group, remaining in groups:
sg = sum(group)
for i in range(len(remaining)):
if group and remaining[i] <= group[-1]:
continue
uv = remaining[:i] + remaining[i + 1 :]
kv = g_quantum * remaining[i], group + (remaining[i],), uv
if sg + remaining[i] == target:
yield kv
elif sg + remaining[i] < target:
new_groups.append(kv)
groups = new_groups
def find_min_quantum(numbers: tuple[int, ...], n_groups: int):
return next(
g_quantum
for g_quantum, group_1v2, group_234v2 in enumerate_single_subset(
sum(numbers) // n_groups, numbers
)
if check_n_groups(sum(group_1v2), ((),) * (n_groups - 1), group_234v2)
)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
numbers = tuple(map(int, input.split()))
yield find_min_quantum(numbers, 3)
yield find_min_quantum(numbers, 4)

View File

@ -0,0 +1,16 @@
import re
from typing import Any, Iterator
from ..base import BaseSolver
from ..tools.math import pow_mod
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
m = re.search(r"row\s*([0-9]+)\s*,\s*column\s*([0-9]+)", input)
assert m is not None
row, col = int(m.group(1)), int(m.group(2))
n = (row * (row - 1)) // 2 + col * (col + 1) // 2 + (row - 1) * (col - 1)
yield (20151125 * pow_mod(252533, n - 1, 33554393)) % 33554393

View File

@ -1,7 +1,7 @@
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
def process(directions: str) -> dict[tuple[int, int], int]: def process(directions: str) -> dict[tuple[int, int], int]:
@ -27,8 +27,7 @@ def process(directions: str) -> dict[tuple[int, int], int]:
return counts return counts
answer_1 = len(process(line)) class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
yield len(process(input))
answer_2 = len(process(line[::2]) | process(line[1::2])) yield len(process(input[::2]) | process(input[1::2]))
print(f"answer 2 is {answer_2}")

View File

@ -1,16 +1,20 @@
import hashlib import hashlib
import itertools import itertools
import sys from typing import Any, Iterator
line = sys.stdin.read().strip() from ..base import BaseSolver
it = iter(itertools.count(1))
answer_1 = next(
i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("00000")
)
print(f"answer 1 is {answer_1}")
answer_2 = next( class Solver(BaseSolver):
i for i in it if hashlib.md5(f"{line}{i}".encode()).hexdigest().startswith("000000") def solve(self, input: str) -> Iterator[Any]:
) it = iter(itertools.count(1))
print(f"answer 2 is {answer_2}") yield next(
i
for i in it
if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("00000")
)
yield next(
i
for i in it
if hashlib.md5(f"{input}{i}".encode()).hexdigest().startswith("000000")
)

View File

@ -1,4 +1,6 @@
import sys from typing import Any, Iterator
from ..base import BaseSolver
VOWELS = "aeiou" VOWELS = "aeiou"
FORBIDDEN = {"ab", "cd", "pq", "xy"} FORBIDDEN = {"ab", "cd", "pq", "xy"}
@ -27,10 +29,8 @@ def is_nice_2(s: str) -> bool:
return True return True
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
answer_1 = sum(map(is_nice_1, lines)) lines = input.splitlines()
print(f"answer 1 is {answer_1}") yield sum(map(is_nice_1, lines))
yield sum(map(is_nice_2, lines))
answer_2 = sum(map(is_nice_2, lines))
print(f"answer 2 is {answer_2}")

View File

@ -1,33 +1,32 @@
import sys from typing import Any, Iterator, Literal, cast
from typing import Literal, cast
import numpy as np import numpy as np
import parse # type: ignore import parse # type: ignore
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
lights_1 = np.zeros((1000, 1000), dtype=bool)
lights_2 = np.zeros((1000, 1000), dtype=int)
for line in lines:
action, sx, sy, ex, ey = cast(
tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
)
ex, ey = ex + 1, ey + 1
match action: class Solver(BaseSolver):
case "turn on": def solve(self, input: str) -> Iterator[Any]:
lights_1[sx:ex, sy:ey] = True lights_1 = np.zeros((1000, 1000), dtype=bool)
lights_2[sx:ex, sy:ey] += 1 lights_2 = np.zeros((1000, 1000), dtype=int)
case "turn off": for line in input.splitlines():
lights_1[sx:ex, sy:ey] = False action, sx, sy, ex, ey = cast(
lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0) tuple[Literal["turn on", "turn off", "toggle"], int, int, int, int],
case "toggle": parse.parse("{} {:d},{:d} through {:d},{:d}", line), # type: ignore
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey] )
lights_2[sx:ex, sy:ey] += 2 ex, ey = ex + 1, ey + 1
answer_1 = lights_1.sum() match action:
print(f"answer 1 is {answer_1}") case "turn on":
lights_1[sx:ex, sy:ey] = True
lights_2[sx:ex, sy:ey] += 1
case "turn off":
lights_1[sx:ex, sy:ey] = False
lights_2[sx:ex, sy:ey] = np.maximum(lights_2[sx:ex, sy:ey] - 1, 0)
case "toggle":
lights_1[sx:ex, sy:ey] = ~lights_1[sx:ex, sy:ey]
lights_2[sx:ex, sy:ey] += 2
answer_2 = lights_2.sum() yield lights_1.sum()
print(f"answer 2 is {answer_2}") yield lights_2.sum()

View File

@ -1,11 +1,7 @@
import logging
import operator import operator
import os from typing import Any, Callable, Iterator
import sys
from typing import Callable
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
OPERATORS = { OPERATORS = {
"AND": operator.and_, "AND": operator.and_,
@ -36,48 +32,6 @@ def value_of(key: str) -> tuple[str, Callable[[dict[str, int]], int]]:
return key, lambda values: values[key] return key, lambda values: values[key]
lines = sys.stdin.read().splitlines()
signals: Signals = {}
values: dict[str, int] = {"": 0}
for line in lines:
command, signal = line.split(" -> ")
if command.startswith("NOT"):
name = command.split(" ")[1]
signals[signal] = (
(name, ""),
(lambda values, _n=name: values[_n], lambda _v: 0),
lambda a, _b: ~a,
)
elif not any(command.find(name) >= 0 for name in OPERATORS):
try:
values[signal] = int(command)
except ValueError:
signals[signal] = (
(command, ""),
(lambda values, _c=command: values[_c], lambda _v: 0),
lambda a, _b: a,
)
else:
op: Callable[[int, int], int] = zero_op
lhs_s, rhs_s = "", ""
for name in OPERATORS:
if command.find(name) >= 0:
op = OPERATORS[name]
lhs_s, rhs_s = command.split(f" {name} ")
break
lhs_s, lhs_fn = value_of(lhs_s)
rhs_s, rhs_fn = value_of(rhs_s)
signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op)
def process( def process(
signals: Signals, signals: Signals,
values: dict[str, int], values: dict[str, int],
@ -91,11 +45,52 @@ def process(
return values return values
values_1 = process(signals.copy(), values.copy()) class Solver(BaseSolver):
logging.info("\n" + "\n".join(f"{k}: {values_1[k]}" for k in sorted(values_1))) def solve(self, input: str) -> Iterator[Any] | None:
answer_1 = values_1["a"] lines = input.splitlines()
print(f"answer 1 is {answer_1}")
values_2 = process(signals.copy(), values | {"b": values_1["a"]}) signals: Signals = {}
answer_2 = values_2["a"] values: dict[str, int] = {"": 0}
print(f"answer 2 is {answer_2}")
for line in lines:
command, signal = line.split(" -> ")
if command.startswith("NOT"):
name = command.split(" ")[1]
signals[signal] = (
(name, ""),
(lambda values, _n=name: values[_n], lambda _v: 0),
lambda a, _b: ~a,
)
elif not any(command.find(name) >= 0 for name in OPERATORS):
try:
values[signal] = int(command)
except ValueError:
signals[signal] = (
(command, ""),
(lambda values, _c=command: values[_c], lambda _v: 0),
lambda a, _b: a,
)
else:
op: Callable[[int, int], int] = zero_op
lhs_s, rhs_s = "", ""
for name in OPERATORS:
if command.find(name) >= 0:
op = OPERATORS[name]
lhs_s, rhs_s = command.split(f" {name} ")
break
lhs_s, lhs_fn = value_of(lhs_s)
rhs_s, rhs_fn = value_of(rhs_s)
signals[signal] = ((lhs_s, rhs_s), (lhs_fn, rhs_fn), op)
values_1 = process(signals.copy(), values.copy())
for k in sorted(values_1):
self.logger.info(f"{k}: {values_1[k]}")
yield values_1["a"]
yield process(signals.copy(), values | {"b": values_1["a"]})["a"]

View File

@ -1,35 +1,32 @@
import logging from typing import Any, Iterator
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
answer_1 = sum( yield sum(
# left and right quotes (not in memory) # left and right quotes (not in memory)
2 2
# each \\ adds one character in the literals (compared to memory) # each \\ adds one character in the literals (compared to memory)
+ line.count(R"\\") + line.count(R"\\")
# each \" adds one character in the literals (compared to memory) # each \" adds one character in the literals (compared to memory)
+ line[1:-1].count(R"\"") + line[1:-1].count(R"\"")
# each \xFF adds 3 characters in the literals (compared to memory), but we must not # each \xFF adds 3 characters in the literals (compared to memory), but we must not
# count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also # count A\\x (A != \), but we must count A\\\x (A != \) - in practice we should also
# avoid \\\\x, etc., but this does not occur in the examples and the actual input # avoid \\\\x, etc., but this does not occur in the examples and the actual input
+ 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x")) + 3 * (line.count(R"\x") - line.count(R"\\x") + line.count(R"\\\x"))
for line in lines for line in lines
) )
print(f"answer 1 is {answer_1}")
answer_2 = sum( yield sum(
# needs to wrap in quotes (2 characters) # needs to wrap in quotes (2 characters)
2 2
# needs to escape every \ with an extra \ # needs to escape every \ with an extra \
+ line.count("\\") + line.count("\\")
# needs to escape every " with an extra \ (including the first and last ones) # needs to escape every " with an extra \ (including the first and last ones)
+ line.count('"') + line.count('"')
for line in lines for line in lines
) )
print(f"answer 2 is {answer_2}")

View File

@ -1,27 +1,28 @@
import itertools import itertools
import sys
from collections import defaultdict from collections import defaultdict
from typing import cast from typing import Any, Iterator, cast
import parse # type: ignore import parse # type: ignore
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
distances: dict[str, dict[str, int]] = defaultdict(dict)
for line in lines:
origin, destination, length = cast(
tuple[str, str, int],
parse.parse("{} to {} = {:d}", line), # type: ignore
)
distances[origin][destination] = distances[destination][origin] = length
distance_of_routes = { class Solver(BaseSolver):
route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:])) def solve(self, input: str) -> Iterator[Any]:
for route in map(tuple, itertools.permutations(distances)) lines = input.splitlines()
}
answer_1 = min(distance_of_routes.values()) distances: dict[str, dict[str, int]] = defaultdict(dict)
print(f"answer 1 is {answer_1}") for line in lines:
origin, destination, length = cast(
tuple[str, str, int],
parse.parse("{} to {} = {:d}", line), # type: ignore
)
distances[origin][destination] = distances[destination][origin] = length
answer_2 = max(distance_of_routes.values()) distance_of_routes = {
print(f"answer 2 is {answer_2}") route: sum(distances[o][d] for o, d in zip(route[:-1], route[1:]))
for route in map(tuple, itertools.permutations(distances))
}
yield min(distance_of_routes.values())
yield max(distance_of_routes.values())

View File

@ -1,14 +1,17 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
values = [int(line) for line in lines]
# part 1 class Solver(BaseSolver):
answer_1 = sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:])) def solve(self, input: str) -> Iterator[Any]:
print(f"answer 1 is {answer_1}") lines = input.splitlines()
# part 2 values = [int(line) for line in lines]
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
answer_2 = sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:])) # part 1
print(f"answer 2 is {answer_2}") yield sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
# part 2
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
yield sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))

View File

@ -1,11 +1,47 @@
import sys from functools import reduce
from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1 BRACKETS = {"{": "}", "[": "]", "<": ">", "(": ")"}
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 CORRUPT_SCORES = {")": 3, "]": 57, "}": 1197, ">": 25137}
answer_2 = ... COMPLETE_SCORES = {")": 1, "]": 2, "}": 3, ">": 4}
print(f"answer 2 is {answer_2}")
def corrupted_or_incomplete(line: str) -> tuple[bool, str]:
opens: list[str] = []
for c in line:
if c in BRACKETS:
opens.append(c)
elif BRACKETS[opens[-1]] != c:
return True, c
else:
opens.pop()
return (False, "".join(opens))
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
answer_1: int = 0
incomplete_scores: list[int] = []
for line in lines:
c, r = corrupted_or_incomplete(line)
if c:
answer_1 += CORRUPT_SCORES[r]
else:
incomplete_scores.append(
reduce(
lambda s, c: s * 5 + COMPLETE_SCORES[BRACKETS[c]],
reversed(r),
0,
),
)
yield answer_1
yield sorted(incomplete_scores)[len(incomplete_scores) // 2]

View File

@ -1,11 +1,66 @@
import sys import itertools as it
from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 def do_step(values: list[list[int]]) -> tuple[list[list[int]], set[tuple[int, int]]]:
answer_2 = ... values = [[c + 1 for c in r] for r in values]
print(f"answer 2 is {answer_2}") flashed: set[tuple[int, int]] = set()
while True:
found = False
for i_row, row in enumerate(values):
for i_col, col in enumerate(row):
if col <= 9 or (i_row, i_col) in flashed:
continue
found = True
flashed.add((i_row, i_col))
for dr, dc in it.product((-1, 0, 1), repeat=2):
if 0 <= i_row + dr < len(values) and 0 <= i_col + dc < len(
values[0]
):
values[i_row + dr][i_col + dc] += 1
if not found:
break
for i, j in flashed:
values[i][j] = 0
return values, flashed
class Solver(BaseSolver):
def print_grid(self, values: list[list[int]], flashed: set[tuple[int, int]]):
for i_row, row in enumerate(values):
s_row = ""
for i_col, col in enumerate(row):
if (i_row, i_col) in flashed:
s_row += f"\033[0;31m{col}\033[0;00m"
else:
s_row += str(col)
self.logger.info(s_row)
self.logger.info("")
def solve(self, input: str) -> Iterator[Any]:
values_0 = [[int(c) for c in r] for r in input.splitlines()]
values = values_0
total_flashed: int = 0
for _ in range(100):
values, flashed = do_step(values)
total_flashed += len(flashed)
yield total_flashed
n_cells = len(values) * len(values[0])
flashed: set[tuple[int, int]] = set()
values, step = values_0, 0
while len(flashed) != n_cells:
values, flashed = do_step(values)
step += 1
yield step

View File

@ -1,11 +1,64 @@
import sys import string
from collections import defaultdict
from functools import cache
from typing import Any, Iterator, Mapping, Sequence
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 @cache
answer_2 = ... def is_small(node: str):
print(f"answer 2 is {answer_2}") return all(c in string.ascii_lowercase for c in node)
def enumerate_paths(
neighbors: Mapping[str, Sequence[str]],
duplicate_smalls: int = 0,
start: str = "start",
current: tuple[str, ...] = ("start",),
) -> Iterator[tuple[str, ...]]:
if start == "end":
yield current
for neighbor in neighbors[start]:
if not is_small(neighbor):
yield from enumerate_paths(
neighbors, duplicate_smalls, neighbor, current + (neighbor,)
)
elif neighbor not in current:
yield from enumerate_paths(
neighbors, duplicate_smalls, neighbor, current + (neighbor,)
)
elif duplicate_smalls > 0:
yield from enumerate_paths(
neighbors, duplicate_smalls - 1, neighbor, current + (neighbor,)
)
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
neighbors: dict[str, list[str]] = defaultdict(list)
for row in input.splitlines():
a, b = row.split("-")
if a != "end" and b != "start":
neighbors[a].append(b)
if b != "end" and a != "start":
neighbors[b].append(a)
if self.files:
graph = "graph {\n"
for node, neighbors_of in neighbors.items():
graph += (
" ".join(
f"{node} -- {neighbor};"
for neighbor in neighbors_of
if node <= neighbor or node == "start" or neighbor == "end"
)
+ "\n"
)
graph += "}\n"
self.files.create("graph.dot", graph.encode(), False)
yield len(list(enumerate_paths(neighbors)))
yield len(list(enumerate_paths(neighbors, 1)))

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,41 +1,38 @@
import sys
from math import prod from math import prod
from typing import Literal, TypeAlias, cast from typing import Any, Iterator, Literal, TypeAlias, cast
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
Command: TypeAlias = Literal["forward", "up", "down"] Command: TypeAlias = Literal["forward", "up", "down"]
commands: list[tuple[Command, int]] = [
(cast(Command, (p := line.split())[0]), int(p[1])) for line in lines
]
class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def depth_and_position(use_aim: bool): commands: list[tuple[Command, int]] = [
aim, pos, depth = 0, 0, 0 (cast(Command, (p := line.split())[0]), int(p[1])) for line in lines
for command, value in commands: ]
d_depth = 0
match command:
case "forward":
pos += value
depth += value * aim
case "up":
d_depth = -value
case "down":
d_depth = value
if use_aim: def depth_and_position(use_aim: bool):
aim += d_depth aim, pos, depth = 0, 0, 0
else: for command, value in commands:
depth += value d_depth = 0
match command:
case "forward":
pos += value
depth += value * aim
case "up":
d_depth = -value
case "down":
d_depth = value
return depth, pos if use_aim:
aim += d_depth
else:
depth += value
return depth, pos
# part 1 yield prod(depth_and_position(False))
answer_1 = prod(depth_and_position(False)) yield prod(depth_and_position(True))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(depth_and_position(True))
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2 class Solver(BaseSolver):
answer_2 = ... def solve(self, input: str) -> Iterator[Any]: ...
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,7 @@
import sys
from collections import Counter from collections import Counter
from typing import Literal from typing import Any, Iterator, Literal
from ..base import BaseSolver
def generator_rating( def generator_rating(
@ -20,20 +21,23 @@ def generator_rating(
return values[0] return values[0]
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
most_and_least_common = [
tuple(
Counter(line[col] for line in lines).most_common(2)[m][0]
for m in range(2)
)
for col in range(len(lines[0]))
]
gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2)
epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2)
yield gamma_rate * epsilon_rate
# part 1 # part 2
most_and_least_common = [ oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2)
tuple(Counter(line[col] for line in lines).most_common(2)[m][0] for m in range(2)) co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2)
for col in range(len(lines[0])) yield oxygen_generator_rating * co2_scrubber_rating
]
gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2)
epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2)
print(f"answer 1 is {gamma_rate * epsilon_rate}")
# part 2
oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2)
co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2)
answer_2 = oxygen_generator_rating * co2_scrubber_rating
print(f"answer 2 is {answer_2}")

View File

@ -1,45 +1,52 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
numbers = [int(c) for c in lines[0].split(",")]
boards = np.asarray( class Solver(BaseSolver):
[ def solve(self, input: str) -> Iterator[Any]:
[[int(c) for c in line.split()] for line in lines[start : start + 5]] lines = input.splitlines()
for start in range(2, len(lines), 6)
]
)
# (round, score) for each board (-1 when not found) numbers = [int(c) for c in lines[0].split(",")]
winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))]
marked = np.zeros_like(boards, dtype=bool)
for round, number in enumerate(numbers): boards = np.asarray(
# mark boards [
marked[boards == number] = True [[int(c) for c in line.split()] for line in lines[start : start + 5]]
for start in range(2, len(lines), 6)
]
)
# check each board for winning # (round, score) for each board (-1 when not found)
for index in range(len(boards)): winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))]
if winning_rounds[index][0] > 0: marked = np.zeros_like(boards, dtype=bool)
continue
if np.any(np.all(marked[index], axis=0) | np.all(marked[index], axis=1)): for round, number in enumerate(numbers):
winning_rounds[index] = ( # mark boards
round, marked[boards == number] = True
number * int(np.sum(boards[index][~marked[index]])),
)
# all boards are winning - break # check each board for winning
if np.all(marked.all(axis=1) | marked.all(axis=2)): for index in range(len(boards)):
break if winning_rounds[index][0] > 0:
continue
# part 1 if np.any(
(_, score) = min(winning_rounds, key=lambda w: w[0]) np.all(marked[index], axis=0) | np.all(marked[index], axis=1)
print(f"answer 1 is {score}") ):
winning_rounds[index] = (
round,
number * int(np.sum(boards[index][~marked[index]])),
)
# part 2 # all boards are winning - break
(_, score) = max(winning_rounds, key=lambda w: w[0]) if np.all(marked.all(axis=1) | marked.all(axis=2)):
print(f"answer 2 is {score}") break
# part 1
(_, score) = min(winning_rounds, key=lambda w: w[0])
yield score
# part 2
(_, score) = max(winning_rounds, key=lambda w: w[0])
yield score

View File

@ -1,48 +1,48 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines: list[str] = sys.stdin.read().splitlines() from ..base import BaseSolver
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
(
(
int(line.split(" -> ")[0].split(",")[0]),
int(line.split(" -> ")[0].split(",")[1]),
),
(
int(line.split(" -> ")[1].split(",")[0]),
int(line.split(" -> ")[1].split(",")[1]),
),
)
for line in lines
]
np_sections = np.array(sections).reshape(-1, 4) class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
x_min, x_max, y_min, y_max = ( sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
min(np_sections[:, 0].min(), np_sections[:, 2].min()), (
max(np_sections[:, 0].max(), np_sections[:, 2].max()), (
min(np_sections[:, 1].min(), np_sections[:, 3].min()), int(line.split(" -> ")[0].split(",")[0]),
max(np_sections[:, 1].max(), np_sections[:, 3].max()), int(line.split(" -> ")[0].split(",")[1]),
) ),
(
int(line.split(" -> ")[1].split(",")[0]),
int(line.split(" -> ")[1].split(",")[1]),
),
)
for line in lines
]
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int) np_sections = np.array(sections).reshape(-1, 4)
counts_2 = counts_1.copy()
for (x1, y1), (x2, y2) in sections: x_max, y_max = (
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1) max(np_sections[:, 0].max(), np_sections[:, 2].max()),
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1) max(np_sections[:, 1].max(), np_sections[:, 3].max()),
)
if x1 == x2 or y1 == y2: counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
counts_1[list(y_rng), list(x_rng)] += 1 counts_2 = counts_1.copy()
counts_2[list(y_rng), list(x_rng)] += 1
elif abs(x2 - x1) == abs(y2 - y1):
for i, j in zip(y_rng, x_rng):
counts_2[i, j] += 1
answer_1 = (counts_1 >= 2).sum() for (x1, y1), (x2, y2) in sections:
print(f"answer 1 is {answer_1}") x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
answer_2 = (counts_2 >= 2).sum() if x1 == x2 or y1 == y2:
print(f"answer 2 is {answer_2}") counts_1[list(y_rng), list(x_rng)] += 1
counts_2[list(y_rng), list(x_rng)] += 1
elif abs(x2 - x1) == abs(y2 - y1):
for i, j in zip(y_rng, x_rng):
counts_2[i, j] += 1
yield (counts_1 >= 2).sum()
yield (counts_2 >= 2).sum()

View File

@ -1,21 +1,21 @@
import sys from typing import Any, Iterator
values = [int(c) for c in sys.stdin.read().strip().split(",")] from ..base import BaseSolver
days = 256
lanterns = {day: 0 for day in range(days)}
for value in values:
for day in range(value, days, 7):
lanterns[day] += 1
for day in range(days): class Solver(BaseSolver):
for day2 in range(day + 9, days, 7): def solve(self, input: str) -> Iterator[Any]:
lanterns[day2] += lanterns[day] values = [int(c) for c in input.split(",")]
# part 1 days = 256
answer_1 = sum(v for k, v in lanterns.items() if k < 80) + len(values) lanterns = {day: 0 for day in range(days)}
print(f"answer 1 is {answer_1}") for value in values:
for day in range(value, days, 7):
lanterns[day] += 1
# part 2 for day in range(days):
answer_2 = sum(lanterns.values()) + len(values) for day2 in range(day + 9, days, 7):
print(f"answer 2 is {answer_2}") lanterns[day2] += lanterns[day]
yield sum(v for k, v in lanterns.items() if k < 80) + len(values)
yield sum(lanterns.values()) + len(values)

View File

@ -1,19 +1,22 @@
import sys from typing import Any, Iterator
positions = [int(c) for c in sys.stdin.read().strip().split(",")] from ..base import BaseSolver
min_position, max_position = min(positions), max(positions)
# part 1 class Solver(BaseSolver):
answer_1 = min( def solve(self, input: str) -> Iterator[Any]:
sum(abs(p - position) for p in positions) positions = [int(c) for c in input.split(",")]
for position in range(min_position, max_position + 1)
)
print(f"answer 1 is {answer_1}")
# part 2 min_position, max_position = min(positions), max(positions)
answer_2 = min(
sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions) # part 1
for position in range(min_position, max_position + 1) yield min(
) sum(abs(p - position) for p in positions)
print(f"answer 2 is {answer_2}") for position in range(min_position, max_position + 1)
)
# part 2
yield min(
sum(abs(p - position) * (abs(p - position) + 1) // 2 for p in positions)
for position in range(min_position, max_position + 1)
)

View File

@ -1,8 +1,7 @@
import itertools import itertools
import os from typing import Any, Iterator
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
digits = { digits = {
"abcefg": 0, "abcefg": 0,
@ -17,71 +16,74 @@ digits = {
"abcdfg": 9, "abcdfg": 9,
} }
lines = sys.stdin.read().splitlines()
# part 1 class Solver(BaseSolver):
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)} def solve(self, input: str) -> Iterator[Any]:
answer_1 = sum( lines = input.splitlines()
len(p) in lengths for line in lines for p in line.split("|")[1].strip().split()
)
print(f"answer 1 is {answer_1}")
# part 2 # part 1
values: list[int] = [] lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
yield sum(
len(p) in lengths
for line in lines
for p in line.split("|")[1].strip().split()
)
for line in lines: # part 2
parts = line.split("|") values: list[int] = []
broken_digits = sorted(parts[0].strip().split(), key=len)
per_length = { for line in lines:
k: list(v) parts = line.split("|")
for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len) broken_digits = sorted(parts[0].strip().split(), key=len)
}
# a can be found immediately per_length = {
a = next(u for u in per_length[3][0] if u not in per_length[2][0]) k: list(v)
for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len)
}
# c and f have only two possible values corresponding to the single entry of # a can be found immediately
# length 2 a = next(u for u in per_length[3][0] if u not in per_length[2][0])
cf = list(per_length[2][0])
# the only digit of length 4 contains bcdf, so we can deduce bd by removing cf # c and f have only two possible values corresponding to the single entry of
bd = [u for u in per_length[4][0] if u not in cf] # length 2
cf = list(per_length[2][0])
# the 3 digits of length 5 have a, d and g in common # the only digit of length 4 contains bcdf, so we can deduce bd by removing cf
adg = [u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])] bd = [u for u in per_length[4][0] if u not in cf]
# we can remove a # the 3 digits of length 5 have a, d and g in common
dg = [u for u in adg if u != a] adg = [
u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])
]
# we can deduce d and g # we can remove a
d = next(u for u in dg if u in bd) dg = [u for u in adg if u != a]
g = next(u for u in dg if u != d)
# then b # we can deduce d and g
b = next(u for u in bd if u != d) d = next(u for u in dg if u in bd)
g = next(u for u in dg if u != d)
# f is in the three 6-length digits, while c is only in 2 # then b
f = next(u for u in cf if all(u in p for p in per_length[6])) b = next(u for u in bd if u != d)
# c is not f # f is in the three 6-length digits, while c is only in 2
c = next(u for u in cf if u != f) f = next(u for u in cf if all(u in p for p in per_length[6]))
# e is the last one # c is not f
e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g}) c = next(u for u in cf if u != f)
mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg")) # e is the last one
e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g})
value = 0 mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg"))
for number in parts[1].strip().split():
digit = "".join(sorted(mapping[c] for c in number))
value = 10 * value + digits[digit]
if VERBOSE: value = 0
print(value) for number in parts[1].strip().split():
digit = "".join(sorted(mapping[c] for c in number))
value = 10 * value + digits[digit]
values.append(value) self.logger.info(f"value for '{line}' is {value}")
values.append(value)
answer_2 = sum(values) yield sum(values)
print(f"answer 2 is {answer_2}")

View File

@ -1,18 +1,18 @@
import sys
from math import prod from math import prod
from typing import Any, Iterator
values = [[int(c) for c in row] for row in sys.stdin.read().splitlines()] from ..base import BaseSolver
n_rows, n_cols = len(values), len(values[0])
def neighbors(point: tuple[int, int]): def neighbors(point: tuple[int, int], n_rows: int, n_cols: int):
i, j = point i, j = point
for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)): for di, dj in ((-1, 0), (+1, 0), (0, -1), (0, +1)):
if 0 <= i + di < n_rows and 0 <= j + dj < n_cols: if 0 <= i + di < n_rows and 0 <= j + dj < n_cols:
yield (i + di, j + dj) yield (i + di, j + dj)
def basin(start: tuple[int, int]) -> set[tuple[int, int]]: def basin(values: list[list[int]], start: tuple[int, int]) -> set[tuple[int, int]]:
n_rows, n_cols = len(values), len(values[0])
visited: set[tuple[int, int]] = set() visited: set[tuple[int, int]] = set()
queue = [start] queue = [start]
@ -23,22 +23,25 @@ def basin(start: tuple[int, int]) -> set[tuple[int, int]]:
continue continue
visited.add((i, j)) visited.add((i, j))
queue.extend(neighbors((i, j))) queue.extend(neighbors((i, j), n_rows, n_cols))
return visited return visited
low_points = [ class Solver(BaseSolver):
(i, j) def solve(self, input: str) -> Iterator[Any]:
for i in range(n_rows) values = [[int(c) for c in row] for row in input.splitlines()]
for j in range(n_cols) n_rows, n_cols = len(values), len(values[0])
if all(values[ti][tj] > values[i][j] for ti, tj in neighbors((i, j)))
]
# part 1 low_points = [
answer_1 = sum(values[i][j] + 1 for i, j in low_points) (i, j)
print(f"answer 1 is {answer_1}") for i in range(n_rows)
for j in range(n_cols)
if all(
values[ti][tj] > values[i][j]
for ti, tj in neighbors((i, j), n_rows, n_cols)
)
]
# part 2 yield sum(values[i][j] + 1 for i, j in low_points)
answer_2 = prod(sorted(len(basin(point)) for point in low_points)[-3:]) yield prod(sorted(len(basin(values, point)) for point in low_points)[-3:])
print(f"answer 2 is {answer_2}")

View File

@ -1,7 +1,12 @@
import sys from typing import Any, Iterator
blocks = sys.stdin.read().split("\n\n") from ..base import BaseSolver
values = sorted(sum(map(int, block.split())) for block in blocks)
print(f"answer 1 is {values[-1]}")
print(f"answer 2 is {sum(values[-3:])}") class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = input.split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
yield values[-1]
yield sum(values[-3:])

View File

@ -1,38 +1,43 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
cycle = 1
x = 1
values = {cycle: x}
for line in lines:
cycle += 1
if line == "noop":
pass
else:
r = int(line.split()[1])
values[cycle] = x
cycle += 1
x += r
values[cycle] = x
answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40))
print(f"answer 1 is {answer_1}")
for i in range(6): class Solver(BaseSolver):
for j in range(40): def solve(self, input: str) -> Iterator[Any]:
v = values[1 + i * 40 + j] lines = [line.strip() for line in input.splitlines()]
if j >= v - 1 and j <= v + 1: cycle, x = 1, 1
print("#", end="") values = {cycle: x}
else:
print(".", end="")
print() for line in lines:
cycle += 1
if line == "noop":
pass
else:
r = int(line.split()[1])
values[cycle] = x
cycle += 1
x += r
values[cycle] = x
answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40))
yield answer_1
yield (
"\n"
+ "\n".join(
"".join(
"#"
if j >= (v := values[1 + i * 40 + j]) - 1 and j <= v + 1
else "."
for j in range(40)
)
for i in range(6)
)
+ "\n"
)

View File

@ -1,7 +1,8 @@
import copy import copy
import sys
from functools import reduce from functools import reduce
from typing import Callable, Final, Mapping, Sequence from typing import Any, Callable, Final, Iterator, Mapping, Sequence
from ..base import BaseSolver
class Monkey: class Monkey:
@ -119,24 +120,28 @@ def monkey_business(inspects: dict[Monkey, int]) -> int:
return sorted_levels[-2] * sorted_levels[-1] return sorted_levels[-2] * sorted_levels[-1]
monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
monkeys = [parse_monkey(block.splitlines()) for block in input.split("\n\n")]
# case 1: we simply divide the worry by 3 after applying the monkey worry operation # case 1: we simply divide the worry by 3 after applying the monkey worry operation
answer_1 = monkey_business( yield monkey_business(
run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3)
) )
print(f"answer 1 is {answer_1}")
# case 2: to keep reasonable level values, we can use a modulo operation, we need to # case 2: to keep reasonable level values, we can use a modulo operation, we need to
# use the product of all "divisible by" test so that the test remains valid # use the product of all "divisible by" test so that the test remains valid
# #
# (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value # (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value
# #
# (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here # (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here
# we use the product of all test value # we use the product of all test value
# #
total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1)
answer_2 = monkey_business( yield monkey_business(
run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value) run(
) copy.deepcopy(monkeys),
print(f"answer 2 is {answer_2}") 10_000,
me_worry_fn=lambda w: w % total_test_value,
)
)

View File

@ -1,6 +1,7 @@
import heapq import heapq
import sys from typing import Any, Callable, Iterator, TypeVar
from typing import Callable, Iterator, TypeVar
from ..base import BaseSolver
Node = TypeVar("Node") Node = TypeVar("Node")
@ -68,30 +69,6 @@ def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] |
return list(reversed(path)) return list(reversed(path))
def print_path(path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None:
end = path[-1]
graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
graph[end[0]][end[1]] = "E"
for i in range(0, len(path) - 1):
cr, cc = path[i]
nr, nc = path[i + 1]
if cr == nr and nc == cc - 1:
graph[cr][cc] = "<"
elif cr == nr and nc == cc + 1:
graph[cr][cc] = ">"
elif cr == nr - 1 and nc == cc:
graph[cr][cc] = "v"
elif cr == nr + 1 and nc == cc:
graph[cr][cc] = "^"
else:
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
print("\n".join("".join(row) for row in graph))
def neighbors( def neighbors(
grid: list[list[int]], node: tuple[int, int], up: bool grid: list[list[int]], node: tuple[int, int], up: bool
) -> Iterator[tuple[int, int]]: ) -> Iterator[tuple[int, int]]:
@ -118,46 +95,82 @@ def neighbors(
# === main code === # === main code ===
lines = sys.stdin.read().splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines] class Solver(BaseSolver):
def print_path(
self, name: str, path: list[tuple[int, int]], n_rows: int, n_cols: int
) -> None:
if not self.files:
return
start: tuple[int, int] | None = None end = path[-1]
end: tuple[int, int] | None = None
# for part 2 graph = [["." for _c in range(n_cols)] for _r in range(n_rows)]
start_s: list[tuple[int, int]] = [] graph[end[0]][end[1]] = "E"
for i_row, row in enumerate(grid): for i in range(0, len(path) - 1):
for i_col, col in enumerate(row): cr, cc = path[i]
if chr(col + ord("a")) == "S": nr, nc = path[i + 1]
start = (i_row, i_col)
start_s.append(start)
elif chr(col + ord("a")) == "E":
end = (i_row, i_col)
elif col == 0:
start_s.append((i_row, i_col))
assert start is not None if cr == nr and nc == cc - 1:
assert end is not None graph[cr][cc] = "<"
elif cr == nr and nc == cc + 1:
graph[cr][cc] = ">"
elif cr == nr - 1 and nc == cc:
graph[cr][cc] = "v"
elif cr == nr + 1 and nc == cc:
graph[cr][cc] = "^"
else:
assert False, "{} -> {} infeasible".format(path[i], path[i + 1])
# fix values self.files.create(
grid[start[0]][start[1]] = 0 f"graph_{name}.txt",
grid[end[0]][end[1]] = ord("z") - ord("a") "\n".join("".join(row) for row in graph).encode(),
text=True,
)
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
lengths_1, parents_1 = dijkstra( grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
start=start, neighbors=lambda n: neighbors(grid, n, True), cost=lambda lhs, rhs: 1
)
path_1 = make_path(parents_1, start, end)
assert path_1 is not None
print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) start: tuple[int, int] | None = None
end: tuple[int, int] | None = None
print(f"answer 1 is {lengths_1[end] - 1}") # for part 2
start_s: list[tuple[int, int]] = []
lengths_2, parents_2 = dijkstra( for i_row, row in enumerate(grid):
start=end, neighbors=lambda n: neighbors(grid, n, False), cost=lambda lhs, rhs: 1 for i_col, col in enumerate(row):
) if chr(col + ord("a")) == "S":
answer_2 = min(lengths_2.get(start, float("inf")) for start in start_s) start = (i_row, i_col)
print(f"answer 2 is {answer_2}") start_s.append(start)
elif chr(col + ord("a")) == "E":
end = (i_row, i_col)
elif col == 0:
start_s.append((i_row, i_col))
assert start is not None
assert end is not None
# fix values
grid[start[0]][start[1]] = 0
grid[end[0]][end[1]] = ord("z") - ord("a")
lengths_1, parents_1 = dijkstra(
start=start,
neighbors=lambda n: neighbors(grid, n, True),
cost=lambda lhs, rhs: 1,
)
path_1 = make_path(parents_1, start, end)
assert path_1 is not None
self.print_path("answer1", path_1, n_rows=len(grid), n_cols=len(grid[0]))
yield lengths_1[end] - 1
lengths_2, _ = dijkstra(
start=end,
neighbors=lambda n: neighbors(grid, n, False),
cost=lambda lhs, rhs: 1,
)
yield min(lengths_2.get(start, float("inf")) for start in start_s)

View File

@ -1,11 +1,8 @@
import json import json
import sys
from functools import cmp_to_key from functools import cmp_to_key
from typing import TypeAlias, cast from typing import Any, Iterator, TypeAlias, cast
blocks = sys.stdin.read().strip().split("\n\n") from ..base import BaseSolver
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
Packet: TypeAlias = list[int | list["Packet"]] Packet: TypeAlias = list[int | list["Packet"]]
@ -28,14 +25,18 @@ def compare(lhs: Packet, rhs: Packet) -> int:
return len(rhs) - len(lhs) return len(rhs) - len(lhs)
answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0) class Solver(BaseSolver):
print(f"answer_1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
blocks = input.split("\n\n")
pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks]
dividers = [[[2]], [[6]]] yield sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0)
packets = [packet for packets in pairs for packet in packets] dividers = [[[2]], [[6]]]
packets.extend(dividers)
packets = list(reversed(sorted(packets, key=cmp_to_key(compare))))
d_index = [packets.index(d) + 1 for d in dividers] packets = [packet for packets in pairs for packet in packets]
print(f"answer 2 is {d_index[0] * d_index[1]}") packets.extend(dividers)
packets = list(reversed(sorted(packets, key=cmp_to_key(compare))))
d_index = [packets.index(d) + 1 for d in dividers]
yield d_index[0] * d_index[1]

View File

@ -1,6 +1,7 @@
import sys
from enum import Enum, auto from enum import Enum, auto
from typing import Callable, cast from typing import Any, Callable, Iterator, cast
from ..base import BaseSolver
class Cell(Enum): class Cell(Enum):
@ -12,26 +13,6 @@ class Cell(Enum):
return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self] return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self]
def print_blocks(blocks: dict[tuple[int, int], Cell]):
"""
Print the given set of blocks on a grid.
Args:
blocks: Set of blocks to print.
"""
x_min, y_min, x_max, y_max = (
min(x for x, _ in blocks),
0,
max(x for x, _ in blocks),
max(y for _, y in blocks),
)
for y in range(y_min, y_max + 1):
print(
"".join(str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1))
)
def flow( def flow(
blocks: dict[tuple[int, int], Cell], blocks: dict[tuple[int, int], Cell],
stop_fn: Callable[[int, int], bool], stop_fn: Callable[[int, int], bool],
@ -84,57 +65,82 @@ def flow(
# === inputs === # === inputs ===
lines = sys.stdin.read().splitlines()
paths: list[list[tuple[int, int]]] = [] class Solver(BaseSolver):
for line in lines: def print_blocks(self, name: str, blocks: dict[tuple[int, int], Cell]):
parts = line.split(" -> ") """
paths.append( Print the given set of blocks on a grid.
[
cast(tuple[int, int], tuple(int(c.strip()) for c in part.split(",")))
for part in parts
]
)
Args:
blocks: Set of blocks to print.
"""
if not self.files:
return
blocks: dict[tuple[int, int], Cell] = {} x_min, y_min, x_max, y_max = (
for path in paths: min(x for x, _ in blocks),
for start, end in zip(path[:-1], path[1:]): 0,
x_start = min(start[0], end[0]) max(x for x, _ in blocks),
x_end = max(start[0], end[0]) + 1 max(y for _, y in blocks),
y_start = min(start[1], end[1]) )
y_end = max(start[1], end[1]) + 1
for x in range(x_start, x_end): self.files.create(
for y in range(y_start, y_end): f"blocks_{name}.txt",
blocks[x, y] = Cell.ROCK "\n".join(
"".join(
str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)
)
for y in range(y_min, y_max + 1)
).encode(),
True,
)
print_blocks(blocks) def solve(self, input: str) -> Iterator[Any]:
print() lines = [line.strip() for line in input.splitlines()]
x_min, y_min, x_max, y_max = ( paths: list[list[tuple[int, int]]] = []
min(x for x, _ in blocks), for line in lines:
0, parts = line.split(" -> ")
max(x for x, _ in blocks), paths.append(
max(y for _, y in blocks), [
) cast(
tuple[int, int], tuple(int(c.strip()) for c in part.split(","))
)
for part in parts
]
)
# === part 1 === blocks: dict[tuple[int, int], Cell] = {}
for path in paths:
for start, end in zip(path[:-1], path[1:]):
x_start = min(start[0], end[0])
x_end = max(start[0], end[0]) + 1
y_start = min(start[1], end[1])
y_end = max(start[1], end[1]) + 1
blocks_1 = flow( for x in range(x_start, x_end):
blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR for y in range(y_start, y_end):
) blocks[x, y] = Cell.ROCK
print_blocks(blocks_1)
print(f"answer 1 is {sum(v == Cell.SAND for v in blocks_1.values())}")
print()
# === part 2 === self.print_blocks("start", blocks)
blocks_2 = flow( y_max = max(y for _, y in blocks)
blocks.copy(),
stop_fn=lambda x, y: x == 500 and y == 0, # === part 1 ===
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
) blocks_1 = flow(
blocks_2[500, 0] = Cell.SAND blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR
print_blocks(blocks_2) )
print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}") self.print_blocks("part1", blocks_1)
yield sum(v == Cell.SAND for v in blocks_1.values())
# === part 2 ===
blocks_2 = flow(
blocks.copy(),
stop_fn=lambda x, y: x == 500 and y == 0,
fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK,
)
blocks_2[500, 0] = Cell.SAND
self.print_blocks("part2", blocks_2)
yield sum(v == Cell.SAND for v in blocks_2.values())

View File

@ -1,90 +1,95 @@
import sys import itertools as it
from typing import Any from typing import Any, Iterator
import numpy as np import numpy as np
import parse # type: ignore import parse # type: ignore
from numpy.typing import NDArray from numpy.typing import NDArray
from ..base import BaseSolver
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
no_beacons_row_l: list[NDArray[np.floating[Any]]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) # closest
no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1)) # type: ignore
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore
beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row)
no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference(beacons_at_row) # type: ignore
return len(no_beacons_row)
def part2_intervals( class Solver(BaseSolver):
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int def part1(
) -> tuple[int, int, int]: self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int
from tqdm import trange ) -> int:
no_beacons_row_l: list[NDArray[np.floating[Any]]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) # closest
no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1)) # type: ignore
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore
beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row)
no_beacons_row = set(it.chain(*no_beacons_row_l)).difference(beacons_at_row) # type: ignore
return len(no_beacons_row)
def part2_intervals(
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
for y in self.progress.wrap(range(xy_max + 1)):
its: list[tuple[int, int]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by)
dx = d - abs(sy - y)
if dx >= 0:
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
its = sorted(its)
_, e = its[0]
for si, ei in its[1:]:
if si > e + 1:
return si - 1, y, 4_000_000 * (si - 1) + y
if ei > e:
e = ei
return (0, 0, 0)
def part2_cplex(
self, sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
from docplex.mp.model import Model
m = Model()
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"])
for y in trange(xy_max + 1):
its: list[tuple[int, int]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items(): for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) d = abs(sx - bx) + abs(sy - by)
dx = d - abs(sy - y) m.add_constraint(
m.abs(x - sx) + m.abs(y - sy) >= d + 1, # type: ignore
ctname=f"ct_{sx}_{sy}",
)
if dx >= 0: m.set_objective("min", x + y)
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
its = sorted(its) s = m.solve()
_, e = its[0] assert s is not None
for si, ei in its[1:]: vx = int(s.get_value(x))
if si > e + 1: vy = int(s.get_value(y))
return si - 1, y, 4_000_000 * (si - 1) + y return vx, vy, 4_000_000 * vx + vy
if ei > e:
e = ei
return (0, 0, 0) def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
def part2_cplex( for line in lines:
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int r: dict[str, str] = parse.parse( # type: ignore
) -> tuple[int, int, int]: "Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
from docplex.mp.model import Model )
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))
m = Model() xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20
row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"]) yield self.part1(sensor_to_beacon, row)
for (sx, sy), (bx, by) in sensor_to_beacon.items(): # x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
d = abs(sx - bx) + abs(sy - by) x, y, a2 = self.part2_intervals(sensor_to_beacon, xy_max)
m.add_constraint(m.abs(x - sx) + m.abs(y - sy) >= d + 1, ctname=f"ct_{sx}_{sy}") # type: ignore self.logger.info(f"answer 2 is {a2} (x={x}, y={y})")
yield a2
m.set_objective("min", x + y)
s = m.solve()
assert s is not None
vx = int(s.get_value(x))
vy = int(s.get_value(y))
return vx, vy, 4_000_000 * vx + vy
lines = sys.stdin.read().splitlines()
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
for line in lines:
r: dict[str, str] = parse.parse( # type: ignore
"Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
)
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))
xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20
row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10
print(f"answer 1 is {part1(sensor_to_beacon, row)}")
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
x, y, a2 = part2_intervals(sensor_to_beacon, xy_max)
print(f"answer 2 is {a2} (x={x}, y={y})")

View File

@ -3,11 +3,10 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import re import re
import sys
from collections import defaultdict from collections import defaultdict
from typing import FrozenSet, NamedTuple from typing import Any, FrozenSet, Iterator, NamedTuple
from tqdm import tqdm from ..base import BaseSolver
class Pipe(NamedTuple): class Pipe(NamedTuple):
@ -36,8 +35,8 @@ def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]:
Runs a BFS from the given pipe and return the shortest distance (in term of hops) Runs a BFS from the given pipe and return the shortest distance (in term of hops)
to all other pipes. to all other pipes.
""" """
queue = [(0, pipe_1)] queue = [(0, pipe)]
visited = set() visited: set[Pipe] = set()
distances: dict[Pipe, int] = {} distances: dict[Pipe, int] = {}
while len(distances) < len(pipes): while len(distances) < len(pipes):
@ -61,98 +60,100 @@ def update_with_better(
node_at_times[flowing] = max(node_at_times[flowing], flow) node_at_times[flowing] = max(node_at_times[flowing], flow)
def part_1(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
lambda: defaultdict(lambda: defaultdict(lambda: 0))
)
node_at_times[0] = {start_pipe: {frozenset(): 0}}
for time in range(max_time):
for c_pipe, nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
for target in relevant_pipes:
distance = distances[c_pipe, target] + 1
if time + distance >= max_time or target in flowing:
continue
update_with_better(
node_at_times[time + distance][target],
flow + sum(pipe.flow for pipe in flowing) * distance,
flowing | {target},
)
update_with_better(
node_at_times[max_time][c_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
def part_2(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
def compute(pipes_for_me: FrozenSet[Pipe]) -> int:
return part_1(start_pipe, max_time, distances, pipes_for_me) + part_1(
start_pipe, max_time, distances, relevant_pipes - pipes_for_me
)
combs = [
frozenset(relevant_pipes_1)
for r in range(2, len(relevant_pipes) // 2 + 1)
for relevant_pipes_1 in itertools.combinations(relevant_pipes, r)
]
return max(compute(comb) for comb in tqdm(combs))
# === MAIN === # === MAIN ===
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def part_1(
self,
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
lambda: defaultdict(lambda: defaultdict(lambda: 0))
)
node_at_times[0] = {start_pipe: {frozenset(): 0}}
for time in range(max_time):
for c_pipe, nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
for target in relevant_pipes:
distance = distances[c_pipe, target] + 1
if time + distance >= max_time or target in flowing:
continue
pipes: dict[str, Pipe] = {} update_with_better(
for line in lines: node_at_times[time + distance][target],
r = re.match( flow + sum(pipe.flow for pipe in flowing) * distance,
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)", flowing | {target},
line, )
)
assert r
g = r.groups() update_with_better(
node_at_times[max_time][c_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", ")) return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
# compute distances from one valve to any other def part_2(
distances: dict[tuple[Pipe, Pipe], int] = {} self,
for pipe_1 in pipes.values(): start_pipe: Pipe,
distances.update( max_time: int,
{ distances: dict[tuple[Pipe, Pipe], int],
(pipe_1, pipe_2): distance relevant_pipes: FrozenSet[Pipe],
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items() ):
} def compute(pipes_for_me: FrozenSet[Pipe]) -> int:
) return self.part_1(
start_pipe, max_time, distances, pipes_for_me
) + self.part_1(
start_pipe, max_time, distances, relevant_pipes - pipes_for_me
)
# valves with flow combs = [
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0) frozenset(relevant_pipes_1)
for r in range(2, len(relevant_pipes) // 2 + 1)
for relevant_pipes_1 in itertools.combinations(relevant_pipes, r)
]
return max(compute(comb) for comb in self.progress.wrap(combs))
# 1651, 1653 def solve(self, input: str) -> Iterator[Any]:
print(part_1(pipes["AA"], 30, distances, relevant_pipes)) lines = [line.strip() for line in input.splitlines()]
# 1707, 2223 pipes: dict[str, Pipe] = {}
print(part_2(pipes["AA"], 26, distances, relevant_pipes)) for line in lines:
r = re.match(
R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)",
line,
)
assert r
g = r.groups()
pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", "))
# compute distances from one valve to any other
distances: dict[tuple[Pipe, Pipe], int] = {}
for pipe_1 in pipes.values():
distances.update(
{
(pipe_1, pipe_2): distance
for pipe_2, distance in breadth_first_search(pipes, pipe_1).items()
}
)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1651, 1653
yield self.part_1(pipes["AA"], 30, distances, relevant_pipes)
# 1707, 2223
yield self.part_2(pipes["AA"], 26, distances, relevant_pipes)

View File

@ -1,23 +1,16 @@
import sys from typing import Any, Iterator, Sequence, TypeAlias, TypeVar
from typing import Sequence, TypeVar
import numpy as np import numpy as np
from numpy.typing import NDArray
from ..base import BaseSolver
T = TypeVar("T") T = TypeVar("T")
Tower: TypeAlias = NDArray[np.bool]
def print_tower(tower: np.ndarray, out: str = "#"):
print("-" * (tower.shape[1] + 2))
non_empty = False
for row in reversed(range(1, tower.shape[0])):
if not non_empty and not tower[row, :].any():
continue
non_empty = True
print("|" + "".join(out if c else "." for c in tower[row, :]) + "|")
print("+" + "-" * tower.shape[1] + "+")
def tower_height(tower: np.ndarray) -> int: def tower_height(tower: Tower) -> int:
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1) return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
@ -45,8 +38,8 @@ def build_tower(
n_rocks: int, n_rocks: int,
jets: str, jets: str,
early_stop: bool = False, early_stop: bool = False,
init: np.ndarray = np.ones(WIDTH, dtype=bool), init: Tower = np.ones(WIDTH, dtype=bool),
) -> tuple[np.ndarray, int, int, dict[int, int]]: ) -> tuple[Tower, int, int, dict[int, int]]:
tower = EMPTY_BLOCKS.copy() tower = EMPTY_BLOCKS.copy()
tower[0, :] = init tower[0, :] = init
@ -95,26 +88,24 @@ def build_tower(
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
line = sys.stdin.read().strip() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
tower, *_ = build_tower(2022, input)
yield tower_height(tower)
tower, *_ = build_tower(2022, line) TOTAL_ROCKS = 1_000_000_000_000
answer_1 = tower_height(tower) _tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True)
print(f"answer 1 is {answer_1}") assert prev_1 > 0
TOTAL_ROCKS = 1_000_000_000_000 # 2767 1513
tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True) remaining_rocks = TOTAL_ROCKS - n_rocks_1
assert prev_1 > 0 n_repeat_rocks = n_rocks_1 - prev_1
n_repeat_towers = remaining_rocks // n_repeat_rocks
# 2767 1513 base_height = heights_1[prev_1]
remaining_rocks = TOTAL_ROCKS - n_rocks_1 repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1]
n_repeat_rocks = n_rocks_1 - prev_1 remaining_height = (
n_repeat_towers = remaining_rocks // n_repeat_rocks heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
)
base_height = heights_1[prev_1] yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1]
remaining_height = (
heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1]
)
answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height
print(f"answer 2 is {answer_2}")

View File

@ -1,50 +1,58 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
xyz = np.asarray( from ..base import BaseSolver
[
tuple(int(x) for x in row.split(",")) # type: ignore
for row in sys.stdin.read().splitlines()
]
)
xyz = xyz - xyz.min(axis=0) + 1
cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool) class Solver(BaseSolver):
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True def solve(self, input: str) -> Iterator[Any]:
xyz = np.asarray(
[
tuple(int(x) for x in row.split(",")) # type: ignore
for row in input.splitlines()
]
)
n_dims = len(cubes.shape) xyz = xyz - xyz.min(axis=0) + 1
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool)
cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True
answer_1 = sum( faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz]
)
print(f"answer 1 is {answer_1}")
visited = np.zeros_like(cubes, dtype=bool) yield sum(
queue = [(0, 0, 0)] 1
for x, y, z in xyz
for dx, dy, dz in faces
if not cubes[x + dx, y + dy, z + dz]
)
n_faces = 0 visited = np.zeros_like(cubes, dtype=bool)
while queue: queue = [(0, 0, 0)]
x, y, z = queue.pop(0)
if visited[x, y, z]: n_faces = 0
continue while queue:
x, y, z = queue.pop(0)
visited[x, y, z] = True if visited[x, y, z]:
continue
for dx, dy, dz in faces: visited[x, y, z] = True
nx, ny, nz = x + dx, y + dy, z + dz
if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))):
continue
if visited[nx, ny, nz]: for dx, dy, dz in faces:
continue nx, ny, nz = x + dx, y + dy, z + dz
if not all(
n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))
):
continue
if cubes[nx, ny, nz]: if visited[nx, ny, nz]:
n_faces += 1 continue
else:
queue.append((nx, ny, nz)) if cubes[nx, ny, nz]:
print(f"answer 2 is {n_faces}") n_faces += 1
else:
queue.append((nx, ny, nz))
yield n_faces

View File

@ -1,10 +1,11 @@
import sys from typing import Any, Iterator, Literal
from typing import Any, Literal
import numpy as np import numpy as np
import parse # pyright: ignore[reportMissingTypeStubs] import parse # pyright: ignore[reportMissingTypeStubs]
from numpy.typing import NDArray from numpy.typing import NDArray
from ..base import BaseSolver
Reagent = Literal["ore", "clay", "obsidian", "geode"] Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = ( REAGENTS: tuple[Reagent, ...] = (
"ore", "ore",
@ -62,29 +63,6 @@ def dominates(lhs: State, rhs: State):
) )
lines = sys.stdin.read().splitlines()
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in lines:
r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
blueprints.append(
{
"ore": {"ore": r[1]},
"clay": {"ore": r[2]},
"obsidian": {"ore": r[3], "clay": r[4]},
"geode": {"ore": r[5], "obsidian": r[6]},
}
)
def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
# since we can only build one robot per time, we do not need more than X robots # since we can only build one robot per time, we do not need more than X robots
# of type K where X is the maximum number of K required among all robots, e.g., # of type K where X is the maximum number of K required among all robots, e.g.,
@ -173,11 +151,31 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int:
return max(state.reagents["geode"] for state in state_after_t[max_time]) return max(state.reagents["geode"] for state in state_after_t[max_time])
answer_1 = sum( class Solver(BaseSolver):
(i_blueprint + 1) * run(blueprint, 24) def solve(self, input: str) -> Iterator[Any]:
for i_blueprint, blueprint in enumerate(blueprints) blueprints: list[dict[Reagent, IntOfReagent]] = []
) for line in input.splitlines():
print(f"answer 1 is {answer_1}") r: list[int] = parse.parse( # type: ignore
"Blueprint {}: "
"Each ore robot costs {:d} ore. "
"Each clay robot costs {:d} ore. "
"Each obsidian robot costs {:d} ore and {:d} clay. "
"Each geode robot costs {:d} ore and {:d} obsidian.",
line,
)
answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32) blueprints.append(
print(f"answer 2 is {answer_2}") {
"ore": {"ore": r[1]},
"clay": {"ore": r[2]},
"obsidian": {"ore": r[3], "clay": r[4]},
"geode": {"ore": r[5], "obsidian": r[6]},
}
)
yield sum(
(i_blueprint + 1) * run(blueprint, 24)
for i_blueprint, blueprint in enumerate(blueprints)
)
yield (run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32))

View File

@ -1,4 +1,6 @@
import sys from typing import Any, Iterator
from ..base import BaseSolver
def score_1(ux: int, vx: int) -> int: def score_1(ux: int, vx: int) -> int:
@ -33,21 +35,23 @@ def score_2(ux: int, vx: int) -> int:
return (ux + vx - 1) % 3 + 1 + vx * 3 return (ux + vx - 1) % 3 + 1 + vx * 3
lines = sys.stdin.readlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using # the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using
# modulo-3 arithmetic # modulo-3 arithmetic
# #
# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move # in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move
# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3))) # if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3)))
# #
# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for # we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for
# easier manipulation # easier manipulation
values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines] values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines]
# part 1 - 13526 # part 1 - 13526
print(f"answer 1 is {sum(score_1(*v) for v in values)}") yield sum(score_1(*v) for v in values)
# part 2 - 14204 # part 2 - 14204
print(f"answer 2 is {sum(score_2(*v) for v in values)}") yield sum(score_2(*v) for v in values)

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import sys from typing import Any, Iterator
from ..base import BaseSolver
class Number: class Number:
@ -65,10 +67,9 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
) )
numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
numbers = [Number(int(x)) for x in input.splitlines()]
answer_1 = decrypt(numbers, 1, 1) yield decrypt(numbers, 1, 1)
print(f"answer 1 is {answer_1}") yield decrypt(numbers, 811589153, 10)
answer_2 = decrypt(numbers, 811589153, 10)
print(f"answer 2 is {answer_2}")

View File

@ -1,6 +1,7 @@
import operator import operator
import sys from typing import Any, Callable, Iterator
from typing import Callable
from ..base import BaseSolver
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int: def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
@ -77,31 +78,31 @@ def invert(
return monkeys return monkeys
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
monkeys: dict[str, int | tuple[str, str, str]] = {} monkeys: dict[str, int | tuple[str, str, str]] = {}
op_monkeys: set[str] = set() op_monkeys: set[str] = set()
for line in lines: for line in lines:
parts = line.split(":") parts = line.split(":")
name = parts[0].strip() name = parts[0].strip()
try: try:
value = int(parts[1].strip()) value = int(parts[1].strip())
monkeys[name] = value monkeys[name] = value
except ValueError: except ValueError:
op1, ope, op2 = parts[1].strip().split() op1, ope, op2 = parts[1].strip().split()
monkeys[name] = (op1, ope, op2) monkeys[name] = (op1, ope, op2)
op_monkeys.add(name) op_monkeys.add(name)
yield compute(monkeys.copy(), "root")
answer_1 = compute(monkeys.copy(), "root") # assume the second operand of 'root' can be computed, and the first one depends on
print(f"answer 1 is {answer_1}") # humn, which is the case is my input and the test input
assert isinstance(monkeys["root"], tuple)
# assume the second operand of 'root' can be computed, and the first one depends on p1, _, p2 = monkeys["root"] # type: ignore
# humn, which is the case is my input and the test input yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
p1, _, p2 = monkeys["root"] # type: ignore
answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn")
print(f"answer 2 is {answer_2}")

View File

@ -1,223 +1,243 @@
import re import re
import sys from typing import Any, Callable, Iterator
from typing import Callable
import numpy as np import numpy as np
from ..base import BaseSolver
VOID, EMPTY, WALL = 0, 1, 2 VOID, EMPTY, WALL = 0, 1, 2
TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL} TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL}
SCORES = {"E": 0, "S": 1, "W": 2, "N": 3} SCORES = {"E": 0, "S": 1, "W": 2, "N": 3}
board_map_s, direction_s = sys.stdin.read().split("\n\n") class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
board_map_s, direction_s = input.split("\n\n")
# board # board
board_lines = board_map_s.splitlines() board_lines = board_map_s.splitlines()
max_line = max(len(line) for line in board_lines) max_line = max(len(line) for line in board_lines)
board = np.array( board = np.array(
[ [
[TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row)) [TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row))
for row in board_map_s.splitlines() for row in board_map_s.splitlines()
] ]
) )
directions = [ directions = [
int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s) int(p1) if p2 else p1
] for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s)
]
# find on each row and column the first and last non-void
row_first_non_void = np.argmax(board != VOID, axis=1)
row_last_non_void = (
board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1
)
col_first_non_void = np.argmax(board != VOID, axis=0)
col_last_non_void = (
board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1
)
# find on each row and column the first and last non-void faces = np.zeros_like(board)
row_first_non_void = np.argmax(board != VOID, axis=1) size = np.gcd(board.shape[0], board.shape[1])
row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1 for row in range(0, board.shape[0], size):
col_first_non_void = np.argmax(board != VOID, axis=0) for col in range(row_first_non_void[row], row_last_non_void[row], size):
col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1 faces[row : row + size, col : col + size] = faces.max() + 1
SIZE = np.gcd(*board.shape)
faces = np.zeros_like(board) # TODO: deduce this from the actual cube...
size = np.gcd(board.shape[0], board.shape[1]) faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
for row in range(0, board.shape[0], size):
for col in range(row_first_non_void[row], row_last_non_void[row], size):
faces[row : row + size, col : col + size] = faces.max() + 1
SIZE = np.gcd(*board.shape) if board.shape == (12, 16): # example
faces_wrap = {
1: {
"W": lambda y, x: (4, 4 + y, "S"), # 3N
"N": lambda y, x: (4, 11 - x, "S"), # 2N
"E": lambda y, x: (11 - y, 15, "W"), # 6E
},
2: {
"W": lambda y, x: (11, 19 - y, "N"), # 6S
"N": lambda y, x: (0, 11 - y, "S"), # 1N
"S": lambda y, x: (11, 11 - x, "N"), # 5S
},
3: {
"N": lambda y, x: (x - 4, 8, "E"), # 1W
"S": lambda y, x: (15 - x, 8, "E"), # 5W
},
4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N
5: {
"W": lambda y, x: (7, 15 - y, "N"), # 3S
"S": lambda y, x: (7, 11 - x, "N"), # 2S
},
6: {
"N": lambda y, x: (19 - x, 11, "W"), # 4E
"E": lambda y, x: (11 - y, 11, "W"), # 1E
"S": lambda y, x: (19 - x, 0, "E"), # 2W
},
}
# TODO: deduce this from the actual cube...
faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]]
if board.shape == (12, 16): # example
faces_wrap = {
1: {
"W": lambda y, x: (4, 4 + y, "S"), # 3N
"N": lambda y, x: (4, 11 - x, "S"), # 2N
"E": lambda y, x: (11 - y, 15, "W"), # 6E
},
2: {
"W": lambda y, x: (11, 19 - y, "N"), # 6S
"N": lambda y, x: (0, 11 - y, "S"), # 1N
"S": lambda y, x: (11, 11 - x, "N"), # 5S
},
3: {
"N": lambda y, x: (x - 4, 8, "E"), # 1W
"S": lambda y, x: (15 - x, 8, "E"), # 5W
},
4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N
5: {
"W": lambda y, x: (7, 15 - y, "N"), # 3S
"S": lambda y, x: (7, 11 - x, "N"), # 2S
},
6: {
"N": lambda y, x: (19 - x, 11, "W"), # 4E
"E": lambda y, x: (11 - y, 11, "W"), # 1E
"S": lambda y, x: (19 - x, 0, "E"), # 2W
},
}
else:
faces_wrap = {
1: {
"W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
"N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W
},
2: {
"N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S
"E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E
"S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E
},
3: {
"W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N
"E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S
},
4: {
"W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W
"N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W
},
5: {
"E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E
"S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E
},
6: {
"W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N
"E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S
"S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N
},
}
def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
if r0 == "E":
return y0, row_first_non_void[y0], r0
elif r0 == "S":
return col_first_non_void[x0], x0, r0
elif r0 == "W":
return y0, row_last_non_void[y0], r0
elif r0 == "N":
return col_last_non_void[x0], x0, r0
assert False
def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
cube = faces[y0, x0]
assert r0 in faces_wrap[cube]
return faces_wrap[cube][r0](y0, x0)
def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]:
y0 = 0
x0 = np.where(board[0] == EMPTY)[0][0]
r0 = "E"
for direction in directions:
if isinstance(direction, int):
while direction > 0:
if r0 == "E":
xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0]
if len(xi):
x0 = x0 + xi[0]
direction = 0
elif (
x0 + direction < board.shape[1]
and board[y0, x0 + direction] == EMPTY
):
x0 = x0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_last_non_void[y0]
direction = 0
else:
direction = direction - (row_last_non_void[y0] - x0) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "S":
yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0]
if len(yi):
y0 = y0 + yi[0]
direction = 0
elif (
y0 + direction < board.shape[0]
and board[y0 + direction, x0] == EMPTY
):
y0 = y0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_last_non_void[x0]
direction = 0
else:
direction = direction - (col_last_non_void[x0] - y0) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "W":
left = max(x0 - direction - 1, 0)
xi = np.where(board[y0, left:x0] == WALL)[0]
if len(xi):
x0 = left + xi[-1] + 1
direction = 0
elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY:
x0 = x0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_first_non_void[y0]
direction = 0
else:
direction = direction - (x0 - row_first_non_void[y0]) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "N":
top = max(y0 - direction - 1, 0)
yi = np.where(board[top:y0, x0] == WALL)[0]
if len(yi):
y0 = top + yi[-1] + 1
direction = 0
elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY:
y0 = y0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_first_non_void[x0]
direction = 0
else:
direction = direction - (y0 - col_first_non_void[x0]) - 1
y0, x0, r0 = y0_t, x0_t, r0_t
else: else:
r0 = { faces_wrap = {
"E": {"L": "N", "R": "S"}, 1: {
"N": {"L": "W", "R": "E"}, "W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W
"W": {"L": "S", "R": "N"}, "N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W
"S": {"L": "E", "R": "W"}, },
}[r0][direction] 2: {
"N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S
"E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E
"S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E
},
3: {
"W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N
"E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S
},
4: {
"W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W
"N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W
},
5: {
"E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E
"S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E
},
6: {
"W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N
"E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S
"S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N
},
}
return y0, x0, r0 def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
if r0 == "E":
return y0, row_first_non_void[y0], r0
elif r0 == "S":
return col_first_non_void[x0], x0, r0
elif r0 == "W":
return y0, row_last_non_void[y0], r0
elif r0 == "N":
return col_last_non_void[x0], x0, r0
assert False
y1, x1, r1 = run(wrap_part_1) def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]:
answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1] cube = faces[y0, x0]
print(f"answer 1 is {answer_1}") assert r0 in faces_wrap[cube]
return faces_wrap[cube][r0](y0, x0)
y2, x2, r2 = run(wrap_part_2) def run(
answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2] wrap: Callable[[int, int, str], tuple[int, int, str]],
print(f"answer 2 is {answer_2}") ) -> tuple[int, int, str]:
y0 = 0
x0 = np.where(board[0] == EMPTY)[0][0]
r0 = "E"
for direction in directions:
if isinstance(direction, int):
while direction > 0:
if r0 == "E":
xi = np.where(
board[y0, x0 + 1 : x0 + direction + 1] == WALL
)[0]
if len(xi):
x0 = x0 + xi[0]
direction = 0
elif (
x0 + direction < board.shape[1]
and board[y0, x0 + direction] == EMPTY
):
x0 = x0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_last_non_void[y0]
direction = 0
else:
direction = (
direction - (row_last_non_void[y0] - x0) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "S":
yi = np.where(
board[y0 + 1 : y0 + direction + 1, x0] == WALL
)[0]
if len(yi):
y0 = y0 + yi[0]
direction = 0
elif (
y0 + direction < board.shape[0]
and board[y0 + direction, x0] == EMPTY
):
y0 = y0 + direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_last_non_void[x0]
direction = 0
else:
direction = (
direction - (col_last_non_void[x0] - y0) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "W":
left = max(x0 - direction - 1, 0)
xi = np.where(board[y0, left:x0] == WALL)[0]
if len(xi):
x0 = left + xi[-1] + 1
direction = 0
elif (
x0 - direction >= 0
and board[y0, x0 - direction] == EMPTY
):
x0 = x0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
x0 = row_first_non_void[y0]
direction = 0
else:
direction = (
direction - (x0 - row_first_non_void[y0]) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t
elif r0 == "N":
top = max(y0 - direction - 1, 0)
yi = np.where(board[top:y0, x0] == WALL)[0]
if len(yi):
y0 = top + yi[-1] + 1
direction = 0
elif (
y0 - direction >= 0
and board[y0 - direction, x0] == EMPTY
):
y0 = y0 - direction
direction = 0
else:
y0_t, x0_t, r0_t = wrap(y0, x0, r0)
if board[y0_t, x0_t] == WALL:
y0 = col_first_non_void[x0]
direction = 0
else:
direction = (
direction - (y0 - col_first_non_void[x0]) - 1
)
y0, x0, r0 = y0_t, x0_t, r0_t
else:
r0 = {
"E": {"L": "N", "R": "S"},
"N": {"L": "W", "R": "E"},
"W": {"L": "S", "R": "N"},
"S": {"L": "E", "R": "W"},
}[r0][direction]
return y0, x0, r0
y1, x1, r1 = run(wrap_part_1)
yield 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
y2, x2, r2 = run(wrap_part_2)
yield 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]

View File

@ -1,6 +1,8 @@
import itertools import itertools
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
Directions = list[ Directions = list[
tuple[ tuple[
@ -18,22 +20,10 @@ DIRECTIONS: Directions = [
def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]: def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]:
ys, xs = {y for y, x in positions}, {x for y, x in positions} ys, xs = {y for y, _x in positions}, {x for _y, x in positions}
return min(ys), min(xs), max(ys), max(xs) return min(ys), min(xs), max(ys), max(xs)
def print_positions(positions: set[tuple[int, int]]):
min_y, min_x, max_y, max_x = min_max_yx(positions)
print(
"\n".join(
"".join(
"#" if (y, x) in positions else "." for x in range(min_x - 1, max_x + 2)
)
for y in range(min_y - 1, max_y + 2)
)
)
def round( def round(
positions: set[tuple[int, int]], positions: set[tuple[int, int]],
directions: Directions, directions: Directions,
@ -69,35 +59,38 @@ def round(
directions.append(directions.pop(0)) directions.append(directions.pop(0))
POSITIONS = { class Solver(BaseSolver):
(i, j) def solve(self, input: str) -> Iterator[Any]:
for i, row in enumerate(sys.stdin.read().splitlines()) POSITIONS = {
for j, col in enumerate(row) (i, j)
if col == "#" for i, row in enumerate(input.splitlines())
} for j, col in enumerate(row)
if col == "#"
}
# === part 1 === # === part 1 ===
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
for r in range(10): for _ in range(10):
round(p1, d1) round(p1, d1)
min_y, min_x, max_y, max_x = min_max_yx(p1) min_y, min_x, max_y, max_x = min_max_yx(p1)
answer_1 = sum( yield sum(
(y, x) not in p1 for y in range(min_y, max_y + 1) for x in range(min_x, max_x + 1) (y, x) not in p1
) for y in range(min_y, max_y + 1)
print(f"answer 1 is {answer_1}") for x in range(min_x, max_x + 1)
)
# === part 2 === # === part 2 ===
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy() p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
answer_2 = 0 answer_2 = 0
while True: while True:
answer_2 += 1 answer_2 += 1
backup = p2.copy() backup = p2.copy()
round(p2, d2) round(p2, d2)
if backup == p2: if backup == p2:
break break
print(f"answer 2 is {answer_2}") yield answer_2

View File

@ -1,98 +1,117 @@
import heapq import heapq
import math import math
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
winds = {
(i - 1, j - 1, lines[i][j])
for i in range(1, len(lines) - 1)
for j in range(1, len(lines[i]) - 1)
if lines[i][j] != "."
}
n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2
CYCLE = math.lcm(n_rows, n_cols)
east_winds = [{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)]
west_winds = [{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)]
north_winds = [
{i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols)
]
south_winds = [
{i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols)
]
def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]): class Solver(BaseSolver):
def heuristic(y: int, x: int) -> int: def solve(self, input: str) -> Iterator[Any]:
return abs(end[0] - y) + abs(end[1] - x) lines = [line.strip() for line in input.splitlines()]
# (distance + heuristic, distance, (start_pos, cycle)) winds = {
queue = [(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))] (i - 1, j - 1, lines[i][j])
visited: set[tuple[tuple[int, int], int]] = set() for i in range(1, len(lines) - 1)
distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {}) for j in range(1, len(lines[i]) - 1)
if lines[i][j] != "."
}
while queue: n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2
_, distance, ((y, x), cycle) = heapq.heappop(queue) CYCLE = math.lcm(n_rows, n_cols)
if ((y, x), cycle) in visited: east_winds = [
continue {j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)
]
west_winds = [
{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)
]
north_winds = [
{i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols)
]
south_winds = [
{i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols)
]
distances[y, x][cycle] = distance def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]):
def heuristic(y: int, x: int) -> int:
return abs(end[0] - y) + abs(end[1] - x)
visited.add(((y, x), cycle)) # (distance + heuristic, distance, (start_pos, cycle))
queue = [
(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))
]
visited: set[tuple[tuple[int, int], int]] = set()
distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {})
if (y, x) == (end[0], end[1]): while queue:
break _, distance, ((y, x), cycle) = heapq.heappop(queue)
for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1): if ((y, x), cycle) in visited:
ty = y + dy
tx = x + dx
n_cycle = (cycle + 1) % CYCLE
if (ty, tx) == end:
heapq.heappush(queue, (distance + 1, distance + 1, ((ty, tx), n_cycle)))
break
if ((ty, tx), n_cycle) in visited:
continue
if (ty, tx) != start and (ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols):
continue
if (ty, tx) != start:
if (ty - n_cycle) % n_rows in south_winds[tx]:
continue
if (ty + n_cycle) % n_rows in north_winds[tx]:
continue
if (tx + n_cycle) % n_cols in west_winds[ty]:
continue
if (tx - n_cycle) % n_cols in east_winds[ty]:
continue continue
heapq.heappush( distances[y, x][cycle] = distance
queue,
((heuristic(ty, tx) + distance + 1, distance + 1, ((ty, tx), n_cycle))),
)
return distances, next(iter(distances[end].values())) visited.add(((y, x), cycle))
if (y, x) == (end[0], end[1]):
break
start = ( for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1):
-1, ty = y + dy
next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1, tx = x + dx
)
end = (
n_rows,
next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1,
)
distances_1, forward_1 = run(start, 0, end) n_cycle = (cycle + 1) % CYCLE
print(f"answer 1 is {forward_1}")
distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start) if (ty, tx) == end:
distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end) heapq.heappush(
print(f"answer 2 is {forward_1 + return_1 + forward_2}") queue, (distance + 1, distance + 1, ((ty, tx), n_cycle))
)
break
if ((ty, tx), n_cycle) in visited:
continue
if (ty, tx) != start and (
ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols
):
continue
if (ty, tx) != start:
if (ty - n_cycle) % n_rows in south_winds[tx]:
continue
if (ty + n_cycle) % n_rows in north_winds[tx]:
continue
if (tx + n_cycle) % n_cols in west_winds[ty]:
continue
if (tx - n_cycle) % n_cols in east_winds[ty]:
continue
heapq.heappush(
queue,
(
(
heuristic(ty, tx) + distance + 1,
distance + 1,
((ty, tx), n_cycle),
)
),
)
return distances, next(iter(distances[end].values()))
start = (
-1,
next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1,
)
end = (
n_rows,
next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1,
)
distances_1, forward_1 = run(start, 0, end)
yield forward_1
distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start)
_distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end)
yield forward_1 + return_1 + forward_2

View File

@ -1,27 +1,28 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
def snafu2number(number: str) -> int: class Solver(BaseSolver):
value = 0 def solve(self, input: str) -> Iterator[Any]:
for c in number: lines = [line.strip() for line in input.splitlines()]
value *= 5
value += coeffs[c]
return value
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
def number2snafu(number: int) -> str: def snafu2number(number: str) -> int:
values = ["0", "1", "2", "=", "-"] value = 0
res = "" for c in number:
while number > 0: value *= 5
mod = number % 5 value += coeffs[c]
res = res + values[mod] return value
number = number // 5 + int(mod >= 3)
return "".join(reversed(res))
def number2snafu(number: int) -> str:
values = ["0", "1", "2", "=", "-"]
res = ""
while number > 0:
mod = number % 5
res = res + values[mod]
number = number // 5 + int(mod >= 3)
return "".join(reversed(res))
answer_1 = number2snafu(sum(map(snafu2number, lines))) yield number2snafu(sum(map(snafu2number, lines)))
print(f"answer 1 is {answer_1}")

View File

@ -1,23 +1,28 @@
import string import string
import sys from typing import Any, Iterator
lines = [line.strip() for line in sys.stdin.readlines()] from ..base import BaseSolver
# extract content of each part
parts = [(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines]
# priorities class Solver(BaseSolver):
priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)} def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# part 1 # extract content of each part
part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2)) parts = [
print(f"answer 1 is {part1}") (set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines
]
# part 2 # priorities
n_per_group = 3 priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)}
part2 = sum(
priorities[c] # part 1
for i in range(0, len(lines), n_per_group) yield sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group])
) # part 2
print(f"answer 2 is {part2}") n_per_group = 3
yield sum(
priorities[c]
for i in range(0, len(lines), n_per_group)
for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group])
)

View File

@ -1,6 +1,6 @@
import sys from typing import Any, Iterator
lines = [line.strip() for line in sys.stdin.readlines()] from ..base import BaseSolver
def make_range(value: str) -> set[int]: def make_range(value: str) -> set[int]:
@ -8,10 +8,13 @@ def make_range(value: str) -> set[int]:
return set(range(int(parts[0]), int(parts[1]) + 1)) return set(range(int(parts[0]), int(parts[1]) + 1))
sections = [tuple(make_range(part) for part in line.split(",")) for line in lines] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections) sections = [
print(f"answer 1 is {answer_1}") tuple(make_range(part) for part in line.split(",")) for line in lines
]
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections) yield sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"answer 1 is {answer_2}") yield sum(bool(s1.intersection(s2)) for s1, s2 in sections)

View File

@ -1,41 +1,43 @@
import copy import copy
import sys from typing import Any, Iterator
blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n")) from ..base import BaseSolver
blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the class Solver(BaseSolver):
# form of '[X] ' (including the trailing space) def solve(self, input: str) -> Iterator[Any]:
# blocks_s, moves_s = (part.splitlines() for part in input.split("\n\n"))
for block in blocks_s[-2::-1]:
for stack, index in zip(blocks, range(0, len(block), 4)):
crate = block[index + 1 : index + 2].strip()
if crate: blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()}
blocks[stack].append(crate)
# part 1 - deep copy for part 2 # this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the
blocks_1 = copy.deepcopy(blocks) # form of '[X] ' (including the trailing space)
#
for block in blocks_s[-2::-1]:
for stack, index in zip(blocks, range(0, len(block), 4)):
crate = block[index + 1 : index + 2].strip()
for move in moves_s: if crate:
_, count_s, _, from_, _, to_ = move.strip().split() blocks[stack].append(crate)
for _i in range(int(count_s)): # part 1 - deep copy for part 2
blocks_1[to_].append(blocks_1[from_].pop()) blocks_1 = copy.deepcopy(blocks)
# part 2 for move in moves_s:
blocks_2 = copy.deepcopy(blocks) _, count_s, _, from_, _, to_ = move.strip().split()
for move in moves_s: for _i in range(int(count_s)):
_, count_s, _, from_, _, to_ = move.strip().split() blocks_1[to_].append(blocks_1[from_].pop())
count = int(count_s)
blocks_2[to_].extend(blocks_2[from_][-count:]) # part 2
del blocks_2[from_][-count:] blocks_2 = copy.deepcopy(blocks)
answer_1 = "".join(s[-1] for s in blocks_1.values()) for move in moves_s:
print(f"answer 1 is {answer_1}") _, count_s, _, from_, _, to_ = move.strip().split()
count = int(count_s)
answer_2 = "".join(s[-1] for s in blocks_2.values()) blocks_2[to_].extend(blocks_2[from_][-count:])
print(f"answer 2 is {answer_2}") del blocks_2[from_][-count:]
yield "".join(s[-1] for s in blocks_1.values())
yield "".join(s[-1] for s in blocks_2.values())

View File

@ -1,4 +1,6 @@
import sys from typing import Any, Iterator
from ..base import BaseSolver
def index_of_first_n_differents(data: str, n: int) -> int: def index_of_first_n_differents(data: str, n: int) -> int:
@ -8,8 +10,7 @@ def index_of_first_n_differents(data: str, n: int) -> int:
return -1 return -1
data = sys.stdin.read().strip() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
yield index_of_first_n_differents(input, 4)
print(f"answer 1 is {index_of_first_n_differents(data, 4)}") yield index_of_first_n_differents(input, 14)
print(f"answer 2 is {index_of_first_n_differents(data, 14)}")

View File

@ -1,80 +1,81 @@
import sys
from pathlib import Path from pathlib import Path
from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
# we are going to use Path to create path and go up/down in the file tree since it
# implements everything we need
#
# we can use .resolve() to get normalized path, although this will add C:\ to all paths
# on Windows but that is not an issue since only the sizes matter
#
# mapping from path to list of files or directories
trees: dict[Path, list[Path]] = {}
# mapping from paths to either size (for file) or -1 for directory
sizes: dict[Path, int] = {}
# first line must be a cd otherwise we have no idea where we are
assert lines[0].startswith("$ cd")
base_path = Path(lines[0].strip("$").split()[1]).resolve()
cur_path = base_path
trees[cur_path] = []
sizes[cur_path] = -1
for line in lines[1:]:
# command
if line.startswith("$"):
parts = line.strip("$").strip().split()
command = parts[0]
if command == "cd":
cur_path = cur_path.joinpath(parts[1]).resolve()
# just initialize the lis of files if not already done
if cur_path not in trees:
trees[cur_path] = []
else:
# nothing to do here
pass
# fill the current path
else:
parts = line.split()
name: str = parts[1]
if line.startswith("dir"):
size = -1
else:
size = int(parts[0])
path = cur_path.joinpath(name)
trees[cur_path].append(path)
sizes[path] = size
def compute_size(path: Path) -> int: class Solver(BaseSolver):
size = sizes[path] def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
if size >= 0: # we are going to use Path to create path and go up/down in the file tree since it
return size # implements everything we need
#
# we can use .resolve() to get normalized path, although this will add C:\ to all paths
# on Windows but that is not an issue since only the sizes matter
#
return sum(compute_size(sub) for sub in trees[path]) # mapping from path to list of files or directories
trees: dict[Path, list[Path]] = {}
# mapping from paths to either size (for file) or -1 for directory
sizes: dict[Path, int] = {}
acc_sizes = {path: compute_size(path) for path in trees} # first line must be a cd otherwise we have no idea where we are
assert lines[0].startswith("$ cd")
base_path = Path(lines[0].strip("$").split()[1]).resolve()
cur_path = base_path
# part 1 trees[cur_path] = []
answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000) sizes[cur_path] = -1
print(f"answer 1 is {answer_1}")
# part 2 for line in lines[1:]:
total_space = 70_000_000 # command
update_space = 30_000_000 if line.startswith("$"):
free_space = total_space - acc_sizes[base_path] parts = line.strip("$").strip().split()
command = parts[0]
to_free_space = update_space - free_space if command == "cd":
cur_path = cur_path.joinpath(parts[1]).resolve()
answer_2 = min(size for size in acc_sizes.values() if size >= to_free_space) # just initialize the lis of files if not already done
print(f"answer 2 is {answer_2}") if cur_path not in trees:
trees[cur_path] = []
else:
# nothing to do here
pass
# fill the current path
else:
parts = line.split()
name: str = parts[1]
if line.startswith("dir"):
size = -1
else:
size = int(parts[0])
path = cur_path.joinpath(name)
trees[cur_path].append(path)
sizes[path] = size
def compute_size(path: Path) -> int:
size = sizes[path]
if size >= 0:
return size
return sum(compute_size(sub) for sub in trees[path])
acc_sizes = {path: compute_size(path) for path in trees}
# part 1
yield sum(size for size in acc_sizes.values() if size <= 100_000)
# part 2
total_space = 70_000_000
update_space = 30_000_000
free_space = total_space - acc_sizes[base_path]
to_free_space = update_space - free_space
yield min(size for size in acc_sizes.values() if size >= to_free_space)

View File

@ -1,53 +1,54 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
trees = np.array([[int(x) for x in row] for row in lines])
# answer 1 class Solver(BaseSolver):
highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1 def solve(self, input: str) -> Iterator[Any]:
highest_trees[1:-1, 1:-1] = [ lines = [line.strip() for line in input.splitlines()]
[
[ trees = np.array([[int(x) for x in row] for row in lines])
trees[:i, j].max(),
trees[i + 1 :, j].max(), # answer 1
trees[i, :j].max(), highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1
trees[i, j + 1 :].max(), highest_trees[1:-1, 1:-1] = [
[
[
trees[:i, j].max(),
trees[i + 1 :, j].max(),
trees[i, :j].max(),
trees[i, j + 1 :].max(),
]
for j in range(1, trees.shape[1] - 1)
]
for i in range(1, trees.shape[0] - 1)
] ]
for j in range(1, trees.shape[1] - 1)
]
for i in range(1, trees.shape[0] - 1)
]
answer_1 = (highest_trees.min(axis=2) < trees).sum() yield (highest_trees.min(axis=2) < trees).sum()
print(f"answer 1 is {answer_1}")
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int:
w = np.where(row_of_trees >= value)[0]
def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int: if not w.size:
w = np.where(row_of_trees >= value)[0] return len(row_of_trees)
if not w.size: return w[0] + 1
return len(row_of_trees)
return w[0] + 1 # answer 2
v_distances = np.zeros(trees.shape + (4,), dtype=int)
v_distances[1:-1, 1:-1, :] = [
# answer 2 [
v_distances = np.zeros(trees.shape + (4,), dtype=int) [
v_distances[1:-1, 1:-1, :] = [ viewing_distance(trees[i - 1 :: -1, j], trees[i, j]),
[ viewing_distance(trees[i, j - 1 :: -1], trees[i, j]),
[ viewing_distance(trees[i, j + 1 :], trees[i, j]),
viewing_distance(trees[i - 1 :: -1, j], trees[i, j]), viewing_distance(trees[i + 1 :, j], trees[i, j]),
viewing_distance(trees[i, j - 1 :: -1], trees[i, j]), ]
viewing_distance(trees[i, j + 1 :], trees[i, j]), for j in range(1, trees.shape[1] - 1)
viewing_distance(trees[i + 1 :, j], trees[i, j]), ]
for i in range(1, trees.shape[0] - 1)
] ]
for j in range(1, trees.shape[1] - 1) yield np.prod(v_distances, axis=2).max()
]
for i in range(1, trees.shape[0] - 1)
]
answer_2 = np.prod(v_distances, axis=2).max()
print(f"answer 2 is {answer_2}")

View File

@ -1,7 +1,10 @@
import sys import itertools as it
from typing import Any, Iterator
import numpy as np import numpy as np
from ..base import BaseSolver
def move(head: tuple[int, int], command: str) -> tuple[int, int]: def move(head: tuple[int, int], command: str) -> tuple[int, int]:
h_col, h_row = head h_col, h_row = head
@ -43,17 +46,14 @@ def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]:
return visited return visited
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = [line.strip() for line in input.splitlines()]
# flatten the commands # flatten the commands
commands: list[str] = [] commands = list(
for line in lines: it.chain(*(p[0] * int(p[1]) for line in lines if (p := line.split())))
d, c = line.split() )
commands.extend(d * int(c))
yield len(set(run(commands, n_blocks=2)))
visited_1 = run(commands, n_blocks=2) yield len(set(run(commands, n_blocks=10)))
print(f"answer 1 is {len(set(visited_1))}")
visited_2 = run(commands, n_blocks=10)
print(f"answer 2 is {len(set(visited_2))}")

View File

@ -1,27 +1,9 @@
import sys from typing import Any, Iterator
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
lookups_1 = {str(d): d for d in range(1, 10)}
lookups_2 = lookups_1 | {
d: i + 1
for i, d in enumerate(
(
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
)
}
def find_values(lookups: dict[str, int]) -> list[int]: def find_values(lines: list[str], lookups: dict[str, int]) -> list[int]:
values: list[int] = [] values: list[int] = []
for line in filter(bool, lines): for line in filter(bool, lines):
@ -41,5 +23,27 @@ def find_values(lookups: dict[str, int]) -> list[int]:
return values return values
print(f"answer 1 is {sum(find_values(lookups_1))}") class Solver(BaseSolver):
print(f"answer 2 is {sum(find_values(lookups_2))}") def solve(self, input: str) -> Iterator[Any]:
lookups_1 = {str(d): d for d in range(1, 10)}
lookups_2 = lookups_1 | {
d: i + 1
for i, d in enumerate(
(
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
)
}
lines = input.splitlines()
yield sum(find_values(lines, lookups_1))
yield sum(find_values(lines, lookups_2))

View File

@ -1,100 +1,99 @@
import os from typing import Any, Iterator, Literal, cast
import sys
from typing import Literal, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"] Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
lines: list[list[Symbol]] = [
[cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines()
]
# find starting point class Solver(BaseSolver):
si, sj = next( def solve(self, input: str) -> Iterator[Any]:
(i, j) lines: list[list[Symbol]] = [
for i in range(len(lines)) [cast(Symbol, symbol) for symbol in line] for line in input.splitlines()
for j in range(len(lines[0])) ]
if lines[i][j] == "S"
)
# find one of the two outputs # find starting point
ni, nj = si, sj si, sj = next(
for ni, nj, chars in ( (i, j)
(si - 1, sj, "|7F"), for i in range(len(lines))
(si + 1, sj, "|LJ"), for j in range(len(lines[0]))
(si, sj - 1, "-LF"), if lines[i][j] == "S"
(si, sj + 1, "-J7"), )
):
if lines[ni][nj] in chars:
break
# part 1 - find the loop (re-used in part 2) # find one of the two outputs
loop = [(si, sj), (ni, nj)] ni, nj = si, sj
while True: for ni, nj, chars in (
pi, pj = loop[-2] (si - 1, sj, "|7F"),
i, j = loop[-1] (si + 1, sj, "|LJ"),
(si, sj - 1, "-LF"),
(si, sj + 1, "-J7"),
):
if lines[ni][nj] in chars:
break
sym = lines[i][j] # part 1 - find the loop (re-used in part 2)
loop = [(si, sj), (ni, nj)]
while True:
pi, pj = loop[-2]
i, j = loop[-1]
if sym == "|" and pi > i or sym in "JL" and pi == i: sym = lines[i][j]
i -= 1
elif sym == "|" and pi < i or sym in "7F" and pi == i:
i += 1
elif sym == "-" and pj > j or sym in "J7" and pj == j:
j -= 1
elif sym == "-" and pj < j or sym in "LF" and pj == j:
j += 1
if (i, j) == (si, sj): if sym == "|" and pi > i or sym in "JL" and pi == i:
break i -= 1
elif sym == "|" and pi < i or sym in "7F" and pi == i:
i += 1
elif sym == "-" and pj > j or sym in "J7" and pj == j:
j -= 1
elif sym == "-" and pj < j or sym in "LF" and pj == j:
j += 1
loop.append((i, j))
answer_1 = len(loop) // 2
print(f"answer 1 is {answer_1}")
# part 2
# replace S by an appropriate character for the loop below
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1]
di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = {
(0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"},
(0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"},
(1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"},
(-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
}
lines[si][sj] = mapping[di1, dj1][di2, dj2]
# find the points inside the loop using an adaptation of ray casting for a discrete
# grid (https://stackoverflow.com/a/218081/2666289)
#
# use a set for faster '... in loop' check
#
loop_s = set(loop)
inside: set[tuple[int, int]] = set()
for i in range(len(lines)):
cnt = 0
for j in range(len(lines[0])):
if (i, j) not in loop_s and cnt % 2 == 1:
inside.add((i, j))
if (i, j) in loop_s and lines[i][j] in "|LJ":
cnt += 1
if VERBOSE:
for i in range(len(lines)):
for j in range(len(lines[0])):
if (i, j) == (si, sj): if (i, j) == (si, sj):
print("\033[91mS\033[0m", end="") break
elif (i, j) in loop:
print(lines[i][j], end="")
elif (i, j) in inside:
print("\033[92mI\033[0m", end="")
else:
print(".", end="")
print()
answer_2 = len(inside) loop.append((i, j))
print(f"answer 2 is {answer_2}")
yield len(loop) // 2
# part 2
# replace S by an appropriate character for the loop below
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1]
di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = {
(0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"},
(0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"},
(1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"},
(-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
}
lines[si][sj] = mapping[di1, dj1][di2, dj2]
# find the points inside the loop using an adaptation of ray casting for a discrete
# grid (https://stackoverflow.com/a/218081/2666289)
#
# use a set for faster '... in loop' check
#
loop_s = set(loop)
inside: set[tuple[int, int]] = set()
for i in range(len(lines)):
cnt = 0
for j in range(len(lines[0])):
if (i, j) not in loop_s and cnt % 2 == 1:
inside.add((i, j))
if (i, j) in loop_s and lines[i][j] in "|LJ":
cnt += 1
if self.files:
rows = [["." for _j in range(len(lines[0]))] for _i in range(len(lines))]
rows[si][sj] = "\033[91mS\033[0m"
for i, j in loop:
rows[i][j] = lines[i][j]
for i, j in inside:
rows[i][j] = "\033[92mI\033[0m"
self.files.create(
"output.txt", "\n".join("".join(row) for row in rows).encode(), True
)
yield len(inside)

View File

@ -1,41 +1,42 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
data = np.array([[c == "#" for c in line] for line in lines])
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
def compute_total_distance(expansion: int) -> int: class Solver(BaseSolver):
distances: list[int] = [] def solve(self, input: str) -> Iterator[Any]:
for g1 in range(len(galaxies_y)): lines = input.splitlines()
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
for g2 in range(g1 + 1, len(galaxies_y)):
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
dx = sum( data = np.array([[c == "#" for c in line] for line in lines])
1 + (expansion - 1) * (x in columns)
for x in range(min(x1, x2), max(x1, x2))
)
dy = sum(
1 + (expansion - 1) * (y in rows)
for y in range(min(y1, y2), max(y1, y2))
)
distances.append(dx + dy) rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
return sum(distances) columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
# part 1 def compute_total_distance(expansion: int) -> int:
answer_1 = compute_total_distance(2) distances: list[int] = []
print(f"answer 1 is {answer_1}") for g1 in range(len(galaxies_y)):
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
for g2 in range(g1 + 1, len(galaxies_y)):
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
# part 2 dx = sum(
answer_2 = compute_total_distance(1000000) 1 + (expansion - 1) * (x in columns)
print(f"answer 2 is {answer_2}") for x in range(min(x1, x2), max(x1, x2))
)
dy = sum(
1 + (expansion - 1) * (y in rows)
for y in range(min(y1, y2), max(y1, y2))
)
distances.append(dx + dy)
return sum(distances)
# part 1
yield compute_total_distance(2)
# part 2
yield compute_total_distance(1000000)

View File

@ -1,9 +1,7 @@
import os
import sys
from functools import lru_cache from functools import lru_cache
from typing import Iterable from typing import Any, Iterable, Iterator
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
@lru_cache @lru_cache
@ -77,31 +75,29 @@ def compute_possible_arrangements(
) )
def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int: class Solver(BaseSolver):
count = 0 def compute_all_possible_arrangements(
self, lines: Iterable[str], repeat: int
) -> int:
count = 0
if VERBOSE: for i_line, line in enumerate(lines):
from tqdm import tqdm self.logger.info(f"processing line {i_line}: {line}...")
parts = line.split(" ")
count += compute_possible_arrangements(
tuple(
filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))
),
tuple(int(c) for c in parts[1].split(",")) * repeat,
)
lines = tqdm(lines) return count
for line in lines: def solve(self, input: str) -> Iterator[Any]:
parts = line.split(" ") lines = input.splitlines()
count += compute_possible_arrangements(
tuple(filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))),
tuple(int(c) for c in parts[1].split(",")) * repeat,
)
return count # part 1
yield self.compute_all_possible_arrangements(lines, 1)
# part 2
lines = sys.stdin.read().splitlines() yield self.compute_all_possible_arrangements(lines, 5)
# part 1
answer_1 = compute_all_possible_arrangements(lines, 1)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = compute_all_possible_arrangements(lines, 5)
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Callable, Iterator, Literal
from typing import Callable, Literal
from ..base import BaseSolver
def split(block: list[str], axis: Literal[0, 1], count: int) -> int: def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
@ -25,19 +26,18 @@ def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
return 0 return 0
blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")] class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
blocks = [block.splitlines() for block in input.split("\n\n")]
# part 1
yield sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks
)
# part 1 # part 2
answer_1 = sum( yield sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0) split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks for block in blocks
) )
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks
)
print(f"answer 2 is {answer_2}")

View File

@ -1,10 +1,9 @@
import sys from typing import Any, Iterator, TypeAlias
from typing import TypeAlias
from ..base import BaseSolver
RockGrid: TypeAlias = list[list[str]] RockGrid: TypeAlias = list[list[str]]
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
def slide_rocks_top(rocks: RockGrid) -> RockGrid: def slide_rocks_top(rocks: RockGrid) -> RockGrid:
top = [0 if c == "." else 1 for c in rocks[0]] top = [0 if c == "." else 1 for c in rocks[0]]
@ -34,35 +33,38 @@ def cycle(rocks: RockGrid) -> RockGrid:
return rocks return rocks
rocks = slide_rocks_top([[c for c in r] for r in rocks0]) class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
rocks0 = [list(line) for line in input.splitlines()]
# part 1 rocks = slide_rocks_top([[c for c in r] for r in rocks0])
answer_1 = sum(
(len(rocks) - i) * sum(1 for c in row if c == "O") for i, row in enumerate(rocks)
)
print(f"answer 1 is {answer_1}")
# part 2 # part 1
rocks = rocks0 yield sum(
(len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(rocks)
)
N = 1000000000 # part 2
cycles: list[RockGrid] = [] rocks = rocks0
i_cycle: int = -1
for i_cycle in range(N):
rocks = cycle(rocks)
if any(rocks == c for c in cycles): N = 1000000000
break cycles: list[RockGrid] = []
i_cycle: int = -1
for i_cycle in range(N):
rocks = cycle(rocks)
cycles.append([[c for c in r] for r in rocks]) if any(rocks == c for c in cycles):
break
cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i])) cycles.append([[c for c in r] for r in rocks])
cycle_length = i_cycle - cycle_start
ci = cycle_start + (N - cycle_start) % cycle_length - 1 cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i]))
cycle_length = i_cycle - cycle_start
answer_2 = sum( ci = cycle_start + (N - cycle_start) % cycle_length - 1
(len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(cycles[ci]) yield sum(
) (len(rocks) - i) * sum(1 for c in row if c == "O")
print(f"answer 2 is {answer_2}") for i, row in enumerate(cycles[ci])
)

View File

@ -1,31 +1,33 @@
import sys
from functools import reduce from functools import reduce
from typing import Any, Iterator
steps = sys.stdin.read().strip().split(",") from ..base import BaseSolver
def _hash(s: str) -> int: def _hash(s: str) -> int:
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0) return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
# part 1 class Solver(BaseSolver):
answer_1 = sum(map(_hash, steps)) def solve(self, input: str) -> Iterator[Any]:
print(f"answer 1 is {answer_1}") steps = input.split(",")
# part 2 # part 1
boxes: list[dict[str, int]] = [{} for _ in range(256)] yield sum(map(_hash, steps))
for step in steps: # part 2
if (i := step.find("=")) >= 0: boxes: list[dict[str, int]] = [{} for _ in range(256)]
label, length = step[:i], int(step[i + 1 :])
boxes[_hash(label)][label] = length
else:
label = step[:-1]
boxes[_hash(label)].pop(label, None)
answer_2 = sum( for step in steps:
i_box * i_lens * length if (i := step.find("=")) >= 0:
for i_box, box in enumerate(boxes, start=1) label, length = step[:i], int(step[i + 1 :])
for i_lens, length in enumerate(box.values(), start=1) boxes[_hash(label)][label] = length
) else:
print(f"answer 2 is {answer_2}") label = step[:-1]
boxes[_hash(label)].pop(label, None)
yield sum(
i_box * i_lens * length
for i_box, box in enumerate(boxes, start=1)
for i_lens, length in enumerate(box.values(), start=1)
)

View File

@ -1,8 +1,6 @@
import os from typing import Any, Iterator, Literal, TypeAlias, cast
import sys
from typing import Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"] CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
Direction: TypeAlias = Literal["R", "L", "U", "D"] Direction: TypeAlias = Literal["R", "L", "U", "D"]
@ -78,33 +76,38 @@ def propagate(
return beams return beams
layout: list[list[CellType]] = [ class Solver(BaseSolver):
[cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines() def solve(self, input: str) -> Iterator[Any]:
] layout: list[list[CellType]] = [
[cast(CellType, col) for col in row] for row in input.splitlines()
]
beams = propagate(layout, (0, 0), "R")
beams = propagate(layout, (0, 0), "R") if self.files:
self.files.create(
"beams.txt",
"\n".join(
"".join("#" if col else "." for col in row) for row in beams
).encode(),
True,
)
if VERBOSE: # part 1
print("\n".join(["".join("#" if col else "." for col in row) for row in beams])) yield sum(sum(map(bool, row)) for row in beams)
# part 1 # part 2
answer_1 = sum(sum(map(bool, row)) for row in beams) n_rows, n_cols = len(layout), len(layout[0])
print(f"answer 1 is {answer_1}") cases: list[tuple[tuple[int, int], Direction]] = []
# part 2 for row in range(n_rows):
n_rows, n_cols = len(layout), len(layout[0]) cases.append(((row, 0), "R"))
cases: list[tuple[tuple[int, int], Direction]] = [] cases.append(((row, n_cols - 1), "L"))
for col in range(n_cols):
cases.append(((0, col), "D"))
cases.append(((n_rows - 1, col), "U"))
for row in range(n_rows): yield max(
cases.append(((row, 0), "R")) sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
cases.append(((row, n_cols - 1), "L")) for start, direction in cases
for col in range(n_cols): )
cases.append(((0, col), "D"))
cases.append(((n_rows - 1, col), "U"))
answer_2 = max(
sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
for start, direction in cases
)
print(f"answer 2 is {answer_2}")

View File

@ -1,13 +1,11 @@
from __future__ import annotations from __future__ import annotations
import heapq import heapq
import os
import sys
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal, TypeAlias from typing import Any, Iterator, Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
Direction: TypeAlias = Literal[">", "<", "^", "v"] Direction: TypeAlias = Literal[">", "<", "^", "v"]
@ -32,202 +30,209 @@ MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
} }
def print_shortest_path( class Solver(BaseSolver):
grid: list[list[int]], def print_shortest_path(
target: tuple[int, int], self,
per_cell: dict[tuple[int, int], list[tuple[Label, int]]], name: str,
): grid: list[list[int]],
assert len(per_cell[target]) == 1 target: tuple[int, int],
label = per_cell[target][0][0] per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
):
if not self.files:
return
path: list[Label] = [] assert len(per_cell[target]) == 1
while True: label = per_cell[target][0][0]
path.insert(0, label)
if label.parent is None:
break
label = label.parent
p_grid = [[str(c) for c in r] for r in grid] path: list[Label] = []
while True:
path.insert(0, label)
if label.parent is None:
break
label = label.parent
for i in range(len(grid)): p_grid = [[str(c) for c in r] for r in grid]
for j in range(len(grid[0])):
if per_cell[i, j]:
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
prev_label = path[0] for i in range(len(grid)):
for label in path[1:]: for j in range(len(grid[0])):
for r in range( if per_cell[i, j]:
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1 p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
):
for c in range( prev_label = path[0]
min(prev_label.col, label.col), for label in path[1:]:
max(prev_label.col, label.col) + 1, for r in range(
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
): ):
if (r, c) != (prev_label.row, prev_label.col): for c in range(
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m" min(prev_label.col, label.col),
max(prev_label.col, label.col) + 1,
):
if (r, c) != (prev_label.row, prev_label.col):
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
p_grid[label.row][label.col] = f"\033[91m{grid[label.row][label.col]}\033[0m" p_grid[label.row][label.col] = (
f"\033[91m{grid[label.row][label.col]}\033[0m"
)
prev_label = label prev_label = label
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m" p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
print("\n".join("".join(row) for row in p_grid)) self.files.create(
name, "\n".join("".join(row) for row in p_grid).encode(), True
)
def shortest_many_paths(self, grid: list[list[int]]) -> dict[tuple[int, int], int]:
n_rows, n_cols = len(grid), len(grid[0])
def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]: visited: dict[tuple[int, int], tuple[Label, int]] = {}
n_rows, n_cols = len(grid), len(grid[0])
visited: dict[tuple[int, int], tuple[Label, int]] = {} queue: list[tuple[int, Label]] = [
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
]
queue: list[tuple[int, Label]] = [ while queue and len(visited) != n_rows * n_cols:
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0)) distance, label = heapq.heappop(queue)
]
while queue and len(visited) != n_rows * n_cols: if (label.row, label.col) in visited:
distance, label = heapq.heappop(queue)
if (label.row, label.col) in visited:
continue
visited[label.row, label.col] = (label, distance)
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
if label.direction == i_direction:
continue
else:
row, col = (label.row + c_row, label.col + c_col)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue continue
heapq.heappush( visited[label.row, label.col] = (label, distance)
queue,
( for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
if label.direction == i_direction:
continue
else:
row, col = (label.row + c_row, label.col + c_col)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
heapq.heappush(
queue,
(
distance
+ sum(
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[row][col],
Label(
row=row,
col=col,
direction=direction,
count=0,
parent=label,
),
),
)
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
def shortest_path(
self,
name: str,
grid: list[list[int]],
min_straight: int,
max_straight: int,
lower_bounds: dict[tuple[int, int], int],
) -> int:
n_rows, n_cols = len(grid), len(grid[0])
target = (len(grid) - 1, len(grid[0]) - 1)
# for each tuple (row, col, direction, count), the associated label when visited
visited: dict[tuple[int, int, str, int], Label] = {}
# list of all visited labels for a cell (with associated distance)
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
# need to add two start labels, otherwise one of the two possible direction will
# not be possible
queue: list[tuple[int, int, Label]] = [
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
]
while queue:
_, distance, label = heapq.heappop(queue)
if (label.row, label.col, label.direction, label.count) in visited:
continue
visited[label.row, label.col, label.direction, label.count] = label
per_cell[label.row, label.col].append((label, distance))
if (label.row, label.col) == target:
break
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
# cannot move in the opposite direction
if label.direction == i_direction:
continue
# other direction, move 'min_straight' in the new direction
elif label.direction != direction:
row, col, count = (
label.row + min_straight * c_row,
label.col + min_straight * c_col,
min_straight,
)
# same direction, too many count
elif label.count == max_straight:
continue
# same direction, keep going and increment count
else:
row, col, count = (
label.row + c_row,
label.col + c_col,
label.count + 1,
)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
distance_to = (
distance distance
+ sum( + sum(
grid[r][c] grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1) for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1) for c in range(min(col, label.col), max(col, label.col) + 1)
) )
- grid[row][col], - grid[label.row][label.col]
Label( )
row=row,
col=col, heapq.heappush(
direction=direction, queue,
count=0, (
parent=label, distance_to + lower_bounds[row, col],
distance_to,
Label(
row=row,
col=col,
direction=direction,
count=count,
parent=label,
),
), ),
),
)
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
def shortest_path(
grid: list[list[int]],
min_straight: int,
max_straight: int,
lower_bounds: dict[tuple[int, int], int],
) -> int:
n_rows, n_cols = len(grid), len(grid[0])
target = (len(grid) - 1, len(grid[0]) - 1)
# for each tuple (row, col, direction, count), the associated label when visited
visited: dict[tuple[int, int, str, int], Label] = {}
# list of all visited labels for a cell (with associated distance)
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
# need to add two start labels, otherwise one of the two possible direction will
# not be possible
queue: list[tuple[int, int, Label]] = [
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
]
while queue:
_, distance, label = heapq.heappop(queue)
if (label.row, label.col, label.direction, label.count) in visited:
continue
visited[label.row, label.col, label.direction, label.count] = label
per_cell[label.row, label.col].append((label, distance))
if (label.row, label.col) == target:
break
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
# cannot move in the opposite direction
if label.direction == i_direction:
continue
# other direction, move 'min_straight' in the new direction
elif label.direction != direction:
row, col, count = (
label.row + min_straight * c_row,
label.col + min_straight * c_col,
min_straight,
) )
# same direction, too many count self.print_shortest_path(f"shortest-path_{name}.txt", grid, target, per_cell)
elif label.count == max_straight:
continue
# same direction, keep going and increment count return per_cell[target][0][1]
else:
row, col, count = (
label.row + c_row,
label.col + c_col,
label.count + 1,
)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
distance_to = ( def solve(self, input: str) -> Iterator[Any]:
distance data = [[int(c) for c in r] for r in input.splitlines()]
+ sum( estimates = self.shortest_many_paths(data)
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[label.row][label.col]
)
heapq.heappush( # part 1
queue, yield self.shortest_path("answer_1", data, 1, 3, lower_bounds=estimates)
(
distance_to + lower_bounds[row, col],
distance_to,
Label(
row=row,
col=col,
direction=direction,
count=count,
parent=label,
),
),
)
if VERBOSE: # part 2
print_shortest_path(grid, target, per_cell) yield self.shortest_path("answer_2", data, 4, 10, lower_bounds=estimates)
return per_cell[target][0][1]
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()]
estimates = shortest_many_paths(data)
# part 1
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates)
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Iterator, Literal, TypeAlias, cast
from typing import Literal, TypeAlias, cast
from ..base import BaseSolver
Direction: TypeAlias = Literal["R", "L", "U", "D"] Direction: TypeAlias = Literal["R", "L", "U", "D"]
@ -33,22 +34,23 @@ def polygon(values: list[tuple[Direction, int]]) -> tuple[list[tuple[int, int]],
return corners, perimeter return corners, perimeter
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1
yield area(
*polygon(
[(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines]
)
)
# part 1 # part 2
answer_1 = area( yield area(
*polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines]) *polygon(
) [
print(f"answer 1 is {answer_1}") (DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
for line in lines
# part 2 ]
answer_2 = area( )
*polygon( )
[
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
for line in lines
]
)
)
print(f"answer 2 is {answer_2}")

View File

@ -1,13 +1,8 @@
import logging
import operator import operator
import os
import sys
from math import prod from math import prod
from typing import Literal, TypeAlias, cast from typing import Any, Iterator, Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
Category: TypeAlias = Literal["x", "m", "a", "s"] Category: TypeAlias = Literal["x", "m", "a", "s"]
Part: TypeAlias = dict[Category, int] Part: TypeAlias = dict[Category, int]
@ -22,119 +17,118 @@ Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
Workflow: TypeAlias = list[tuple[Check, str]] Workflow: TypeAlias = list[tuple[Check, str]]
def accept(workflows: dict[str, Workflow], part: Part) -> bool: class Solver(BaseSolver):
workflow = "in" def accept(self, workflows: dict[str, Workflow], part: Part) -> bool:
decision: bool | None = None workflow = "in"
decision: bool | None = None
while decision is None: while decision is None:
for check, target in workflows[workflow]: for check, target in workflows[workflow]:
passed = check is None passed = check is None
if check is not None: if check is not None:
category, sense, value = check category, sense, value = check
passed = OPERATORS[sense](part[category], value) passed = OPERATORS[sense](part[category], value)
if passed: if passed:
if target in workflows: if target in workflows:
workflow = target workflow = target
else: else:
decision = target == "A" decision = target == "A"
break break
return decision return decision
def propagate(self, workflows: dict[str, Workflow], start: PartWithBounds) -> int:
def _fmt(meta: PartWithBounds) -> str:
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}"
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int: def transfer_or_accept(
def _fmt(meta: PartWithBounds) -> str: target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
return "{" + ", ".join(f"{k}={v}" for k, v in meta.items()) + "}" ) -> int:
count = 0
def transfer_or_accept( if target in workflows:
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]] self.logger.info(f" transfer to {target}")
) -> int: queue.append((meta, target))
count = 0 elif target == "A":
if target in workflows: count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" transfer to {target}") self.logger.info(f" accepted ({count})")
queue.append((meta, target))
elif target == "A":
count = prod((high - low + 1) for low, high in meta.values())
logging.info(f" accepted ({count})")
else:
logging.info(" rejected")
return count
accepted = 0
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
n_iterations = 0
while queue:
n_iterations += 1
meta, workflow = queue.pop()
logging.info(f"{workflow}: {_fmt(meta)}")
for check, target in workflows[workflow]:
if check is None:
logging.info(" end-of-workflow")
accepted += transfer_or_accept(target, meta, queue)
continue
category, sense, value = check
bounds, op = meta[category], OPERATORS[sense]
logging.info(f" checking {_fmt(meta)} against {category} {sense} {value}")
if not op(bounds[0], value) and not op(bounds[1], value):
logging.info(" reject, always false")
continue
if op(meta[category][0], value) and op(meta[category][1], value):
logging.info(" accept, always true")
accepted += transfer_or_accept(target, meta, queue)
break
meta2 = meta.copy()
low, high = meta[category]
if sense == "<":
meta[category], meta2[category] = (value, high), (low, value - 1)
else: else:
meta[category], meta2[category] = (low, value), (value + 1, high) self.logger.info(" rejected")
logging.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}") return count
accepted += transfer_or_accept(target, meta2, queue) accepted = 0
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
logging.info(f"run took {n_iterations} iterations") n_iterations = 0
return accepted
while queue:
n_iterations += 1
meta, workflow = queue.pop()
self.logger.info(f"{workflow}: {_fmt(meta)}")
for check, target in workflows[workflow]:
if check is None:
self.logger.info(" end-of-workflow")
accepted += transfer_or_accept(target, meta, queue)
continue
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n") category, sense, value = check
bounds, op = meta[category], OPERATORS[sense]
workflows: dict[str, Workflow] = {} self.logger.info(
for workflow_s in workflows_s.split("\n"): f" checking {_fmt(meta)} against {category} {sense} {value}"
name, block_s = workflow_s.split("{") )
workflows[name] = []
for block in block_s[:-1].split(","): if not op(bounds[0], value) and not op(bounds[1], value):
check: Check self.logger.info(" reject, always false")
if (i := block.find(":")) >= 0: continue
check = (
cast(Category, block[0]),
cast(Literal["<", ">"], block[1]),
int(block[2:i]),
)
target = block[i + 1 :]
else:
check, target = None, block
workflows[name].append((check, target))
# part 1 if op(meta[category][0], value) and op(meta[category][1], value):
parts: list[Part] = [ self.logger.info(" accept, always true")
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")} accepted += transfer_or_accept(target, meta, queue)
for part_s in parts_s.split("\n") break
]
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
print(f"answer 1 is {answer_1}")
meta2 = meta.copy()
low, high = meta[category]
if sense == "<":
meta[category], meta2[category] = (value, high), (low, value - 1)
else:
meta[category], meta2[category] = (low, value), (value + 1, high)
self.logger.info(f" split {_fmt(meta2)} ({target}), {_fmt(meta)}")
# part 2 accepted += transfer_or_accept(target, meta2, queue)
answer_2 = propagate(
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]} self.logger.info(f"run took {n_iterations} iterations")
) return accepted
print(f"answer 2 is {answer_2}")
def solve(self, input: str) -> Iterator[Any]:
workflows_s, parts_s = input.split("\n\n")
workflows: dict[str, Workflow] = {}
for workflow_s in workflows_s.split("\n"):
name, block_s = workflow_s.split("{")
workflows[name] = []
for block in block_s[:-1].split(","):
check: Check
if (i := block.find(":")) >= 0:
check = (
cast(Category, block[0]),
cast(Literal["<", ">"], block[1]),
int(block[2:i]),
)
target = block[i + 1 :]
else:
check, target = None, block
workflows[name].append((check, target))
# part 1
parts: list[Part] = [
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
for part_s in parts_s.split("\n")
]
yield sum(sum(part.values()) for part in parts if self.accept(workflows, part))
# part 2
yield self.propagate(
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
)

View File

@ -1,43 +1,43 @@
import math import math
import sys from typing import Any, Iterator, Literal, TypeAlias, cast
from typing import Literal, TypeAlias, cast
from ..base import BaseSolver
CubeType: TypeAlias = Literal["red", "blue", "green"] CubeType: TypeAlias = Literal["red", "blue", "green"]
MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14} MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14}
# parse games
lines = sys.stdin.read().splitlines()
games: dict[int, list[dict[CubeType, int]]] = {}
for line in filter(bool, lines):
id_part, sets_part = line.split(":")
games[int(id_part.split(" ")[-1])] = [ class Solver(BaseSolver):
{ def solve(self, input: str) -> Iterator[Any]:
cast(CubeType, s[1]): int(s[0]) lines = input.splitlines()
for cube_draw in cube_set_s.strip().split(", ") games: dict[int, list[dict[CubeType, int]]] = {}
if (s := cube_draw.split(" ")) for line in filter(bool, lines):
} id_part, sets_part = line.split(":")
for cube_set_s in sets_part.strip().split(";")
]
# part 1 games[int(id_part.split(" ")[-1])] = [
answer_1 = sum( {
id cast(CubeType, s[1]): int(s[0])
for id, set_of_cubes in games.items() for cube_draw in cube_set_s.strip().split(", ")
if all( if (s := cube_draw.split(" "))
n_cubes <= MAX_CUBES[cube] }
for cube_set in set_of_cubes for cube_set_s in sets_part.strip().split(";")
for cube, n_cubes in cube_set.items() ]
)
)
print(f"answer 1 is {answer_1}")
# part 2 yield sum(
answer_2 = sum( id
math.prod( for id, set_of_cubes in games.items()
max(cube_set.get(cube, 0) for cube_set in set_of_cubes) for cube in MAX_CUBES if all(
) n_cubes <= MAX_CUBES[cube]
for set_of_cubes in games.values() for cube_set in set_of_cubes
) for cube, n_cubes in cube_set.items()
print(f"answer 2 is {answer_2}") )
)
yield sum(
math.prod(
max(cube_set.get(cube, 0) for cube_set in set_of_cubes)
for cube in MAX_CUBES
)
for set_of_cubes in games.values()
)

View File

@ -1,161 +1,172 @@
import logging
import os
import sys
from collections import defaultdict from collections import defaultdict
from math import lcm from math import lcm
from typing import Literal, TypeAlias from typing import Any, Iterator, Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
from ..base import BaseSolver
ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"] ModuleType: TypeAlias = Literal["broadcaster", "conjunction", "flip-flop"]
PulseType: TypeAlias = Literal["high", "low"] PulseType: TypeAlias = Literal["high", "low"]
modules: dict[str, tuple[ModuleType, list[str]]] = {}
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
_modules: dict[str, tuple[ModuleType, list[str]]]
for line in lines: def _process(
name, outputs_s = line.split(" -> ") self,
outputs = outputs_s.split(", ") start: tuple[str, str, PulseType],
if name == "broadcaster": flip_flop_states: dict[str, Literal["on", "off"]],
modules["broadcaster"] = ("broadcaster", outputs) conjunction_states: dict[str, dict[str, PulseType]],
else: ) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
modules[name[1:]] = ( pulses: list[tuple[str, str, PulseType]] = [start]
"conjunction" if name.startswith("&") else "flip-flop", counts: dict[PulseType, int] = {"low": 0, "high": 0}
outputs, inputs: dict[str, dict[PulseType, int]] = defaultdict(
lambda: {"low": 0, "high": 0}
) )
self.logger.info("starting process... ")
def process( while pulses:
start: tuple[str, str, PulseType], input, name, pulse = pulses.pop(0)
flip_flop_states: dict[str, Literal["on", "off"]], self.logger.info(f"{input} -{pulse}-> {name}")
conjunction_states: dict[str, dict[str, PulseType]], counts[pulse] += 1
) -> tuple[dict[PulseType, int], dict[str, dict[PulseType, int]]]:
pulses: list[tuple[str, str, PulseType]] = [start]
counts: dict[PulseType, int] = {"low": 0, "high": 0}
inputs: dict[str, dict[PulseType, int]] = defaultdict(lambda: {"low": 0, "high": 0})
logging.info("starting process... ") inputs[name][pulse] += 1
while pulses: if name not in self._modules:
input, name, pulse = pulses.pop(0)
logging.info(f"{input} -{pulse}-> {name}")
counts[pulse] += 1
inputs[name][pulse] += 1
if name not in modules:
continue
type, outputs = modules[name]
if type == "broadcaster":
...
elif type == "flip-flop":
if pulse == "high":
continue continue
if flip_flop_states[name] == "off": type, outputs = self._modules[name]
flip_flop_states[name] = "on"
pulse = "high" if type == "broadcaster":
...
elif type == "flip-flop":
if pulse == "high":
continue
if flip_flop_states[name] == "off":
flip_flop_states[name] = "on"
pulse = "high"
else:
flip_flop_states[name] = "off"
pulse = "low"
else: else:
flip_flop_states[name] = "off" conjunction_states[name][input] = pulse
pulse = "low"
else: if all(state == "high" for state in conjunction_states[name].values()):
conjunction_states[name][input] = pulse pulse = "low"
else:
pulse = "high"
if all(state == "high" for state in conjunction_states[name].values()): pulses.extend((name, output, pulse) for output in outputs)
pulse = "low"
return counts, inputs
def solve(self, input: str) -> Iterator[Any]:
self._modules = {}
lines = input.splitlines()
for line in lines:
name, outputs_s = line.split(" -> ")
outputs = outputs_s.split(", ")
if name == "broadcaster":
self._modules["broadcaster"] = ("broadcaster", outputs)
else: else:
pulse = "high" self._modules[name[1:]] = (
"conjunction" if name.startswith("&") else "flip-flop",
outputs,
)
pulses.extend((name, output, pulse) for output in outputs) if self.files:
contents = "digraph G {\n"
contents += "rx [shape=circle, color=red, style=filled];\n"
for name, (type, outputs) in self._modules.items():
if type == "conjunction":
shape = "diamond"
elif type == "flip-flop":
shape = "box"
else:
shape = "circle"
contents += f"{name} [shape={shape}];\n"
for name, (type, outputs) in self._modules.items():
for output in outputs:
contents += f"{name} -> {output};\n"
contents += "}\n"
return counts, inputs self.files.create("day20.dot", contents.encode(), False)
# part 1
flip_flop_states: dict[str, Literal["on", "off"]] = {
name: "off"
for name, (type, _) in self._modules.items()
if type == "flip-flop"
}
conjunction_states: dict[str, dict[str, PulseType]] = {
name: {
input: "low"
for input, (_, outputs) in self._modules.items()
if name in outputs
}
for name, (type, _) in self._modules.items()
if type == "conjunction"
}
counts: dict[PulseType, int] = {"low": 0, "high": 0}
for _ in range(1000):
result, _ = self._process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
for pulse in ("low", "high"):
counts[pulse] += result[pulse]
yield counts["low"] * counts["high"]
with open("./day20.dot", "w") as fp: # part 2
fp.write("digraph G {\n")
fp.write("rx [shape=circle, color=red, style=filled];\n")
for name, (type, outputs) in modules.items():
if type == "conjunction":
shape = "diamond"
elif type == "flip-flop":
shape = "box"
else:
shape = "circle"
fp.write(f"{name} [shape={shape}];\n")
for name, (type, outputs) in modules.items():
for output in outputs:
fp.write(f"{name} -> {output};\n")
fp.write("}\n")
# part 1 # reset states
flip_flop_states: dict[str, Literal["on", "off"]] = { for name in flip_flop_states:
name: "off" for name, (type, _) in modules.items() if type == "flip-flop" flip_flop_states[name] = "off"
}
conjunction_states: dict[str, dict[str, PulseType]] = {
name: {input: "low" for input, (_, outputs) in modules.items() if name in outputs}
for name, (type, _) in modules.items()
if type == "conjunction"
}
counts: dict[PulseType, int] = {"low": 0, "high": 0}
for _ in range(1000):
result, _ = process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
for pulse in ("low", "high"):
counts[pulse] += result[pulse]
answer_1 = counts["low"] * counts["high"]
print(f"answer 1 is {answer_1}")
# part 2 for name in conjunction_states:
for input in conjunction_states[name]:
conjunction_states[name][input] = "low"
# reset states # find the conjunction connected to rx
for name in flip_flop_states: to_rx = [
flip_flop_states[name] = "off" name for name, (_, outputs) in self._modules.items() if "rx" in outputs
]
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx"
assert (
self._modules[to_rx[0]][0] == "conjunction"
), "can only handle conjunction as input to rx"
for name in conjunction_states: to_rx_inputs = [
for input in conjunction_states[name]: name for name, (_, outputs) in self._modules.items() if to_rx[0] in outputs
conjunction_states[name][input] = "low" ]
assert all(
self._modules[i][0] == "conjunction" and len(self._modules[i][1]) == 1
for i in to_rx_inputs
), "can only handle inversion as second-order inputs to rx"
# find the conjunction connected to rx count = 1
to_rx = [name for name, (_, outputs) in modules.items() if "rx" in outputs] cycles: dict[str, int] = {}
assert len(to_rx) == 1, "cannot handle multiple module inputs for rx" second: dict[str, int] = {}
assert ( while len(second) != len(to_rx_inputs):
modules[to_rx[0]][0] == "conjunction" _, inputs = self._process(
), "can only handle conjunction as input to rx" ("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
to_rx_inputs = [name for name, (_, outputs) in modules.items() if to_rx[0] in outputs] for node in to_rx_inputs:
assert all( if inputs[node]["low"] == 1:
modules[i][0] == "conjunction" and len(modules[i][1]) == 1 for i in to_rx_inputs if node not in cycles:
), "can only handle inversion as second-order inputs to rx" cycles[node] = count
elif node not in second:
second[node] = count
count += 1
count = 1 assert all(
cycles: dict[str, int] = {} second[k] == cycles[k] * 2 for k in to_rx_inputs
second: dict[str, int] = {} ), "cannot only handle cycles starting at the beginning"
while len(second) != len(to_rx_inputs):
_, inputs = process(
("button", "broadcaster", "low"), flip_flop_states, conjunction_states
)
for node in to_rx_inputs: yield lcm(*cycles.values())
if inputs[node]["low"] == 1:
if node not in cycles:
cycles[node] = count
elif node not in second:
second[node] = count
count += 1
assert all(
second[k] == cycles[k] * 2 for k in to_rx_inputs
), "cannot only handle cycles starting at the beginning"
answer_2 = lcm(*cycles.values())
print(f"answer 2 is {answer_2}")

View File

@ -1,9 +1,6 @@
import logging from typing import Any, Iterator
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
def reachable( def reachable(
@ -21,129 +18,133 @@ def reachable(
return tiles return tiles
map = sys.stdin.read().splitlines() class Solver(BaseSolver):
start = next( def solve(self, input: str) -> Iterator[Any]:
(i, j) for i in range(len(map)) for j in range(len(map[i])) if map[i][j] == "S" map = input.splitlines()
) start = next(
(i, j)
# part 1 for i in range(len(map))
answer_1 = len(reachable(map, {start}, 6 if len(map) < 20 else 64)) for j in range(len(map[i]))
print(f"answer 1 is {answer_1}") if map[i][j] == "S"
# part 2
# the initial map is a square and contains an empty rhombus whose diameter is the size
# of the map, and has only empty cells around the middle row and column
#
# after ~n/2 steps, the first map is filled with a rhombus, after that we get a bigger
# rhombus every n steps
#
# we are going to find the number of cells reached for the initial rhombus, n steps
# after and n * 2 steps after
#
cycle = len(map)
rhombus = (len(map) - 3) // 2 + 1
values: list[int] = []
values.append(len(tiles := reachable(map, {start}, rhombus)))
values.append(len(tiles := reachable(map, tiles, cycle)))
values.append(len(tiles := reachable(map, tiles, cycle)))
if logging.root.getEffectiveLevel() == logging.INFO:
n_rows, n_cols = len(map), len(map[0])
rows = [
[
map[i % n_rows][j % n_cols] if (i, j) not in tiles else "O"
for j in range(-2 * cycle, 3 * cycle)
]
for i in range(-2 * cycle, 3 * cycle)
]
for i in range(len(rows)):
for j in range(len(rows[i])):
if (i // cycle) % 2 == (j // cycle) % 2:
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
print("\n".join("".join(row) for row in rows))
logging.info(f"values to fit: {values}")
# version 1:
#
# after 3 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# H A F A L
# C E A K B
# C G B
#
# after 4 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# I J A B A K D
# H A B A B A L
# C E A B A N F
# C E A N F
# C G F
#
# the 'radius' of the rhombus is the number of cycles minus 1
#
# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I,
# C, B) are each counted radius times, the blocks with everything but one corner (J, K,
# E, N) are each counted radius - 1 times
#
# there are two versions of the whole block, A and B in the above (or odd and even),
# depending on the number of cycles, either A or B will be in the center
#
counts = [
[
sum(
(i, j) in tiles
for i in range(ci * cycle, (ci + 1) * cycle)
for j in range(cj * cycle, (cj + 1) * cycle)
) )
for cj in range(-2, 3)
]
for ci in range(-2, 3)
]
radius = (26501365 - rhombus) // cycle - 1 # part 1
A = counts[2][2] if radius % 2 == 0 else counts[2][1] yield len(reachable(map, {start}, 6 if len(map) < 20 else 64))
B = counts[2][2] if radius % 2 == 1 else counts[2][1]
answer_2 = (
(radius + 1) * A
+ radius * B
+ 2 * radius * (radius + 1) // 2 * A
+ 2 * radius * (radius - 1) // 2 * B
+ sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1)))
+ sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3))) * (radius + 1)
+ sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius
)
print(f"answer 2 (v1) is {answer_2}")
# version 2: fitting a polynomial # part 2
#
# the value we are interested in (26501365) can be written as R + K * C where R is the
# step at which we find the first rhombus, and K the repeat step, so instead of fitting
# for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much
# simpler equation for the a, b and c coefficient
#
# we get:
# - (a * 0² + b * 0 + c) = y1 => c = y1
# - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1
# => b = y2 - y1 - a
# - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1
# => 4a + 2(y2 - y1 - a) = y3 - y1
# => a = (y1 + y3) / 2 - y2
#
y1, y2, y3 = values
a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1
n = (26501365 - rhombus) // cycle # the initial map is a square and contains an empty rhombus whose diameter is
answer_2 = a * n * n + b * n + c # the size of the map, and has only empty cells around the middle row and column
print(f"answer 2 (v2) is {answer_2}") #
# after ~n/2 steps, the first map is filled with a rhombus, after that we get a
# bigger rhombus every n steps
#
# we are going to find the number of cells reached for the initial rhombus, n
# steps after and n * 2 steps after
#
cycle = len(map)
rhombus = (len(map) - 3) // 2 + 1
values: list[int] = []
values.append(len(tiles := reachable(map, {start}, rhombus)))
values.append(len(tiles := reachable(map, tiles, cycle)))
values.append(len(tiles := reachable(map, tiles, cycle)))
if self.files:
n_rows, n_cols = len(map), len(map[0])
rows = [
[
map[i % n_rows][j % n_cols] if (i, j) not in tiles else "O"
for j in range(-2 * cycle, 3 * cycle)
]
for i in range(-2 * cycle, 3 * cycle)
]
for i in range(len(rows)):
for j in range(len(rows[i])):
if (i // cycle) % 2 == (j // cycle) % 2:
rows[i][j] = f"\033[91m{rows[i][j]}\033[0m"
self.files.create(
"cycle.txt", "\n".join("".join(row) for row in rows).encode(), True
)
self.logger.info(f"values to fit: {values}")
# version 1:
#
# after 3 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# H A F A L
# C E A K B
# C G B
#
# after 4 cycles, the figure looks like the following:
#
# I M D
# I J A K D
# I J A B A K D
# H A B A B A L
# C E A B A N F
# C E A N F
# C G F
#
# the 'radius' of the rhombus is the number of cycles minus 1
#
# the 4 'corner' (M, H, L, G) are counted once, the blocks with a corner triangle (D, I,
# C, B) are each counted radius times, the blocks with everything but one corner (J, K,
# E, N) are each counted radius - 1 times
#
# there are two versions of the whole block, A and B in the above (or odd and even),
# depending on the number of cycles, either A or B will be in the center
#
# counts = [
# [
# sum(
# (i, j) in tiles
# for i in range(ci * cycle, (ci + 1) * cycle)
# for j in range(cj * cycle, (cj + 1) * cycle)
# )
# for cj in range(-2, 3)
# ]
# for ci in range(-2, 3)
# ]
# radius = (26501365 - rhombus) // cycle - 1
# A = counts[2][2] if radius % 2 == 0 else counts[2][1]
# B = counts[2][2] if radius % 2 == 1 else counts[2][1]
# answer_2 = (
# (radius + 1) * A
# + radius * B
# + 2 * radius * (radius + 1) // 2 * A
# + 2 * radius * (radius - 1) // 2 * B
# + sum(counts[i][j] for i, j in ((0, 2), (-1, 2), (2, 0), (2, -1)))
# + sum(counts[i][j] for i, j in ((0, 1), (0, 3), (-1, 1), (-1, 3)))
# * (radius + 1)
# + sum(counts[i][j] for i, j in ((1, 1), (1, 3), (-2, 1), (-2, 3))) * radius
# )
# version 2: fitting a polynomial
#
# the value we are interested in (26501365) can be written as R + K * C where R is the
# step at which we find the first rhombus, and K the repeat step, so instead of fitting
# for X values (R, R + K, R + 2 K), we are going to fit for (0, 1, 2), giving us much
# simpler equation for the a, b and c coefficient
#
# we get:
# - (a * 0² + b * 0 + c) = y1 => c = y1
# - (a * 1² + b * 1 + c) = y2 => a + b = y2 - y1
# => b = y2 - y1 - a
# - (a * 2² + b * 2 + c) = y3 => 4a + 2b = y3 - y1
# => 4a + 2(y2 - y1 - a) = y3 - y1
# => a = (y1 + y3) / 2 - y2
#
y1, y2, y3 = values
a, b, c = (y1 + y3) // 2 - y2, 2 * y2 - (3 * y1 + y3) // 2, y1
n = (26501365 - rhombus) // cycle
yield a * n * n + b * n + c

View File

@ -1,111 +1,109 @@
import itertools import itertools
import logging
import os
import string import string
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
def _name(i: int) -> str:
if len(lines) < 26:
return string.ascii_uppercase[i]
return f"B{i:04d}"
def _name(i: int) -> str: def build_supports(
if len(lines) < 26: bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]],
return string.ascii_uppercase[i] ) -> tuple[dict[int, set[int]], dict[int, set[int]]]:
return f"B{i:04d}" # 1. compute locations where a brick of sand will land after falling by processing
# them in sorted order of bottom z location
levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1)
for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
assert sx <= ex and sy <= ey and sz <= ez
xs, ys = range(sx, ex + 1), range(sy, ey + 1)
def build_supports( for z in range(sz - 1, 0, -1):
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]], if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)):
) -> tuple[dict[int, set[int]], dict[int, set[int]]]: break
# 1. compute locations where a brick of sand will land after falling by processing sz, ez = sz - 1, ez - 1
# them in sorted order of bottom z location
levels: dict[tuple[int, int, int], int] = defaultdict(lambda: -1)
for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
assert sx <= ex and sy <= ey and sz <= ez
xs, ys = range(sx, ex + 1), range(sy, ey + 1) bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez))
zs = range(sz, ez + 1)
for z in range(sz - 1, 0, -1): for x, y, z in itertools.product(xs, ys, zs):
if any(levels[x, y, z] >= 0 for x, y in itertools.product(xs, ys)): levels[x, y, z] = i_brick
break
sz, ez = sz - 1, ez - 1
bricks[i_brick] = ((sx, sy, sz), (ex, ey, ez)) # 2. compute the bricks that supports any brick
zs = range(sz, ez + 1) supported_by: dict[int, set[int]] = {}
supports: dict[int, set[int]] = {
i_brick: set() for i_brick in range(len(bricks))
}
for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
name = _name(i_brick)
for x, y, z in itertools.product(xs, ys, zs): supported_by[i_brick] = {
levels[x, y, z] = i_brick v
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1))
if (v := levels[x, y, sz - 1]) != -1
}
self.logger.info(
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}"
)
# 2. compute the bricks that supports any brick for support in supported_by[i_brick]:
supported_by: dict[int, set[int]] = {} supports[support].add(i_brick)
supports: dict[int, set[int]] = {i_brick: set() for i_brick in range(len(bricks))}
for i_brick, ((sx, sy, sz), (ex, ey, ez)) in enumerate(bricks):
name = _name(i_brick)
supported_by[i_brick] = { return supported_by, supports
v
for x, y in itertools.product(range(sx, ex + 1), range(sy, ey + 1)) bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = []
if (v := levels[x, y, sz - 1]) != -1 for line in lines:
} bricks.append(
logging.info( (
f"{name} supported by {', '.join(map(_name, supported_by[i_brick]))}" tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore
tuple(int(c) for c in line.split("~")[1].split(",")), # type: ignore
)
)
# sort bricks by bottom z position to compute supports
bricks = sorted(bricks, key=lambda b: b[0][-1])
supported_by, supports = build_supports(bricks)
# part 1
yield len(bricks) - sum(
any(len(supported_by[supported]) == 1 for supported in supports_to)
for supports_to in supports.values()
) )
for support in supported_by[i_brick]: # part 2
supports[support].add(i_brick) falling_in_chain: dict[int, set[int]] = {}
for i_brick in range(len(bricks)):
to_disintegrate: set[int] = {
supported
for supported in supports[i_brick]
if len(supported_by[supported]) == 1
}
return supported_by, supports supported_by_copy = dict(supported_by)
falling_in_chain[i_brick] = set()
while to_disintegrate:
falling_in_chain[i_brick].update(to_disintegrate)
bricks: list[tuple[tuple[int, int, int], tuple[int, int, int]]] = [] to_disintegrate_v: set[int] = set()
for line in lines:
bricks.append(
(
tuple(int(c) for c in line.split("~")[0].split(",")), # type: ignore
tuple(int(c) for c in line.split("~")[1].split(",")), # type: ignore
)
)
# sort bricks by bottom z position to compute supports for d_brick in to_disintegrate:
bricks = sorted(bricks, key=lambda b: b[0][-1]) for supported in supports[d_brick]:
supported_by, supports = build_supports(bricks) supported_by_copy[supported] = supported_by_copy[supported] - {
d_brick
}
# part 1 if not supported_by_copy[supported]:
answer_1 = len(bricks) - sum( to_disintegrate_v.add(supported)
any(len(supported_by[supported]) == 1 for supported in supports_to)
for supports_to in supports.values()
)
print(f"answer 1 is {answer_1}")
# part 2 to_disintegrate = to_disintegrate_v
falling_in_chain: dict[int, set[int]] = {}
for i_brick in range(len(bricks)):
to_disintegrate: set[int] = {
supported
for supported in supports[i_brick]
if len(supported_by[supported]) == 1
}
supported_by_copy = dict(supported_by) yield sum(len(falling) for falling in falling_in_chain.values())
falling_in_chain[i_brick] = set()
while to_disintegrate:
falling_in_chain[i_brick].update(to_disintegrate)
to_disintegrate_v: set[int] = set()
for d_brick in to_disintegrate:
for supported in supports[d_brick]:
supported_by_copy[supported] = supported_by_copy[supported] - {d_brick}
if not supported_by_copy[supported]:
to_disintegrate_v.add(supported)
to_disintegrate = to_disintegrate_v
answer_2 = sum(len(falling) for falling in falling_in_chain.values())
print(f"answer 2 is {answer_2}")

View File

@ -1,11 +1,7 @@
import logging
import os
import sys
from collections import defaultdict from collections import defaultdict
from typing import Literal, Sequence, TypeAlias, cast from typing import Any, Iterator, Literal, Sequence, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True" from ..base import BaseSolver
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"] DirectionType: TypeAlias = Literal[">", "<", "^", "v", ".", "#"]
@ -35,6 +31,7 @@ def neighbors(
Compute neighbors of the given node, ignoring the given set of nodes and considering Compute neighbors of the given node, ignoring the given set of nodes and considering
that you can go uphill on slopes. that you can go uphill on slopes.
""" """
n_rows, n_cols = len(grid), len(grid[0])
i, j = node i, j = node
for di, dj in Neighbors[grid[i][j]]: for di, dj in Neighbors[grid[i][j]]:
@ -103,65 +100,66 @@ def compute_direct_links(
return direct return direct
def longest_path_length( class Solver(BaseSolver):
links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]], def longest_path_length(
start: tuple[int, int], self,
target: tuple[int, int], links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]],
) -> int: start: tuple[int, int],
max_distance: int = -1 target: tuple[int, int],
queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [ ) -> int:
(start, 0, frozenset({start})) max_distance: int = -1
] queue: list[tuple[tuple[int, int], int, frozenset[tuple[int, int]]]] = [
(start, 0, frozenset({start}))
]
nodes = 0 nodes = 0
while queue: while queue:
node, distance, path = queue.pop() node, distance, path = queue.pop()
nodes += 1 nodes += 1
if node == target: if node == target:
max_distance = max(distance, max_distance) max_distance = max(distance, max_distance)
continue continue
queue.extend( queue.extend(
(reach, distance + length, path | {reach}) (reach, distance + length, path | {reach})
for reach, length in links.get(node, []) for reach, length in links.get(node, [])
if reach not in path if reach not in path
)
self.logger.info(f"processed {nodes} nodes")
return max_distance
def solve(self, input: str) -> Iterator[Any]:
lines = cast(list[Sequence[DirectionType]], input.splitlines())
start = (0, 1)
target = (len(lines) - 1, len(lines[0]) - 2)
direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {
start: [reachable(lines, start, target)]
}
direct_links.update(
compute_direct_links(lines, direct_links[start][0][0], target)
) )
logging.info(f"processed {nodes} nodes") # part 1
yield self.longest_path_length(direct_links, start, target)
return max_distance # part 2
reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = (
defaultdict(list)
)
for origin, links in direct_links.items():
for destination, distance in links:
if origin != start:
reverse_links[destination].append((origin, distance))
links = {
k: direct_links.get(k, []) + reverse_links.get(k, [])
for k in direct_links.keys() | reverse_links.keys()
}
lines = cast(list[Sequence[DirectionType]], sys.stdin.read().splitlines()) yield self.longest_path_length(links, start, target)
n_rows, n_cols = len(lines), len(lines[0])
start = (0, 1)
target = (len(lines) - 1, len(lines[0]) - 2)
direct_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = {
start: [reachable(lines, start, target)]
}
direct_links.update(compute_direct_links(lines, direct_links[start][0][0], target))
# part 1
answer_1 = longest_path_length(direct_links, start, target)
print(f"answer 1 is {answer_1}")
# part 2
reverse_links: dict[tuple[int, int], list[tuple[tuple[int, int], int]]] = defaultdict(
list
)
for origin, links in direct_links.items():
for destination, distance in links:
if origin != start:
reverse_links[destination].append((origin, distance))
links = {
k: direct_links.get(k, []) + reverse_links.get(k, [])
for k in direct_links.keys() | reverse_links.keys()
}
answer_2 = longest_path_length(links, start, target)
print(f"answer 2 is {answer_2}")

View File

@ -1,63 +1,69 @@
import sys from typing import Any, Iterator
import numpy as np import numpy as np
from sympy import solve, symbols from sympy import solve, symbols
lines = sys.stdin.read().splitlines() from ..base import BaseSolver
positions = np.array(
[[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
)
velocities = np.array(
[[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
)
# part 1
low, high = [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
count = 0
for i1, (p1, v1) in enumerate(zip(positions, velocities)):
p, r = p1[:2], v1[:2]
q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
rs = np.cross(r, s)
q, s, rs = q[m := (rs != 0)], s[m], rs[m]
t = np.cross((q - p), s) / rs
u = np.cross((q - p), r) / rs
t, u = t[m := ((t >= 0) & (u >= 0))], u[m]
c = p + np.expand_dims(t, 1) * r
count += np.all((low <= c) & (c <= high), axis=1).sum()
answer_1 = count class Solver(BaseSolver):
print(f"answer 1 is {answer_1}") def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 2 positions = np.array(
# equation [[int(c) for c in line.split("@")[0].strip().split(", ")] for line in lines]
# p1 + t1 * v1 == p0 + t1 * v0 )
# p2 + t2 * v2 == p0 + t2 * v0 velocities = np.array(
# p3 + t3 * v3 == p0 + t3 * v0 [[int(c) for c in line.split("@")[1].strip().split(", ")] for line in lines]
# ... )
# pn + tn * vn == p0 + tn * v0
#
# we can solve with only 3 lines since each lines contains 3 # part 1
# equations (x / y / z), so 3 lines give 9 equations and 9 low, high = (
# variables: position (3), velocities (3) and times (3). [7, 27] if len(positions) <= 10 else [200000000000000, 400000000000000]
n = 3 )
x, y, z, vx, vy, vz, *ts = symbols( count = 0
"x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1)) for i1, (p1, v1) in enumerate(zip(positions, velocities)):
) p, r = p1[:2], v1[:2]
equations = []
for i1, ti in zip(range(n), ts):
for p, d, pi, di in zip((x, y, z), (vx, vy, vz), positions[i1], velocities[i1]):
equations.append(p + ti * d - pi - ti * di)
r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0] q, s = positions[i1 + 1 :, :2], velocities[i1 + 1 :, :2]
answer_2 = r[x] + r[y] + r[z] rs = np.cross(r, s)
print(f"answer 2 is {answer_2}")
q, s, rs = q[m := (rs != 0)], s[m], rs[m]
t = np.cross((q - p), s) / rs
u = np.cross((q - p), r) / rs
t, u = t[m := ((t >= 0) & (u >= 0))], u[m]
c = p + np.expand_dims(t, 1) * r
count += np.all((low <= c) & (c <= high), axis=1).sum()
yield count
# part 2
# equation
# p1 + t1 * v1 == p0 + t1 * v0
# p2 + t2 * v2 == p0 + t2 * v0
# p3 + t3 * v3 == p0 + t3 * v0
# ...
# pn + tn * vn == p0 + tn * v0
#
# we can solve with only 3 lines since each lines contains 3
# equations (x / y / z), so 3 lines give 9 equations and 9
# variables: position (3), velocities (3) and times (3).
n = 3
x, y, z, vx, vy, vz, *ts = symbols(
"x y z vx vy vz " + " ".join(f"t{i}" for i in range(n + 1))
)
equations = []
for i1, ti in zip(range(n), ts):
for p, d, pi, di in zip(
(x, y, z), (vx, vy, vz), positions[i1], velocities[i1]
):
equations.append(p + ti * d - pi - ti * di)
print(equations)
r = solve(equations, [x, y, z, vx, vy, vz] + list(ts), dict=True)[0]
yield r[x] + r[y] + r[z]

View File

@ -1,25 +1,25 @@
import sys # pyright: reportUnknownMemberType=false
from typing import Any, Iterator
import networkx as nx import networkx as nx
components = { from ..base import BaseSolver
(p := line.split(": "))[0]: p[1].split() for line in sys.stdin.read().splitlines()
}
targets = {t for c in components for t in components[c] if t not in components}
graph = nx.Graph() class Solver(BaseSolver):
graph.add_edges_from((u, v) for u, vs in components.items() for v in vs) def solve(self, input: str) -> Iterator[Any]:
components = {
(p := line.split(": "))[0]: p[1].split() for line in input.splitlines()
}
cut = nx.minimum_edge_cut(graph) graph: "nx.Graph[str]" = nx.Graph()
graph.remove_edges_from(cut) graph.add_edges_from((u, v) for u, vs in components.items() for v in vs)
c1, c2 = nx.connected_components(graph) cut = nx.minimum_edge_cut(graph)
graph.remove_edges_from(cut)
# part 1 c1, c2 = nx.connected_components(graph)
answer_1 = len(c1) * len(c2)
print(f"answer 1 is {answer_1}")
# part 2 # part 1
answer_2 = ... yield len(c1) * len(c2)
print(f"answer 2 is {answer_2}")

View File

@ -1,53 +1,53 @@
import string import string
import sys
from collections import defaultdict from collections import defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
NOT_A_SYMBOL = "." + string.digits NOT_A_SYMBOL = "." + string.digits
lines = sys.stdin.read().splitlines()
values: list[int] = [] class Solver(BaseSolver):
gears: dict[tuple[int, int], list[int]] = defaultdict(list) def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
for i, line in enumerate(lines): values: list[int] = []
j = 0 gears: dict[tuple[int, int], list[int]] = defaultdict(list)
while j < len(line):
# skip everything until a digit is found (start of a number)
if line[j] not in string.digits:
j += 1
continue
# extract the range of the number and its value for i, line in enumerate(lines):
k = j + 1 j = 0
while k < len(line) and line[k] in string.digits: while j < len(line):
k += 1 # skip everything until a digit is found (start of a number)
if line[j] not in string.digits:
j += 1
continue
value = int(line[j:k]) # extract the range of the number and its value
k = j + 1
while k < len(line) and line[k] in string.digits:
k += 1
# lookup around the number if there is a symbol - we go through the number value = int(line[j:k])
# itself but that should not matter since it only contains digits
found = False
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
assert i2 >= 0 and i2 < len(lines)
assert j2 >= 0 and j2 < len(line)
if lines[i2][j2] not in NOT_A_SYMBOL: # lookup around the number if there is a symbol - we go through the number
found = True # itself but that should not matter since it only contains digits
found = False
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
assert i2 >= 0 and i2 < len(lines)
assert j2 >= 0 and j2 < len(line)
if lines[i2][j2] == "*": if lines[i2][j2] not in NOT_A_SYMBOL:
gears[i2, j2].append(value) found = True
if found: if lines[i2][j2] == "*":
values.append(value) gears[i2, j2].append(value)
# continue starting from the end of the number if found:
j = k values.append(value)
# part 1 # continue starting from the end of the number
answer_1 = sum(values) j = k
print(f"answer 1 is {answer_1}")
# part 2 yield sum(values)
answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values())) yield sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import sys
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Iterator
from ..base import BaseSolver
@dataclass(frozen=True) @dataclass(frozen=True)
@ -9,33 +11,34 @@ class Card:
values: list[int] values: list[int]
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
cards: list[Card] = [] cards: list[Card] = []
for line in lines: for line in lines:
id_part, e_part = line.split(":") id_part, e_part = line.split(":")
numbers_s, values_s = e_part.split("|") numbers_s, values_s = e_part.split("|")
cards.append( cards.append(
Card( Card(
id=int(id_part.split()[1]), id=int(id_part.split()[1]),
numbers=[int(v.strip()) for v in numbers_s.strip().split()], numbers=[int(v.strip()) for v in numbers_s.strip().split()],
values=[int(v.strip()) for v in values_s.strip().split()], values=[int(v.strip()) for v in values_s.strip().split()],
) )
) )
winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards] winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards]
# part 1 # part 1
answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0) yield sum(2 ** (winning - 1) for winning in winnings if winning > 0)
print(f"answer 1 is {answer_1}")
# part 2 # part 2
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)} card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
card2values = {i: 0 for i in range(len(cards))} card2values = {i: 0 for i in range(len(cards))}
for i in range(len(cards)): for i in range(len(cards)):
card2values[i] += 1 card2values[i] += 1
for j in card2cards[i]: for j in card2cards[i]:
card2values[j] += card2values[i] card2values[j] += card2values[i]
print(f"answer 2 is {sum(card2values.values())}") yield sum(card2values.values())

View File

@ -1,5 +1,6 @@
import sys from typing import Any, Iterator, Sequence
from typing import Sequence
from ..base import BaseSolver
MAP_ORDER = [ MAP_ORDER = [
"seed", "seed",
@ -12,55 +13,6 @@ MAP_ORDER = [
"location", "location",
] ]
lines = sys.stdin.read().splitlines()
# mappings from one category to another, each list contains
# ranges stored as (source, target, length), ordered by start and
# completed to have no "hole"
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
# parsing
index = 2
while index < len(lines):
p1, _, p2 = lines[index].split()[0].split("-")
# extract the existing ranges from the file - we store as (source, target, length)
# whereas the file is in order (target, source, length)
index += 1
values: list[tuple[int, int, int]] = []
while index < len(lines) and lines[index]:
n1, n2, n3 = lines[index].split()
values.append((int(n2), int(n1), int(n3)))
index += 1
# sort by source value
values.sort()
# add a 'fake' interval starting at 0 if missing
if values[0][0] != 0:
values.insert(0, (0, 0, values[0][0]))
# fill gaps between intervals
for i in range(len(values) - 1):
next_start = values[i + 1][0]
end = values[i][0] + values[i][2]
if next_start != end:
values.insert(
i + 1,
(end, end, next_start - end),
)
# add an interval covering values up to at least 2**32 at the end
last_start, _, last_length = values[-1]
values.append((last_start + last_length, last_start + last_length, 2**32))
assert all(v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:]))
assert values[0][0] == 0
assert values[-1][0] + values[-1][-1] >= 2**32
maps[p1, p2] = values
index += 1
def find_range( def find_range(
values: tuple[int, int], map: list[tuple[int, int, int]] values: tuple[int, int], map: list[tuple[int, int, int]]
@ -111,19 +63,71 @@ def find_range(
return ranges return ranges
def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]: class Solver(BaseSolver):
for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]): def solve(self, input: str) -> Iterator[Any]:
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])] lines = input.splitlines()
return seeds
# mappings from one category to another, each list contains
# ranges stored as (source, target, length), ordered by start and
# completed to have no "hole"
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
# part 1 - use find_range() with range of length 1 def find_location_ranges(
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()] seeds: Sequence[tuple[int, int]],
answer_1 = min(start for start, _ in find_location_ranges(seeds_p1)) ) -> Sequence[tuple[int, int]]:
print(f"answer 1 is {answer_1}") for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]):
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])]
return seeds
# # part 2 # parsing
parts = lines[0].split(":")[1].strip().split() index = 2
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])] while index < len(lines):
answer_2 = min(start for start, _ in find_location_ranges(seeds_p2)) p1, _, p2 = lines[index].split()[0].split("-")
print(f"answer 2 is {answer_2}")
# extract the existing ranges from the file - we store as (source, target, length)
# whereas the file is in order (target, source, length)
index += 1
values: list[tuple[int, int, int]] = []
while index < len(lines) and lines[index]:
n1, n2, n3 = lines[index].split()
values.append((int(n2), int(n1), int(n3)))
index += 1
# sort by source value
values.sort()
# add a 'fake' interval starting at 0 if missing
if values[0][0] != 0:
values.insert(0, (0, 0, values[0][0]))
# fill gaps between intervals
for i in range(len(values) - 1):
next_start = values[i + 1][0]
end = values[i][0] + values[i][2]
if next_start != end:
values.insert(
i + 1,
(end, end, next_start - end),
)
# add an interval covering values up to at least 2**32 at the end
last_start, _, last_length = values[-1]
values.append((last_start + last_length, last_start + last_length, 2**32))
assert all(
v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:])
)
assert values[0][0] == 0
assert values[-1][0] + values[-1][-1] >= 2**32
maps[p1, p2] = values
index += 1
# part 1 - use find_range() with range of length 1
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()]
yield min(start for start, _ in find_location_ranges(seeds_p1))
# # part 2
parts = lines[0].split(":")[1].strip().split()
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])]
yield min(start for start, _ in find_location_ranges(seeds_p2))

View File

@ -1,5 +1,7 @@
import math import math
import sys from typing import Any, Iterator
from ..base import BaseSolver
def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]: def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
@ -25,23 +27,23 @@ def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
return t1, t2 return t1, t2
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
# part 1 # part 1
times = list(map(int, lines[0].split()[1:])) times = list(map(int, lines[0].split()[1:]))
distances = list(map(int, lines[1].split()[1:])) distances = list(map(int, lines[1].split()[1:]))
answer_1 = math.prod( yield math.prod(
t2 - t1 + 1 t2 - t1 + 1
for t1, t2 in ( for t1, t2 in (
extreme_times_to_beat(time, distance) extreme_times_to_beat(time, distance)
for time, distance in zip(times, distances) for time, distance in zip(times, distances)
) )
) )
print(f"answer 1 is {answer_1}")
# part 2 # part 2
time = int(lines[0].split(":")[1].strip().replace(" ", "")) time = int(lines[0].split(":")[1].strip().replace(" ", ""))
distance = int(lines[1].split(":")[1].strip().replace(" ", "")) distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
t1, t2 = extreme_times_to_beat(time, distance) t1, t2 = extreme_times_to_beat(time, distance)
answer_2 = t2 - t1 + 1 yield t2 - t1 + 1
print(f"answer 2 is {answer_2}")

View File

@ -1,5 +1,7 @@
import sys
from collections import Counter, defaultdict from collections import Counter, defaultdict
from typing import Any, Iterator
from ..base import BaseSolver
class HandTypes: class HandTypes:
@ -32,18 +34,17 @@ def extract_key(hand: str, values: dict[str, int], joker: str = "0") -> tuple[in
) )
lines = sys.stdin.read().splitlines() class Solver(BaseSolver):
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())] def solve(self, input: str) -> Iterator[Any]:
lines = input.splitlines()
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
# part 1
values = {card: value for value, card in enumerate("23456789TJQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
# part 1 # part 2
values = {card: value for value, card in enumerate("23456789TJQKA")} values = {card: value for value, card in enumerate("J23456789TQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values)) cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1)) yield sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
print(f"answer 1 is {answer_1}")
# part 2
values = {card: value for value, card in enumerate("J23456789TQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
answer_2 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
print(f"answer 2 is {answer_2}")

Some files were not shown because too many files have changed in this diff Show More