这是一个每年 12 月举办的编程活动,每天都会开放一个程序设计题,不过只有一个样例,而且没有任何语言和时间复杂度的限制,只要算对答案就可以了。每道题有两个部分,附带圣诞节主题的小剧情。总体来说活动性质偏娱乐,所以难度不算太高。可以用它来熟悉一些新语言,我用的是 JavaScript。
传送门:https://adventofcode.com/
今年一共放出了 12 道题目,没做出来的题目有第九天和第十天的 Part 2,以及最后一天所有部分。可以看到后面会逐渐开始上强度。
Day 1: Secret Entrance
Part 1
求最终值为 0 的次数,比较简单。L 代表减,R 代表加,对最终结果求模判断一下就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => [item.slice (0 , 1 ), Number (item.slice (1 ))]); let curr = 50 ;const ans = data.reduce ((acc, item ) => { curr = (((item[0 ] === "L" ? curr - item[1 ] : curr + item[1 ]) % 100 ) + 100 ) % 100 ; return curr === 0 ? acc + 1 : acc; }, 0 ); console .log (ans);
Part 2
求转动过程中经过刻度 0 的总次数。
稍微转换了下思路,每经过一个 100 的倍数,都相当于经过一次 0 刻度,计算完之后再取模,转换成最终所在的刻度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => [item.slice (0 , 1 ), Number (item.slice (1 ))]); let curr = 50 ;const ans = data.reduce ((acc, item ) => { if (item[0 ] === "L" ) { if (curr === 0 ) { curr -= item[1 ]; acc += -Math .trunc (curr / 100 ); } else { curr -= item[1 ]; if (curr <= 0 ) acc += 1 - Math .trunc (curr / 100 ); } } else { curr += item[1 ]; acc += Math .trunc (curr / 100 ); } curr = ((curr % 100 ) + 100 ) % 100 ; return acc; }, 0 ); console .log (ans);
Day 2: Gift Shop
Part 1
重复两次 的字符串组成的数字被视为无效数字,求出区间内无效数字之和。
首先奇数长度的数字肯定是有效的,字符串重复两次怎么都是偶数嘛。对于偶数长度的数字,只要判断前半段字符串和后半段是否相等就可以了。这样的判断对于奇数同样有效,所以可以省掉判断奇偶的步骤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("," ) .map (item => item.split ("-" ).map (value => Number (value))); const ans = data.reduce ((acc, item ) => { let sum = 0 ; for (let i = item[0 ]; i <= item[1 ]; i++) { const s = i.toString (); const halfLength = Math .floor (s.length / 2 ); if (s.slice (0 , halfLength) === s.slice (halfLength)) { sum += i; } } return acc + sum; }, 0 ); console .log (ans);
Part 2
这下不强制要求重复两次了,只要能被一个重复字符串拼接出来都被视为无效数字。枚举一下子字符串长度就可以了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("," ) .map (item => item.split ("-" ).map (value => Number (value))); const isValid = (s, l ) => { if (s.length % l !== 0 ) return false ; for (let i = 0 ; i < s.length ; i += l) { if (s.slice (i, i + l) !== s.slice (0 , l)) { return false ; } } return true ; }; const ans = data.reduce ((acc, item ) => { let sum = 0 ; for (let i = item[0 ]; i <= item[1 ]; i++) { const s = i.toString (); for (let j = 1 ; j <= Math .floor (s.length / 2 ); j++) { if (!isValid (s, j)) continue ; sum += i; break ; } } return acc + sum; }, 0 ); console .log (ans);
Day 3: Lobby
Part 1
找到两个字符拼接成的最大两位数,不能变换顺序,对每个这样的两位数求和。
算是一个贪心吧,找到前 n - 1 个字符中最大的一个,保留最后一个防止取不到个位数,同样大取序列较小的。个位数就从这个序列之后的部分中取最大。
1 2 3 4 5 6 7 8 9 10 const data = require ("fs" ).readFileSync ("1.txt" , "utf-8" ).split ("\n" );const ans = data.reduce ((acc, item ) => { const a = Math .max (...item.slice (0 , -1 )); const b = Math .max (...item.slice (item.indexOf (a) + 1 )); return acc + a * 10 + b; }, 0 ); console .log (ans);
Part 2
同样的要求,不过现在变成 12 个字符了。
乍一看很唬人,其实是同样的策略,不过需要注意一下边界条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const data = require ("fs" ).readFileSync ("1.txt" , "utf-8" ).split ("\n" );const ans = data.reduce ((acc, item ) => { let sum = 0 , pos = 0 ; for (let i = 1 ; i <= 12 ; i++) { const subItem = i !== 12 ? item.slice (pos, i - 12 ) : item.slice (pos); const value = Math .max (...subItem); pos += subItem.indexOf (value) + 1 ; sum = sum * 10 + value; } return acc + sum; }, 0 ); console .log (ans);
Day 4: Printing Department
Part 1
暴力穷举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => ["." , ...item, "." ]); const width = data[0 ].length ;const height = data.length ;data.unshift (Array (width).fill ("." )); data.push (Array (width).fill ("." )); let ans = 0 ;const dir = [[-1 , -1 ], [-1 , 0 ], [-1 , 1 ], [0 , -1 ], [0 , 1 ], [1 , -1 ], [1 , 0 ], [1 , 1 ]];for (let i = 1 ; i <= height; i++) { for (let j = 1 ; j <= width - 2 ; j++) { if (data[i][j] === "." ) continue ; const num = dir.reduce ((acc, item ) => (data[i + item[0 ]][j + item[1 ]] === "@" ? acc + 1 : acc), 0 ); ans += num < 4 ; } } console .log (ans);
Part 2
跟 Part 1 一样,套一层循环就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => ["." , ...item, "." ]); const width = data[0 ].length ;const height = data.length ;data.unshift (Array (width).fill ("." )); data.push (Array (width).fill ("." )); let ans = 0 , sum;const dir = [[-1 , -1 ], [-1 , 0 ], [-1 , 1 ], [0 , -1 ], [0 , 1 ], [1 , -1 ], [1 , 0 ], [1 , 1 ]];do { sum = 0 ; const x = []; for (let i = 1 ; i <= height; i++) { for (let j = 1 ; j <= width - 2 ; j++) { if (data[i][j] === "." ) continue ; const num = dir.reduce ((acc, item ) => (data[i + item[0 ]][j + item[1 ]] === "@" ? acc + 1 : acc), 0 ); if (num >= 4 ) continue ; sum++; x.push ([i, j]); } } ans += sum; x.forEach (item => (data[item[0 ]][item[1 ]] = "." )); } while (sum !== 0 ); console .log (ans);
Day 5: Cafeteria
Part 1
找给定数据中不在有效区间内的数据总量。
一开始想直接用集合,结果爆栈了,然后想到只要枚举所有区间就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const fs = require ("fs" );const data = fs .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => item.split ("-" ).map (value => Number (value))); const question = fs .readFileSync ("2.txt" , "utf-8" ) .split ("\n" ) .map (item => Number (item)); const ans = question.reduce ((acc, value ) => (data.some (item => value >= item[0 ] && value <= item[1 ]) ? acc + 1 : acc), 0 );console .log (ans);
Part 2
求区间内有效数据的总量
其实就是合并区间的问题,直接上双重循环,有交集就合并,否则直接下一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const fs = require ("fs" );const data = fs .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => item.split ("-" ).map (value => Number (value))); let ans = 0 ;for (let i = 0 ; i < data.length ; i++) { for (let j = i + 1 ; j < data.length ; j++) { if (data[j][0 ] > data[i][1 ] || data[j][1 ] < data[i][0 ]) continue ; data[i][0 ] = Math .min (data[i][0 ], data[j][0 ]); data[i][1 ] = Math .max (data[i][1 ], data[j][1 ]); data.splice (j, 1 ); j = i; } ans += data[i][1 ] - data[i][0 ] + 1 ; } console .log (ans);
Day 6: Trash Compactor
Part 1
这一天的题目挺有意思的。
这一部分让我们竖着算,虽然有点反直觉,不过调一调还是能写出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map ((item, index ) => item .split (" " ) .filter (value => value !== "" ) .map (value => (index === 4 ? value : Number (value))) ); let ans = 0 ;for (let i in data[0 ]) { ans += data[4 ][i] === "+" ? data[0 ][i] + data[1 ][i] + data[2 ][i] + data[3 ][i] : data[0 ][i] * data[1 ][i] * data[2 ][i] * data[3 ][i]; } console .log (ans);
Part 2
仍然是竖着算,不过要精确到每一位了。只好自己把竖式掰成横的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const raw = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => [...item]); const data = [];for (let i in raw[0 ]) { if (raw[4 ][i] !== " " ) data.push ([raw[4 ][i], []]); let num = 0 ; for (let j = 0 ; j < 4 ; j++) { if (raw[j][i] === " " ) continue ; num = num * 10 + Number (raw[j][i]); } num !== 0 && data.at (-1 )[1 ].push (num); } const ans = data.reduce ( (acc, item ) => acc + (item[0 ] === "+" ? item[1 ].reduce ((acc, item ) => acc + item, 0 ) : item[1 ].reduce ((acc, item ) => acc * item, 1 )), 0 ); console .log (ans);
Day 7: Laboratories
Part 1
用集合模拟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const data = require ("fs" ).readFileSync ("1.txt" , "utf-8" ).split ("\n" );let ans = 0 ;const st = new Set ().add (data[0 ].indexOf ("S" ));for (let i = 2 ; i < data.length ; i += 2 ) { st.forEach (index => { if (data[i][index] === "." ) return ; st.add (index + 1 ); st.add (index - 1 ); st.delete (index); ans++; }); } console .log (ans);
Part 2
直接 dfs 时间复杂度会拉到指数级,需要配合记忆化搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const data = require ("fs" ).readFileSync ("1.txt" , "utf-8" ).split ("\n" );const d = {};const dfs = (pos, layer ) => { if (layer >= data.length ) return 1 ; if (d[`${layer} -${pos} ` ]) return d[`${layer} -${pos} ` ]; d[`${layer} -${pos} ` ] = data[layer][pos] === "." ? dfs (pos, layer + 2 ) : dfs (pos - 1 , layer + 2 ) + dfs (pos + 1 , layer + 2 ); return d[`${layer} -${pos} ` ]; }; const ans = dfs (data[0 ].indexOf ("S" ), 2 );console .log (ans);
Day 8: Playground
Part 1
存储每个接线盒之间的距离并排序,然后使用并查集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 let data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => item.split ("," ).map (value => Number (value))); let d = {};const size = data.length for (let i = 0 ; i < size; i++) { for (let j = i + 1 ; j < size; j++) { d[`${i} -${j} ` ] = Math .hypot (data[i][0 ] - data[j][0 ], data[i][1 ] - data[j][1 ], data[i][2 ] - data[j][2 ]); } } d = Object .entries (d) .sort ((a, b ) => a[1 ] - b[1 ]) .map (item => [item[0 ].split ("-" ).map (value => Number (value)), item[1 ]]); class Unionfind { constructor (n ) { this .p = Array .from ({ length : n }, (_, index ) => index); } query (x ) { return this .p [x] == x ? x : (this .p [x] = this .query (this .p [x])); } merge (x, y ) { this .p [this .query (x)] = this .query (y); } } const uf = new Unionfind (size);for (let i = 0 ; i < 1000 ; i++) { const [a, b] = d[i][0 ]; uf.merge (a, b); } let cnt = Array (size).fill (0 );for (let i = 0 ; i < size; i++) { cnt[uf.query (i)]++; } cnt = cnt.sort ((a, b ) => b - a) console .log (cnt[0 ] * cnt[1 ] * cnt[2 ])
Part 2
继续按照原来的方式连线直到所有接线盒都在一个集合内
有一个坑点是如果两个盒本来就在一个集合内,则不需要累加到答案中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 let data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => item.split ("," ).map (value => Number (value))); let d = {};const size = data.length ;for (let i = 0 ; i < size; i++) { for (let j = i + 1 ; j < size; j++) { d[`${i} -${j} ` ] = Math .hypot (data[i][0 ] - data[j][0 ], data[i][1 ] - data[j][1 ], data[i][2 ] - data[j][2 ]); } } d = Object .entries (d) .sort ((a, b ) => a[1 ] - b[1 ]) .map (item => [item[0 ].split ("-" ).map (value => Number (value)), item[1 ]]); class Unionfind { constructor (n ) { this .p = Array .from ({ length : n }, (_, index ) => index); } query (x ) { return this .p [x] == x ? x : (this .p [x] = this .query (this .p [x])); } merge (x, y ) { this .p [this .query (x)] = this .query (y); } } let ans;const cnt = Array (size).fill (1 );const uf = new Unionfind (size);for (let i = 0 ; ; i++) { const [a, b] = d[i][0 ]; if (uf.query (a) === uf.query (b)) continue ; const num = cnt[uf.query (a)] + cnt[uf.query (b)]; uf.merge (a, b); cnt[uf.query (a)] = num; if (i >= 1000 && num >= size) { ans = data[a][0 ] * data[b][0 ]; break ; } } console .log (ans);
Day 9: Movie Theater
Part 1
穷举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => item.split ("," ).map (value => Number (value))); let ans = 0 ;for (let i = 0 ; i < data.length ; i++) { for (let j = i + 1 ; j < data.length ; j++) { ans = Math .max (ans, Math .abs ((data[i][0 ] - data[j][0 ] + 1 ) * (data[i][1 ] - data[j][1 ] + 1 ))) } } console .log (ans);
Day 10: Factory
Part 1
注意到最优解法每个按钮最多只按一次,故使用位运算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => [ item .slice (1 , item.indexOf ("]" )) .split ("" ) .map (value => Number (value === "#" )), item .slice (item.indexOf ("(" ), item.lastIndexOf (")" ) + 1 ) .split (" " ) .map (value => value .slice (1 , -1 ) .split ("," ) .map (v => Number (v)) ), ]); const ans = data.reduce ((acc, item ) => { let minn = Infinity ; const [target, btns] = item; for (let i = 0 ; i < 1 << btns.length ; i++) { const tmp = Array (target.length ).fill (0 ); let cnt = 0 ; for (let j = 0 ; j < btns.length ; j++) { if (((i >> j) & 1 ) === 0 ) continue ; for (let k = 0 ; k < btns[j].length ; k++) { tmp[btns[j][k]] ^= 1 ; } cnt++; } if (!tmp.some ((item, index ) => item !== target[index])) { minn = Math .min (cnt, minn); } } return acc + minn; }, 0 ); console .log (ans);
Day 11: Reactor
Part 1
因为是 DAG,所以直接搜索就可以。代码跟第七天 Part 2 差不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => [item.slice (0 , 3 ), item.slice (5 ).split (" " )]); const d = {};const dfs = p => { if (p === "out" ) return 1 ; if (d[p]) return d[p]; d[p] = 0 ; const idx = data.findIndex (item => item[0 ] === p); data[idx][1 ].forEach (item => { d[p] += dfs (item); }); return d[p]; }; const ans = dfs ("you" );console .log (ans);
Part 2
要求路径必须经过两个中间点。对是否经过这两点进行状态压缩,通过 mask 进行状态传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const data = require ("fs" ) .readFileSync ("1.txt" , "utf-8" ) .split ("\n" ) .map (item => [item.slice (0 , 3 ), item.slice (5 ).split (" " )]); const d = {};const dfs = (p, mask ) => { if (p === "out" ) return mask === 3 ? 1 : 0 ; d[p] ??= Array (4 ).fill (undefined ); if (d[p][mask] !== undefined ) return d[p][mask]; d[p][mask] = 0 ; const idx = data.findIndex (item => item[0 ] === p); data[idx][1 ].forEach (item => { if (item === "dac" ) d[p][mask] += dfs (item, mask | 1 ); else if (item === "fft" ) d[p][mask] += dfs (item, mask | 2 ); else d[p][mask] += dfs (item, mask); }); return d[p][mask]; }; const ans = dfs ("svr" , 0 );console .log (ans);
总结
这次活动总的来说学到了不少新东西,不过最大的收获倒不是巩固了算法知识,而是对 JavaScript 这个语言更加熟悉了。了解了浅拷贝的逆天机制,函数传参直接影响原变量的精妙设计,还有两个相同数组不一定取等的神秘构思。也算是理解为什么那么多人讨厌 JavaScript。不过好的一面也是存在的,比如说各种方便的现成函数,简单易用的数据结构等等。有种又爱又恨的感觉。
没做出来的题目,以后有时间的话可能会补。不过按照我的性格大概率没机会就是了(笑