人工智能课程的实验(我的解法其实更像是算法课程的实验)
用到的算法:深度优先搜索、宽度优先搜索(状态扩展的不同策略)
数据结构:表示状态的结构体、多维数组
(可能是最近做算法竞赛题的影响,这次并不像以前那样依赖类和面向对象了,而是用最简单(几乎没有封装)的数据表示方法和大量的全局变量来存储数据,用面向过程的写法,以快速解决某一问题为目的设计程序。安全性和可扩展性势必降低,有些技巧的使用也让代码变得难懂;但是代码简洁,节省运行的时间和空间开销,这应该就是算法竞赛更加看重的吧)
这次用了C++写了控制台版,打印状态三元组;又用C#写了图形界面版,打印出圆盘的状态。
二者的算法是完全一致的,执行结果也一致,只是语法和输出的一些处理不同。
下面是运行结果截图,以深度优先为例
下面以C++版为例介绍实现
结构体和全局变量的定义
1 struct State 2 2 { 3 3 int a, b, c;//表示圆盘朝南方向的数字 4 4 State(){} 5 5 State(int na, int nb, int nc) :a(na), b(nb), c(nc){} 6 6 }; 7 7 8 8 int vis[5][5][5];//用来记录每个状态是否被访问过 9 9 int num;//用来记录第几个状态
DFS(用栈实现)
1 int dfs() 2 { 3 stack<State> sta; 4 sta.push(State(4,4,4)); 5 vis[4][4][4] = 1; 6 while (sta.size()) 7 { 8 State cur = sta.top(); 9 sta.pop(); 10 cout << ++num << ". ("<<cur.a<<","<<cur.b<<","<<cur.c<<")"<<endl; 11 if (cur.a == 1 && cur.b == 4 && cur.c == 3) 12 return 1;//命中 13 if (cur.c - 1>0 && vis[cur.a][cur.b][cur.c-1] == 0) 14 { 15 vis[cur.a][cur.b][cur.c-1] = 1; 16 sta.push(State(cur.a, cur.b, cur.c-1)); 17 } 18 if (cur.b - 1>0 && vis[cur.a][cur.b-1][cur.c] == 0) 19 { 20 vis[cur.a][cur.b-1][cur.c] = 1; 21 sta.push(State(cur.a, cur.b-1, cur.c)); 22 } 23 if (cur.a - 1>0 && vis[cur.a-1][cur.b][cur.c] == 0) 24 { 25 vis[cur.a-1][cur.b][cur.c] = 1; 26 sta.push(State(cur.a-1, cur.b, cur.c)); 27 } 28 } 29 return 0; 30 }
BFS(用队列实现)
1 int bfs() 2 { 3 queue<State> que; 4 que.push(State(4, 4, 4)); 5 vis[4][4][4] = 1; 6 while (que.size()) 7 { 8 State cur = que.front(); 9 que.pop(); 10 11 cout << ++num << ". ("<<cur.a<<","<<cur.b<<","<<cur.c<<")"<<endl; 12 if (cur.a == 1 && cur.b == 4 && cur.c == 3) 13 return 1;//命中 14 if (cur.a - 1>0 && vis[cur.a - 1][cur.b][cur.c] == 0) 15 { 16 vis[cur.a-1][cur.b][cur.c] = 1; 17 que.push(State(cur.a-1, cur.b, cur.c)); 18 } 19 if (cur.b - 1>0 && vis[cur.a][cur.b - 1][cur.c] == 0) 20 { 21 vis[cur.a][cur.b-1][cur.c] = 1; 22 que.push(State(cur.a, cur.b-1, cur.c)); 23 } 24 if (cur.c - 1>0 && vis[cur.a][cur.b][cur.c - 1] == 0) 25 { 26 vis[cur.a][cur.b][cur.c-1] = 1; 27 que.push(State(cur.a, cur.b, cur.c-1)); 28 } 29 } 30 return 0; 31 }
你会发现DFS和BFS的不同仅在于对某一状态(节点)进行扩展时,其兄弟结点被暂存的顺序。这个顺序决定了节点扩展的顺序,即将“树”这样的“半线性结构”转化为“线性结构”的不同策略。
因为这两种搜索策略都是从一开始就知道要对每一个未命中的节点进行怎样的处理,即搜索过程中产生的结果对搜索策略没有影响,所以这两种都属于“盲目式搜索”。相对而言,人工智能领域发展出“启发式搜索”,即在进行待扩展节点的选取时,要根据一个“估价函数”来选取“最有希望”的节点分支进行下一步扩展。
老师的习题解析中对DFS进行了改进,在扩展某个节点时判断的是它的子节点,这样可以尽早找到目标。个人认为这相当于在局部做了广度优先。
改进后的访问过的状态由17个降到5个,但其实加上判断过又未命中的兄弟节点,是降到了11个。如下
改进版DFS的代码
1 int dfs_a() 2 { 3 stack<State> sta; 4 sta.push(State(4,4,4)); 5 vis[4][4][4] = 1; 6 while (sta.size()) 7 { 8 State cur = sta.top(); 9 sta.pop(); 10 cout << ++num << ". ("<<cur.a<<","<<cur.b<<","<<cur.c<<")"<<endl; 11 12 if (cur.c - 1>0 && vis[cur.a][cur.b][cur.c-1] == 0) 13 { 14 vis[cur.a][cur.b][cur.c-1] = 1; 15 if (cur.a == 1 && cur.b == 4 && cur.c-1 == 3) 16 { 17 cout << ++num << ". ("<<cur.a<<","<<cur.b<<","<<cur.c-1<<")"<<endl; 18 return 1;//命中 19 } 20 sta.push(State(cur.a, cur.b, cur.c-1)); 21 } 22 if (cur.b - 1>0 && vis[cur.a][cur.b-1][cur.c] == 0) 23 { 24 vis[cur.a][cur.b-1][cur.c] = 1; 25 if (cur.a == 1 && cur.b-1 == 4 && cur.c == 3) 26 { 27 cout << ++num << ". ("<<cur.a<<","<<cur.b-1<<","<<cur.c<<")"<<endl; 28 return 1;//命中 29 } 30 sta.push(State(cur.a, cur.b-1, cur.c)); 31 } 32 if (cur.a - 1>0 && vis[cur.a-1][cur.b][cur.c] == 0) 33 { 34 vis[cur.a-1][cur.b][cur.c] = 1; 35 if (cur.a-1 == 1 && cur.b == 4 && cur.c == 3) 36 { 37 cout << ++num << ". ("<<cur.a-1<<","<<cur.b<<","<<cur.c<<")"<<endl; 38 return 1;//命中 39 } 40 sta.push(State(cur.a-1, cur.b, cur.c)); 41 } 42 } 43 return 0; 44 }
以上就是算法的内容。
关于C#图形界面的实现,也很简单。下面是显示每个圆盘状态的函数
1 //打印状态函数 2 private void print_circle(State st,int num) 3 { 4 Thread.Sleep(300); 5 Graphics gra = this.groupBox1.Controls[num-1].CreateGraphics();//指定第num个圆盘显示 6 7 gra.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; 8 9 Pen pen = new Pen(Color.Black);//画笔颜色 10 11 gra.DrawEllipse(pen, 0, 0, 100, 100);//画圆的方法,x坐标、y坐标、宽、高 12 gra.DrawEllipse(pen, 10, 10, 80, 80); 13 gra.DrawEllipse(pen, 20, 20, 60, 60); 14 15 Font font1 = new Font("", 7, FontStyle.Bold); 16 Font font2 = new Font("", 10, FontStyle.Bold); 17 Brush bush = new SolidBrush(Color.Red);//字填充的颜色 18 gra.DrawString(num + "", font2, bush, 0, 0); 19 gra.DrawString("A", font1, bush, 50, 20); 20 gra.DrawString("B", font1, bush, 50, 10); 21 gra.DrawString("C", font1, bush, 50, 0); 22 gra.DrawString(st.a+"", font1, bush, 50, 70); 23 gra.DrawString(st.b+"", font1, bush, 50, 80); 24 gra.DrawString(st.c+"", font1, bush, 50, 90); 25 }
关于界面的设计,预先放好足够多的picturebox,将它们放入一个groupbox里,然后根据传递进来的参数找特定的picturebox画图。
在遍历groupbox时,我发现里面元素的顺序如果不是按顺序拖入的,会非常错乱。。。由于很菜鸟,我百度了很久都没有找到直接修改内部顺序的方法,只好打开groupbox的定义代码,调整添加元素函数的顺序才得以重排。
以上是我对这个圆盘问题的求解。
在检查实验时,老师说这个问题更通用的抽象是一个5个参数的“搜索机”,5个参数分别为:初始状态集合、目标状态集合、初始状态、目标状态、状态转移函数。(百度后发现好像图灵机的模型)这种方法需要在一开始生成所有可能状态。相比我的“动态生成状态”策略,这样势必会增加空间开销,但在搜索次数多时可以节省重复生成状态的时间,两种方法各有千秋吧。后一种方法更切合现在学习人工智能课的“状态空间搜索”思想。