1 引言
华容道游戏是古老的中国传统益智游戏,以其变化多端百玩不厌的特点,与魔方独立钻石棋一起被国外智力专家并称为智力游戏界的三个不可思议。日本藤村幸三朗曾在《数理科学》杂志上发表华容道基本布局的最少步法为85步。后来清水达雄找出更少的步法为83步。美国著名数学家马丁·加德纳又进一步把它减少为81步。此后,至今还未曾见到打破这一记录的报道。而对解题效率的提升,则有数学家指出,此问题单靠数学方法很难求解。
华容道游戏的棋盘是由20个小正方形组成的长方形宽4 格长5 格,共有10 个棋子。其中五个棋子为张飞,赵云,马超,黄忠和关羽等。五虎将每个棋子占两格,可以是横的,也可以是竖的。四个棋子为刘备兵,各占一格。另一个棋子为曹操,占四格形状为正方形。棋盘的下部有两个空置的小格,作为华容道的出口。棋盘上仅有两个小方格,空着玩法就是通过棋子在这个长方形的棋盘内滑动,用最少的步数把曹操移出华容道。
图1 所示的布局即为华容道的一种初始布局,俗称横刀立马。应用计算机对每种布局都可以快速地找到最优走法或判断它无解。按一个棋子走一个空格,或连续走两个空格算一步的规则,横刀立马的布局的最优走法为81 步。近年来虽然出现了几款华容道游戏的软件,但在电脑自动解题方面效率不够理想。有学者提到的应用双向广度优先搜索策略寻找最优走法,但也仍存在着一些不足之处。从计算机人工智能的角度看华容道游戏,可以表示成下述问题:给定一个三元组(S F G), S 为初始状态(初始布局)构成的集合,F为算符(移动棋子的操作)构成的集合,G 为目标状态(曹操在出口位置的布局)构成的集合。从初始状态出发,应用算符搜索一条到达目标状态的路径。本文中将该问题称为华容道问题文中从搜索策略数据结构算法优化程序设计等几方面详细讨论华容道问题最优解的求解方法。
2 经典的搜索策略以及方法
目前在对华容道的研究上有几个有效的算法例程,一个是PASCAL的,一个是VB的,一个是C的,还有一个针对手机的java源代码,都指明使用广度优先算法及一些剪枝办法。但算法效率仍然不高。天津师范大李学武《华容道游戏的搜索策略》说到使用双向搜索可提高效率,但本文未采用这种方法。
2.1广度优先的树型结构
一个盘面状态理解为一个节点,在编程时表示节点的方法是多样的,可用一串数字来表示盘面状态节点,通过压缩处理,甚至可用一个int32整型数来表示一个节点。首先考查起始盘面(节点)的所有走法,然后逐一试走,设当前有n1种走法,则生成n1个儿子节点。接下来,考查这n1个儿子节点的所有走法,并分别试走,生成n2个孙子节点。接下来,考查这n2个孙子节点的所有走法,并分别试走,生成n3个曾孙节点。再接下,就不多说了,依上循环,一代一代的往下生成节点,直到找到目标节点。以上搜索思想朴素、简单,这也正是程序设计所需要的!可是摆在我们面前的问题有两个: a、代代生成子节点,将会有很多个节点,如何存取这些节点呢,也就是说如何有序的排放节点而不至于造成混乱。b、程序大概结构应是怎样的。第1个问题可这样解决:设第一代节点放在A[1]中,第二代节点放在A[2]中,第三代节点放在A[3]……注意A[1]中含有多个节点,A[2]也是这样的……。由于整个棋局的可行解可以描述成一个树型结构,并且为了得到最少移动步数需要采用广度优先的搜索算法,因此考虑将链表与树型结构整合起来,便于进行广度搜索。如图,当我们试图搜索第三步可行解时,只需要顺着第二步的链表依次搜索便可以实现了。
而在人工智能中,对由三元组表示的状态空间,求解问题的方法是通过运用某种搜索策略生成状态空间的部分状态(节点)来寻找问题的解。其基本思想是首先把问题的初始状态(初始节点)作为当前状态(当前节点),选择适用的算符,对其进行操作生成一组子状态(子节点), 然后检查目标状态(目标节点)是否在其中出现。若出现则搜索成功,找到问题的解,若不出现,则按某种搜索策略从已生成的节点中再选一个节点作为当前节点。重复上述过程,直到目标节点出现,或者不再有可供操作的节点及算符为止。在各种搜索策略中,广度优先搜索策略对于存在解的问题,总是可以搜索到它的解,而且得到的是路径最短的解。对华容道问题,棋子每走一步是对当前节点生成它的一个子节点在走法的步数最少为最优的意义下,寻找它的最优解,实际上也就是寻找路径最短的解。因此只要某种初始布局存在解,则应用广度优先搜索策略,总是可以搜索到它的最优解。对无解的初始布局,由于棋子和格子都是有限个,所生成的相异的节点个数也必然是有限个,则应用广度优先搜索策略也能在有限步内判断它无解。求解华容道问题广度优先搜索是一种合适的搜索策略。在搜索过程中,所有节点及其指向父节点的指针的反向指针构成一棵以初始节点为根的搜索树。如果把从初始节点算起,路径深度相同的节点称为同层节点,那么广度优先搜索就是逐层地对节点进行扩展并考察它是否为目标节点。
2.2 伪代码
展开首节点得所有儿子节点A[1]
for( i=1;i<=n层;I++){ //查找n代(层)
P1=A[i],P2=A[i+1]
for(j=1;j<=P1内节点个数;j++){
B=P1[j] //读取P中的第j个节点
检查B是否为目标节点,如果是,结束搜索
展开B并将所有节点追加到P2中 //P2为P1下一代的节点集
}
}
2.3 对于本方法的一些思考
以上代码基本上给出了搜索算法,这种搜索本质上是广度优先算法。接下个我们来优化这个程序。把第一代儿子节点放在A[1]中,那么A[1]要有多大空间来放节点所,显然第一代只需能放10个节点就够了,因为最多可能的走步不会超过10步,那第二代呢,肯定就多了,第三代还会更多……,每代所需空间都不一样,那我们要如何分配空间,如果使用javascript、PHP等脚本语言来编程,内存空间分配问题基本不用管,但用C语言呢。假如每代最多10000个节点,然后,您想搜索200代,为了简化程序,您可以简单的分配一个200*10000即可解决问题。现在电脑内存很多,用这些空间虽不算奢侈,并且会取得很高的搜索速度,但本着求精、节约的精神,有必要优化A[][]数组问题。基本思想方法就是将这个二维数组压入一个一维数组中去。这个特殊的一维数据,称为队。队和数组还有些区别,构成一个队一般还需要队头指针和队尾指针,当我们读写节点数据时,高度有序的移动这两个指针进行存取节点而不至于混乱。伪程序中看到,第一代n1个儿子节点放在A[1]中,第二代放在A[2]中,这时A[1]中的很多空间就浪费了,不妨这样吧,读第一代节点时,把生成的第二代节点数据接在A[1]中最后一个节点之后,当第一代读完时,下一个就是第二代了;读第二代时,生成第三代节点,同样第三代也接往A[1]里的最后一节点之后,读的过程称出队,写过程过程称为入队。我们统一从队头开始读(出队),队尾处开始写(入队)。由于搜索时是一代代有序往下搜索,则队里的节点也是一代一代的往下接。为了有序进行,读取儿子节点时,我们将队头指针指向儿子节点开始处,然后读取节点,读完后队头指针往后移动一步,当移动n1次后,就读完n1个儿子节点。在读儿子节点的过程中,我们同时试走儿子节点,生成孙子节点并入队。如此过程,在队头指针读完儿子节点后,队头指针就会指向第一个孙子节点上。伪代码如下一步展开首节点A得所有儿子节点D数组(队)中P=1,P2=最后一个; //P指向D的第一个(队头指针),P2指向D的最后一个(队尾指针)
for(i=1;i<=n层;I++){ //查找n代(层)
k=P2-P //当前层节点个数
for(j=1;j<=k;j++){
B=D[P] //读取D中的第P个节点
检查B是否为目标节点,如果是,结束搜索
展开B并将所有节点追加到D[P2]中
P++,P2+=B展开的个数
}
}
2.4 数据结构
在求解华容道问题时,将棋盘的方格按列从左至右编为0,1,2,3 列,按行从上至下编为0,1,2,3,4 行。即每个方格都给定一个由行号和列号组成的二维坐标,可将棋子编号固定为0,1,2,3表示四个刘备兵,4,5,6,7,8 表示五虎将,9 表示曹操,有必要时将两个空格也看作两个棋子编号为10,11。这样棋盘的布局可用一个5 行4 列的二维数组来表示。图1 所示的布局可表示为二维数组判断,是否为目标布局,仅需判断该数组中的第四五行中的第二三列元素的取值是否都等于9。由于棋子的形状不全相同,因此需要一个一维数组来表示按棋子编号顺序排列的棋子的形状编号。可用1 表示占一个方格,2 表示占竖排的两个方格,3 表示占横排的两个方格,4 表示占四个方格,5 表示空格。给定初始布局相当于初始化两个数组,对图1所示的初始布局可进行如下初始化:
int initstatus[5][4]={4,9,9,5,4,9,9,5,6,8,8,7,6,0,1,7,2,10,11,3};
int mantype[12]={1,1,1,1,2,2,2,2,3,4,5,5};
为了提高程序的运行效率.根据华容道问题的特点以下介绍三种表示方法。
(1) typedef struct { unsigned int num; 其中num 表示节点编号father 表示父unsigned int father; 节点编号二维数组status 的元素表示unsigned char status[5][4]; 棋盘上相应位置的棋子编号} NODETYPE1;
这种表示方法比较直接,在扩展节点及进行节点的比较时,不必进行数据转换。但每个节点占用24 个字节的存储空间较浪费空间。
(2) typedef struct { unsigned int num; 其中num 和father 的含义同上字节数组unsigned int father; status 的元素表示相应编号的棋子所占的方unsigned char status[12]; 格中左上角这一格的坐标高四位表示行
} NODETYPE2; 坐标低四位表示列坐标。
这种表示方法,通过存储每个棋子在棋盘上位置的坐标,来描述节点状态,在扩展节点及进行节点的比较时,需将它转换成一个二维数组。但每个节点占用16 个字节较节省空间。
(3) typedef struct { unsigned int num; 其中num 和father 的含义同上对棋盘上的unsigned int father; 方格按从左到右从上到下的顺序扫描将unsigned char status[6]; 棋子的编号按出现顺序存储在数组status 中
} NODETYPE3;
每个棋子的编号仅存储一次,占四位。这种表示方法同样在扩展节点及进行节点的比较时,需进行数据转换。每个节点仅占10个字节,大大地节省了空间,但需做较多的运算。
图1 所示的节点状态在上述三种方法下可分别表示成以下三组数据
NODETYPE1:
{4,9,9,5,4,9,9,5,6,8,8,7,6,0,1,7,2,10,11,3} ,每个数据占一个字节,共20 个字节。
NODETYPE2:
{0x31,0x32, 0x40,0x43,0x00,0x03,0x20,0x23,0x21,0x01,0x41,0x42}共12 个字节
NODETYPE3:
{0x49,0x56,0x87,0x01,0x2a,0xb3 } 共6 个字节。
3使用HashTable以及索引的方法来解决问题
要解决索引问题,可能有很多种方法,首先想到的是使用哈希表的办法,哈希表是棋类游戏常用的方法,算法原理不难,不过实现起来也挺麻烦的,使用哈希表时一般使用随机数来建立索引,因此一定要选择有足够散列度随机数(或准随机算法),以免造成大量哈希冲突而降底效率。以下介绍的方法是通过编码及查表方法来完成节点索引,建立一种不会发生重复的索引。总之,就是要建立一个索引,哈希表是个有重复的索引(碰到冲突的一般要做二次哈希),编码方法是建立一个无重复索引。本文讲述的的编码方法得到的速度不会很快,如果你追求速度,最好使用哈希表建立索引,并且在计算哈希值时采用增量技术,这样计算索引号的速度可提高10至20倍,程序在10ms内可搜索出最优解。
盘面的状态(节点)数量是十分有限的,状态总数不会超过50万种。(横刀立马为例)曹操的走法只有12种,任你如何排放,只有12种,不是20种。横将(关羽)的排法最多只有11种。接下来对4个竖将排列(组合),排列第一个竖将的排法最多10种,第二个8种,第三个6种,第四个4种。组合数是10*8*6*4/4!=80,后来为来编程方便,做了更多冗于,组合数用C10取4,即C(10,4)=10*9*8*7/4!=210,这样,4个竖将的某一排列组合必对应0—209中的一个数,这个数就是我们所要的竖将组合编码值。同理小兵的组合为C(6,4)=15,编码范围在0—14因此对这4种(10个)棋子全排列,种数最多为12*11*210*15=415800,即4百多K。最后易得盘面编码:各种棋子的编码值乘以码权,然后取和。码权只需遵照排列规律,随你定,是比较简单的。可设兵的码权为1,竖将则是15,横将则为15*210,曹操为15*210*11。要如何对各种棋子高速有效的编码呢?如“横刀立马”开局,如何编码?这又变成一个组合问题。我们一个一个的排放“横刀立马”棋子并演示编码过程。曹操有12个可排放位置,这12个位置编号为0-11,曹操位置在1,注意,首个是0。关羽有11个可排放位置,这11个位置编号为0-10,关羽位置在1个。竖将有10个可排放的位置,编号为0-9,一将是0,二将是1,三将是4,四将是5。小兵有6个可排放的位置,编号为0-5,一兵是0,二兵是1,三兵是2,四兵是5。竖将编号序列为0,1,4,5,这一组合对应的组合序号(编码)是多少呢,如何定义?真还有点不好处理,有人说这与群论有关。0,1,4,5表示的是各个将的位置,竖将在位用1表示,不在位用0表示,则0,1,4,5可示意为11001100000,这不就成了二进制数,不妨把0145转为二进数,用查表法转换是很快的,只需4个加法语名即可完成,再用这个二进数当作数组的下标来查组合的编号表,从表中得到它的编号,设表为Sb,则编号值为Sb[11001100000]或Sb[816],这样就完成了高速编码。这个编号表如何建立呢?这也好办,事前把0000000000—1111111111共1024个数中含4个1的数按顺序编号,其中只有C(10,4)=210个数被编号,其余不用。由此建立一个1024长的有序表Sb[],表中位置下标含4个1的被编号,共210个。竖将编码过程表示为:0145=>1100110000=>Sb[100110000]即Sb[816],小兵同样方式编码0125=>111001=>Bb[111001]即Bb[57]。上述,编码后盘面总数最多为415800,当我们记录每一个节点是否已经遍历时,最多只需415800个字节,如果是广度搜索,还可按比特方法压缩8倍,只需415800/8=51975个字节,现在的计算机,内存很存都很强,随便一个new,就可得几兆内存,几百兆也没问题,哪里在乎这4百多K,跟本无需压缩,压缩也是在浪费时间。有了上述排列组合的关系,便可很轻松的写一个编码函数,从而建立与节点相关的表或索引表等。如,可用编号做为数组的下标来询址,找到相应的节点记录。这样速度就会很快,在检查节点是否已经遍历过的时候,无需一个一个的往前查!速度要快几十倍!用广度搜索时,每层一般有数百个甚至上千个节点,一个一个的查过去是很费时的。有了编码函数,也可用来对棋盘压缩。“横刀立马”最大编号为415800,只占用24bit(3个字节)。压缩棋盘后,就还要有个解压缩,用的当然是相应的解码算法。在DOS模式下,内存不够用多考虑压缩,用VC就没必要压缩了。在华容道程序中,要搜索以前出现过的布局,及时消除重复布局。早最初的设计中,有人使用AVLTree来达到这个目的。AVLTree是基于比较运算来进行搜索的,其算法的复杂度为O(Log2(n))级别,也是基于比较的搜索算法所能达到的最高效率。要想继续提高搜索效率,就得考虑使用非基于比较的运算。Hash算法就是一个非常值得推荐的算法,它理论的平均时间复杂度是1。
链地址法如下图所示:
链地址法的数据插入和删除比较简单,如果HashCode的计算是高效的,那么在同一个位置的键值就很少,搜索长度也会很短,而且可以很节省空间。我的Hash算法经验证,一个HashCode下最多出现10个节点,平均为3个节点左右。
HashCode的计算如下:
public static int ToHashCode(Int64 layout)
{
Int64 mask = 16383; // 二进制 11111111111111
int hashCode = 0;
for(int i = 0; i <= 5; i++)
hashCode += (int)((layout & (mask << i * 14)) >> i * 14);
return hashCode;
}
完整的HashTable代码如下:
using System;
public class HashTable : IHashTable
{
protected class Node
{
public Int64 info = 0;
public Node link = null;
public Node()
{
}
public Node(Int64 layout)
{
this.info = layout;
}
}
private const int HashTableCapacity = 66000;
private Node[] _hashTable = new Node[HashTableCapacity];
//=============================================
// 构造函数
//=============================================
public HashTable()
{
for(int i=0; i<HashTableCapacity; i++)
_hashTable[i] = new Node();
}
//=============================================
// 插入测试,判断是否有重复
//=============================================
public bool Insert(Int64 layout)
{
int hashCode = HashTable.ToHashCode(layout);
Node current = _hashTable[hashCode];
while(current.link != null)
{
if(current.link.info == layout)
return false;
current = current.link;
}
Node newNode = new Node(layout);
current.link = newNode;
return true;
}
//=============================================
// 释放内存
//=============================================
public void Release()
{
for(int i=0; i<HashTableCapacity; i++)
_hashTable[i].link = null;
}
//===========================================================
// 计算HashCode
//===========================================================
public static int ToHashCode(Int64 layout)
{
Int64 mask = 16383; // 二进制 11111111111111
int hashCode = 0;
for(int i = 0; i <= 5; i++)
hashCode += (int)((layout & (mask << i * 14)) >> i * 14);
return hashCode;
}
}
3目前一些学者的探讨
3.1使用堆栈结构输出使得步数最少
由于在树型结构设计上,每个子节点都保留了一个对父节点的引用。所以一旦找到最优解,我们就需要从最底层向上追溯所有移动步骤(如下图)。但这个顺序与走棋的顺序正好相反。借助一个堆栈结构实现后进先出便把这个次序逆转过来了。
3.2 引用环形链表解决求解下一步走法的问题
广度搜索中我们还面临着以下几个问题:
第一,4个字节的棋局表示虽然减小了存储空间,但不利于求解。为了实现步法移动,我们至少要分清棋子的上下左右,4字节的表示方式很难达到这一点。我们需要另外一种带方位的棋局表示方式以便进行求解,而这种表示方式的棋局数量还不能太多,在使用完后要及时释放,否则就会占用大量内存。
第二,为了降低系统模块间的耦合度,尽量让带方位的棋局表示与4字节表示剥离开,两者能够独立变化。
胡华旦的《"华容道"难题的计算机解》一文中记录了某一步走法的最大可能布局数(树型结构某层级的最多布局数)在200到800之间(10棋子),或1400左右(11棋子),或1800左右(12棋子)。由于我们只研究10棋子布局,所以这个池的大小设置在800比较合适(实际检验的结果是池的大小在1100左右,因为上一步走法与下一步走法存在相关性,并且10子布局中某一步最大可能布局数经检验会大于1000)。通过一个环形链表(加入必要的溢出检验)可以很容易的达到目的。设计如下:
环形链表中的每一个节点都是一个带方位关系的布局表述,有了这个环形链表,就可以将在分析1中的链表结构从树型结构中剥离开了。
3.3 AVL树(平衡树)
每当我们求解得到下一步的一个可行走法后,都要检查该走法完成后的棋盘布局是否在以前搜索过的布局中出现过,如果出现过的化则直接剔除,不再添加到树型结构和环形链表中。这就需要一个检索的过程。
我们知道,在一个未排序的队列中进行检索是一个很耗时的工作,需要遍历每个结点。如果经过排序则可以使用二分法迅速定位。我们可以把所有出现过的布局组织到一个结构中,这个结构应当满足以下两点:一是能够快速的进行查找,二是不允许该结构中出现重复值。在这里,AVL树是再合适不过了。它基于二叉树,并且不允许树中两个节点具有相同取值,同时还可以保证最高的检索效率。在下一篇文章中我将重点介绍AVL树,并说明它在华容道求解过程中的应用。
平衡树其实就是一特殊结构的二叉树。由于二叉树的搜索算法的性能取决于二叉树的结构,如果二叉搜索树构造出来是线性的,搜索算法的效率不高。如果结构合理,则查找速度较快。实际上,树的高度越小,查找速度越快。大家可以比较一下下面两个二叉树在检索时,哪个效率更高一些:
AVL树(也称作平衡树),在这种树型结构中,二叉树结构近似于平衡。AVL树具有如下特征:
1. 根的左子树和右子树的高度差的最大值为1。
2. 根的左子树和右子树都是AVL树。
补充:AVL树中不允许出现重复值,这正好与我们的目的相同。
在对AVL 树执行插入操作时,我们通过"旋转"达到确保高度差的目的。如图:
AVL树的重构过程就是"旋转",有两种类型的旋转,左旋与右旋。常见的旋转方法如下:
举个例子
3.4 剪枝问题:
第n层(代)的某一节点M,往前试走一步生成Q,当然Q就是n+1层节点。Q有没有可能同以前走过的节点相同呢,当然有可能,走象棋时很明显,不同走法可能产生相同结果!设以前的走过节点都没有重复,Q不可能与小于n-1层的节点重复,如果重复会有什么会结果?Q到M只一步,M到Q也只需一步,Q与n-2层重复,则Q为n-2层而不是n+1层,Q可生成M,M就会在n-2+1=n-1层出现过,这时n和n-1层都有M,与题设矛盾。因此,每走一步,一直往前查到n-1层,如果Q没有重复即为新生节点,否则应剪枝(即这样的节点不能入队)。再细想,单是以上算法还是不够快。如:父亲A在移动曹操时,生了儿子P。儿子P生孙子时也移动曹操得到R,从棋局中发现这时R和A是同一节点,即父亲A等于孙子P,这是明显的节点重复,为这样的R去检查重复也是浪费时间。因此,发现要移动的棋子与父节点当时移动的子是同一个棋子就别试走,事实证明这样可少测试节点达1/3,使遍历速度提高20%。我们还可进一步优化,某一盘面A1必存在与之左右对称的的盘面A2,目标节点M1必存在与之左右对称的盘面节点M2,设两节点最短路径为min(点1,点2),则min(A1,M1)==min(A2,M2),当M1为目标结点并且M2也为目标节点时,搜索A1和搜A2得到的最优结果是等价的,只需搜结果A1。华容道只要求曹操从正下方移出,所以M1,M2都是目标节点,因此,搜索了A1就没必要搜索其对称节点A2,这样可使程序效率提高近一倍。通过以上剪枝优化,生成的树已接近最小树。
3.5 回朔问题:
当我写完程序时,发现把曹操移出后,却不知道是具体的移动过程,也就是说不知道具体的路径。原因在哪里呢?伪代码中没有记录走过的路径,当找到目标节点却不知道是如何走来的,因此产生儿子节点的过程中还应告诉儿子:它的父亲是谁。当我们得到目标结点后,我们就问目标结点:你的父亲是谁,然后我们找其父,再问:你的父亲是谁,如此一层一层往上追问,最后就知道了全路径。这个过程我常称“回溯”(也许不准确)。
4 今后的研究方向
算法的不足处在于事先知道最优解状态的情况下去搜索最优解,它只是一种事后验证的方法。例如对布局横刀立马,给定一种最优解状态下搜索节点数仍高达11811。与而通常情况下,是不知道哪种目标状态是最优解的目标状态的。人工智能中比较高效率的搜索策略是,应用与问题相关信息的启发式搜索。如重排九宫图与移动将牌问题,它们均使用当前状态与目标状态的差异程度作为估价函数。但对华容道问题这种方法显然行不通,如布局近在咫尺与目标状态的差异极小,却需要多达98 步才能完成,所以使用启发式搜索来解该问题难度比较大,应用广度优先搜索策略应是一种比较好的方法。
更多内容:
软件课程设计--C语言设计火车票订票系统之源代码(模拟数据库功能)
http://blog.csdn.net/ctu_85/archive/2007/09/20/1793381.aspxa
C++Primer 第四版 部分习题解答
http://blog.csdn.net/ctu_85/archive/2007/07/04/1677834.aspx
麻省理工算法导论翻译
http://blog.csdn.net/ctu_85/archive/2007/06/08/1643179.aspx
浙江大学ACM试题解答(四月)
http://blog.csdn.net/ctu_85/archive/2007/04/24/1576831.aspx
浙江大学ACM试题解答(三月)
http://blog.csdn.net/ctu_85/archive/2007/03/20/1535556.aspx
华容道游戏与算法
http://blog.csdn.net/ctu_85/archive/2007/05/15/1610722.aspx
中国象棋对战程序C语言源代码
http://blog.csdn.net/ctu_85/archive/2007/05/04/1596351.aspx
Prim算法代码分析
http://blog.csdn.net/ctu_85/archive/2006/12/16/1445156.aspx
Kruskal算法代码分析
http://blog.csdn.net/ctu_85/archive/2006/12/16/1445147.aspx
文件加密解密源代码分析
http://blog.csdn.net/ctu_85/archive/2006/12/23/1455515.aspx
银行家算法代码分析
http://blog.csdn.net/ctu_85/archive/2006/09/09/1198551.aspx
更多试题解答:
两种计算Ack(m,n)的非递归算法
http://blog.csdn.net/ctu_85/archive/2006/11/29/1419396.aspx
上海交通大学1999年
http://blog.csdn.net/ctu_85/archive/2006/11/09/1376289.aspx
东北大学2001年
http://blog.csdn.net/ctu_85/archive/2006/11/09/1376287.aspx
清华大学1994年
http://blog.csdn.net/ctu_85/archive/2006/10/24/1349754.aspx
中国科学院2002年
http://blog.csdn.net/ctu_85/archive/2006/10/24/1349704.aspx
浙江大学计算机复试解答1
http://blog.csdn.net/ctu_85/archive/2006/10/15/1334936.aspx
浙江大学计算机复试解答2
http://blog.csdn.net/ctu_85/archive/2006/10/16/1336101.aspx
浙江大学计算机复试解答3
http://blog.csdn.net/ctu_85/archive/2006/11/02/1363159.aspx
软件可行性报告
http://blog.csdn.net/ctu_85/archive/2006/06/06/775894.aspx
软件需求分析报告
http://blog.csdn.net/ctu_85/archive/2006/06/06/775892.aspxa
发表于 @ 2007年05月15日 22:48:00|评论(loading...)|编辑