51.N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
思路
直观的做法是暴力枚举,把皇后放在每一个位置的情况都考虑一遍,并去掉那些有皇后在同一行或同一列或同一斜线上的情况。但是暴力枚举时间复杂度过高,所以需要进行优化。
因为每个皇后必须位于不同行不同列,因此每一行有且仅有一个皇后,每一列有且仅有一个皇后,并且每两个皇后都不能处在同一斜线上。
基于上述发现,我们可以使用回溯法来求解该问题。
使用一个数组记录每行皇后放置的位置的列下标,依次在每一行放置一个皇后,并且该皇后不能与已有的皇后有相互攻击关系:即新皇后不能和任何一个已有的皇后处在同一列以及同一斜线上。当N个皇后都放置完毕,则找到一个可能的解,并将数组转换成答案要求的列表格式。
方法一 使用集合判断一个位置所在列和两条斜线上是否已经有皇后
使用三个集合columns、d1和d2分别记录每一列以及两个方向上的每条写线上是否有皇后。
列的表示用当前列的下标即可。
难点在于斜线如何表示,通过观察可以发现,从左上到右下的斜线中,行下表-列下标始终不变;而左下到右上的斜线中,行下标+列下标始终不变,如果在一个位置放了一个皇后,那么我们就把列下标、行下标-列下标、行下标+列下标的值分别存入columns、d1、d2三个集合中,在下一行的递归过程里,若遇到集合中已存在的位置则直接跳过。
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> solutions = new ArrayList<List<String>>();
int[] queens = new int[n];
Arrays.fill(queens, -1);
Set<Integer> columns = new HashSet<Integer>();
Set<Integer> diagonals1 = new HashSet<Integer>();
Set<Integer> diagonals2 = new HashSet<Integer>();
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
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++) {
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;
columns.add(i);
diagonals1.add(diagonal1);
diagonals2.add(diagonal2);
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) {
List<String> board = new ArrayList<String>();
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;
}
}
方法二 使用位运算来节省空间开销
这个方法很妙啊,既运用了位运算又结合了棋盘结构,非常的灵活。
具体做法是,使用三个整数columns、d1、d2来记录每一列以及两个方向上的每条斜线上是否有皇后,每个整数有N个二进制位,棋盘左边对应整数最低二进制位。我当时就想,用这样的d1和d2怎样表示斜线上的皇后呢?原来只要在当前行落子之后,将落子位置记录在d1和d2上,然后d1左移一位,d2右移一位,因为期盼左边对应最低位,所以实际操作中相当于d1相对棋盘右移一位,d2左移一位,而d1代表左上到右下斜线,d2代表左下到右上斜线,所以这样就把当前行的落子在下一行的攻击位占好了。
此外,我们可以使用(2n-1)&(~(columns|d1|d2)得到每一行可以放置皇后的位置(1<<n-1创建一个全为可放置棋子的行,column、diagonals1、diagonals2表示所有已占用位置,1表示占用,所以取反,与1<<n-1与运算得到当前行可放置棋子位置,1为可放置)。然后遍历这些位置,找到可能的解。
遍历这些位置时,可以利用以下两个按位与运算的性质:
x&(-x)可以获得x的二进制表示中的最低位的1的位置(原理是补码)
x&(x-1)可以将x最低位的1置0.
具体代码如下
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> solutions=new ArrayList<List<String>>();
int[] queens=new int[n];
Arrays.fill(queens,-1);
dfs(solutions,queens,n,0,0,0,0);
return solutions;
}
public void dfs(List<List<String>> solutions,int[] queens,int n,int row,int columns,int diagonals1,int diagonals2){
if(row==n){
List<String> board=generateBoard(queens,n);
solutions.add(board);
}else{
int available=((1<<n)-1)&(~(columns|diagonals1|diagonals2));//1<<n-1创建一个全为可放置棋子的行,column、diagonals1、diagonals2表示所有已占用位置,1表示占用,所以取反
//与1<<n-1与得到当前行可放置棋子位置,1为可放置。
//若available为0,则当前行已经没有可以放置的位置
while(available!=0){
int position=available&(-available);//原理是负数使用二进制补码表示
available=available&(available-1);//将最低位的1置0表示已经考虑过
int column=Integer.bitCount(position-1);//计算前面有几个1
queens[row]=column;
dfs(solutions,queens,n,row+1,columns|position,(diagonals1|position)<<1,(diagonals2|position)>>1);
queens[row]=-1;
}
}
}
public List<String> generateBoard(int[] queens,int n){
List<String> board=new ArrayList<String>();
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;
}
}
37.解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
示例 1:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]] 输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]] 解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
思路
通过递归加回溯的方法按行遍历空白格,将能够填入的数字依次填入,当递归到最后一个空白格后,如果依然没有冲突,说明找到了答案,如果填不了任何一个数字,则进行回溯。
由于每个数字在同一行、同一列、同一九宫格中只会出现一次,因此我们可以使用line[i],column[j],block[x][y]来表示第i行,第y列,第(x,y)个九宫中填写数字的情况。九宫格的范围为0<=x<=2以及0<=y<=2,具体地,第i行第y列的格子位于第([i/3],[y/3])个九宫格中,其中除法向下取整。
方法一 回溯
使用一个数组记录每个数字是否出现,比如用line[2][3]=true表示数字4在第二行已经出现过。
我们首先先对整个数组进行遍历,当遍历到i行j列时:
如果为空白格,那么我们将其加入一个存储空白格位置的列表中,方便后续递归操作
如果是一个数字x那么将line[i][x-1],column[j][x-1],block[i/3][j/3][x-1]均置为true。
结束遍历之后开始递归枚举,当枚举到spaces最后一个元素后,返回答案。
当枚举到i行j列时,枚举填入的数字x,根据要求,此时line[i][x-1],column[j][x-1],block[i/3][j/3][x-1]必须均为false。
填入数字后,将它们置为true,并递归下一个空白格位置,并再将它们置为false。
class Solution {
private boolean[][] line=new boolean[9][9];
private boolean[][] column=new boolean[9][9];
private boolean[][][] block=new boolean[3][3][9];
private boolean valid=false;
private List<int[]> spaces=new ArrayList<int[]>();
public void solveSudoku(char[][] board) {
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]=='.'){
spaces.add(new int[]{i,j});
}else{
int digit=board[i][j]-'0'-1;
line[i][digit]=column[j][digit]=block[i/3][j/3][digit]=true;
}
}
}
dfs(board,0);
}
public void dfs(char[][] board,int pos){
if(pos==spaces.size()){
valid=true;
return;
}
int[] space=spaces.get(pos);
int i=space[0],j=space[1];
for(int digit=0;digit<9&&!valid;digit++){
if(!line[i][digit]&&!column[j][digit]&&!block[i/3][j/3][digit]){
line[i][digit]=column[j][digit]=block[i/3][j/3][digit]=true;
board[i][j]=(char)(digit+'0'+1);
dfs(board,pos+1);
line[i][digit]=column[j][digit]=block[i/3][j/3][digit]=false;
}
}
}
}
方法二 位运算优化
与n皇后优化思路相同,仅使用一个整数来表示某行、某列或者某九宫格是否有某个数字出现过。
具体地,b的二进制表示的第i位为1,当且仅当数字i+1已经出现过。
定义一个函数flip,用于将数digit填入i行j列,该方法用以将line[i]的第digit位置为1,表示填入。
public void flip(int i,int j,int digit){
line[i]^=(1<<digit);
column[j]^=(1<<digit);
block[i/3][j/3]^=(1<<digit);
}
其他思路与n皇后相似
class Solution {
private int[] line=new int[9];
private int[] column=new int[9];
private int[][] block=new int[3][3];
private boolean valid=false;
private List<int[]> spaces=new ArrayList<int[]>();
public void solveSudoku(char[][] board) {
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]=='.'){
spaces.add(new int[]{i,j});
}else{
int digit=board[i][j]-'0'-1;
flip(i,j,digit);
}
}
}
dfs(board,0);
}
public void dfs(char[][] board,int pos){
if(pos==spaces.size()){
valid=true;
return;
}
int[] space=spaces.get(pos);
int i=space[0],j=space[1];
int mask=~(line[i]|column[j]|block[i/3][j/3])&0x1ff;
for(;mask!=0&&!valid;mask&=(mask-1)){
int digitMask=mask&(-mask);
int digit=Integer.bitCount(digitMask-1);
flip(i,j,digit);
board[i][j]=(char)(digit+'0'+1);
dfs(board,pos+1);
flip(i,j,digit);
}
}
public void flip(int i,int j,int digit){
line[i]^=(1<<digit);
column[j]^=(1<<digit);
block[i/3][j/3]^=(1<<digit);
}
}
方法3 枚举优化
在数独问题中,总有一些格子只能填入一个数字,此时该格子能填入的数字为确定的,所以可以在递归开始前将所有确定的数字填入。
class Solution {
private int[] line=new int[9];
private int[] column=new int[9];
private int[][] block=new int[3][3];
private boolean valid=false;
private List<int[]> spaces=new ArrayList<int[]>();
public void solveSudoku(char[][] board) {
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] != '.') {
int digit = board[i][j] - '0' - 1;
flip(i, j, digit);
}
}
}
while (true) {
boolean modified = false;
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] == '.') {
int mask = ~(line[i] | column[j] | block[i / 3][j / 3]) & 0x1ff;
if ((mask & (mask - 1)) == 0) {
int digit = Integer.bitCount(mask - 1);
flip(i, j, digit);
board[i][j] = (char) (digit + '0' + 1);
modified = true;
}
}
}
}
if (!modified) {
break;
}
}
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] == '.') {
spaces.add(new int[]{i, j});
}
}
}
dfs(board,0);
}
public void dfs(char[][] board,int pos){
if(pos==spaces.size()){
valid=true;
return;
}
int[] space=spaces.get(pos);
int i=space[0],j=space[1];
int mask=~(line[i]|column[j]|block[i/3][j/3])&0x1ff;
for(;mask!=0&&!valid;mask&=(mask-1)){
int digitMask=mask&(-mask);
int digit=Integer.bitCount(digitMask-1);
flip(i,j,digit);
board[i][j]=(char)(digit+'0'+1);
dfs(board,pos+1);
flip(i,j,digit);
}
}
public void flip(int i,int j,int digit){
line[i]^=(1<<digit);
column[j]^=(1<<digit);
block[i/3][j/3]^=(1<<digit);
}
}
总结
太难了