农夫过河问题也算是个老问题了,小学的时候经常出现在奥数教材上,和鸡兔同笼一起(笑),但是原理可比鸡兔同笼高端多了。

我们看下题目:

农夫需要把狼、羊、菜和自己运到河对岸去,只有农夫能够划船,而且船比较小,除农夫之外每次只能运一种东西,还有一个棘手问题,就是如果没有农夫看着,羊会偷吃菜,狼会吃羊。请考虑一种方法,让农夫能够安全地安排这些东西和他自己过河。

我们在图结构这一章遇到了这个题目,就要思考怎么使用图相关的知识解决这个问题。

有一种比较有趣的抽象方法:状态抽象法,可用于解决这类问题。

我们将某一时刻的状态单独抽象出来,用一组布尔变量去表示这个状态。我们规定,某一时刻,如果农夫在河对岸,那么表示农夫的布尔变量就置为true,菜、山羊和狼同理。

我们规定,某一时刻的状态表示为(狼, 羊, 菜, 农夫)。两个状态之间就是可以相互转化的,如Python编程实现农夫过河问题 农夫过河流程图_算法Python编程实现农夫过河问题 农夫过河流程图_结点_02这两个状态就是可以转化的,代表着“农夫带着菜过到了河对岸”。于是,整个过程就转化为了状态之间的转换。

现在,我们将状态抽象为图中的结点,那么状态间的转化就可以抽象为结点间的有向路。我们的任务就变成了在起始与结束状态间找到一条有向路即可。

以下是Node结点类的设计:

class Node {
    private boolean wolf;
    private boolean sheep;
    private boolean vegetable;
    private boolean farmer;
    public int index;
    public int flag = 0;

    public Node(boolean wolf, boolean sheep, boolean vegetable, boolean farmer) {
        this.wolf = wolf;
        this.sheep = sheep;
        this.vegetable = vegetable;
        this.farmer = farmer;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public int getIndex() {
        return index;
    }

    public boolean getWolf() {
        return wolf;
    }

    public boolean getSheep() {
        return sheep;
    }

    public boolean getVegetable() {
        return vegetable;
    }

    public boolean getFarmer() {
        return farmer;
    }
}

每个结点就是一个状态。

我们首先遍历所有的状态结点,如果是有效的,就添加到结点列表,以下是验证结点有效的代码,根据题目的描述,在农夫和羊的状态不同的情况下(羊没有农夫看着),狼不能和羊状态一样,羊不能和菜一样:

private static boolean isValid(boolean wolf, boolean sheep, boolean vegetable, boolean farmer) {
    if((farmer != sheep) && ((wolf == sheep) || (sheep == vegetable))) {
        return false;
    } else {
        return true;
    }
}

接着添加所有的状态有效的结点:

boolean booleans[] = new boolean[]{true, false};
for(boolean wolf : booleans) {
    for(boolean sheep : booleans) {
        for(boolean vegetable : booleans) {
            for(boolean farmer : booleans) {
                if(isValid(wolf, sheep, vegetable, farmer)) {
                    Node node = new Node(wolf, sheep, vegetable, farmer);
                    node.setIndex(nodes.size());
                    nodes.add(node);
                }
            }
        }
    }
}

结点之间的连接状态是由邻接矩阵存储的,判断两个结点是否能连接使用canConnect方法,每次农夫可以带一样东西过河:

private static boolean canConnect(Node i, Node j) {
    int change = 0;
    if(i.getWolf() != j.getWolf()) change ++;
    if(i.getSheep() != j.getSheep()) change ++;
    if(i.getVegetable() != j.getVegetable()) change ++;
    if(i.getFarmer() != j.getFarmer() && change <= 1) {
        return true;
    }
    return false;
}

最后二重循环遍历判断每两个结点是否可连接:

edges = new int[nodes.size()][nodes.size()];
for(int i = 0; i < nodes.size(); i ++) {
    for(int j = 0; j < nodes.size(); j ++) {
        if(canConnect(nodes.get(i), nodes.get(j))) {
            edges[i][j] = 1;
        } else {
            edges[i][j] = 0;
        }
    }
}

完成了图的构建之后,使用暴力深度优先搜索即可,简单粗暴,注意,如果只想要搜出一条路即可,那么在搜索到之后直接退出即可,而如果想要搜出所有的路,则需要使用栈。

searchWay函数初始化一些参数,并且找到了初始状态结点(所有的都不在对岸)和结束状态结点(所有的都在对岸):

private static void searchWay() {
    int firstNodeIndex = 0;
    int lastNodeIndex = 0;
    for(int i = 0; i < nodes.size(); i ++) {
        Node tempNode = nodes.get(i);
        if(!tempNode.getFarmer() && !tempNode.getSheep() &&   !tempNode.getVegetable() && !tempNode.getWolf()) {
            firstNodeIndex = i;
        }
        if(tempNode.getFarmer() && tempNode.getSheep() &&  tempNode.getVegetable() && tempNode.getWolf()) {
            lastNodeIndex = i;
        }
    }
    DFS(nodes.get(firstNodeIndex), nodes.get(lastNodeIndex));
}

DFS方法是一个递归的深度优先搜索方法:

private static void DFS(Node startNode, Node lastNode) {
    stack.push(startNode);
    visit[startNode.index] = true;
    while(!stack.isEmpty()) {
        int tempNodeIndex = stack.getTop().getIndex();
        if(tempNodeIndex == lastNode.index) {
            results.add(stack.getList());
            stack.pop();
            visit[lastNode.index] = false;
            break;
        }
        while(startNode.flag < nodes.size()) {
            if(edges[tempNodeIndex][startNode.flag] == 1 && !visit[startNode.flag]) {
                DFS(nodes.get(startNode.flag), lastNode);
            }
            startNode.flag ++;
        }
        if(startNode.flag == nodes.size()) {
            stack.pop();
            startNode.flag = 0;
            visit[startNode.index] = false;
            break;
        }
    }
}

每个结点都有一个flag字段,用来标示当前结点已经搜索到和哪一个结点的连接,visit数组用于表示结点是否被访问过。

最终,results数组中就存有所有的路,你需要做的就只是将结果输出而已,我把输出结果格式化了一下:

private static void showResult() {
    for(int i = 0; i < results.size(); i ++) {
        System.out.println("第" + (i+1) + "种答案");
        ArrayList<Node> result = results.get(i);
        for(int j = 0; j < result.size() - 1; j ++) {
            Node before = result.get(j);
            Node after = result.get(j + 1);
            StringBuilder builder = new StringBuilder("农夫");
            if(before.getSheep() != after.getSheep()) {
                builder.append("带着羊");
            }else if(before.getWolf() != after.getWolf()){
                builder.append("带着狼");
            }else if(before.getVegetable() != after.getVegetable()) {
                builder.append("带着菜");
            }else {
                builder.append("什么都不带");
            }
            if(after.getFarmer()) {
                builder.append("过到对岸。");
            }else {
                builder.append("回到原岸。");
            }
            System.out.println(builder.toString());
        }
        System.out.println();
    }
}

运行结果:

GuodeMacBook-Air:DS04_Farmer guoziyang$ java Main
第1种答案
农夫带着羊过到对岸。
农夫什么都不带回到原岸。
农夫带着狼过到对岸。
农夫带着羊回到原岸。
农夫带着菜过到对岸。
农夫什么都不带回到原岸。
农夫带着羊过到对岸。

第2种答案
农夫带着羊过到对岸。
农夫什么都不带回到原岸。
农夫带着菜过到对岸。
农夫带着羊回到原岸。
农夫带着狼过到对岸。
农夫什么都不带回到原岸。
农夫带着羊过到对岸。