LeetCode算法题4:递归和回溯-N皇后问题

文章目录

  • N 皇后
    • 初始算法 :
    • 修改后的算法
    • 优化后的算法:
  • 总结


N 皇后

      题目链接:https://leetcode-cn.com/problems/n-queens/

      题目描述:n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

初始算法 :

      基于回溯的思想,一个较为粗略的程序如下:solve 函数中,每次选择一个合适的位置(flag 为 false)来设置为 Q,然后更新当前棋盘上已有皇后的活动区域(设置为 true),然后继续下一个位置的查找,直到 Q 的个数 num 等于 n,即表示找到了一种正确的结果,保存并返回。 其中 flag 数组表示棋盘上已有的 Q 的活动区域。

	List<List<String>> ans=new LinkedList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] tmp=new char[n][n];
        boolean[][] flag=new boolean[n][n];//用来标记棋盘上已有皇后的占领区域。
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                tmp[i][j]='.';
        solve(tmp,n,flag,0);
        return ans;
    }
    public void solve(char[][] tmp,int n,boolean[][] flag,int num){
        if(num==n){ //成功时保存,且程序返回
            save(ans,tmp,n);
            return;
        }
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++){
                if(flag[i][j]==false){
                    tmp[i][j]='Q';
                    setDomin(flag,n,i,j,true);
                    solve(tmp,n,flag,num+1);

                    setDomin(flag,n,i,j,false);//需要采用别的方法恢复flag到上一个状态。它和 setDomin(true)不是可逆的。

                    tmp[i][j]='.';
                }
            }
    }
    public void setDomin(boolean[][] flag,int n,int i,int j,boolean status){
        for(int k=0;k<n;k++){//水平 竖直 方向
            flag[i][k]=status;
            flag[k][j]=status;
        }
        for(int k=1;k<n;k++){
            if(i+k<n&&j+k<n)//右下
                flag[i+k][j+k]=status;
            if(i-k>=0&&j-k>=0)//左上
                flag[i-k][j-k]=status;
            if(i-k>=0&&j+k<n)//左下
                flag[i-k][j+k]=status;
            if(i+k<n&&j-k>=0)//右上
                flag[i+k][j-k]=status;
        }
    }
    public void save(List<List<String>> ans,char[][] tmp,int n){ //将 char[][] 转化为 List 并保存
        List<String> t=new LinkedList<>();
        for(int i=0;i<n;i++)
            t.add(new String(tmp[i]));
        ans.add(t);
    }

      上面代码在回溯时出现问题,因为它不能恢复 flag 到上一个状态,所以需要修改,拟采用的解决方法为重新绘制除之间的所有 Q 的活动区域,而不包括当前的 Q ,当然,这需要保存之前的 Q 坐标,修改后的算法如下:

修改后的算法

       1,增加变量 chosen 记录了已经选取的 Q 位置,方便我们重新设置 flag 数组(在将 flag 数组回退到上一个状态时)。2,修改变量 ans 的类型为 Set ,因为在执行的时候发现,虽然此算法的正确性没有问题,但是需要优化(包括剪枝),在 n 为 4 ,使用 List 存储答案的情况下,得到的最终结果有 48 个, 而每 24 个一摸一样,所以目前先建立了一个 Set 来去掉重复的元素,保证可以得到正确的结果。

	Set<List<String>> ans=new HashSet<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] tmp=new char[n][n];
        boolean[][] flag=new boolean[n][n];//用来标记棋盘上已有皇后的占领区域。
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                tmp[i][j]='.';
        LinkedList<int[]> chosen=new LinkedList<>();//用来保存已有的皇后坐标。
        solve(tmp,n,flag,0,chosen);
        List<List<String>> re=new LinkedList<>();
        re.addAll(ans);
        return re;
    }
    public void solve(char[][] tmp,int n,boolean[][] flag,int num,LinkedList<int[]> chosen){
        if(num==n){ //成功时保存,且程序返回
            save(ans,tmp,n);
            return;
        }
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++){
                if(flag[i][j]==false){
                    tmp[i][j]='Q';
                    chosen.addLast(new int[]{i,j});
                    setDomin(flag,n,i,j,true);

                    solve(tmp,n,flag,num+1,chosen);

                    chosen.removeLast();//删除当前 Q 的位置
                    cancel(flag,n,chosen);// 重新设置 flag 数组到上一个状态。
                    tmp[i][j]='.';
                }
            }
    }
    public void cancel(boolean[][] flag,int n,LinkedList<int[]> chosen){
        for(int i=0;i<n;i++)
            Arrays.fill(flag[i],false);

        for(int[] t:chosen)
            setDomin(flag,n,t[0],t[1],true);

    }
    public void setDomin(boolean[][] flag,int n,int i,int j,boolean status){
        for(int k=0;k<n;k++){//水平 竖直 方向
            flag[i][k]=status;
            flag[k][j]=status;
        }
        for(int k=1;k<n;k++){
            if(i+k<n&&j+k<n)//右下
                flag[i+k][j+k]=status;
            if(i-k>=0&&j-k>=0)//左上
                flag[i-k][j-k]=status;
            if(i-k>=0&&j+k<n)//左下
                flag[i-k][j+k]=status;
            if(i+k<n&&j-k>=0)//右上
                flag[i+k][j-k]=status;
        }
    }
    public void save(Set<List<String>> ans,char[][] tmp,int n){
        List<String> t=new LinkedList<>();
        for(int i=0;i<n;i++)
            t.add(new String(tmp[i]));
        ans.add(t);
    }   

       上面算法属于暴力枚举,其时间复杂度很高。对它进行优化如下:

优化后的算法:

      参考对应官方题解,由于上述暴力枚举的时间复杂度非常高,需要利用限制条件加以优化。

      需要说明的是,下面算法仍然和之前的算法类似,大体的回溯框架没有变化。唯一变化的是如何判断标记棋盘上已有皇后的占领区域,或者说是下一个皇后可以放置的位置区域。它采用了 3 个集合来降低之前算法进行标记的时间复杂度。

      将之前的二维数组表示皇后位置转化为只用一个数组 queens 来表示,比如对于 queens[0]=1 ,表示在第0行第1列放置一个皇后。一旦 queens 数组被填满元素,那么我们就得到了一个放置结果。

      当在 queens 数组放置新元素时(即在下一行填充新的皇后位置时):因为肯定是不同行,所以需要关心的是新皇后位置和已有的皇后位置是否在相同列;和已有的皇后位置是否在同一条左上右下斜线上;和已有的皇后位置是否在同一条右上左下斜线上。而 3 个集合的功能就是用来分别判断这三种情况的。

      columns 数组保存已有皇后的列下标(判断 columns 中是否已经包含了新位置的列下标)
      diagonals1 数组保存已有皇后的左上右下斜线位置,比如位置(1,2),由于经过(1,2)的此斜线上所有点(x,y)满足" 1-2 == x-y “关系,那么这一条斜线就可以用 -1(1-2) 来表示,即保存 -1 到diagonals1 中(判断 diagonals1 是否已经包含的新位置所在左上右下斜线,即新位置的行下标减去列下标的值)
      diagonals2 数组保存已有皇后的右上左下斜线位置,比如位置(1,2),由于经过(1,2)的此斜线上所有点(x,y)满足” 1+2 == x+y "关系,那么这一条斜线就可以用 3(1+2) 来表示,即保存 3 到diagonals2 中(判断 diagonals2 是否已经包含的新位置所在左上右下斜线,即新位置的行下标加上列下标的值)

      如下面算法所示:(每行代码的功能已经注明,可参考理解)

	public List<List<String>> solveNQueens(int n) {
        List<List<String>> solutions=new ArrayList<>();
        int[] queens=new int[n]; //保存每一行中成功放置皇后的下标。
        Arrays.fill(queens,-1);
        Set<Integer> columns=new HashSet<>();//记录每一列是否可以放置新的皇后
        Set<Integer> diagonals1=new HashSet<>();//记录左上右下斜线上是否可以放置新的皇后
        Set<Integer> diagonals2=new HashSet<>();//记录右上左下斜线上是否可以放置新的皇后
        backtrack(solutions,queens,n,0,columns,diagonals1,diagonals2); // row 表示已经放置了多少行的皇后
        return solutions;
    }
    public void backtrack(List<List<String>> solutions,int[] queens,int n,int row,Set<Integer> columns,
                          Set<Integer> diagonals1,Set<Integer> diagonals2){
        if(row==n){
            List<String> board=generateBoard(queens,n);
            solutions.add(board);
        }
        else{
            for(int i=0;i<n;i++){

                //上述三个判断语句如果有任意一个为 true,即表示当前位置(row,i)不能被放置新的皇后,所以需要跳过。
                //并且使用 Set 来存储,可以顺带着去重。比如columns中不同的两行但是列下标相同,就没必要存储了;diagonals1中同一条斜线上的元素也没必要存储了...
                if(columns.contains(i))
                    continue;
                int diagonal1=row-i;//左上右下一条斜线上元素的行下标与列下标之差相等。
                if(diagonals1.contains(diagonal1))
                    continue;
                int diagonal2=row+i;//右上左下一条斜线上元素的行下标与列下标之和相等。
                if(diagonals2.contains(diagonal2))
                    continue;


                queens[row]=i;  //此时在位置(row,i)上找到了一个可以放置新皇后的位置
                columns.add(i);//将该列下标加入columns。
                diagonals1.add(diagonal1);//将 (row,i) 表示的对应方向的斜线加入 diagonals1
                diagonals2.add(diagonal2);//将 (row,i) 表示的对应方向的斜线加入 diagonals2
                
                backtrack(solutions,queens,n,row+1,columns,diagonals1,diagonals2); //回溯判断每一种放置的可能性。
                
                queens[row]=-1; //恢复到在此位置放置新皇后之前的状态。
                columns.remove(i);
                diagonals1.remove(diagonal1);
                diagonals2.remove(diagonal2);
            }
        }
    }
    public List<String> generateBoard(int[] queens,int n){ //将已经找到符合条件的 queens 数组转换为标准的返回值(List类型)
        List<String> board=new ArrayList<>();
        for(int i=0;i<n;i++){
            char[] row=new char[n];
            Arrays.fill(row,'.');
            row[queens[i]]='Q';
            board.add(new String(row));
        }
        return board;
    }

总结

      目前来看,这道题的整体思路和代码框架是很容易实现的,关键的问题在于如何描述新皇后的可选位置区域这块,并尽可能地降低时间复杂度。

你可能感兴趣的:(数据结构与算法,leetcode,算法,java)