Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

14-g0rnn #53

Merged
merged 2 commits into from
Jan 21, 2025
Merged

14-g0rnn #53

merged 2 commits into from
Jan 21, 2025

Conversation

g0rnn
Copy link
Collaborator

@g0rnn g0rnn commented Jan 7, 2025

🔗 문제 링크

N-Queen

✔️ 소요된 시간

50m

✨ 수도 코드

어떻게 접근해야할까

문제에서 핵심이 되는 수도 코드는 꽤나 간단합니다. 이전 13pr에서 언급했던 백트레킹 문제들을 풀어보았다면 수도 코드는 간단히 생각할 수 있었을겁니다.

0. 둘 수 있는 칸에 대해
1. 퀸을 둔다.
2. total += backtracking()
3. 퀸을 제거한다.
4. print total

수도 코드의 구현은 쉬울것 같은데.. 둘 수 있는 칸은 어떻게 정해야할까요? 아마 이미 놓여진 퀸들에게 영향을 받지 않는 위치여야 할겁니다. 이전 퀸들과 같은 행과 열에 위치해서는 안될 것이고 대각선 상에 위치해서도 안됩니다.

행과 열을 구분하는 건 쉬우니 나중에 구현할 때 더 생각해보고.. 대각선은 어떻게 알 수 있을가요? 저는 이전 퀸(prev)의 위치와 지금 놓을 퀸(cur)의 위치의 차이로 계산했습니다. prev.column - cur.column == prev.row - cur.row인 경우가 대각선입니다.

아이디어 구상은 끝났으니 이제 구현해봅시다.

주어진 row에 대해 가능한 경우의 수를 반환하는 함수

func findQueen (int row) :
    if  n개의 퀸을 두었으면 return 1
    else
        체스 판에서 퀸을 둘 수 있는 칸에 대해
            퀸을 둔다.
            total += findQueen
            퀸을 제거한다.
    return total

좀 더 자세한 수도코드는 위와 같습니다.

backtracking하는 함수의 종료 조건은 n개의 퀸을 체스판에 모두 놓은 경우이므로 1을 반환합니다. 이후 total에 이 값들이 쌓여 최종 결과값이 됩니다.

구현할 때 n*n의 체스판으로 해도 될것 같으나 제 생각엔 그렇게 많은 배열은 필요없다고 생각하여 1차원 배열(chess)을 사용하였습니다.

2차원 배열로 구현했을 때에 백트레킹은 아마 위에서 아래로 내려가면서 퀸을 둘 것입니다. 그렇기에 당연히 같은 행에 위치할 일은 없습니다. 그리고 대각선의 위치를 판별하는 방법은 서로의 위칫값이므로 4가지 값만 있으면 됩니다.

이제 체스판의 각 위치 정보를 1차원 배열에 담아 풀면 됩니다. 참고로 chess의 index는 퀸이 놓여진 column을 의미하고 value는 row를 의미합니다.

📚 새롭게 알게된 내용

Copy link
Collaborator

@kangrae-jo kangrae-jo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N-Queen 문제를 직접 구현해 볼 수 있어서 좋았습니다.
처음에 2차원 배열에 저장했습니다. 진짜 체스판 처럼요..

근데 구현을 진행하다가 옛날에 구현에대한 설명을 봤던 기억이 떠올랐습니다.
1차원 배열로 구현한다고 했을 때, indexvalue를 생각해보면 N-Queen 문제에서 충분히 쓸 수 있을 것 같았습니다.
그리고 균호님 설명을 보니 그렇게 하셨더군요

전반적인 backtracking 과정은 균호님과 같습니다.
그런데 체스판의 , indexvalue의 관계를 서로 다르게 지정한 것 같더라구요.
그래도 성능이나 조건에 큰 차이는 없어 보이네요.

그리고 저는 검사 로직에서 이미 놓인 칸까지만 반복문으로 검사하게 해서 continue로직이 필요없도록 해봤습니다!

CPP CODE
#include <iostream>
#include <vector>

#define EMPTY -1

using namespace std;

int N;
vector<int> chess;

// chess의 index는 x좌표, 값은 y좌표를 뜻 함
bool canPut(int x, int y) {
    // 지금까지 놓은 칸과 비교하며 검사
    for (int x_ = 0; x_ < x; x_++) {
        int y_ = chess[x_];
        // 같은 행 검사
        if (y_ == y) return false;
        // 대각선 검사 (x좌표의 차 == y좌표의 차 이면 대각선에서 공격 가능)
        if (abs(x - x_) == abs(y - y_)) return false;
    }
    return true;
}

int nQueen(int x) {
    if (x == N) return 1; 

    int result = 0;
    for (int y = 0; y < N; y++) { 
        if (!canPut(x, y)) continue;
        chess[x] = y;          
        result += nQueen(x + 1); 
        chess[x] = EMPTY;   
    }
    return result;
}

int main() {
    cin >> N;

    chess = vector<int>(N, EMPTY);
    cout << nQueen(0);            

    return 0;
}

bool canMoveTo(int col, int row) {
for (int i = 0; i < n; i++) {
if (chess[i] == EMPTY) continue;
if (abs(col - i) == row - chess[i]) return false; // 대각선인 경우 (row는 항상 chess[i]보다 큼)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

처음에 2차원 배열로 진행하다가, '이게 아닌데..? ' 싶어서 균호님 설명을 좀 봤습니다.
균호님 설명을 보니까 저도 이 로직이 생각나서 이렇게 진행했습니다.

대각선인 경우 (row는 항상 chess[i]보다 큼)

이 말은 처음에 잘 몰랐는데 곰곰히 생각해보니까 그렇네요!
대각선은 우하향 또는 우상향이기 때문에... 맞죠.. ?
(저는 절대값으로 비교했습니다... )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! 체스판 기준 맨 위에서 밑으로 내려가기 때문에 좌상 또는 우상을 고려할 필요가 없어요 :)

Copy link
Collaborator

@wnsmir wnsmir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nqueen문제를 보니 작년 알고리즘 수업시간이 슬쩍 떠올라 반가웠습니다.
퀸을 배치할때, 서로 공격하지 못하도록 배치하는 방법의 수를 구하는 대표적인 백트래킹문제입니다.

제가 당시 이문제를 보며 고민했던 부분은, 브루트포스와 백트래킹이 어떤 다른의미를 가지느냐 였습니다.
결론은 백트래킹은 브루트포스를 최적화한 기법으로, 탐색도중 만족하지 않는 경로는 배제하고 탐색을 중단하여 효율성을 높입니다.

정리하자면, 백트래킹으로 해결할 수 있는 문제는 전부 브루트포스로 해결가능하지만, 브루트포스로 해결가능한 모든문제를 백트래킹으로는 해결할 수 없다는 것입니다.

백트래킹은 문제자체에 조건이 있을경우 그 조건에 기반하여 중단할 기준을 세우고 탐색을 하기 때문입니다.

Nqueen문제는 퀸의 위치를 배치할때 서로 공격하지 못하는 조건을 만족해야하므로 백트래킹으로 탐색공간을 대폭 줄일 수 있는 대표적인 알고리즘 문제입니다.

def Nqueens(n):
    def is_safe(row, col):
        for r, c in queens:
            if c == col or abs(row - r) == abs(col - c):
                return False
        return True

    def backtrack(row):
        if row == n:
            solutions.append(queens[:])
            return
        for col in range(n):
            if is_safe(row, col):
                queens.append((row, col))
                backtrack(row + 1)
                queens.pop()

    solutions = []
    queens = []
    backtrack(0)
    return solutions

N = int(input())
solutions = Nqueens(N)

print(len(solutions))

is_safe로 퀸을 놓을 수 있는지 확인하고, col과 대각선조건이 충돌하지 않으면 안전한 위치입니다.

백트래킹으로 각 행에 퀸을 배치하고, solution에 결과를 저장합니다.

queens리스트는 현재 배치된 퀸들의 위치를 저장하고, N번째 행까지 성공적으로 배치되면, 한가지 솔루션을 찾은 것입니다. 그리고 다음행으로 row + 1해주고, 재귀호출이 끝나면 퀸을pop으로 제거해줍니다.

즉 n이 4일때는 4개의 행에 각 열에 어떻게 배치해야 가능한지를 모두 놓아보는 것입니다.
다만 완전히 모두 놓아보는것은 아닌데, 만약 중간에 조건이 일치하지 않는다면 그 방법은 조기중단되어 브루트포스보다 훨씬 복잡도를 낮출 수 있습니다.

#define EMPTY -1

int n;
int chess[15] = {0};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 2차원 배열을 만들어서 구현하려고 했는데(결국 구현을 못했지만)
열을 인덱스로 행을 값으로 두는 방식은 생각하지 못했네요.
저는 반대로 행을 인덱스로 두고 열을 값으로 두고 해보았습니다.
개인적으로는 그게 더 편하다고 생각했습니다.

그런데 한가지 궁금한 게 canMoveTo에서
if (chess[I] == EMPTY) continue;
는 꼭 필요한건가요??

@kokeunho
Copy link
Collaborator

백트래킹 한번 풀어본 유형인데
이번 문제는 개인적으로 정말 힘들었습니다...
코드에 지선생의 흔적이 가득;

저는 map의 인덱스를 행으로하고 값을 열로 설정하였습니다.

그리고 한 행에는 어차피 하나의 퀸 밖에 놓을 수 없으므로
backTracking함수에서 count 없이 row만을 받았습니다.
canPut함수에서도 행 검사는 하지 않았습니다.

java code
import java.util.*;

public class Main {
    static int n;
    static int[] map;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        n = sc.nextInt();
        map = new int[n];
        for (int i = 0; i < n; i++) {
            map[i] = -1;
        }

        System.out.print(backTracking(0));
    }
    public static boolean canPut(int row, int col) {
        for (int i = 0; i < row; i++) {
            if (map[i] == col) return false;
            if (Math.abs(i-row) == Math.abs(map[i]-col)) return false;
        }
        return true;
    }
    public static int backTracking(int row) {
        if (row == n) return 1;

        int total = 0;
        for (int i = 0; i < n; i++) {
            if(canPut(row, i)){
                map[row] = i;
                total += backTracking(row+1);
                map[row] = -1;
            }
        }
        return total;
    }
}

백트래킹에 대해 더 공부할 수 있는 어렵지만 재밌는 문제였습니다.
다음에는 혼자 풀 수 있도록 공부해두어야겠습니다.
이번 PR 수고하셨습니다!

@g0rnn g0rnn merged commit 3cc8431 into main Jan 21, 2025
@g0rnn g0rnn deleted the 14-g0rnn branch January 21, 2025 04:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants