机房打卡29天:从数据化训练到Tarjan算法与Python性能优化
今天是我在DHU机房上机打卡的第29天。真算起来中间断过两天所以准确说这是第29个“打卡日”但一天没落地的训练节奏已经让这个机房变成了我生活里很稳定的一个坐标。DHU是学院上机系统里分配给我的固定机房的代号每天进门刷校园卡、再在离开时刷一下系统自动把时间段记下来。这个过程听起来挺机械但坚持下来以后我发现打卡这个动作真正的价值不在数字而是在于它给了我一个每天可以被固定的时间锚点。这篇内容我想把D29这一天做的事和前面28天攒下来的方法、教训一起讲清楚机房打卡机制、第29天的完整实操、我自己写的打卡统计脚本以及这一个月踩过的各种坑。适合想用固定场地加固定时间做自我训练的人也适合那些正在纠结怎么把一件事真正坚持下去的朋友。1. DHU上机打卡是什么我的29天推进主线1.1 打卡机制本身并不保证努力DHU不是某个机构的名字它只是机房系统里给我这台固定机器编的号。刷卡签到、签退系统自动统计在机时长。学院搞这套机制的本意是把上机时间数据化方便查看设备使用率。但数据化有一个副作用人会下意识去优化“数据好看”。前三天我就犯了这个毛病——每天在机房里待到很晚时长刷得漂亮实际有效产出可能还不到40分钟。后来我给自己定了一条铁律打卡时长只是底线上机必须有可检查的输出。输出可以是跑通了一个脚本、写完一篇笔记、修好一个bug、生成一份日报。没有输出的打卡本质上只是换个地方刷手机。想通这件事之后我才开始真正从打卡机制里获利。因为机房的物理环境天然带着一种“来都来了”的心理暗示只要我坐到那台固定编号的机器前就能比在宿舍滑向娱乐软件的概率低不少。对一个自律性普通的人来说这已经很值了。1.2 29天的阶段拆解不是随机刷题很多人一听到“打卡29天”第一反应是每天随便学点东西就凑一天。我一开始也差点这么干。到了第5天我发现这样不行因为没有主线每天学的内容互相之间不连贯学到第15天回头看脑子里全是碎片。后来我把29天拆成了六个阶段第1-5天环境整理期。搞定Python基础、常用库、代码风格把机房的开发环境调到能随时开工的状态。第6-10天数据结构巩固期。链表、栈、队列、堆、哈希表逐个手写实现不调现成类。第11-15天搜索与动态规划。DFS、BFS、回溯、经典DP模型每天两道题加一道推导。第16-20天图论专项。图的存储、遍历、最短路、最小生成树然后进入连通分量。第21-25天机器学习基础。线性回归、梯度下降、简单神经网络反向传播的手写实现。第26-29天综合应用与工具沉淀。把前面写的模块整合起来同时完善打卡统计脚本。这种阶段式安排的好处是每天的任务不再需要临时决策。到机房打开任务清单今天做什么早就定了坐下来直接开干。自主训练最怕的不是难而是每天打开电脑那一刻要想“今天学什么”这一想20分钟就没了。1.3 打卡和真实的训练之间差一个复盘我必须承认前两周我很多天都在“无效打卡”。症状很典型代码写不下去就开始刷网页一刷就是半小时遇到不会的题直接看答案看完以为自己会了实际第二天再来还是写不出来。第13天撞上一道动态规划题前一天明明看懂了题解第二天连状态定义都想不起来。从那之后我开始强制写复盘而且不是写“今天学了DP”这种废话而是写清楚三件事今天做了什么、卡在哪一步、下一步该补什么。这一改训练质量立刻不一样。后面我甚至发现写复盘花掉的15分钟能帮我省掉第二天的热身半小时。2. 第29天的实操记录从早到晚我做了什么2.1 开机后的十五分钟先看遗留问题再定今日目标第29天是周五我把这一天设计成“综合收尾日”没有排新的学习主题。早上到机房按照惯例先花15分钟做了三件事查看昨天遗留问题列表、确认今日两个核心目标、把任务写进日报模板。昨天的遗留问题有两个一是手写的图算法模块里Tarjan强连通分量在大数据量下会抛递归深度错误二是打卡统计脚本缺少连续天数统计功能。所以今日目标就明确了手写实现并调通Tarjan强连通分量算法且能在百万级边的图上稳定运行。给打卡统计脚本加上连续打卡天数统计顺带修复一个时区导致的日期边界bug。为什么选Tarjan作为收尾任务因为图论这块我前五天刚学完连通分量是里面最绕的一部分拿它来验收自己的掌握程度比较合适。目标定得足够具体就不会出现坐到屏幕前不知道干什么的情况。2.2 上午核心任务手写Tarjan强连通分量强连通分量的定义不复杂在一个有向图里如果两个顶点互相可达那它们就在同一个强连通分量里。Tarjan算法的核心是基于深度优先搜索加一个栈给每个节点记录两个值dfn[u]节点u被dfs发现的顺序编号low[u]节点u通过子树中的边最多能回溯到的、dfn最小的祖先节点编号。当节点u的dfn[u]等于low[u]时说明u是某个强连通分量的根此时栈里从u往上的所有节点就构成了一个完整的分量。这部分逻辑如果不自己写一遍光看题解很容易误以为自己懂了。我给的版本先用递归实现方便理解主流程import sys sys.setrecursionlimit(1000000) def tarjan(adj, n): dfn [0] * n low [0] * n in_stack [False] * n stack [] scc [] idx 0 def dfs(u): nonlocal idx idx 1 dfn[u] low[u] idx stack.append(u) in_stack[u] True for v in adj[u]: if dfn[v] 0: dfs(v) low[u] min(low[u], low[v]) elif in_stack[v]: low[u] min(low[u], dfn[v]) if low[u] dfn[u]: comp [] while True: w stack.pop() in_stack[w] False comp.append(w) if w u: break scc.append(comp) for u in range(n): if dfn[u] 0: dfs(u) return scc写这个代码时最值得注意的地方是回溯时更新low[u]。如果v还没有被访问就递归进去出来之后用low[v]更新low[u]如果v已经在当前栈里说明v是祖先节点用dfn[v]更新low[u]。这个“用dfn还是用low更新”的区别就是我之前看题解最容易忽略的细节手写一遍之后印象才真正深了。2.3 下午的硬仗从1万条边到200万条边递归版遍历在1万条边的图上跑得很欢可当我用一个200万条边、30万个节点的测试数据来压的时候问题噼里啪啦全冒出来了。第一个问题是递归深度超出了Python默认限制。我虽然在一开始写了sys.setrecursionlimit(1000000)但Python的递归依然有额外的限制而且深递归在性能上很吃亏。所以我做了一个更彻底的改动把dfs改成手写栈实现。虽然代码比递归版长不少但胜在可控——既不用担心递归深度又可以把每个节点的状态转移写得非常明确。第二个问题是遍历的时候我在内层循环里用了min()函数导致每个节点在回溯时多做了一次函数调用。对百万级数据规模这个开销被放得很明显。第三个问题也是最隐蔽的我初始化时把dfn和low都开成了Python的list每个元素是一个int对象内存里其实有大量对象头开销。对于300万节点的图内存占用可能多出一倍。定位这三个问题我花了不少时间做法很简单先在小数据量上确认结果正确然后把数据规模逐步往上加每次只改一个规模变量比较耗时和内存。我列了个简单的性能记录数据规模递归版耗时手写栈版耗时手写栈预分配优化后1万条边0.4秒0.35秒0.3秒20万条边8.6秒1.9秒1.1秒200万条边运行到一半递归报错28秒6.2秒从28秒优化到6.2秒主要靠两件事把min()调用替换成两次朴素比较循环里能局部化的变量全部提到函数入口处。这个优化思路很朴素但对海量数据循环来说效果立竿见影。最终这个手写栈版本在200万条边上的耗时降到了可接受范围也没有再触发递归深度问题。2.4 傍晚收尾回归测试、代码提交、当日日报下午调完性能之后我没有急着换新任务而是先把整个图算法模块的回归测试跑了一遍。回归测试的内容是把今天写的Tarjan函数和之前的图遍历、最短路模块放在同一批测试数据上跑确保今天的改动没有引入新的问题。测试都通过之后我把代码提交到了本地的代码仓库并生成了当天日报。日报内容不长但结构固定日期D29 今日目标Tarjan强连通分量 打卡脚本连续天数功能 完成情况 1. Tarjan手写栈版完成200万条边耗时6.2秒 2. 打卡脚本新增连续天数统计修复时区边界bug 遇到的问题 1. 递归深度限制导致大数据量崩溃改用手写栈解决 2. 内层循环min()调用被放大为性能瓶颈 明日安排进入下一个训练主题整理图论模块笔记写日报最大的价值不是给别人看而是第二天坐到机房时可以只看日报就能无缝接续。如果两天的记录之间没有衔接每次重新进入状态都要额外消耗十几分钟积少成多非常浪费。3. 29天里沉淀下来的打卡统计工具3.1 为什么自己写一个统计脚本机房自带的打卡系统非常基础它只记录你哪天几点进来、几点出去导出的报表也只是一条条原始记录。对于“今天学了多久”“连续打卡多少天”“平均每天有效时长”这类问题它完全回答不了。既然每天都得面对这些数据我干脆自己写了一个轻量统计脚本。没有复杂的架构就是Python加SQLite所有数据存在机房本地稳定可靠。3.2 数据表设计与核心代码脚本的思路很简单每次打卡时记录一条签到记录脚本从机房的原始导出数据里读取签到签退时间然后写入SQLite表同时支持按天汇总。import sqlite3 import datetime def init_db(db_pathpunches.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS punches ( id INTEGER PRIMARY KEY AUTOINCREMENT, day TEXT NOT NULL, in_time TEXT NOT NULL, out_time TEXT NOT NULL, duration_minutes INTEGER NOT NULL ) ) conn.commit() conn.close() def add_punch(day, in_time, out_time, db_pathpunches.db): in_dt datetime.datetime.fromisoformat(in_time) out_dt datetime.datetime.fromisoformat(out_time) duration int((out_dt - in_dt).total_seconds() // 60) conn sqlite3.connect(db_path) conn.execute( INSERT INTO punches (day, in_time, out_time, duration_minutes) VALUES (?, ?, ?, ?), (day, in_time, out_time, duration) ) conn.commit() conn.close() def daily_summary(day, db_pathpunches.db): conn sqlite3.connect(db_path) row conn.execute( SELECT COUNT(*), SUM(duration_minutes) FROM punches WHERE day ?, (day,) ).fetchone() conn.close() return row连续天数统计稍微麻烦一点因为要判断日期间是否是连续的。我用了一个直观的办法把打卡过的所有天去重排序然后从最新一天往前遍历遇到断档就停止计数。def consecutive_days(db_pathpunches.db): conn sqlite3.connect(db_path) rows conn.execute(SELECT DISTINCT day FROM punches).fetchall() conn.close() days sorted([r[0] for r in rows]) seq [] for d in days: if not seq: seq.append(d) continue prev datetime.date.fromisoformat(seq[-1]) cur datetime.date.fromisoformat(d) if (cur - prev).days 1: seq.append(d) else: seq [d] return len(seq)这个脚本我用了大概两个下午的时间写完第29天收尾时又补上了时区边界bug的修复现在已经能稳定运行。代码本身很简单但它真真切切解决了一个问题每天打开电脑心里对“我练了多久”有数不再凭感觉。3.3 日报模板让每次上机都留下痕迹打卡统计脚本解决的是时间维度的问题日报解决的是内容维度的问题。我用的是一个纯文本模板每天花5到10分钟填完。模板固定就四块今日目标、完成情况、遇到的问题、明日安排。这四块里最有用的其实是“遇到的问题”。因为很多训练中的卡点如果当下不写下来过一周就完全忘了。而训练效率的提升恰恰是建立在对卡点的准确认识上。比如我前两周总是卡在递归回溯上第13天写完日报才发现“递归的回溯条件”出现了四次这才意识到这个薄弱点值得集中突破。3.4 时间分块适合机房的番茄钟变体番茄钟很多人都试过但在机房环境下我做了点调整。固定的番茄钟是25分钟工作加5分钟休息我改成45分钟工作加10分钟休息。原因是机房程序员的注意力热起来比较慢25分钟的块太碎刚进入状态就被打断了。我上午通常安排两个45分钟的大块下午安排两个傍晚一个。每两个大块之间休息10分钟正好用来站起来走动一下看看窗外喝口水。这个节奏不比普通番茄钟更高级但更适合长时间连续写代码的场景。休息的10分钟不能刷短视频一刷就停不下来这点我踩坑踩得很深。4. 这29天踩过的坑环境、代码、习惯三层4.1 环境配置类离线依赖安装与机房系统重置机房网络环境比较受限不是所有第三方包都能直接下载。我前两次安装库都卡在下载超时上后来摸索出一套离线安装流程在有公网访问条件的机器上先把需要的包下载好再拷到机房机器安装。# 在能正常访问公网的机器上执行 pip download numpy1.23.5 -d ./pkgs # 在机房机器上执行离线安装 pip install --no-index --find-links./pkgs numpy这套方法帮我解决了很多次依赖问题。但新的坑马上来了机房系统会不定期重置磁盘上非白名单的目录可能被清空。我有一次辛辛苦苦配好的conda环境被重置掉了当天晚上痛定思痛写了一个环境恢复脚本放在U盘里每次重置后能在一小时内恢复到可用状态。同样的教训还有路径问题代码文件不要放在有中文路径的目录下否则很多库在处理文件路径的时候会报编码错误。我第9天因为这个bug浪费了整整一个小时。4.2 代码与性能类递归爆栈、循环内打印、无脑deepcopy这一个月里代码层面的坑我总结出三个最有代表性的。第一个是递归爆栈。不只是Tarjan前几天写DFS时也遇到过。Python默认递归深度是一千层数据规模稍微一大就崩。对策很简单要么sys.setrecursionlimit()加大限制要么直接改成手写栈。手写栈虽然写起来繁琐但在大数据量下更稳排查问题也更直观。第二个是循环里打印。第19天调试一个最短路算法时我在双重循环里加了打印语句数据量小时没什么感觉一放大才发现程序速度慢到令人发指。因为每打印一行都要做一次控制台IO操作十万次循环下来能多出好几秒。调试时的临时打印用完一定记得删干净。第三个是无脑deepcopy。很多人写代码时为了避免修改原始数据喜欢到处用copy.deepcopy()。这个操作在数据量小的时候完全无感但复制一个大对象可能比跑一次算法还贵。我第22天用梯度下降手写线性回归时就因为在每轮迭代里deepcopy了一次参数向量导致3000轮训练跑了近两分钟。去掉deepcopy改成记录中间值的方式后训练直接降到十几秒。4.3 习惯与方法类断卡后的补救策略打卡难免会断。我第7天就断了一次原因是晚上临时有个活动回宿舍已经十点半机房也关门了。那天晚上我一度有点焦虑觉得“连续打卡归零了”。后来我想明白了一个道理打卡的目的是训练不是追求一个完美连续的数字。断卡之后正确做法不是懊恼而是把断卡那天该做的事顺延到第二天然后继续。我甚至给自己定了条原则断卡当天必须补写一条“断卡原因”到记录里这样月底复盘时能看出自己是因为什么事情被中断哪些中断是可以提前规避的。还有一个很常见的坑就是把当天任务排得太满。我第11天给自己安排了五道题加一篇笔记结果一道图论题卡住之后后面全部崩盘。后来我把每天的核心任务压缩到一到两个每个任务都要求“有一个明确的完成标志”。任务越小越能推着自己开始一旦开始很多事就顺下去了。4.4 常见问题速查表为了方便后面继续用我把这29天遇到的高频问题整理成了一张表贴在代码仓库的README里问题现象根本原因解决办法代码运行时提示RecursionError递归超过Python默认深度限制加大递归限制或改写为手写栈第三方库下载超时机房网络受限在可访问公网的机器下载whl离线安装中文路径下无法导入数据文件文件路径编码问题项目目录统一使用英文路径数据量大时程序突然变慢循环内print或min()等函数调用去掉打印把高频函数调用提到循环外deepcopy后内存迅速吃满复制开销被循环放大改为记录中间值或原地修改打卡记录当天日期错位时区边界处理不严谨统一用ISO日期字符串存储并校验机房系统重置导致环境丢失非白名单目录被清理编写环境恢复脚本备份配置任务排太满导致全面拖延一次性目标过大压缩到1-2个带完成标志的核心任务这些问题的共性在于它们都不是算法本身的问题而是工程习惯和流程管理的问题。很多自学的人把注意力全放在“学哪个算法”上忽略了环境、工具、记录这些支撑系统。实际上恰恰是这些支撑系统决定了你每天能稳定投入几小时。第29天结束后我把整个图论模块的笔记重新整理了一遍也把打卡统计脚本更新到了可以稳定使用的版本。回看这一个月对我触动最大的不是哪个算法终于写出来了而是“每天固定时间出现在机房”这件事本身带来的确定性。如果你也想试试这种训练方式我的建议很朴素不需要规划一个完美的百天计划今天只需要坐到那台机器前把昨天的代码跑起来就算开始了。剩下的日子会帮你完成。