赵归渺收回思绪,抬眼看向说话的人,嘴角弯了弯,是很淡的弧度,却让那双总是显得有些清冷的眼睛,忽然透出一点暖意:“好。”
他低下头,翻开学姐给的资料,视线落在印着密密麻麻代码的纸页,心里却莫名地,想起了那只猫,和那个抱着猫的女生。
明明才分开不到一小时,却感觉好像已经过了一个礼拜那么久。
窗外的银杏叶,还在一片一片地往下落。
突然,赵归渺猛地抬起头,视线再次落回那本《算法导论》上。
双向 Dijkstra的优化思路,他在校赛时只是仓促尝试,如今被学长点破,那些散落在记忆里的碎片忽然拼凑起来,在脑海中形成一张细密的逻辑网。
他下意识地翻开笔记本,越过扉页那半行模糊的动态规划方程,翻到记录着校赛代码的页面。
笔尖抵在“双向搜索终止条件”那一行,墨色的字迹被他圈了又圈。
“上次校赛的测试数据规模不大,双向搜索的终止条件设为‘两端相遇’就足够了,但省赛的数据集肯定会扩容,”他低声自语,眉头微微蹙起,“如果节点数达到十万级,单纯的相遇判断会导致大量无效遍历,时间复杂度还是会飙升到 O (M log N),根本达不到省赛要求的运行效率。”
窗外的银杏叶又一次贴在玻璃上,沙沙的声响像是数据在内存中流转的杂音。
赵归渺的目光落在草稿纸的空白处,迅速写下双向 Dijkstra的核心公式:dist[s][u]+ dist[t][u]>= dist[s][t]。“这里的问题在于,相遇节点 u未必是最短路径上的节点。”他的笔尖在“>=”符号上重重一划,“如果能找到一个更精准的终止阈值,提前终止搜索,或许能减少不必要的计算。”
他想起《算法导论》里关于 A*算法的启发函数章节,在纸上快速勾勒出启发函数的雏形:h(u)=|x_u - x_t|+|y_u - y_t|。“如果把 A*的启发函数融入双向 Dijkstra,用预估距离来预判当前路径是否有优化空间。”他的眼睛亮了亮,笔在纸上疾走着,“当 dist[s][u]+ h(u)>=当前最短路径时,就可以剪枝这一条分支,这样能大大减少搜索的节点数。”
但念头刚起,就被他自己推翻了。
“不行,省赛的图可能是带权图,曼哈顿距离的启发函数只适用于无权网格图。”他摇了摇头,在启发函数上打了个叉,“如果边权是随机分布的,启发函数的预估会偏差太大,反而会导致剪枝过度,错过最优解。”
会议室里的讨论声渐渐模糊,赵归渺的世界只剩下草稿纸上的公式和跳动的思绪。
他抬手揉了揉眉心,目光落在学长给的资料上,其中一页印着省赛历年的时间限制——1秒。
“1秒内要处理百万级的边,时间复杂度必须压到 O (M log (N/2))以下。”他在草稿纸上写下这个目标,手指轻轻敲击桌面,“双向 Dijkstra的优势在于两端同时搜索,搜索空间是单向的一半,但如果能优化优先级队列的实现,或许能再提速。”
他想起校赛时用的是普通的二叉堆,插入和删除的时间复杂度是 O (log N)。
“如果换成斐波那契堆,摊还时间复杂度能降到 O (1),但实现太复杂,容易出错,”他在“斐波那契堆”几个字旁边犹豫地画了个圈,“而且省赛的编程环境可能不支持某些底层优化,反而不如用二叉堆配合启发式剪枝来得稳妥。”
忽然,他的目光落在笔记本上记录的一组校赛数据上:当双向搜索的两端节点数达到总节点数的 30%时,相遇的概率最大。
“或许可以设置一个动态阈值,”他猛地挺直脊背,在纸上飞快地演算,“当任意一端的搜索节点数超过总节点数的 25%,就切换成单向 Dijkstra,这样既能利用双向搜索的优势,又能避免无效遍历。”
他顺着这个思路往下推导,假设总节点数为 N,双向搜索各处理 N/4个节点,那么优先级队列的操作次数就从 O (M log N)降到了 O (M log (N/4)),相当于减少了近一半的计算量。
“再配合边的筛选,只保留权值小于当前最短路径的边,”他的嘴角不自觉地微微上扬,“这样一来,时间复杂度应该能压到 1秒以内。”
窗外的银杏叶被风吹走,皎洁的月光透过玻璃洒在草稿纸上,照亮了密密麻麻的公式和演算步骤。
赵归渺抬手擦掉额角的薄汗,笔尖轻轻划过那些被圈改、被标注的字迹,心里忽然涌起一股难掩的笃定。
他拿出手机,快速翻找出之前收藏的一篇关于双向 Dijkstra优化的论文,打算回去后结合自己的推导再验证一遍。
“归渺?你在想什么呢,这么入神?”旁边的学长轻轻碰了碰他的胳膊。
赵归渺回过神,眼底还残留着思考时的专注,他扬了扬手里的草稿纸,声音里带着一丝难掩的兴奋:“学长,我觉得双向 Dijkstra可以结合动态阈值和边筛选来优化,或许能把时间复杂度再降一级。”
“动态阈值?”刚才提议分工的学长立刻凑了过来,目光紧紧盯着草稿纸上的演算步骤。