用栈来求解汉诺塔问题
【题目】
汉诺塔问题比较经典,这里修改一下游戏规则:现在限制不能从左侧的塔直接移动到右侧,也不能从最右侧直接移动到最左侧,而是必须经过中间。求当塔又N层的时候,打印最优移动过程和最有移动总步数。
例如,当塔数为两层时,最上层的塔记为1,最下层的塔记为2,则打印:
Move 1 from left to mid
Move 1 from mid right
Move 2 form to mid
Move 1 from right to mid
Move 1 from mid to left
Move 2 from mid to right
Move 1 from left to mid
Move 1 from mid to right
It will move steps
【要求】
用以下两种方法解决:
- 方法一:递归的方法
- 方法二:非递归的方法,用栈来模拟汉诺塔的三个塔
【解答】
方法一:递归的方法
首先:如果只剩下最上层的塔需要移动,则有如下处理:
- 如果希望从“左”移到“中”,打印“Move 1 from left to mid”。
- 如果希望从“中”移到“左”,打印“Move 1 from mid to left 。
- 如果希望从“中”移到“右”,打印“Move 1 from mid to right
- 如果希望从“右”移到“中”,打印“Move 1 from right to mid
- 如果希望从“左”移到“右”,打印“Move 1 from left to mid”。“Move 1 from mid to right” 。
- 如果希望从“右”移到“左”,打印“Move 1 from right to mid”和“Move 1 from mid to left”。
以上就是递归的中之条件,也就是只剩上层塔时的打印过程
接下来,我们分析剩下多层塔的情况
如果剩下N层,从最上到最下依次为1~N,则有如下判断:
- 如果剩下的N层塔都在“左”,希望全部移到“中”,则有三个步骤:
- 将1~N-1层塔先从“左”移到“右”,明显交给递归过程。
- 将第N层塔从“左”移到“中”。
- 再将1-N-1层塔全部从“右”移到“中”,明显交给递归过程
- 如果把剩下的N层塔全部从“中”移到“左”,从“中”移到“右”,从“右”移到“中”,过程与1同理,一样分为三步。
- 如果剩下的塔都在“左”,希望全部移到“右”则有五个步骤。
- 将1-N-1层塔先全部从“左”移到“右”,明显交给递归过程。
- 将第N层塔从“左”移到“中”。
- 将1~N-1层塔全部从“右”移到“左”,明显交给递归过程。
- 将第N层塔从“中”移到“右”。
- 将1~N-1层塔全部从“左”移到“右”,明显交给递归过程。
- 如果剩下的N层都在“右”,希望全部移到“左”过程与情况3同理,一样是分解为5部。
以上递归过程经过逻辑化简之后的代码请参考如下hanoProblem1方法。
public class HanoiProblem1 {
public int hanoiProblem1(int num, String left, String mid, String right) {
if (num < 1) {
return 0;
}
return process(num, left, mid, right, left, right);
}
public int process(int num, String left, String mid, String right, String from, String to) {
if (num == 1) {
if (from.equals(mid) || to.equals(mid)) {
System.out.println("Move 1 from " + from + " to " + to);
return 1;
} else {
System.out.println("Move 1 from " + from + " to " + mid);
System.out.println("Move 1 from " + mid + " to " + to);
return 2;
}
}
if (from.equals(mid) || to.equals(mid)) {
String another = (from.equals(left) || to.equals(left)) ? right : left;
int part1 = process(num - 1, left, mid, right, from, another);
int part2 = 1;
System.out.println("Move " + num + " from " + from + " to " + to);
int part3 = process(num - 1, left, mid, right, another, to);
return part1 + part2 + part3;
} else {
int part1 = process(num - 1, left, mid, right, from, to);
int part2 = 1;
System.out.println("Move " + num + " from " + from + " to " + mid);
int part3 = process(num - 1, left, mid, right, to, from);
int part4 = 1;
System.out.println("Move " + num + " from " + mid + " to " + to);
int part5 = process(num - 1, left, mid, right, from, to);
return part1 + part2 + part3 + part4 + part5;
}
}
public static void main(String[] args) {
HanoiProblem1 test = new HanoiProblem1();
test.hanoiProblem1(3, "left", "mid", "right");
}
}
方法二:非递归的方法——用栈来模拟整个过程。
修改后的汉诺塔问题不能让任何塔从“左”直接移动到“右”,也不能从“右”直接移到“左”,而是要通过中间过程。也就是说,实际动作只有4个:“左”到“中”、“中”到“左”、“中”到“右”。
现在我们把左、中、右三个地点抽象成栈,依次记为LS、MS和RS。最初所有的塔都在LS上。那么如上4个动作就可以看作是:某一个栈(from)把栈顶元素弹出,然后压入另一个栈里(to),作为这一栈(to)的栈顶
例如,如果是7层塔,再最初时所有的塔都在LS上,LS从栈顶到栈底依次时1~7,如果现在发生了“左”到“中”的动作,这个动作对应的操作时LS将栈顶元素1弹出,然后1压入到MS栈中,成为MS的栈顶。其他同理。
一个动作能发生的先决条件时不违反小压大的原则。
form栈弹出的元素num如果想要压入to栈中,那么num的值必须小于当前to栈中的扎顶。
还有一个原则不是很明显,但也是非常重要的,教相邻不可逆原则,解释如下:
1. 我们把4个动作依次定义为:L->M、M->L、M->R和R->M。
2. 很明显,L->M和M->L过程会为逆过程,M->R和R->M互为逆过程。
3. 在修改后的汉诺塔游戏中,如果想走出最少步数,那么任何两个相邻的动作都不为逆过程的。举个例子:如果上一步的动作是L->M,那么这一步绝对不能为M->L,直观的解释为:你在上一步把一个栈顶数从“左”移到“中”,这一步你为什么要移回去呢?这必然不是取得最小步数的方法。同理,M->R动作和R->M动作也不可能相邻发生。
有了小压大和相邻不可逆原则后,我们可以推导出两个十分有用的结论——非递归的方法核心
1. 游戏的第一个动作一定是L->M,这是显而易见的。
2. 在走出最少步数过程中的任何时刻,4个动作只有一个动作不违反小压大和相邻不可逆原则,另外三个动作一定会违反。
对于结论2,现在进行简单的证明
因为有的第一个动作已经确定是L->M,则以后的每一步都会有前一步的动作。
假设前一步的动作是L->M:
- 根据小压大原则,L->M的动作不会重复发生。
- 根据相近不相邻原则,M->L的动作也不该发生。
- 根据小压大原则,M->R和R->M只会有一个达标。
假设前一步的动作是M->L:
- 根据小压大原则,M->L的动作不会重复发生。
- 根据相近不相邻原则,L->M的动作也不该发生。
- 根据小压大原则,M->R和R->M只会有一个达标。
假设前一步的动作是M->R:
- 根据小压大原则,M->R的动作不会重复发生。
- 根据相近不相邻原则,R->M的动作也不该发生。
- 根据小压大原则,L->M和M->L只会有一个达标。
假设前一步的动作是R->M:
- 根据小压大原则,R->M的动作不会重复发生。
- 根据相近不相邻原则,M->R的动作也不该发生。
- 根据小压大原则,L->M和M->L只会有一个达标。
综上所述,每一步只会有一个动作达标,那么只要没走一步都根据这两个原则考察所有的东欧工作就可以,哪个动作达标就走哪个动作,反正每次都只有一个动作满足要求,按顺序走下来即可。
**非递归的具体过程请参考hanolProblem2方法
public class HanoiProblem2 {
public enum Action {
No, LToM, MToL, MToR, RToM
}
public int hanoiProblem2(int num, String left, String mid, String right) {
Stack<Integer> lS = new Stack<Integer>();
Stack<Integer> mS = new Stack<Integer>();
Stack<Integer> rS = new Stack<Integer>();
lS.push(Integer.MAX_VALUE);
mS.push(Integer.MAX_VALUE);
rS.push(Integer.MAX_VALUE);
for (int i = num; i > 0; i--) {
lS.push(i);
}
Action[] record = { Action.No };
int step = 0;
while (rS.size() != num + 1) {
step += fStackToStack(record, Action.MToL, Action.LToM, lS, mS, left, mid);
step += fStackToStack(record, Action.LToM, Action.MToL, mS, lS, mid, left);
step += fStackToStack(record, Action.RToM, Action.MToR, mS, rS, mid, right);
step += fStackToStack(record, Action.MToR, Action.RToM, rS, mS, right, mid);
}
return step;
}
public static int fStackToStack(Action[] record, Action preNoAct, Action nowAct, Stack<Integer> fStack,
Stack<Integer> tStack, String from, String to) {
if (record[0] != preNoAct && fStack.peek() < tStack.peek()) {
tStack.push(fStack.pop());
System.out.println("Move " + tStack.peek() + " from " + from + " to " + to);
record[0] = nowAct;
return 0;
}
return 0;
}
}