1. 问题分析:

面对这种穷举难度不大的问题首先想到的自然是穷举法。但是由于是算法问题,所以思考是否能对穷举法进行一定的优化。

根据题目已知,狼和羊不能同时呆在一侧,羊和白菜也不能同时呆在一侧,由于在运输的过程中不可避免的会出现有两样东西在一侧的情况,即仅能出现狼和白菜同时出现在一侧的情况。(1)

由于农夫每次只能运送一样物品,结合该条件和条件(1)可以推断,最后一次和第一次运送都一定是将羊运送到了河对岸(2)。(羊和其他两者都不兼容)

发现羊为不兼容物后,问题就简化为如何令羊永远不离开农夫身边(或让羊单独呆着)(3),如此一来第一次一定要运送羊去河对岸,然后农夫空船回来,然后运送白菜或者狼去河对岸,然后把羊带回来,然后把羊放在这边,把狼和白菜剩余的另一个运送到河对岸,空着船回来,最后再把羊运送到河对岸。

这样题目的分析就完毕了,根据题目分析,我认为在代码实现过程中只要遵守条件(3)就可以求得可行解。同时利用判定条件三,也可以使得代码不必遍历所有的可能性,一定程度上提高了代码的时间效率。

2. 代码设计:

  1. 设计两个固定长度的数组a、b,每个数组的长度为3,其中0、1、2位置分别表示羊、菜、狼。其中a为原河岸,b为河对岸。每个位置为1则代表该物品在数组相应河岸,值为0代表该位置物品不在该河岸。
  2. 设计一个flag为布尔或整型,其值0、1分别表示农夫当前在原河岸或河对岸。
  3. 利用while循环推动程序前进,退出循环条件为数组b内每个元素均为1。
  4. 农夫一共有8种动作,前三种是把a岸的某一位置由1变为0,把b岸相应位置变为1,第四种是不对数组做变换,而直接改变flag的值0变成1(即空船过岸),后四种与前四种相反。将这八种动作进行编编码1——8,用该编码顺序表示方案。
  5. 利用随机数生成此次农夫采用的方案,若flag=0,则随机生成1——4,若flag=1,则随机生成2——8.
  6. 若农夫不在的那一侧数组的0号位置为1,则必须其他两个位置全为0。
  7. 每次执行一次动作后,就用上一条判断该动作的可行性,若可行则将该动作加入动作序列的末尾,若不可行则不加入,重新生成一个随机数。
    注意:在判定的过程中,农夫当前在的河岸发生任何组合都可以,但农夫不在的河岸有限制
  8. 由于每次生成的数是随机数,所以当执行次数足够多的情况下基本可以避免死循环的情况。但生成的解很难是最优解(即解的过程中可能会有很多没有意义的动作(如农夫空船来回)),可以对生成的解进行优化,但由于原题目仅要求给出农夫解决问题的一个方案,故在本次代码中不考虑优化。

实现代码:

根据以上设计,建立了农夫类,该类内属性由两岸的状态,农夫所在位置,和最终执行的计划,农夫类源码如下:

package CH_2;
import java.util.Random;

import CH_2.SinglyLinkedList.SinglyList;

public class Farmer {
	boolean flag = true;
	int []a = {1,1,1};  //初始状态:分别代表羊、菜、狼
	int []b = {0,0,0};
	SinglyLinkedList<Integer> p = new SinglyLinkedList();  //建立一个外部类的对象
	SinglyLinkedList<Integer>.SinglyList<Integer> plan = p.new SinglyList<Integer> (); //通过外部类的对象直接new一个内部类对象
	
	public boolean judge(int[] tempA, int[] tempB, boolean tempF) {	//对传入的两个数组进行判断,看此情况是否被允许
		if(!tempF){  //若农夫在此岸,此时只用判定对岸
			if((tempB[0]==1) && (tempB[1]+tempB[2] != 0))
				return false;  //若此时对岸羊和其他任何生物在同一边,则返回false
			else return true;
		}
		else {  //若农夫在对岸,此时只用判定此岸
			if((tempA[0]==1) && (tempA[1]+tempA[2] != 0))
				return false;  //若此岸羊和其他任何生物在同一边,则返回false
			else return true;
		}
	}
	public int perform() {	//执行动作方法
		Random r=new Random();
		if(this.flag) {  //若农夫在河的此岸
	        return r.nextInt(4)+1;	//返回1-4的随机数
		}
		else {
			return r.nextInt(4)+5;  //返回5-8的随机数
		}
	}
	public int rst() {  //返回b岸所有值的累加结果,用于判定是否结束
		return this.b[0]+this.b[1]+this.b[2];
	}
	public void crossing() {	//过河
		this.flag = !this.flag;  //改变状态
	}
	public void act1(){  //动作1:从此岸空船到对岸
		if(judge(this.a,this.b,this.flag)) {
			crossing();  
			this.plan.insert(1);
		}
	}
	public void act2() {  //动作2,把羊运过河(若可行则改变plan,否则什么都不做)
		 if(this.a[0] == 1) {  //仅仅当这边有羊的时候(否则该函数什么都不用做)
			int []tempA = new int[3];
			int []tempB = new int[3];
			for(int i=0;i<3;i++) {  //拷贝过来用于备份
				tempA[i] = this.a[i];
				tempB[i] = this.b[i];
			}
			tempA[0] = 0;
			tempB[0] = 1;  //运羊
			if(judge(tempA,tempB,this.flag)) {  //如果执行完该动作仍合法
				//这里传入!flag的原因:执行该动作后农夫的flag属性与当前相反
				this.a[0] = 0;
				this.b[0] = 1;  //更改真正的值(真运)
				this.plan.insert(2);  //告诉上级可行,把这个写在paln后面
				crossing();  //之后就可以安心按照这个方案渡河了
			}
		}
	}
	public void act3() {  //动作3,把菜运过河(若可行则改变plan,否则什么都不做)
		 if(this.a[1] == 1) {  //仅仅当这边有白菜的时候
			int []tempA = new int[3];
			int []tempB = new int[3];
			for(int i=0;i<3;i++) {  //拷贝过来用于备份
				tempA[i] = this.a[i];
				tempB[i] = this.b[i];
			}
			tempA[1] = 0;
			tempB[1] = 1;  //运白菜
			if(judge(tempA,tempB,this.flag)) {  //如果执行完该动作仍合法
				this.a[1] = 0;
				this.b[1] = 1;  //更改真正的值(真运)
				this.plan.insert(3);  //告诉上级可行,把这个写在paln后面
				crossing();  //之后就可以安心按照这个方案渡河了
			}
		}
	}
	public void act4() {  //动作4,把狼运过河(若可行则改变plan,否则什么都不做)
		 if(this.a[2] == 1) {  //仅仅当这边有狼的时候
			int []tempA = new int[3];
			int []tempB = new int[3];
			for(int i=0;i<3;i++) {  //拷贝过来用于备份
				tempA[i] = this.a[i];
				tempB[i] = this.b[i];
			}
			tempA[2] = 0;
			tempB[2] = 1;  //运狼
			if(judge(tempA,tempB,this.flag)) {  //如果执行完该动作仍合法
				this.a[2] = 0;
				this.b[2] = 1;  //更改真正的值(真运)
				this.plan.insert(4);  //告诉上级可行,把这个写在paln后面
				crossing();  //之后就可以安心按照这个方案渡河了
			}
		}
	}
	public void act5() {
		if(judge(this.a,this.b,this.flag)) {
			crossing();  //从对岸空船到此岸
			this.plan.insert(5);
		}
	}
	public void act6() {
		if(this.b[0] == 1) {  //仅仅当对岸有羊的时候(否则该函数什么都不用做)
			int []tempA = new int[3];
			int []tempB = new int[3];
			for(int i=0;i<3;i++) {  //拷贝过来用于备份
				tempA[i] = this.a[i];
				tempB[i] = this.b[i];
			}
			tempA[0] = 1;
			tempB[0] = 0;  //运羊
			if(judge(tempA,tempB,this.flag)) {  //如果执行完该动作仍合法
				//这里传入!flag的原因:执行该动作后农夫的flag属性与当前相反
				this.a[0] = 1;
				this.b[0] = 0;  //更改真正的值(真运)
				this.plan.insert(6);  //告诉上级可行,把这个写在paln后面
				crossing();  //之后就可以安心按照这个方案渡河了
			}
		}
	}
	public void act7() {
		 if(this.b[1] == 1) {  //仅仅当对岸有白菜的时候
			int []tempA = new int[3];
			int []tempB = new int[3];
			for(int i=0;i<3;i++) {  //拷贝过来用于备份
				tempA[i] = this.a[i];
				tempB[i] = this.b[i];
			}
			tempA[1] = 1;
			tempB[1] = 0;  //运白菜
			if(judge(tempA,tempB,this.flag)) {  //如果执行完该动作仍合法
				this.a[1] = 1;
				this.b[1] = 0;  //更改真正的值(真运)
				this.plan.insert(7);  //告诉上级可行,把这个写在paln后面
				crossing();  //之后就可以安心按照这个方案渡河了
			}
		}
	}
	public void act8() {
		if(this.b[2] == 1) {  //仅仅对岸边有狼的时候
			int []tempA = new int[3];
			int []tempB = new int[3];
			for(int i=0;i<3;i++) {  //拷贝过来用于备份
				tempA[i] = this.a[i];
				tempB[i] = this.b[i];
			}
			tempA[2] = 1;
			tempB[2] = 0;  //运狼
			if(judge(tempA,tempB,this.flag)) {  //如果执行完该动作仍合法
				this.a[2] = 1;
				this.b[2] = 0;  //更改真正的值(真运)
				this.plan.insert(8);  //告诉上级可行,把这个写在paln后面
				crossing();  //之后就可以安心按照这个方案渡河了
			}
		}
	}
}

随后,针对农夫类,写了测试代码如下:

package CH_2;
import java.util.Random;

public class FarmerDemo {
	public static void main(String[] args) {
       Farmer f = new Farmer();  //新建一个农夫对象
       while(f.rst() != 3) {  //只要对岸还没齐
    	   int per = f.perform();  //令农夫生成一个与当前状态相关的动作
    	   switch(per) {
    	   case 1:
    		   f.act1();
    		   break;
    	   case 2:
    		   f.act2();
    		   break;
    	   case 3:
    		   f.act3();
    		   break;
    	   case 4:
    		   f.act4();
    		   break;
    	   case 5:
    		   f.act5();
    		   break;
    	   case 6:
    		   f.act6();
    		   break;
    	   case 7:
    		   f.act7();
    		   break;
    	   case 8:
    		   f.act8();
    		   break;
    	   }
       }
       System.out.println(f.plan.toString());
       
	}
}

测试结果如下:

狼羊白菜 python 回溯 把狼羊白菜_算法


在写代码的过程中遇到过几个需要注意的问题:

  1. 对两岸状态进行检验时,不需要都检验,只需要检验农夫不在的那一岸就行,因为农夫在的那一岸是允许任何状态的。
  2. 尽管农夫空船过河不影响两岸的状态,但空船过河也是必须经过judge()函数判断后才能进行的,因为由1可知,尽管两岸状态不变,但农夫状态改变后会导致被判断的岸变为另一侧的岸。(即农夫不能空船随便跑)

4、算法改进(结果优化)及遇到问题

可以看到,虽然上述方法可以给出一个一定可行的解,但由于方法的随机性,很可能将同一动作执行多次(如农夫不断空船来回),从而使解的长度很长,为了解决这个问题,我们对上述算法求解出来的方案进行优化,优化方法的代码如下:

String answer = f.plan.toString();
       char[] a = answer.toCharArray();
       
       SinglyLinkedList<Integer> la = new SinglyLinkedList();  //建立一个外部类的对象
		SinglyLinkedList<Integer>.SinglyList<Integer> laa= la.new SinglyList<Integer> (); //通过外部类的对象直接new一个内部类对象
       laa.insert(Integer.valueOf((int)a[1]-(int)'0'));
       for(int i=3;i<a.length;i+=2) {
    	   if(Math.abs(((int)a[i]-(int)'0')-((int)a[i-2]-(int)'0')) == 4) {
    		   i+=2;
    	   }
    	   else{
    		   laa.insert(Integer.valueOf(((int)a[i]-(int)'0')));
    	   }
       }
       System.out.println(answer);
       System.out.println(laa.toString());

该方法第一次运行确实得出了正确的解:

狼羊白菜 python 回溯 把狼羊白菜_顺序表_02


但多跑了几组之后出现如下出现问题:

狼羊白菜 python 回溯 把狼羊白菜_羊菜狼_03


如图,原答案虽然冗长但是逻辑性和正确性都是没有问题的,经过“优化”后,方案却在逻辑性和正确性上出现了问题:

如4,6,4,6片段,其意义为将狼运到对岸,将羊运回此岸,将狼运到对岸,将羊运到此岸。显然,第一次将狼运到对岸后此岸已经没有狼了,因此无法再次将狼运到对岸,羊同理。故此方案已经出现错误。

狼羊白菜 python 回溯 把狼羊白菜_羊菜狼_04


狼羊白菜 python 回溯 把狼羊白菜_java_05


在条件内打印,输出了一组做差的元素。找到了错误原因,即跳过一个元素后,下一个元素依旧与其相对位置的上一个元素做差并以此做判定条件,按照想法,应该让其与当前已经被加入的最后一个元素比较才是,且在if中令i+=2,之后又在for循环中加2导致跳过了一个元素,于是修改if循环内的i+=2位continue

还是错,研究序列后明白了:设有答案序列(a,b,c,d,e),若其中abs(d-c) == 0,此时不应该只跳过d,应该把c和d都删掉,按照该思想修改代码:
由于链表内删除最后一个元素不方便,故在这里把链表换成了顺序表
最终方案如下:

String answer = new String("(2,5,4,8,1,6,2,5,1,6,2,6,2,6,2,6,2,5,4,6,2,8,3,7,3,6,4,7,2,6,3,5,2)");
   //---------以下为优化方法
   //方法思路:定义一个动态顺序表,用于存放被优化后的方案
   //在保证顺序表不会为空的情况下(至少有一个值)
   //令指针在原方案中后移,若指针所指数字与当前顺序表中的最后一个数字差值为4
   //则说明该动作与当前优化方案的最后一个动作相互抵消(如空船来回)
   //此时,首先删除顺序表中当前的最后一个元素,随后令指针后移
   //如此循环判断,最终顺序表中留下的元素就是没有重复动作的最优解
   char[] a = answer.toCharArray();
   SeqList<Integer> lc = new SeqList<Integer>();  //创建一个顺序表,方便之后的动态操作
   
   lc.insert(Integer.valueOf((int)a[1]-(int)'0'));  //首先把序列的第一个动作放进顺序表
   for(int i=3;i<a.length;i+=2) {  //考虑到初始字符串中的逗号和括号,所以以此方式步进
	   if(lc.size()==0) {  //若顺序表中的动作已经被删光了,则不管三七二十一先放进去一个动作
		   lc.insert(Integer.valueOf((int)a[1]-(int)'0'));  //放进去当前指针所指的动作
		   continue;  //利用for循环让指针后移
	   }
	   if(Math.abs(((int)a[i]-(int)'0')-lc.get(lc.size()-1)) == 4) {  //若指针所指操作与当前
		   lc.remove(lc.size()-1);
		   continue;
	   }
	   else{
		   lc.insert(Integer.valueOf(((int)a[i]-(int)'0')));
	   }
   }
   System.out.println(answer);
   System.out.println(lc.toString());

最终结果如下,可以看到结果已经完全符合我们“最优解”的要求!

狼羊白菜 python 回溯 把狼羊白菜_算法_06


我们再跑几组:

狼羊白菜 python 回溯 把狼羊白菜_算法_07


狼羊白菜 python 回溯 把狼羊白菜_java_08

可以看到所有结果都是完全正确的,本次任务圆满成功,干杯!