数据结构-16枚硬币问题
本题主要考查对图的结构和图的广度优先遍历操作的掌握。
实现效果:
什么是图?
定义:图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。图又分为有向图、无向图,有向图即边为有向边的图,无向图即边没有方向的图。
图的两种遍历(深度优先遍历、广度优先遍历)
深度优先遍历:(Depth First Search),简称DFS,其遍历类似树的前序遍历。
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
广度优先搜索(Breadth First Search),简称DFS,其遍历类似树的层次遍历。
假设从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点“先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则选中图中一个未曾被访问的顶点 作起始点,重复上述过程直至图中所有顶点都被访问到为止。
eg:
用邻接表存储方式表示图:
顶点 | 邻接点 |
---|---|
V0 | V1 、V2 |
V1 | V0、V5、V6 |
V2 | V0、V4、V5 |
V3 | V5、V6 |
V4 | V2 |
V5 | V1、V2、V3 |
V6 | V1、V3 |
深度优先遍历:V0,V1,V5,V3,V6,V2,V4.
广度优先遍历:V0,V1,V2,V5,V6,V3,V4.
首先根据图1分析16枚硬币问题的游戏规则,一枚硬币只有正反面两个结果。当翻动一枚硬币,该硬币周围(只包括上、下、左、右)的硬币也要翻动。我们不妨将16枚硬币正反状态看作一个16位的二进制数字(0表示正面,1表示反面),十六枚硬币正反状态表示的二进制数作为图的一个顶点(例如:只有第一枚硬币处于反面,其他十五枚硬币处于正面,那么它表示而二进制数为1000000000000000)。然后翻动一枚为正面的硬币,记录此时16枚硬币表示的16位二进制数(即边的另一个顶点),接着根据这两个顶点构建一条边。构建图的所有边,从所有硬币都是正面(0000000000000000)到所有硬币都是反面(1111111111111111),构建所有可能的边。代码如下(为了方便顶点为十进制数)
/** 创建图的所有边 */
private List<AbstractGraph.Edge> getEdges() {
List<AbstractGraph.Edge> edges =new ArrayList<AbstractGraph.Edge>();
for (int u = 0; u < NUMBER_OF_NODES; u++) {
for (int k = 0; k < 16; k++) {
char[] node = getNode(u);
if (node[k] == 'H') {
//getFlippedNode为翻转硬币后求得十进制数(返回值根据翻转规则设定)
int v = getFlippedNode(node, k);
// 添加边缘(V,U)的法律行动从节点U到节点V
edges.add(new AbstractGraph.Edge(v, u));
}
}
}
return edges;
}
图构建好后,硬币的最终状态为所有硬币为反面(1111111111111111即目标顶点),16硬币游戏开始之前有一个初始状态(未知)。然后我们用目标顶点作为图遍历的起点进行广度优先遍历(bfs),用一个list来记录图的遍历。遍历的下一个顶点作为边的另一端顶点的子顶点,用一个数组parent[]来记录,这样便于下面树的构建。然后通过目标顶点作为根节点、数组parant、遍历结果list来构建树。图的广度优先遍历代码如下:
/** 从顶点v搜索BFS*/
public Tree bfs(int v) {
List<Integer> searchOrders = new ArrayList<Integer>();
int[] parent = new int[vertices.size()];
for (int i = 0; i < parent.length; i++)
parent[i] = -1; // 初始化parent[i]到- 1
java.util.LinkedList<Integer> queue =
new java.util.LinkedList<Integer>(); // list用作队列
boolean[] isVisited = new boolean[vertices.size()];
queue.offer(v); // 入队V
isVisited[v] = true; // 标记它访问
while (!queue.isEmpty()) {
int u = queue.poll(); // 出队到U
searchOrders.add(u); // u 搜索
for (int w : neighbors.get(u)) {
if (!isVisited[w]) {
queue.offer(w); // 入队 w
parent[w] = u; //把遍历的下一个顶点作为一个子顶点,便于后面树的构建
isVisited[w] = true; // 标记它访问
}
}
}
return new Tree(v, parent, searchOrders);
}
重载树的方法Tree代码如下
/** 树的内部类里面的抽象图形类 */
public class Tree {
private int root; // 树的根
private int[] parent; // 存储每个顶点的父
private List<Integer> searchOrders; // 高阶的搜索
/** 构建具有根,父树,和搜索命令 */
public Tree(int root, int[] parent, List<Integer> searchOrders) {
this.root = root;
this.parent = parent;
this.searchOrders = searchOrders;
}
/** 返回树的根 */
public int getRoot() {
return root;
}
/** 返回顶点V的父节点*/
public int getParent(int v) {
return parent[v];
}
/** 返回表示搜索顺序的数组 */
public List<Integer> getSearchOrders() {
return searchOrders;
}
/** 返回的顶点数 */
public int getNumberOfVerticesFound() {
return searchOrders.size();
}
/** 从顶点索引到根的顶点的路径 */
public List<V> getPath(int index) {
ArrayList<V> path = new ArrayList<V>();
do {
path.add(vertices.get(index));
index = parent[index];
}
while (index != -1);
return path;
}
}
最后我们根据游戏开始硬币的状态作为顶点,从顶点索引到根的顶点的路径。代码如上代码的getpath()方法。这样我们就找到了从某个硬币状态到每个硬币都为反面的最短路径之一(但一定是最短路径)。 我们根据路径渲染GUI界面就可以了。