首页 > 技术文章 > 农夫过河问题

wuqianling 2016-09-25 14:00 原文

问题描述:

    农夫要把狼、白菜、羊带到河的对岸,但每次最多只能带一个,当农夫不在时,狼会吃羊,羊会吃白菜,因此狼和羊在同一岸时农夫必须也在,羊和白菜在同一岸时农夫也必须在。问:农夫应该怎样把他们安全带过去?

 

我们可以想到这样一个解决方案:

1、都在左岸
2、农夫把羊带到右岸
3、农夫独自回到左岸
4、农夫把白菜带到右岸
5、农夫把羊带回左岸
6、农夫把狼带到右岸
7、农夫独自回到左岸
8、农夫把羊带到右岸
9、都到了右岸

 

 

1、Java实现代码:

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * 农夫过河问题
 * <p>
 * JDK: 1.6
 * 
 * @author wuqianling
 * 
 */
public class FarmerProblem {

    /**
     * 判断农夫的位置(1000) <br>
     * 0表示在左岸,1表示在右岸
     * 
     * @param location
     *            当前位置状态
     * @return false 左岸,true 右岸
     */
    public static boolean farmerLocation(int location) {
        return (0 != (location & 0x08)); // &是按位与,0x08表示十六进制数8(二进制为00001000)
    }

    /**
     * 判断狼的位置(0100) <br>
     * 0表示在左岸,1表示在右岸
     * 
     * @param location
     *            当前位置状态
     * @return false 左岸,true 右岸
     */
    public static boolean wolfLocation(int location) {
        return (0 != (location & 0x04)); // 0x04表示十六进制数4(二进制为00000100)
    }

    /**
     * 判断白菜的位置(0010) <br>
     * 0表示在左岸,1表示在右岸
     * 
     * @param location
     *            当前位置状态
     * @return false 左岸,true 右岸
     */
    public static boolean cabbagaLocation(int location) {
        return (0 != (location & 0x02)); // 0x02表示十六进制数2(二进制为00000010)
    }

    /**
     * 判断羊的位置(0001) <br>
     * 0表示在左岸,1表示在右岸
     * 
     * @param location
     *            当前位置状态
     * @return false 左岸,true 右岸
     */
    public static boolean goatLocation(int location) {
        return (0 != (location & 0x01)); // 0x01表示十六进制数1(二进制为00000001)
    }

    /**
     * 判断安全状态
     * <p>
     * 1、羊与白菜在同岸,且羊与农夫不在同岸,羊会吃白菜,不安全<br>
     * 2、羊与狼在同岸,且羊与农夫不在同岸,狼会吃羊,不安全<br>
     * 3、其他状态安全
     * 
     * @param location
     *            当前位置状态
     * @return 返回true为安全,false为不安全
     */
    public boolean isSafe(int location) {
        // 羊与白菜在同岸,且羊与农夫不在同岸,羊会吃白菜,不安全
        if ((goatLocation(location) == cabbagaLocation(location))
                && (goatLocation(location) != farmerLocation(location)))
            return false;
        // 羊与狼在同岸,且羊与农夫不在同岸,狼会吃羊,不安全
        if ((goatLocation(location) == wolfLocation(location))
                && (goatLocation(location) != farmerLocation(location)))
            return false;

        // 其他状态均安全
        return true;
    }

    /**
     * 农夫过河问题
     * <p>
     * 问题描述:农夫要把狼、白菜、羊带到对岸,但每次最多只能带一个,当农夫不在时,狼会吃羊,羊会吃白菜。
     * 
     * @return
     */
    public List<Integer> farmerProblem() {

        Queue<Integer> moveTO = new LinkedList<Integer>(); // 创建空队列,用于记录可以安全到达的中间状态
        moveTO.add(new Integer(0));// 初始状态进队列

        int[] route = new int[16]; // 用于记录已考虑的状态路径
        // 初始化数组route
        for (int i = 0; i < 16; i++) {
            route[i] = -1;
        }
        route[0] = 0;

        while (!moveTO.isEmpty() && (route[15] == -1)) {
            int location = moveTO.poll(); // 取队顶状态为当前状态,并移除队顶元素

            for (int movers = 1; movers <= 8; movers <<= 1) { // 考虑各种物品移动。
                if ((0 != (location & 0x8)) == (0 != (location & movers))) { // 农夫与移动的物品在同一岸
                    int newlocation = location ^ (0x08 | movers); // 计算新状态,^为按位异或,相同为0,不同为1
                    if (isSafe(newlocation) && (route[newlocation] == -1)) { // 新状态安全且未处理
                        route[newlocation] = location; // 记录新状态的前驱
                        moveTO.add(new Integer(newlocation)); // 新状态入队
                    }
                }
            }
        }
        // 保存状态
        List<Integer> resultList = new ArrayList<Integer>();
        if (route[15] != -1) { // 到达最终状态
            for (int location = 15; location >= 0; location = route[location]) {
                resultList.add(location);
                if (location == 0) {
                    break;
                }
            }
        }
        Collections.reverse(resultList); // 反转顺序
        return resultList;
    }

    /** 记录上次位置 */
    private int oldLoca = 0;

    /**
     * 中文解释
     * 
     * @param location
     */
    private String explainToChinese(int location) {
        String str = "";

        if (farmerLocation(location) != farmerLocation(oldLoca))
            str += "农夫"; // 判断农夫的位置是否改变

        if (wolfLocation(location) != wolfLocation(oldLoca))
            str += "把狼带到了"; // 判断狼的位置是否改变
        else if (cabbagaLocation(location) != cabbagaLocation(oldLoca))
            str += "把白菜带到了"; // 判断白菜的位置是否改变
        else if (goatLocation(location) != goatLocation(oldLoca))
            str += "把羊带到了"; // 判断羊的位置是否改变
        else
            str += "独自回到了"; // 狼、白菜、羊的位置都未改变

        if (farmerLocation(location))
            str += "右岸"; // 判断下一步农夫是在哪一岸,若农夫当前在左岸,则他下一步必在右岸
        else
            str += "左岸";

        oldLoca = location; // 记录本次位置,为下次作准备

        return str;
    }

    /**
     * 获得整型数转换为四位二进制的字符串<br>
     * 例如:9 = 1001 、 2 = 0010
     * 
     * @param n
     *            0到15的十进制整数
     * @return 一个四位的二进制字符串
     */
    private String getIntToFourBinaryStr(int n) {
        char[] bnum = Integer.toBinaryString(n).toCharArray(); // 转为二进制,但是缺少前导0
        char[] bit = { '0', '0', '0', '0' };
        for (int i = bit.length - 1, j = bnum.length - 1; i >= 0 && j >= 0; i--, j--) {
            bit[i] = bnum[j];
        }
        return new String(bit);
    }

    /**
     * 输出结果
     * 
     * @param list
     *            一个解集合
     */
    private void outputResult(List<Integer> list) {
        System.out.println("0代表在左岸,1代表在右岸");
        System.out.println("0000 每位数分别代表农夫、狼、白菜、羊");
        System.out.println();

        System.out.println("[位置]\t[二进制]\t[中文解释]");
        for (int i = 0; i < list.size(); i++) {
            int location = list.get(i);
            String str = "位置:" + location + "\t";
            str += getIntToFourBinaryStr(location); // 为了利于观察,转换为二进制输出

            if (location == 0) {
                str += "\t都在左岸";
            } else {
                str += "\t" + explainToChinese(location); // 中文解释
            }
            System.out.println(str);
            if (location == 15) {
                System.out.println("\t\t都在右岸");
            }
        }
    }

    /**
     * 主方法
     */
    public static void main(String[] args) {
        FarmerProblem fp = new FarmerProblem();
        List<Integer> list = fp.farmerProblem();
        fp.outputResult(list);
    }

}

 

运行结果:

 

 

 

2、为了更直观的模拟过程,我们使用图形界面动画来实现,下面是图形界面的代码:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;

/**
 * 窗口类
 * 
 * @author wuqianling
 * 
 */
public class MyWindow extends JFrame {

    private List<Integer> list = null;
    private DrawPanel drawPanel;
    private TextField textField;

    public MyWindow() {
        this.setTitle("农夫过河");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 设置窗口关闭时退出程序
        this.setSize(460, 320);
        this.setResizable(false); // 固定窗口大小
        this.setLocationRelativeTo(null); // 居中
        // 顶部面板容器
        JPanel top = new JPanel();
        top.setLayout(new BorderLayout()); // 设置边界布局管理器

        textField = new TextField("0000 每位数分别代表农夫、狼、白菜、羊");
        textField.setEditable(false); // 设置不可编辑
        top.add(textField, BorderLayout.CENTER);

        JPanel topRight = new JPanel();
        JButton calbtn = new JButton("计算方案");
        JButton simbtn = new JButton("模拟方案");
        topRight.add(calbtn);
        topRight.add(simbtn);
        top.add(topRight, BorderLayout.EAST);

        this.getContentPane().add(top, BorderLayout.NORTH); // 添加到窗口中

        // 添加模拟动画面板
        drawPanel = new DrawPanel();
        this.getContentPane().add(drawPanel);

        // 添加按钮事件监听
        calbtn.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                calculate();
            }
        });

        simbtn.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (list == null)
                    calculate();
                drawPanel.startDraw();
            }
        });
    }

    /**
     * 计算方案
     */
    private void calculate() {
        FarmerProblem nf = new FarmerProblem();
        list = nf.farmerProblem();

        String str = "一个解决方案:";
        int i = 0;
        for (; i < list.size() - 1; i++) {
            str += (list.get(i) + ",");
        }
        str += list.get(i);

        textField.setText(str);
        System.out.println("计算得" + str);
    }

    /**
     * 主方法
     * 
     * @param args
     */
    public static void main(String[] args) {
        MyWindow win = new MyWindow();
        win.setVisible(true);
    }

    /**
     * 绘制模拟的动画,实现Runnable接口开启线程
     * 
     * @author wuqianling
     * 
     */
    final class DrawPanel extends JPanel implements Runnable {

        private boolean isRun = false; // 是否正在进行模拟

        private final static int INITVALUE = 4;// 默认初始坐标值
        private int nx = INITVALUE; // 农夫的x轴坐标
        private int lx = INITVALUE; // 狼的x轴坐标
        private int bx = INITVALUE; // 白菜的x轴坐标
        private int yx = INITVALUE; // 羊的x轴坐标

        @Override
        public void paint(Graphics g) {
            // g.clearRect(0, 0, getWidth(), getHeight());
            Color c = g.getColor();
            // 画背景
            g.setColor(Color.lightGray);
            g.fillRect(0, 0, getWidth(), getHeight()); // 画左、右岸
            g.setColor(new Color(208, 238, 239));
            g.fillRect(100, 0, this.getWidth() - 200, this.getHeight()); // 画河道
            // 画河道说明文字
            g.setColor(Color.blue);
            g.drawString("左岸", 60, this.getHeight() / 2 - 6);
            g.drawString("右岸", this.getWidth() - 80, this.getHeight() / 2 - 6);
            g.drawString("河道", this.getWidth() / 2 - 10,
                    this.getHeight() / 2 - 6);

            g.setColor(Color.red);
            g.fillRect(nx, 4, 40, 50); // 画农夫
            g.fillRect(lx, 64, 40, 50); // 画狼
            g.fillRect(bx, 124, 40, 50); // 画白菜
            g.fillRect(yx, 184, 40, 50); // 画羊

            // 画相应文字
            g.setColor(Color.yellow);
            g.drawString("农夫", nx + 8, 30);
            g.drawString("狼", lx + 15, 90);
            g.drawString("白菜", bx + 8, 150);
            g.drawString("羊", yx + 15, 210);

            g.setColor(c); // 还原颜色
        }

        /**
         * 开始模拟绘制动画
         * 
         * @return
         */
        public boolean startDraw() {
            if (isRun) // 是否有正在模拟的线程
                return false;
            isRun = true; // 设置模拟开始标识
            // 初始化坐标
            nx = INITVALUE;
            lx = INITVALUE;
            bx = INITVALUE;
            yx = INITVALUE;
            // 开启线程
            Thread th = new Thread(this);
            th.start();

            return true;
        }

        /**
         * 绘制一个过程的模拟动画
         * 
         * @param loca1
         *            原状态
         * @param loca2
         *            新状态
         */
        private void drawSim(int loca1, int loca2) {
            int distance = 2; // 每次移动的像素
            int endRight = this.getWidth() - 45; // 右边结束坐标
            // 判断状态是否发生改变
            boolean nxb = FarmerProblem.farmerLocation(loca1) != FarmerProblem
                    .farmerLocation(loca2);
            boolean lxb = FarmerProblem.wolfLocation(loca1) != FarmerProblem.wolfLocation(loca2);
            boolean bxb = FarmerProblem.cabbagaLocation(loca1) != FarmerProblem
                    .cabbagaLocation(loca2);
            boolean yxb = FarmerProblem.goatLocation(loca1) != FarmerProblem.goatLocation(loca2);

            while (isRun) {
                // 判断农夫在左岸还是右岸,来判断应该加或减坐标
                if (FarmerProblem.farmerLocation(loca2)) {
                    // 农夫在左岸往右岸移动
                    nx += distance;
                    if (lxb)
                        lx += distance;
                    if (bxb)
                        bx += distance;
                    if (yxb)
                        yx += distance;
                    if (nx > endRight) // 右结束边界坐标
                        break;
                } else {
                    // 农夫在右岸往左岸移动
                    nx -= distance;
                    if (lxb)
                        lx -= distance;
                    if (bxb)
                        bx -= distance;
                    if (yxb)
                        yx -= distance;
                    if (nx <= INITVALUE) // 左结束边界坐标
                        break;
                }
                repaint(); // 重绘
                try {
                    Thread.sleep(15);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void run() {
            System.out.println("模拟开始...");
            // 逐个取出状态进行模拟
            for (int i = 1; isRun && i < list.size(); i++) {
                int loca1 = list.get(i - 1);
                int loca2 = list.get(i);
                System.out.print(loca1 + ",");
                drawSim(loca1, loca2);
            }
            System.out.println(list.get(list.size() - 1));
            System.out.println("模拟结束...");
            isRun = false; // 设置模拟结束标识
        }
    }
}

运行结果:

 

推荐阅读