79泊松分酒是一款经典的C语言数学游戏,最早出现在20世纪80年代的计算机编程教材中。这个游戏模拟了法国数学家泊松提出的著名分酒问题:如何用三个不同容量的容器精确量取出特定体积的酒液。作为早期计算机编程教学的典型案例,它不仅训练了初学者的逻辑思维能力,更展示了算法设计的基本思路。
我在整理老式编程教材时偶然发现了这个游戏的原始代码。由于年代久远,这份代码存在几个典型问题:使用了过时的Turbo C图形库函数、存在内存泄漏风险、变量命名不符合现代规范,而且核心算法逻辑的注释严重缺失。更可惜的是,随着DOS系统的消亡,原本依赖BGI图形驱动的界面已经完全无法在现代系统上运行。
修复这样的古董代码具有多重价值:首先,它是计算机教育史的活化石,保存了早期编程教学的原貌;其次,算法本身非常精妙,对理解递归和穷举算法有很好的教学意义;最后,通过现代编程规范重构这类经典案例,可以为传统算法教学提供新的实践素材。
原始代码由一个约300行的C文件组成,采用经典的DOS程序结构:
c复制#include <graphics.h> /* Turbo C特有的图形库 */
#include <conio.h> /* 控制台IO */
#include <stdlib.h>
void draw_bottles() { /* 使用BGI绘图 */
bar(100,200,150,300);
/* ...更多绘图代码... */
}
int main() {
int gd=DETECT,gm;
initgraph(&gd,&gm,""); /* 初始化图形模式 */
/* 游戏主逻辑 */
closegraph();
return 0;
}
主要问题集中在:
泊松分酒问题的算法本质是状态空间搜索。游戏设定三个酒瓶(如8L/5L/3L),通过互相倾倒寻找得到特定容量的方法。原始代码采用深度优先搜索(DFS)实现:
c复制struct State {
int bottles[3]; // 当前各瓶酒量
struct State* next;
};
void dfs(struct State* current) {
if (is_target(current)) return;
for (int i=0; i<3; i++) {
for (int j=0; j<3; j++) {
if (i==j) continue;
struct State* new_state = pour(current, i, j);
if (!is_visited(new_state)) {
dfs(new_state); // 递归搜索
}
}
}
}
这个实现存在明显的优化空间:没有剪枝策略导致重复计算,使用递归可能栈溢出,且缺少路径记录功能。
放弃Turbo C依赖,改用现代工具链:
新的项目结构:
code复制poisson-wine/
├── CMakeLists.txt
├── include/
│ ├── game.h
│ └── sdl_utils.h
├── src/
│ ├── main.c
│ ├── algorithm.c
│ └── render.c
└── assets/ # 图形资源
将DFS改为更高效的广度优先搜索(BFS)并引入记忆化:
c复制#define MAX_STATES 1000
typedef struct {
int volumes[3];
int steps;
int prev_index; // 回溯路径
} GameState;
GameState state_queue[MAX_STATES];
int visited[MAX_STATES];
int bfs_solve(int capacities[3], int target) {
int front = 0, rear = 0;
// 初始化队列
state_queue[rear++] = (GameState){{0,0,capacities[2]},0,-1};
while (front < rear) {
GameState current = state_queue[front];
if (has_target(current.volumes, target)) {
return front; // 返回目标状态索引
}
for (int i=0; i<3; i++) {
for (int j=0; j<3; j++) {
if (i==j) continue;
GameState next = pour_operation(current, i, j);
int hash = state_hash(next.volumes);
if (!visited[hash]) {
visited[hash] = 1;
next.steps = current.steps + 1;
next.prev_index = front;
state_queue[rear++] = next;
}
}
}
front++;
}
return -1; // 无解
}
使用SDL2实现跨平台GUI:
c复制void render_bottles(SDL_Renderer* renderer, int* volumes) {
// 绘制瓶身
for (int i=0; i<3; i++) {
SDL_Rect bottle = {100+i*120, 300, 80, 200};
SDL_SetRenderDrawColor(renderer, 0,0,255,255);
SDL_RenderDrawRect(renderer, &bottle);
// 绘制酒液
int height = (volumes[i]*180)/capacities[i];
SDL_Rect wine = {100+i*120+2, 300+(180-height), 76, height};
SDL_SetRenderDrawColor(renderer, 255,0,0,255);
SDL_RenderFillRect(renderer, &wine);
}
}
为避免重复状态检查,设计专用哈希函数:
c复制int state_hash(int volumes[3]) {
// 假设每个瓶子的容量不超过100
return volumes[0]*10000 + volumes[1]*100 + volumes[2];
}
精确模拟液体转移过程:
c复制GameState pour_operation(GameState s, int from, int to) {
int pour_amount = min(s.volumes[from],
capacities[to] - s.volumes[to]);
GameState new_state = s;
new_state.volumes[from] -= pour_amount;
new_state.volumes[to] += pour_amount;
return new_state;
}
解决原版无法显示完整步骤的问题:
c复制void print_solution(int end_index) {
int path[100], depth = 0;
for (int i=end_index; i!=-1; i=state_queue[i].prev_index) {
path[depth++] = i;
}
for (int i=depth-1; i>=0; i--) {
GameState s = state_queue[path[i]];
printf("Step %d: [%d,%d,%d]\n",
depth-i, s.volumes[0], s.volumes[1], s.volumes[2]);
}
}
使用AddressSanitizer工具:
bash复制gcc -fsanitize=address -g main.c
./a.out
SDL2常见问题排查流程:
SDL_Init()返回值当瓶子容量较大时,可以:
改造后的代码特别适合用于:
我实际在计算机社团中使用这个案例教学时,学生们最感兴趣的是如何将数学问题转化为可执行的算法。一个特别有用的技巧是让他们先用纸笔模拟几个简单案例(如4L/3L/1L分2L),理解状态转移规律后再看代码。