브루트포스 문제는 풀이방식이 다양해서 더 어려운 것 같다. 익숙해지기 위해 브루트포스 문제 리뷰를 많이 하려고 한다.
이번에 풀어본 문제는 N-Queen 문제인데, 여러번의 3번의 시도끝에 성공한 문제라, 내가 잘못했던 부분이 무엇이었는지를 기록하고자 내 시도를 정리하려고 한다.
package prob16;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class DayProb16 {
private static int N;
private static boolean[][] queens;
public static void main(String[] argv) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = Integer.parseInt(br.readLine());
int count = 0;
for (int i = 0; i < N*N; i++){
queens = new boolean[N][N];
count += bf(i, 1);
}
System.out.println(count);
}
public static int bf(int start, int cnt){
attackable(start/N, start%N);
int incount = 0;
for (int i = start+1; i < N*N; i++){
if(queens[i/N][i%N]){continue;}
if (cnt + 1 >= N) {
incount++;
continue;
}
incount += bf(i, cnt + 1);
}
return incount;
}
public static void attackable(int row, int col){
// 가로,세로 열 모두 공격가능
for (int i = 0; i < N; i++){
queens[row][i] = true;
queens[i][col] = true;
}
for (int i = -N; i < N; i++){
if (row+i >= 0 && row+i < N && col+i >=0 && col+i < N) {
queens[row+i][col+i] = true;
}
if (row+i >= 0 && row+i < N && col-i < N && col-i >= 0) {
queens[row+i][col-i] = true;
}
}
}
}
=> 재귀마다 queens 배열을 원상복구 시켜줘야 하는데 그렇지 않았다. 재귀 후 배치했던 queen은, 재귀에서 나온 후 다시 제거한 후 설정한 attackable 영역을 다시 원상복구 해야한다.
attackable() 이 변경했던 좌표를 저장한 후, 재귀에서 나올 때 해당 좌표들을 다시 원상복구 시키는 방법이다.
하지만, 원상복구좌표를 위한 추가 메모리 공간이 "매 재귀마다" 필요하다.
package prob16;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class DayProb16 {
private static int N;
private static boolean[][] queens;
public static void main(String[] argv) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = Integer.parseInt(br.readLine());
int count = 0;
for (int i = 0; i < N*N; i++){
queens = new boolean[N][N];
count += bf(i, 1);
}
System.out.println(count);
}
public static int bf(int start, int cnt){
List<int[]> changed = attackable(start/N, start%N);
int incount = 0;
// 다음열부터
for (int i = (start/N+1)*N; i < N*N; i++){
if(queens[i/N][i%N]){continue;}
if (cnt + 1 >= N) {
incount++;
continue;
}
incount += bf(i, cnt + 1);
}
for (int[] pair : changed){
queens[pair[0]][pair[1]] = false;
}
return incount;
}
public static List<int[]> attackable(int row, int col){
List<int[]> changed = new ArrayList<>();
// 가로,세로 열 모두 공격가능
for (int i = 0; i < N; i++){
if (!queens[row][i]) {
changed.add(new int[]{row, i});
queens[row][i] = true;
}
if(!queens[i][col]){
changed.add(new int[]{i, col});
queens[i][col] = true;
}
}
for (int i = -N; i < N; i++){
if (row+i >= 0 && row+i < N && col+i >=0 && col+i < N) {
if(!queens[row+i][col+i]) {
changed.add(new int[]{row+i, col+i});
queens[row+i][col+i] = true;
}
}
if (row+i >= 0 && row+i < N && col-i < N && col-i >= 0) {
if(!queens[row+i][col-i]) {
changed.add(new int[]{row+i, col-i});
queens[row+i][col-i] = true;
}
}
}
return changed;
}
}
우선, 복잡했던 재귀식에서 end case 를 명확히 한 후 (row의 범위가 N을 벗어날 때 까지) backtracking 형식으로 변경했다.
public static int dfs(int depth){
if (depth >= N) {return 1;}
...
그리고, attackable 영역인지를 더 쉽게 확인하기 위해 2차원 배열을 선언한 것이 아니라, 같은 column 상, 같은 \ 대각선상, 같은 / 대각선상을 의미하는 배열을 정의했다.
private static boolean[] col;
private static boolean[] diag1; // "/"
private static boolean[] diag2; // "\"
int N = Integer.parseInt(br.readLine());
col = new boolean[N];
diag1 = new boolean[2*N-1];
diag2 = new boolean[2*N-1];
이렇게하면 attackable 영역인지 확인하는 로직과, 영역을 설정하는 로직과, 원상복구 로직이 훨씬 더 간편하다.
그리고, 매 row 마다 1~9 까지의 col을 확인하며, 각 row에서 Queen을 둘 수 있는 column과 대각선상을 확인하며 재귀한다.
int incount = 0;
for (int i = 0; i < N; i++){
int d1 = depth + i;
int d2 = depth - i + (N - 1);
if (col[i] || diag1[d1] || diag2[d2]) {continue;}
col[i] = true;
diag1[d1] = true;
diag2[d2] = true;
incount += dfs(depth+1);
col[i] = false;
diag1[d1] = false;
diag2[d2] = false;
}
return incount;
package prob16;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class DayProb16 {
private static int N;
private static boolean[] col;
private static boolean[] diag1; // "/"
private static boolean[] diag2; // "\"
public static void main(String[] argv) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = Integer.parseInt(br.readLine());
col = new boolean[N];
diag1 = new boolean[2*N-1];
diag2 = new boolean[2*N-1];
System.out.println(dfs(0));
}
public static int dfs(int depth){
if (depth >= N) {return 1;}
int incount = 0;
for (int i = 0; i < N; i++){
int d1 = depth + i;
int d2 = depth - i + (N - 1);
if (col[i] || diag1[d1] || diag2[d2]) {continue;}
col[i] = true;
diag1[d1] = true;
diag2[d2] = true;
incount += dfs(depth+1);
col[i] = false;
diag1[d1] = false;
diag2[d2] = false;
}
return incount;
}
}
드디어 성공했다!
인상깊었던 스터디원의 풀이가 있어 공유하려고 한다. 코드도 훨씬 간단하고 직관적이다. 나는 왜저렇게 어렵게 생각하고 풀려 했는지 현타가 올 정도이다.
class Solution {
int[] board;
int count = 0;
int answer = 0;
public int solution(int n) {
board = new int[n];
backtracking(0, n);
return answer;
}
// 같은 idx 이거나 대각선이면
private void backtracking(int start, int n){
// n개 배치 완료 시 종료
if(start == n){
answer++;
return;
}
// 이번 행에 넣을 숫자를 0~n-1 반복
for(int i=0; i<n; i++){
// 이전과 비교해서 이번에 놓을 게 안전하면 백트래킹
if(isSafe(start, i)){
board[start] = i;
backtracking(start+1, n);
}
}
}
private boolean isSafe(int now, int num){
for(int prev=0; prev<now; prev++){
// 같은 행이거나 대각선
if(board[prev] == num || Math.abs(board[prev] - num) == Math.abs(prev - now)){
return false;
}
}
return true;
}
}
int[] board = new int[n];
하나만 선언한다.Math.abs(board[prev] - num) == Math.abs(prev - now)