今天是我在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_path="punches.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_path="punches.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_path="punches.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_path="punches.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 numpy==1.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天结束后,我把整个图论模块的笔记重新整理了一遍,也把打卡统计脚本更新到了可以稳定使用的版本。回看这一个月,对我触动最大的不是哪个算法终于写出来了,而是“每天固定时间出现在机房”这件事本身带来的确定性。如果你也想试试这种训练方式,我的建议很朴素:不需要规划一个完美的百天计划,今天只需要坐到那台机器前,把昨天的代码跑起来,就算开始了。剩下的,日子会帮你完成。