概览
本文原载于我的博客,地址: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队。至于数据集我们可以用随机数生成,五五开的概率即可。我们需要找的,就是一条经过所有节点的路(哈密顿通路),那么问题就转化成了:有一个有向图,如何找到一条哈密顿通路?
我们可以模拟一下我们走迷宫的场景:我们选择一个起点,走走走走走,遇到了一个路口,于是我们随便选择一条岔路,继续走走走走走,遇到岔路还是随便选一个走,如果到了一个死胡同,就回到上一个岔路口,换一条道路继续走,一直走到终点为止。
算法的语言描述
那么我们的算法就出来了,我们定义一个栈,来存储我们的路:
- 随机选择一个节点入栈,并标记为被走过的节点
- 寻找顶栈节点的相邻的未被走过的节点
- 如果没有找到未被走过的相邻节点,就将顶栈节点的所有相邻节点标记为未被走过的,并将顶栈节点弹出,返回第二步
- 如果找到了未被走过的相邻节点,随机选择一个标记为被走过的并压入栈
- 如果栈满了,则退出算法,否则返回第二步
栈满了就是栈内的节点数和队伍数相同。这个算法也可以改为递归形式,当然迭代形式更加直观。
算法确定了,下面就是语言实现了。
算法的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 ]
最后好像还是有一些问题,有的时候找不到通路,可是比赛图的哈密顿通路又是肯定存在的……这个问题之后再说吧。