倒水问题: 引例:有装满水的6升的杯子,空的3升杯子和1升杯子,3个杯子都没有刻度; 在不使用道具的情况下,如何称出4升的水呢?
相信聪明的你已经算出来了; (6,0,0)->(3,3,0)->(3,2,1)->(4,3,0);
现在你的任务是解决一个一般性的问题: 设有大中小3个杯子的容量分别是a,b,c最初只有大杯子装满水,其他两个杯子为空;
最少需要多少步才能让某一个杯子中的谁有x升呢? 你需要打印出每次操作后各个杯子中的水量(0<c<b<a<1000);
输入:三个杯子的容积a,b,c然后是要称出的水量d;(d<=c);
输出:操作的次数,每一次操作后的三个杯子的水量(a,b,c)回车;
我们分析一下这个问题,发现是要分好多步来完成的,但是由于让找到最少的步数,所以不能用深度优先遍历方法枚举呢,
那么我们可以在解答树上用广度优先遍历,这样就能找到最少的了;
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 /*下面定义结点的类型*/ 5 #define Min(a,b) ((a>b)?b:a) 6 //宏定义的操作符Min作用 取最小值; 7 const int MAX=1010; 8 //解答树的结点最多 9 typedef struct Node 10 { 11 int v[3];//三个杯子的剩余水量; 12 int fa;//第一次访问到这个结点是的前一个结点的下标; 13 //通过下标可以随机访问真不错; 14 int last_op;//记录从上个结点到这个结点的变化方式 15 //last_op认为是两位数字,第一位表示往外倒水的杯子 16 //的编号,第二位表示被倒入水的杯子; 17 int deepth; 18 //记录从原始状态到这个状态变化的最少的步数;也就是解答树的深度; 19 }Node; 20 int cup_capacity[3];//存储三个杯子的容量; 21 int water_to_get; //存储要量出的水量; 22 Node q[MAX*MAX]; //定义这么多个状态组成的数组; 用这个数组来表示队列真不错; 23 int vis[MAX][MAX];//两个下标分别是中杯的余量和小杯的余量; 24 void print_path(int destination);//函数的作用是输出从下标为0到下标为destination的结点的路径; 25 void bfs();//breath-first-search;广度优先遍历 ; 26 int main() 27 { 28 /*首先读入三个杯子的容量和要量出的水量*/ 29 scanf("%d%d%d%d",&cup_capacity[0],&cup_capacity[1],&cup_capacity[2],&water_to_get); 30 /*进行遍历搜索*/ 31 memset(vis,0,sizeof vis);//sizeof 对于变量名可以不用括号 表明 它并不是一个函数 ; 32 /* 33 memset()起初是为字符数组赋值的所以需要包含头文件string.h, 34 但是它可以为结构体,一维数组,二维数组等赋值了 ; 35 这里其实并不需要赋值,因为全局变量已经被初始化了; 36 */ 37 bfs(); 38 return 0; 39 } 40 void bfs() 41 { 42 q[0].v[0]=cup_capacity[0]; 43 q[0].v[1]=q[0].v[2]=0; 44 q[0].fa=0;//因为第一个结点没有父节点了; 45 q[0].deepth=0;//也可以不写这些0;因为全局变量会自动初始化; 46 q[0].last_op=0;//因为是第一个结点 ; 47 vis[q[0].v[1]][q[0].v[2]]=1;//表示这个结点已经在队列里了 48 //如果在遇到它就不再将它放进队列了; 49 //上个句子也可以这么写vis[0][0]=1; 50 int front=0,rear=1;//模拟队列;可以实现广度优先遍历; 51 while(front<rear) 52 { 53 Node N=q[front];//当前队列中将要访问的变量; 54 if(N.v[0]==water_to_get||N.v[1]==water_to_get||N.v[2]==water_to_get) 55 {//如果当前结点正好就是要访问的,那么就来输出它的路径; 56 //首先输出需要的步数; 57 printf("%d\n",N.deepth); 58 //然后调用递归函数依次输出各个中间状态; 59 print_path(front); 60 return; 61 } 62 for(int i=0;i<3;i++)//往外倒水的杯子进行枚举; 63 { 64 65 for(int j=0;j<3;j++)//被倒入水的杯子的枚举; 66 {//i!=j;是避免了那些自己往自己里面倒水的情况发生; 67 Node &IN=q[rear]; 68 if(i==j)continue; 69 int amount=Min(N.v[i],cup_capacity[j]-N.v[j]);//这个是用来决定倒出水的多少 70 //要么倒水的杯子倒完;要么被倒入的杯子被倒满; 71 for (int k=0;k<3;k++) 72 { 73 IN.v[k]=N.v[k]; 74 } 75 IN.v[i]=N.v[i]-amount;//从i杯倒出amount量的水; 76 IN.v[j]=N.v[j]+amount;//倒入j杯amou量的水; 77 //至此就产生了一个状态;然后就要看它是否已经存在于队列里面了; 78 if(!vis[IN.v[1]][IN.v[2]]) 79 { 80 vis[IN.v[1]][IN.v[2]]=1; 81 IN.fa=front;//设置父节点的下标; 82 IN.deepth=N.deepth+1;//设置最少步骤数; 83 IN.last_op=10*i+j;//记录变换的方法; 84 85 rear++; 86 }//if 87 } //for(j) 88 } //for(i) 89 front++;//从队列的下一个结点向下一次考察; 90 91 }//while 92 } 93 void print_path(int destination) 94 { 95 if(q[destination].fa!=destination) 96 { 97 print_path(q[destination].fa);//先找到原始的元素;递归栈机制可以记得路径 98 int from=q[destination].last_op/10,to=q[destination].last_op%10; 99 int num=q[q[destination].fa].v[from]-q[destination].v[from]; 100 printf("cup %d (-%d)->cup %d\n",from,num,to); 101 } 102 printf("(%d,%d,%d)\n",q[destination].v[0],q[destination].v[1],q[destination].v[2]); 103 104 }
(1)一个杯子往外倒水,由于没有其它工具,所以有两种情况;要么水倒完,要么被倒水的杯子满了,这样才会停止,
具体选择哪一个方案,取决于当前往外倒水的杯子的水量和被倒水的剩余空间谁打谁小;Min()就是做这个工作的,
(2)vis的作用在于保证已经存在于队列中的或者已经被访问过的元素不会再次被访问,这样即可以减少操作,
还能保证步骤的最少;
(3)结点结构体包含的信息有:父节点的下标,步骤数,三个杯子的状态,从父节点到本结点的操作方式可以从last_op推算得到;
(4) memset()起初是为字符数组赋值的所以需要包含头文件string.h,
但是它可以为结构体,一维数组,二维数组等赋值了 ;
这里其实并不需要赋值,因为全局变量已经被初始化了;
(5)这段代码用数组和front rear来模拟队列,即使用了队列先进先出的特性,有用到数组随机访问的特性(在访问父节点的时候很方便)
(6)数组的大小是够用的;分析解答树就会知道;