codingInterview 用栈来求解汉诺塔问题

用栈来求解汉诺塔问题

【题目】

汉诺塔问题比较经典,这里修改一下游戏规则:现在限制不能从左侧的塔直接移动到右侧,也不能从最右侧直接移动到最左侧,而是必须经过中间。求当塔又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

【要求】

用以下两种方法解决:

  • 方法一:递归的方法
  • 方法二:非递归的方法,用栈来模拟汉诺塔的三个塔

【解答】

方法一:递归的方法

首先:如果只剩下最上层的塔需要移动,则有如下处理:

  1. 如果希望从“左”移到“中”,打印“Move 1 from left to mid”。
  2. 如果希望从“中”移到“左”,打印“Move 1 from mid to left 。
  3. 如果希望从“中”移到“右”,打印“Move 1 from mid to right
  4. 如果希望从“右”移到“中”,打印“Move 1 from right to mid
  5. 如果希望从“左”移到“右”,打印“Move 1 from left to mid”。“Move 1 from mid to right” 。
  6. 如果希望从“右”移到“左”,打印“Move 1 from right to mid”和“Move 1 from mid to left”。

以上就是递归的中之条件,也就是只剩上层塔时的打印过程
接下来,我们分析剩下多层塔的情况

如果剩下N层,从最上到最下依次为1~N,则有如下判断:

  1. 如果剩下的N层塔都在“左”,希望全部移到“中”,则有三个步骤:
    1. 将1~N-1层塔先从“左”移到“右”,明显交给递归过程。
    2. 将第N层塔从“左”移到“中”。
    3. 再将1-N-1层塔全部从“右”移到“中”,明显交给递归过程
  2. 如果把剩下的N层塔全部从“中”移到“左”,从“中”移到“右”,从“右”移到“中”,过程与1同理,一样分为三步。
  3. 如果剩下的塔都在“左”,希望全部移到“右”则有五个步骤。
    1. 将1-N-1层塔先全部从“左”移到“右”,明显交给递归过程。
    2. 将第N层塔从“左”移到“中”。
    3. 将1~N-1层塔全部从“右”移到“左”,明显交给递归过程。
    4. 将第N层塔从“中”移到“右”。
    5. 将1~N-1层塔全部从“左”移到“右”,明显交给递归过程。
  4. 如果剩下的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:

  1. 根据小压大原则,L->M的动作不会重复发生。
  2. 根据相近不相邻原则,M->L的动作也不该发生。
  3. 根据小压大原则,M->R和R->M只会有一个达标。

假设前一步的动作是M->L:

  1. 根据小压大原则,M->L的动作不会重复发生。
  2. 根据相近不相邻原则,L->M的动作也不该发生。
  3. 根据小压大原则,M->R和R->M只会有一个达标。

假设前一步的动作是M->R:

  1. 根据小压大原则,M->R的动作不会重复发生。
  2. 根据相近不相邻原则,R->M的动作也不该发生。
  3. 根据小压大原则,L->M和M->L只会有一个达标。

假设前一步的动作是R->M:

  1. 根据小压大原则,R->M的动作不会重复发生。
  2. 根据相近不相邻原则,M->R的动作也不该发生。
  3. 根据小压大原则,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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值