屏幕上的光标在最后一行代码的末尾闪了一下。
闪的频率是稳定的——像水龙头滴水的节奏,半秒一滴,半秒一亮。光标是绿色的,黑底绿字,DOS界面上从上到下一千四百二十三行代码排成一面墙——不是普通的墙,是每一块砖都编了号的墙。每行代码是管网的一个节点,每个函数是连接节点的管段,主函数在最底下,像管网的源头,水从那里流出。机房里只有我一个人。下午四点过了,阳光从窗户里横着照进来,照在椅背上切成一条亮带。灰尘在亮带里浮着,慢慢转,像管网上游漂浮的颗粒。
我按下了编译键。
屏幕闪了一下。绿色光标跳到最底下,弹出两行字:
0 error(s), 0 warning(s)
Build complete.
管网优化程序。三千行代码。从大二写的八十行算起,到今天——将近两年。八十行变成一百五十行,一百五十行变成三百行,三百行变成一千行,一千行变成三千行。每一版都像管网的改扩建——先是一根支管,加一根变两根,两根汇成干管,干管分出更多支管,最后覆盖整个城市。
程序是两个月前开始写的。
白天上完课去机房。机房在主楼三楼,下午四点之后人少了,靠窗的位置灯光最亮。代码从主函数开始写——int main()的大括号打开,先写数据输入模块:管网拓扑结构、节点标高、管段长度、需水量。拓扑用什么存?用邻接矩阵。节点和节点之间有没有管段连着——有写1,没有写0。邻接矩阵铺开来像一张管网简化图,1是管段,0是空地。
管网拓扑输完之后是水力计算模块。达西-魏斯巴赫公式的代码早就写过了——上一版从三百行改到一千行的时候就有了。但这一版加了新东西:最优管径选择。不是从五个标准管径里挑一个最接近的,是用算法在所有可能的管径组合里找最优解。
最优解怎么找?
Dijkstra算法找最短路径——这一版已经在第45章的时候用过了。但管网不是最短路径问题。管网是多源多汇的最优流量分配问题——水不只从起点流向终点,水从多个水源出发,经过多根干管和支管,到达每个节点的时候压力不能太低、流速不能太快、管径不能太大。
我坐在笔记本前面想了一个晚上。笔记本翻到数据结构那一章——图的最小生成树。Kruskal算法、Prim算法——两种找法,一种按边排序从小到大加,一种从任意节点出发贪心地加。管网的优化不是最小生成树——管网允许有环,环是冗余,冗余是安全。但它给了一个思路:用贪心策略搜索。
搜索。图的搜索。宽度优先搜、深度优先搜。但我要在所有可能的管径组合里搜——十二个节点,五个标准管径,五的十二次方种组合。这个数字太大了——穷举一遍要一万年。
然后想到了遗传算法。
周老师上课提过一次——不是给排水课,是选修课上随口说的。他在黑板上写了一行字:"适者生存,劣者淘汰。"是达尔文的那行字,但周老师在旁边加了一个括号:"(优化算法)"。种群初始化、适应度计算、选择、交叉、变异——一代一代地选,选出来的就是最优的。
我花了两周写遗传算法模块。种群从一百组随机管径组合开始,每一组是一个个体,用适应度函数打分——压力偏差异常的大扣分,管径偏大浪费的大扣分,流速在合理范围的大加分。每一代选最好的二十组,交叉生成新个体,以百分之五的概率随机变异一根管径——像基因突变,大部分突变是有害的,但偶尔有一个突变恰好更好。
程序跑完的时候屏幕上输出了一组管径。十二个节点的管径从DN100到DN300,压力偏差在允许范围内,总造价最低。
三千行代码。输入管网拓扑和需水量,输出最优管径和压力分配。
答辩那天是五月下旬。
阳光在主楼走廊的磨石子地面上画出两道长方形,光和影的边界很硬——走到光里就亮,走到影里就暗,没有过渡。答辩教室在二楼最里面一间,门开着,日光从窗户照进来,照在答辩委员坐的那排桌子上。
三个老师坐在长桌后面。周老师在中间,左边是教水力学的孙老师,右边是教建筑给排水的陈老师。桌上放着每人一份打印出来的论文和程序说明——十六页A4纸,用曲别针别着。
我站在长桌前面。投影仪把笔记本电脑的屏幕打到白墙上——黑底绿字的界面,光标在最后一行代码下方闪。
周老师翻开论文:"'基于图论的给水管网优化设计'——你来说说。"
我按了一下运行键。屏幕上弹出输入界面:节点数12,管段数17,需水量矩阵,管段长度。数据一行一行地从文件读入——读入的速度很快,十二个节点和十七根管段的数据在屏幕上滚过,像水从管口冲出来的速度。
然后是计算。屏幕上显示第一代种群的适应度:平均分72.4。然后第二代:73.1。第三代:74.8。每一代的平均分往上走,像水从低处往高处走——但水里是有泵的,泵给水加压了。算法是泵,数据是水。
第一百代。平均分91.2。
最优管径组合弹出:DN100×4、DN150×5、DN200×3、DN250×3、DN300×2。造价最低,压力偏差在允许范围内。
孙老师推了推眼镜。
"你是给排水专业的,为什么用程序做?"
"因为管网是一个图。图的最优路径应该用算法找,不应该靠经验估算。"
他顿了一下。
"经验估算也能选管径。"
"经验估算只能保证不犯错。算法能找到最优。不犯错和最优之间差了一组管径。差的那组管径就是造价。"
孙老师看了周老师一眼。周老师没看他,在看我的论文——翻到了遗传算法那一页,手指点在适应度函数上。
"有道理。"周老师说。三个字。
陈老师从头到尾没说话。他一直在看屏幕上跑出来的那组管径数据——DN100到DN300,十二个节点的管径排成一行,像一组管道截面从左到右从小到大。他最后说了一句:"程序能再跑一遍吗?输入换个管网试试。"
我换了一组数据。十五个节点,二十三根管段。程序又跑了一遍。一百零四代之后输出结果。DN100到DN300,造价最低。
陈老师点了一下头。"可以。"
答辩完了在走廊里。阳光还画在磨石子地面上——两道长方形,一道亮一道暗。王强从楼下跑上来,跑得鞋底在楼梯上蹬蹬响。
"怎么样?"
"过了。"
"啥评语?"
"有道理。"
他笑了。"有道理是啥意思?行还是不行?"
"行。"
他拍了一下我的肩膀——手劲很大,骨头都震了一下。然后他拉我下楼。楼梯间里他的皮鞋蹬蹬蹬敲着台阶,像木锤敲管壁。
走到楼下机房,我把程序又跑了一遍。王强站在我后面看。黑底绿字的界面,数据滚过,一代一代的分数往上走。他的眼睛从刚开始的茫然变成了盯着屏幕不动——不是看懂了,是看到了规律。每一代都比上一代好一点,像水涨的速度——看不出来,但数字在变大。
"能修车吗?"他问。
"不能。"
"那没用。"他说这话的时候嘴角是往上翘的——笑了。
林小月是傍晚来的。
她推开机房的门,门轴响了一声。机房里只有我和屏幕的光,灯没开——嫌亮,屏幕反光。她走到我旁边拉了把椅子坐下来,椅子腿在地上刮了一声。
"你说有空给我看。现在有空了。"
我把椅子往旁边让了让,把屏幕转向她。程序在待机状态——光标在左上角闪。她看着屏幕上的代码一行一行地往上翻,翻到水力计算模块的时候停了。
"你把水的情况写成了数据。"她说。
"不完全是。"我说,"水还是水。只是每个节点都有压力值,程序能处理的是这些值。"
她看了一会儿。手指在膝盖上点了两下——那是她思考的习惯,食指中指无名单指交替点,像打节拍。
"拓扑是你自己定义的?"
"邻接矩阵。节点和节点之间有管段就1,没有就0。"
"像结构力学的刚度矩阵。"
"对。结构力学里1和0代表有没有梁柱连接,我的程序里1和0代表有没有管段连接。写到矩阵里以后,很多东西就能算了。"
她点了一下头。椅子在地上碰了一声——她站起来了。
"你的方向找到了。"她看了一眼屏幕。"不是管道,不是代码。是你把管道变成代码的那条路。"
我看着屏幕上的光标闪了闪。半秒。亮。半秒。暗。像水龙头在滴水。
她走了。门合上的时候机房又暗了一点。屏幕上代码的绿光把我的手照成两只浅绿色的手——十根手指搭在键盘上,像十根管段连着十个节点。
我翻开作文本。蓝色钢笔在最后一页写了一行字:
"管网优化程序完成。三千行代码。方向可能不只有一条——但在管道和代码之间,我看到了第三条路。"
笔尖在"第三条路"下面画了一道横线。横线很直——赵启明式的直线。横线的末端顿了一下,笔力比中间重。
窗外天已经黑了。白杨树的叶子在晚风里哗哗响——新叶子全开了,绿得还嫩,风一吹就翻过来,翻过来的叶背是浅色的,一深一浅像管网图上标注管径的粗实线和细实线交替出现。路灯光穿过枝叶打在地上,碎了一地,更像数据了——每一片光是一个1,每一片影是一个0。1和0铺满整条路,脚踩上去,1和0跟着脚底一起响。
关掉屏幕。代码在硬盘里,程序在运行过的痕迹里——一千四百二十三代,每代都比上一代好一点。好的方向是对的。方向对了,水就往那流。