
우선, 본격적인 비교에 앞서 간단하게 깊이우선탐색이 무엇인지 간단하게 집고 넘어가봅시다. 오늘은, 간단하게 어떤 느낌인지 찍먹 해보는 느낌으로 머릿속에 인지해두고, 이후 포스팅에서 깊이우선탐색과 너비우선탐색 알고리즘에 대해 자세히 다뤄보겠습니다.
백트래킹은 모든 경우의 수에서 조건을 만족하는 경우를 탐색하는 것이기 때문에, 완전탐색기법인 DFS와 BFS 로 구현이 가능합니다.
백트래킹 특성에서 조건에 부합하지 않으면 이전 수행으로 돌아가야 함으로 BFS보다는 DFS이 구현하기 더 편하기 때문에 주로 DFS를 사용합니다.

백트래킹 알고리즘은 모든 경우의 수를 탐색하는 문제에서 특히 유용합니다! 하지만 단순히 모든 경우의 수를 탐색하는 것 뿐만 아니라 탐색 중 불필요한 경로를 가지치기하여 성능을 최적화할 수 있다는 점에서 유용합니다.
=> 그러나 탐색해야하는 공간이 매우 큰 경우, 가지치기가 어렵다면 백트래킹 알고리즘을 적용하기에 현실적으로 어려울수도 있습니다! 그렇기 때문에 문제를 마주할때, 그리디 알고리즘과 동적 프로그래밍도 같이 고려해야합니다!


2, 4, 또는 3 중 하나를 선택.package BackTracking;
import java.util.ArrayList;
import java.util.List;
public class BacktrackingExample {
static int[][] board = {
{2, 4, 3}, // Row 0
{1, 3, 7}, // Row 1
{6, 5, 6} // Row 2
};
static boolean[] visited; // 열 선택 여부를 나타내는 배열
static int N = 3; // 행렬 크기 (3×3)
static int[] result; // 현재 경로에서 선택된 숫자를 저장
static List<int[]> allResults = new ArrayList<>(); // 모든 결과를 저장
public static void main(String[] args) {
visited = new boolean[N]; // 열 방문 여부 초기화
result = new int[N]; // 선택된 숫자를 저장할 배열
backtrack(0); // 백트래킹 시작 (첫 번째 행부터)
// 모든 결과 출력
for (int[] res : allResults) {
for (int num : res) {
System.out.print(num + " ");
}
System.out.println();
}
}
static void backtrack(int row) {
// 기저 조건: 모든 행에서 숫자를 선택한 경우
if (row == N) {
allResults.add(result.clone()); // 결과 저장
return;
}
// 현재 행(row)의 가능한 열 탐색
for (int col = 0; col < N; col++) {
if (!visited[col]) { // 해당 열이 선택되지 않았다면
visited[col] = true; // 열 선택
result[row] = board[row][col]; // 숫자 선택
System.out.printf("Row %d -> Col %d 선택: %d%n", row, col, board[row][col]); // 디버깅 메시지
backtrack(row + 1); // 다음 행으로 이동
visited[col] = false; // 열 선택 해제 (백트래킹)
}
}
}
}
public static void main(String[] args) {
visited = new boolean[N]; // 열 방문 여부 초기화
result = new int[N]; // 선택된 숫자를 저장할 배열
backtrack(0); // 백트래킹 시작 (첫 번째 행부터)
// 모든 결과 출력
for (int[] res : allResults) {
for (int num : res) {
System.out.print(num + " ");
}
System.out.println();
}
}
if (row == N) {
allResults.add(result.clone()); // 결과 저장
return;
}
for (int col = 0; col < N; col++) {
if (!visited[col]) { // 해당 열이 선택되지 않았다면
visited[col] = true; // 열 선택
result[row] = board[row][col]; // 숫자 선택
backtrack(row + 1); // 다음 행으로 이동
visited[col] = false; // 열 선택 해제 (백트래킹)
}
}
row = 0 (현재 탐색 중인 행은 첫 번째 행)
가능한 열(col)은 모두 선택 가능: col = 0, 1, 2.
아직 아무 숫자도 선택되지 않았고, visited = [false, false, false].
현재 상태: row = 0, col = 0
숫자 2를 선택합니다 (board[0][0]).
선택한 열 col = 0은 방문 표시: visited = [true, false, false].
현재 경로(result): [2, _, _].
다음 단계:
- 행을 한 칸 아래로 이동: backtrack(1) 호출.
현재 상태: row = 1, 가능한 열은 col = 1, col = 2 (왜냐하면 col = 0은 이미 방문했음).
선택한 열: col = 1 → 숫자 3을 선택 (board[1][1]).
선택한 열 col = 1은 방문 표시: visited = [true, true, false].
현재 경로(result): [2, 3, _].
다음 단계:
- 행을 한 칸 아래로 이동: backtrack(2) 호출.
현재 상태: row = 2, 가능한 열은 col = 2 (왜냐하면 col = 0과 col = 1은 이미 방문했음).
선택한 열: col = 2 → 숫자 6을 선택 (board[2][2]).
선택한 열 col = 2은 방문 표시: visited = [true, true, true].
현재 경로(result): [2, 3, 6].
기저 조건:
- 현재 행(row = 2)에서 숫자를 선택했으므로, 모든 행에서 숫자를 선택한 상태입니다 (row == N).
- 경로 [2, 3, 6]을 allResults에 저장.
현재 상태: 탐색을 종료하고 이전 단계로 돌아가기 위해 백트래킹을 시작.
백트래킹 동작:
- 열 선택 해제: col = 2 방문 취소 → visited = [true, true, false].
- 현재 경로에서 선택된 숫자를 제거: [2, 3, _].
- 이전 단계(row = 1)로 돌아갑니다.
현재 상태: row = 1, visited = [true, true, false].
마지막에 선택한 열(col = 1) 해제: visited = [true, false, false].
현재 경로에서 선택된 숫자를 제거: [2, _, _].
다음 탐색:
- 두 번째 행에서 col = 2를 선택해 새로운 경로를 탐색.
백트래킹으로 탐색을 계속 진행하면서 모든 가능한 숫자 경로를 탐색.
새로운 경로가 완성될 때마다 결과를 저장.
마지막까지 탐색을 마치면 최종적으로 allResults에 모든 경로가 저장됩니다.
| Row | Col 선택 | 선택된 숫자 경로 | Visited 상태 | 결과 저장 |
|---|---|---|---|---|
| 0 | 0 | [2, _, _] | [true, false, false] | |
| 1 | 1 | [2, 3, _] | [true, true, false] | |
| 2 | 2 | [2, 3, 6] | [true, true, true] | 저장 |
| 2 | 백트랙 | [2, 3, _] | [true, true, false] | |
| 1 | 백트랙 | [2, _, _] | [true, false, false] | |
| 1 | 2 | [2, 7, _] | [true, false, true] | |
| 2 | 1 | [2, 7, 5] | [true, true, true] | 저장 |
| ... | ... | ... | ... | ... |
탐색 제한: 이미 선택된 열(visited)은 제외하여 중복을 방지.
결과 저장: 기저 조건(row == N)에 도달하면 결과를 저장.
되돌아가기: 선택을 해제하고 이전 상태로 되돌아가 새로운 경로를 탐색.
문제를 통해 백트래킹의 동작을 완벽하게 이해해봅시다!

→ 백트래킹을 이용하여 1-N까지 자연수에서 중복없이 길이가 M인 수열을 출력하는 문제라고 생각이 듭니다!
package Baekjoon;
import java.io.*;
import java.util.*;
//아래 조건을 만족하는 길이가 M인 수열을 모두 구하는 프로그램을 작성
//1부터 N까지 자연수 중에서 중복 없이 M개를 고른 수열
//중복되는 수열을 여러 번 출력하면 안되며, 각 수열은 공백으로 구분해서 출력
//수열은 사전 순으로 증가하는 순서로 출력
public class beckjoon15649 {
static int N,M;
static boolean[] visited;
static List<Integer> sequence = new ArrayList<>(); // 현재 수열 저장
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] nums = br.readLine().split(" ");
N = Integer.parseInt(nums[0]); //1부터 N까지 자연수
M = Integer.parseInt(nums[1]); //중복없이 M개를 고른 수열
visited = new boolean[N+1];
backtrack(0);//탐색 시작
}
public static void backtrack(int depth){
if(depth == M){ //길이가 M인 수열이 완성된 경우
for(int i =0; i<M; i++){
int num = sequence.get(i);
System.out.print(num + " ");
}
System.out.println();
}
for(int i=1; i<=N; i++){
if(!visited[i]){//숫자 i가 아직 사용되지 않은 경우
visited[i] = true;// 방문처리
sequence.add(i);//수열에 추가
backtrack(depth+1);//재귀호출
visited[i] = false;
sequence.remove(sequence.size()-1);//수열에서 제거
}
}
}
}
visit boolean 배열로 방문 여부를 관리
if (!visited[i]) {
// 방문하지 않은 숫자만 처리
}
visited[i]: 숫자 i가 이미 수열에 포함되어있는지 확인하는 배열입니다.
특정 숫자가 이미 포함되어있는 경우 visited[i]=true로 해당 숫자는 다시 선택되지 않습니다.
따라서, 중복되는 수열 (2 2 , 3 3..)같은 수열은 출력되지 않습니다.
재귀호출 후 상태 복구(visited[i] == false)
visited[i] = true; // 숫자 i 사용
sequence.add(i); // 수열에 추가
backtrack(depth + 1); // 재귀 호출
visited[i] = false; // 숫자 i 사용 해제
sequence.remove(sequence.size() - 1); // 수열에서 제거
재귀 호출을 통해 수열을 완성한 후, 현재 숫자를 다시 사용할수 있도록 상태를 복구합니다.
이 과정에서 visited[i] == false로 설정되어 다음 경우의 수를 탐색할때는 이 숫자를 다시 선택할수 있게 합니다.
suquence.remove(sequence.size()-1) : 중복되지않은 수열을 출력후 다시 상태를 복구하기 위해 사용됩니다.
- 입력: N = 3, M = 2
- 초기 상태:
- sequence = []
- 탐색 과정:
1. i = 1 선택 → sequence = [1]:
- 재귀 호출 시작.
2. 재귀에서 i = 2 선택 → sequence = [1, 2]:
- 길이가 2이므로 출력: 1 2.
- 재귀 종료 후 복구 → sequence = [1].
3. 다음 숫자 선택: i = 3 → sequence = [1, 3]:
- 길이가 2이므로 출력: 1 3.
- 재귀 종료 후 복구 → sequence = [1].
4. 복구 후 반복문으로 돌아가 i = 2 선택 → sequence = [2]:
- 이후 탐색 반복...
⇒ 백트래킹에는 상태복구가 필수적입니다!!

→ 퀸은 열 ,행 , 대각선 선으로 움직일 수 있음! 만약 N*N 체스판 위에 N개의 퀸이 놓여있을때 그 N개의 퀸이 서로 공격하는 경우의 수를 제외해야함!
package Baekjoon;
import java.io.*;
//N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제
//N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램
public class beckjoon9663 {
static int[] queen; //각 행에 놓은 퀸의 위치 저장
static int N; //체스판 크기
static int count = 0;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = Integer.parseInt(br.readLine()); //퀸 N개
queen = new int[N];
solve(0); //0행부터 탐색 시작
System.out.println(count); //해결 방법 갯수 출력
}
//row번째 행에 퀸 배치 시도
public static void solve(int row){
if(row ==N){ //모든 퀸을 배치한 경우
count++; //해결 방법 증가
return;
}
for(int col =0; col< N; col++){
if(isSafe(row,col)){ //현재 위치에 퀸 배치가 가능한지 확안
queen[row] = col;//퀸 배치
solve(row+1); //다음 행으로 이동
//백트래킹: 이전으로 돌아와 다른 경우 탐색
}
}
}
public static boolean isSafe(int row, int col){
for(int i=0; i<row; i++){
//현재 퀸을 놓으려는 위치 (row, col)가 이미 배치된 퀸들과 충돌하는지 확인하는 부분
//같은 열 || 같은 대각선 확인
if (queen[i] == col || Math.abs(queen[i] - col) == Math.abs(i - row)) { // Math.abs 숫자의 절댓값을 계산하는 메서드
return false;
}
}
return true;
}
}
public class beckjoon9663 {
static int[] queen; //각 행에 놓은 퀸의 위치 저장
static int N; //체스판 크기
static int count = 0;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
N = Integer.parseInt(br.readLine()); //퀸 N개
queen = new int[N];
solve(0); //0행부터 탐색 시작
System.out.println(count); //해결 방법 갯수 출력
}
왜 N*N 체스판인데 2차원 배열은 쓰지않고 1차원 배열을 이용하는걸까?
1차원 배열로도 문제를 풀수있고 공간 복잡도 최적화에 더 적합하기 때문입니다.
int[N][N] 2차원 배열은 N*N 공간이 필요하지만 int[N]으로 문제를 풀면 N공간의 배열만 필요하기 때문에 더 적합합니다.
static void solve(int row) {
if (row == N) { // 모든 퀸을 배치한 경우
count++; // 해결 방법 증가
return;
}
for (int col = 0; col < N; col++) {
if (isSafe(row, col)) { // 현재 위치에 배치 가능한지 확인
queen[row] = col; // 퀸 배치
solve(row + 1); // 다음 행으로 이동
// 백트래킹: 다시 돌아와서 다른 경우 탐색
}
}
}
if (row ==N) : 현재 행이 N에 도달했다는 것은 모든 퀸을 성공적으로 배치
해결방법의 경우의 수 count의 갯수를 늘리고 종료합니다.
for(int col =0; col < N; col++)
isSafe(row,col) → 이 행과 열에 퀸을 배치할수 있는지를 판단합니다.
queen[row] : 각 퀸의 행에 놓은 열의 번호를 저장합니다.
재귀호출 : solve(row+1) 퀸을 현재 row,col에 배치후에 , 다음 행에 퀸을 배치하기 위해 재귀적으로 solve 함수 호출합니다.
public static boolean isSafe(int row, int col) {
for (int i = 0; i < row; i++) {
// 현재 퀸을 놓으려는 위치 (row, col)가 이미 배치된 퀸들과 충돌하는지 확인
if (queen[i] == col || Math.abs(queen[i] - col) == Math.abs(i - row)) {
return false;
}
}
return true;
}
(row,col)→ 현재 퀸을 놓으려는 행과 열입니다.
for(int i =0; i<row; i++) : 현재 이미 배치된 퀸의 행.
이제 충돌 여부를 검사하는 조건문이 들어옵니다.
- queen[i]==col : 같은 열에 충돌하는 조건입니다.
- Math.abs(queen[i]-col) - Math.abs( i - row) : 대각선에서 충돌하는 조건입니다,.
→ 이렇게 충돌하는 경우에는 false 반환하여 현재 놓으려는 퀸의 위치가 안전하지 않다는것을 알리기위해 return false;를 반환합니다.
→ 충돌조건에서 통과하면 퀸을 그 자리에 놓아도 안전하다는 뜻이기 때문에 retuen true; 를 반환 합니다.
백트래킹, 재귀적 사고, 제약 조건 처리, 데이터 구조 활용, 수학적 사고, 그리고 문제 해결 능력을 요하는 수준이 낮지않은 문제였습니다.
오늘은 백트래킹의 개념과 문제풀이를 통해 백트래킹 알고리즘에 대해 상세하게 알아보았습니다. 다음 포스팅은 DFS & BFS로 찾아보겠습니다❣️