类深度优先算法解决n阶比赛图问题

概览

本文原载于我的博客,地址:https://blog.guoziyang.top/archives/29/

题目来源于数据结构的Homework,是一个在有向图中求一条哈密顿通路的问题,这个问题可以用类似于深度优先的算法解决,我使用的语言是Java(因为不想纠结于C/C++的指针)。

我们先看一看题目:

在有n个选手 P 1 , P 2 , P 3 , . . . , P n P_1,P_2,P_3,...,P_n P1,P2,P3,...,Pn参加的单循环赛中,每对选手之间非胜即负。现要求求出一个选手序列: P 1 ′ , P 2 ′ , P 3 ′ , . . . , P n ′ P_1',P_2',P_3',...,P_n' P1,P2,P3,...,Pn,使其满足 P i ′ P_i' Pi P i + 1 ′ P_{i+1}' Pi+1(i=1,…,n-1)。

我们可以举10个队伍为例,我们可以将比赛的数据存成一个有向图,则该图就有十个节点,A节点指向B节点表示A队赢了B队。至于数据集我们可以用随机数生成,五五开的概率即可。我们需要找的,就是一条经过所有节点的路(哈密顿通路),那么问题就转化成了:有一个有向图,如何找到一条哈密顿通路?

我们可以模拟一下我们走迷宫的场景:我们选择一个起点,走走走走走,遇到了一个路口,于是我们随便选择一条岔路,继续走走走走走,遇到岔路还是随便选一个走,如果到了一个死胡同,就回到上一个岔路口,换一条道路继续走,一直走到终点为止。

算法的语言描述

那么我们的算法就出来了,我们定义一个栈,来存储我们的路:

  1. 随机选择一个节点入栈,并标记为被走过的节点
  2. 寻找顶栈节点的相邻的未被走过的节点
  3. 如果没有找到未被走过的相邻节点,就将顶栈节点的所有相邻节点标记为未被走过的,并将顶栈节点弹出,返回第二步
  4. 如果找到了未被走过的相邻节点,随机选择一个标记为被走过的并压入栈
  5. 如果栈满了,则退出算法,否则返回第二步

栈满了就是栈内的节点数和队伍数相同。这个算法也可以改为递归形式,当然迭代形式更加直观。

算法确定了,下面就是语言实现了。

算法的Java实现

首先我们让用户输入队伍数,并随机产生一些数据集,来模拟比赛的结果:

Scanner scanner = new Scanner(System.in);
System.out.print("Please input the number of the team: ");
teamNumber = scanner.nextInt();
System.out.println("\nThe graph of the match is as follows:");
graph = new int[teamNumber][teamNumber];
for(int i = 0; i < teamNumber; i ++) {
	graph[i] = new int[teamNumber];
}
for(int i = 0; i < teamNumber; i ++) {
    graph[i][i] = 0;
    for(int j = i + 1; j < teamNumber; j ++) {
        int temp = (int)(Math.random() * 10) + 1;
        if(temp <= 5){
            graph[i][j] = 0;
            graph[j][i] = 1;
        }else{
            graph[i][j] = 1;
            graph[j][i] = 0;
        }
    }
}

for(int i = 0; i < teamNumber; i ++) {
    int[] temp = graph[i];
    for(int j = 0; j < teamNumber; j ++) {
        System.out.print(temp[j] + " ");
    }
    System.out.println();
}

for(int i = 0; i < teamNumber; i ++) {
    nodeList.add(new Node(i));
}

其中,graph是存储着比赛结果的二维数组,如果graph[i][j]是1则说明i队赢了j队,若为0说明i队没有赢j队;teamNumber是用户输入的队伍数量,nodeList是一个按照下标顺序存储节点的列表。每一个节点类是这样定义的:

static class Node{
    private int index;
    private boolean hasGet;
    
    public Node(int index) {
        this.index = index;
        hasGet = false;
    }
    
    public int getIndex() {
        return index;
    }
    
    public Boolean getHasGet() {
        return hasGet;
    }
    
    public void setTrue() {
        this.hasGet = true;
    }
    
    public void setFalse() {
        this.hasGet = false;
    }

    public void setAllUnGet() {
        ArrayList<Node> tempList = new ArrayList<>();
        for(int i = 0; i < teamNumber; i ++) {
            if(graph[index][i] == 1) {
                tempList.add(nodeList.get(i));
            }
        }
        for(Node node : tempList){
            node.setFalse();
        }
    }
}

index字段是该节点的索引,hasGet表示该节点是否被走过。

接着定义那个栈,用一个ArrayList模拟栈的行为:

static class Stack{
    private List<Node> nodeList = new ArrayList<>();
    
    public void push(Node node) {
        nodeList.add(node);
    }
    
    public Node pop() {
        Node node = getTop();
        nodeList.remove(nodeList.size() - 1);
        return node;
    }
    
    public Node getTop() {
        return nodeList.get(nodeList.size() - 1);
    }
    
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[ " + nodeList.get(0).getIndex());
        for(int i = 1; i < nodeList.size(); i ++) {
            builder.append(" --> " + nodeList.get(i).getIndex());
        }
        builder.append(" ]");
        return builder.toString();
    }

    public Boolean isFull() {
        if(nodeList.size() == teamNumber) {
            return true;
        } else {
            return false;
        }
    }
}

定义了一些入栈、弹栈、获取栈顶的操作,并重写了toString()方法,方便最后的结果的输出。

最后是最核心的地方——findAWay()方法,找出哈密顿通路的方法:

private static void findAWay() {
    stack = new Stack();
    stack.push(nodeList.get((int)(Math.random() * teamNumber)));
    stack.getTop().setTrue();
    while(!stack.isFull()) {
        int[] tempArray = graph[stack.getTop().getIndex()];
        ArrayList<Node> tempList = new ArrayList<>();
        for(int i = 0; i < teamNumber; i ++) {
            if(tempArray[i] == 1 && nodeList.get(i).getHasGet() == false) {
                tempList.add(nodeList.get(i));
            }
        }
        if(tempList.isEmpty()) {
            stack.getTop().setAllUnGet();
            stack.pop();
        }else{
            stack.push(tempList.get((int)(Math.random() * tempList.size())));
            stack.getTop().setTrue();
        }
    }
}

挺简单的不是吗?先随机选节点,压栈,设为走过,当栈未满的时候,搜索相邻未走过节点,如果没有就将栈顶节点的所有相邻节点设为为走过并弹栈,继续下一次循环;如果存在相邻节点就随机选择一个,压栈,设为走过,继续下一次循环。完事!

最后把结果输出来一下就好了。

算法的最终实现代码

事实证明,由于比赛图的特殊性(不存在双向路),退栈时不需要将遍历过的节点设为未走过的,如果设置的话可能导致成环的情况发生,所以最终的代码:

import java.util.List;
import java.util.ArrayList;
import java.util.Scanner;

public class DS01_3 {

    private static int teamNumber = 0;
    private static ArrayList<Node> nodeList = new ArrayList<>();
    private static int[][] graph;
    private static Stack stack;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入队伍总数:");
        teamNumber = scanner.nextInt();
        System.out.println("\n比赛结果的邻接矩阵如下:");
        graph = new int[teamNumber][teamNumber];
        for(int i = 0; i < teamNumber; i ++) {
            graph[i] = new int[teamNumber];
        }
        //随机生成比赛结果的有向图的邻接矩阵
        for(int i = 0; i < teamNumber; i ++) {
            graph[i][i] = 0;
            for(int j = i + 1; j < teamNumber; j ++) {
                int temp = (int)(Math.random() * 10) + 1;
                if(temp <= 5){
                    graph[i][j] = 0;
                    graph[j][i] = 1;
                }else{
                    graph[i][j] = 1;
                    graph[j][i] = 0;
                }
            }
        }
        for(int i = 0; i < teamNumber; i ++) {
            int[] temp = graph[i];
            for(int j = 0; j < teamNumber; j ++) {
                System.out.print(temp[j] + " ");
            }
            System.out.println();
        }


        for(int i = 0; i < teamNumber; i ++) {
            nodeList.add(new Node(i));
        }
        findAWay();
        System.out.println("\n查找到一条哈密顿路如下:");
        System.out.println(stack.toString());
        scanner.close();
    }
    //使用深度优先算法寻找一条哈密顿通路,找到即停止
    private static void findAWay() {
        int[] tempArray;
        stack = new Stack();
        stack.push(nodeList.get((int)(Math.random() * teamNumber)));
        stack.getTop().setTrue();
        while(!stack.isFull()) {
            if(stack.isEmpty()) {
                for(int i = 0; i < teamNumber; i ++) {
                    if(nodeList.get(i).getHasGet() == false) {
                        stack.push(nodeList.get(i));
                        stack.getTop().setTrue();
                        break;
                    }
                }
            }
            if(stack.isEmpty()) {
                System.out.println("\n该组数据无哈密顿通路!");
                System.exit(0);
            }
            tempArray = graph[stack.getTop().getIndex()];
            ArrayList<Node> tempList = new ArrayList<>();
            for(int i = 0; i < teamNumber; i ++) {
                if(tempArray[i] == 1 && nodeList.get(i).getHasGet() == false) {
                    tempList.add(nodeList.get(i));
                }
            }
            if(tempList.isEmpty()) {
                stack.pop();
            }else{
                stack.push(tempList.get((int)(Math.random() * tempList.size())));
                stack.getTop().setTrue();
            }
        }
    }
    //自定义的栈
    static class Stack{
        private List<Node> nodeList = new ArrayList<>();
    
        public void push(Node node) {
            nodeList.add(node);
        }
    
        public Node pop() {
            Node node = getTop();
            nodeList.remove(nodeList.size() - 1);
            return node;
        }
    
        public Node getTop() {
            return nodeList.get(nodeList.size() - 1);
        }
    
        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder("[ " + nodeList.get(0).getIndex());
            for(int i = 1; i < nodeList.size(); i ++) {
                builder.append(" --> " + nodeList.get(i).getIndex());
            }
            builder.append(" ]");
            return builder.toString();
        }

        public Boolean isFull() {
            if(nodeList.size() == teamNumber) {
                return true;
            } else {
                return false;
            }
        }

        public Boolean isEmpty() {
            if(nodeList.size() == 0) {
                return true;
            } else {
                return false;
            }
        }
    }
    //节点类,等效于队伍
    static class Node{
        private int index;
        private boolean hasGet;
    
        public Node(int index) {
            this.index = index;
            hasGet = false;
        }
    
        public int getIndex() {
            return index;
        }
    
        public Boolean getHasGet() {
            return hasGet;
        }
    
        public void setTrue() {
            this.hasGet = true;
        }
    
        public void setFalse() {
            this.hasGet = false;
        }
    }
}

运行结果:

GuodeMacBook-Air:DS01 guoziyang$ java DS01_3
请输入队伍总数:20

比赛结果的邻接矩阵如下:
0 0 1 1 0 1 0 0 0 0 1 1 0 0 1 0 0 0 0 0 
1 0 0 0 1 1 0 1 1 0 0 1 0 1 0 0 1 1 1 1 
0 1 0 0 1 1 0 1 0 1 1 0 0 0 0 1 0 1 1 1 
0 1 1 0 0 0 0 1 1 0 1 0 0 1 0 0 1 1 1 1 
1 0 0 1 0 1 0 1 1 1 0 0 1 0 1 1 0 1 0 0 
0 0 0 1 0 0 0 0 1 1 0 1 0 0 1 0 0 0 0 0 
1 1 1 1 1 1 0 1 0 0 1 0 0 0 0 0 0 1 0 0 
1 0 0 0 0 1 0 0 1 1 0 1 1 0 0 1 0 0 0 1 
1 0 1 0 0 0 1 0 0 1 0 0 0 1 0 1 0 0 1 0 
1 1 0 1 0 0 1 0 0 0 0 0 1 0 0 1 1 1 1 0 
0 1 0 0 1 1 0 1 1 1 0 1 0 0 0 0 0 0 1 0 
0 0 1 1 1 0 1 0 1 1 0 0 0 0 1 1 0 0 0 1 
1 1 1 1 0 1 1 0 1 0 1 1 0 1 0 0 0 0 1 0 
1 0 1 0 1 1 1 1 0 1 1 1 0 0 0 0 0 0 1 0 
0 1 1 1 0 0 1 1 1 1 1 0 1 1 0 1 0 0 1 1 
1 1 0 1 0 1 1 0 0 0 1 0 1 1 0 0 0 1 0 1 
1 0 1 0 1 1 1 1 1 0 1 1 1 1 1 1 0 1 1 0 
1 0 0 0 0 1 0 1 1 0 1 1 1 1 1 0 0 0 0 0 
1 0 0 0 1 1 1 1 0 0 0 1 0 0 0 1 0 1 0 1 
1 0 0 0 1 1 1 0 1 1 1 0 1 1 0 0 1 1 0 0 

查找到一条哈密顿路如下:
[ 8 --> 9 --> 16 --> 0 --> 11 --> 3 --> 13 --> 7 --> 15 --> 6 --> 17 --> 5 --> 14 --> 2 --> 4 --> 12 --> 18 --> 19 --> 10 --> 1 ]

最后好像还是有一些问题,有的时候找不到通路,可是比赛图的哈密顿通路又是肯定存在的……这个问题之后再说吧。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值