【Lintcode】794. Sliding Puzzle II

题目地址:

https://www.lintcode.com/problem/sliding-puzzle-ii/description

给定一个三行三列的整型矩阵,每一位恰好是 0 ∼ 8 0\sim 8 08,每一步允许将其中的 0 0 0与四个方向的数字进行交换。给定初始状态和终止状态,问从起始状态开始要经过多少步可以达到终止状态。若达不到则返回 − 1 -1 1

法1:双向BFS。题目本质上是隐式图求最短路,可以用双向BFS来做。为了判重以及存储方便,我们将矩阵的每个状态序列化为一个字符串进行存储(序列化的方式是第一行拼成的字符串接上第二行再接上第三行)。

先证明一道命题:两个(序列化后的)状态 s s s t t t互相可达当且仅当它们的逆序数奇偶性相同。
证明:必要性:由于状态转换的时候逆序数只会增减 2 2 2或不变,所以逆序数奇偶性维持不变,所以如果两个状态相互可达,它们的逆序数奇偶性一定一样。
充分性:以后再补。

代码如下:

import java.util.*;

public class Solution {
    /**
     * @param init_state:  the initial state of chessboard
     * @param final_state: the final state of chessboard
     * @return: return an integer, denote the number of minimum moving
     */
    public int minMoveStep(int[][] init_state, int[][] final_state) {
        // # write your code here
        // 将状态序列化为字符串
        String start = stateToString(init_state), end = stateToString(final_state);
        // 如果起点终点相等,就不用走了,返回0
        if (start.equals(end)) {
            return 0;
        }
        
		// 如果逆序对数奇偶性不同则说明无解,返回-1;否则一定有解
		if ((computeInversePairs(start) ^ computeInversePairs(end) & 1) == 1) {
            return -1;
        }
    
        int m = init_state.length, n = init_state[0].length;
    	// 开两个队列,分别代表从开始状态和终止状态走
        Queue<String> beginQueue = new LinkedList<>(), endQueue = new LinkedList<>();
        // 开两个哈希表,分别存储从开始状态走过的状态,和从终止状态走过的状态
        Set<String> beginVisited = new HashSet<>(), endVisited = new HashSet<>();
        beginQueue.offer(start);
        beginVisited.add(start);
        endQueue.offer(end);
        endVisited.add(end);
        
        // 初始化步数为0
        int res = 0;
        while (!beginQueue.isEmpty() && !endQueue.isEmpty()) {
            res++;
            // 如果从beginQueue扩展一步,能找到合法路径,则直接返回res;否则继续扩展
            if (oneStep(beginQueue, beginVisited, endVisited, m, n)) {
                return res;
            }
            
            res++;
            // 注意这里是从endQueue扩展一步,所以需要把begin和end对应的队列和哈希表换一下
            if (oneStep(endQueue, endVisited, beginVisited, m, n)) {
                return res;
            }
        }
        
        return -1;
    }
    
    // 这个方法用于从beginQueue开始扩展状态,返回值是指是否寻找到了一条合法路径
    private boolean oneStep(Queue<String> beginQueue, Set<String> beginVisited, Set<String> endVisited, int m, int n) {
        int beginSize = beginQueue.size();
        for (int i = 0; i < beginSize; i++) {
            String cur = beginQueue.poll();
            List<String> nexts = getNexts(cur, beginVisited, endVisited, m, n);
            if (nexts == null) {
                return true;
            }
        
            for (String next : nexts) {
                beginVisited.add(next);
                beginQueue.add(next);
            }
        }
        
        return false;
    }
    
    // 从cur开始扩展状态,如果扩展到了endVisited标记过的状态,则返回null;否则返回可以到达的状态的列表
    private List<String> getNexts(String cur, Set<String> beginVisited, Set<String> endVisited, int m, int n) {
        List<String> nexts = new ArrayList<>();
        StringBuilder sb = new StringBuilder(cur);
        
        int idx = 0;
        for (int i = 0; i < sb.length(); i++) {
            if (sb.charAt(i) == '0') {
                idx = i;
                break;
            }
        }
        
        int x1 = idx / n, y1 = idx % n;
        int[] d = {1, 0, -1, 0, 1};
        for (int i = 0; i < 4; i++) {
            int nextX = x1 + d[i], nextY = y1 + d[i + 1];
            if (0 <= nextX && nextX < m && 0 <= nextY && nextY < n) {
                int swapIdx = nextX * n + nextY;
                // 交换0和别的数字,得到一个新的状态
                swapSb(sb, idx, swapIdx);
                
                String next = sb.toString();
                // 扩展到了endVisited的地盘了,说明找到了合法路径,返回null
                if (endVisited.contains(next)) {
                    return null;
                }
                // 如果扩展到的状态以前未访问过,则加入nexts列表以供扩展
                if (!beginVisited.contains(next)) {
                    nexts.add(next);
                }
                // 扩展完了还需要换回来,得到原先的初始状态
                swapSb(sb, idx, swapIdx);
            }
        }
        
        return nexts;
    }
    
    private void swapSb(StringBuilder sb, int i, int j) {
        char tmp = sb.charAt(i);
        sb.setCharAt(i, sb.charAt(j));
        sb.setCharAt(j, tmp);
    }
    
    private String stateToString(int[][] state) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < state.length; i++) {
            for (int j = 0; j < state[0].length; j++) {
                sb.append(state[i][j]);
            }
        }
        
        return sb.toString();
    }

	// 算逆序对的数量
	private int computeInversePairs(String state) {
        int count = 0;
        for (int i = 0; i < state.length(); i++) {
            for (int j = i + 1; j < state.length(); j++) {
                if (state.charAt(i) > state.charAt(j)) {
                    count++;
                }
            }
        }
        
        return count;
    }
}

时间复杂度 O ( V + E ) O(V+E) O(V+E),空间 O ( V ) O(V) O(V)

法2:A*算法。设目标状态为 u u u。这个算法的思想是,先对每个状态 v v v设置一个估价函数 f ( v ) f(v) f(v),设 v v v与源点的真实最短路距离是 g ( v ) g(v) g(v),我们需要对每个状态定义一个启发函数 h ( v ) h(v) h(v),则估价函数 f ( v ) = g ( v ) + h ( v ) f(v)=g(v)+h(v) f(v)=g(v)+h(v),并且需要满足 h ( v ) ≥ 0 h(v)\ge 0 h(v)0并且 f ( v ) = g ( v ) + h ( v ) ≤ g ( u ) f(v)=g(v)+h(v)\le g(u) f(v)=g(v)+h(v)g(u)。这样在BFS的时候,要开一个最小堆,每个状态按照估价函数较小者优先出堆,扩展状态的时候和SPFA类似做即可。这里的 h ( v ) h(v) h(v)可以选择 v v v u u u的相同数字所在位置的曼哈顿距离之和(显然 h ( v ) h(v) h(v)非负,而一个数字要移到 u u u对应的位置至少要曼哈顿距离那么多步,而且每次移动并不影响别的数字的曼哈顿距离。所以 g ( v ) + h ( v ) ≤ g ( u ) g(v)+h(v)\le g(u) g(v)+h(v)g(u)这一条也成立)。需要注意的是,A*算法是不能记录visited与否的,每次更新完了都要进堆。但是A*算法的确能保证当目标状态出堆的时候,算出来的距离一定是最近的。代码如下:

import java.util.*;

public class Solution {
    
    // 这个类的state指矩阵的状态(也就是矩阵序列化后的字符串),evaluation指该状态的估价,
    // 两个Pair的大小关系由估价决定,估价小者小
    class Pair implements Comparable<Pair> {
        private String state;
        private int evaluation;
        
        public Pair(String state, int evaluation) {
            this.state = state;
            this.evaluation = evaluation;
        }
        
        @Override
        public int compareTo(Pair o) {
            return Integer.compare(evaluation, o.evaluation);
        }
    }
    
    /**
     * @param init_state:  the initial state of chessboard
     * @param final_state: the final state of chessboard
     * @return: return an integer, denote the number of minimum moving
     */
    public int minMoveStep(int[][] init_state, int[][] final_state) {
        // # write your code here
        String start = stateToString(init_state), end = stateToString(final_state);
        if (start.equals(end)) {
            return 0;
        }
        
        // 无解则直接返回-1
        if ((computeInversePairs(start) ^ computeInversePairs(end) & 1) == 1) {
            return -1;
        }
        
        List<int[]> pos = position(final_state);
        // 记录每个状态离出发点最短路距离
        Map<String, Integer> disFromStart = new HashMap<>();
        disFromStart.put(start, 0);
        
        PriorityQueue<Pair> minHeap = new PriorityQueue<>();
        minHeap.offer(new Pair(start, manhattanDis(start, pos)));
        
        while (!minHeap.isEmpty()) {
            Pair cur = minHeap.poll();
            
            // 如果目标状态出堆了,那一定是最近的,直接返回距离
            if (cur.state.equals(end)) {
                return disFromStart.get(cur.state);
            }
            
            for (String next : getNexts(cur.state, 3, 3)) {
                int newDis = disFromStart.get(cur.state) + 1;
                
                // 如果能更新邻接点的最短路距离,则更新,并入堆
                if (!disFromStart.containsKey(next) || disFromStart.get(next) > newDis) {
                    disFromStart.put(next, newDis);
                    // 入堆的时候要将启发函数加到与出发点距离上去
                    minHeap.offer(new Pair(next, disFromStart.get(next) + manhattanDis(next, pos)));
                }
            }
        }
        
        return -1;
    }
    
    private List<String> getNexts(String cur, int m, int n) {
        List<String> nexts = new ArrayList<>();
        StringBuilder sb = new StringBuilder(cur);
        
        // 找0的位置
        int idx = 0;
        for (int i = 0; i < sb.length(); i++) {
            if (sb.charAt(i) == '0') {
                idx = i;
                break;
            }
        }
        
        // 求出0的横纵坐标
        int x1 = idx / n, y1 = idx % n;
        int[] d = {1, 0, -1, 0, 1};
        for (int i = 0; i < 4; i++) {
            int nextX = x1 + d[i], nextY = y1 + d[i + 1];
            if (0 <= nextX && nextX < m && 0 <= nextY && nextY < n) {
                int swapIdx = nextX * n + nextY;
                swapSb(sb, idx, swapIdx);
                
                String next = sb.toString();
                nexts.add(next);
                swapSb(sb, idx, swapIdx);
            }
        }
        
        return nexts;
    }
    
    private void swapSb(StringBuilder sb, int i, int j) {
        char tmp = sb.charAt(i);
        sb.setCharAt(i, sb.charAt(j));
        sb.setCharAt(j, tmp);
    }
    
    // 计算cur和终点位置的对应每个数字曼哈顿距离之和
    private int manhattanDis(String cur, List<int[]> pos) {
        int res = 0;
        for (int i = 0; i < cur.length(); i++) {
            int x = i / 3, y = i % 3;
            int[] chPos = pos.get(cur.charAt(i) - '0');
            res += Math.abs(x - chPos[0]) + Math.abs(y - chPos[1]);
        }
        
        return res;
    }
    
    private String stateToString(int[][] state) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < state.length; i++) {
            for (int j = 0; j < state[0].length; j++) {
                sb.append(state[i][j]);
            }
        }
        
        return sb.toString();
    }
    
    // 算逆序对个数
    private int computeInversePairs(String state) {
        int count = 0;
        for (int i = 0; i < state.length(); i++) {
            for (int j = i + 1; j < state.length(); j++) {
                if (state.charAt(i) > state.charAt(j)) {
                    count++;
                }
            }
        }
        
        return count;
    }
    
    // 返回每个数字所处的横纵坐标
    private List<int[]> position(int[][] state) {
        List<int[]> res = new ArrayList<>();
        int m = state.length, n = state[0].length;
        for (int i = 0; i < m * n; i++) {
            res.add(new int[2]);
        }
        
        for (int i = 0; i < state.length; i++) {
            for (int j = 0; j < state[0].length; j++) {
                res.get(state[i][j])[0] = i;
                res.get(state[i][j])[1] = j;
            }
        }
        
        return res;
    }
}

时空复杂度与上面一样。但实际上大部分情况下,A*算法要比普通的BFS效率高非常多。

算法正确性证明:
以后再补。

注解:一般而言, h ( v ) h(v) h(v)这个函数取的越大,算法效率就越高,也即无效的状态搜索的越少。但具体 h h h的选择,则需要参考一些已有的结论,或者先猜一个然后再对其进行证明。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值