by 꽈배기
최적의 경로 찾기를 위한 길찾기 알고리즘.
구현 가능한 지에 대한 문제
최적의 문제 결과 도출 (minimum resource)
도출 가능한 모든 경우의 수 판단
BackTracking은 기본적으로 recursion 개념을 수행한다.
가장 큰 차이점은 아래와 같다
Recursion | BackTracking |
---|---|
재귀 함수는 자신의 복사본을 호출하고 원래 문제의 더 작은 하위 문제를 해결하는 방식으로 특정 문제를 해결합니다. | 모든 단계에서 역추적을 통해 해결책을 제시할 수 없는 선택지를 제거하고 해결책을 제시할 가능성이 있는 선택지로 진행합니다. |
#include<iostream>
using namespace std;
void PrintArray(int *arr, int e)
{
for (int t = 0; t < e; t++)
{
cout << arr[t] << ", ";
}
}
// int *arr just gets first element's of pointer in arr[]
void fillArray(int *arr,int s,int e, int val)
{
# base case
if(s == e)
{
//can't use foreach cause we can't specify array size with pointer
// print out array
for (int j = 0; j < e; j++)
{
cout << arr[j] << ", ";
}
return;
}
# rec case
arr[s] = val;
fillArray(arr,s+1,e, val+1);
# backtracking step
arr[s] = -1 * arr[s];
cout << endl;
PrintArray(arr,e);
}
int main()
{
int arr[100] = {0};
int e;
int val;
cin >> e >> val;
fillArray(arr,0,e,val);
return 0;
}
Result : n = 3, val = 1
1, 2, 3,
1, 2, -3,
1, -2, -3,
-1, -2, -3,
결과를 보면 call stack에 적재된 순서에 따라 재귀 함수를 호출하고 BackTracking 부분이 실행되는 모습을 볼 수 있다.
# backtracking step
arr[s] = -1 * arr[s];
cout << endl;
PrintArray(arr,e);
위에서 작성한 부분이 backTracking 구현부가 된다.
#include<iostream>
#include<vector>
using namespace std;
void pirntArray(vector<int> &arr)
{
for (auto &&i : arr)
{
cout << i <<", ";
}
}
// reference shared accross all func called
void fillArray(vector<int> &arr,int i,int e, int val)
{
//base case
if(i == e)
{
pirntArray(arr);
return;
}
//recursion case
arr[i] = val;
fillArray(arr,i+1, e, val+1);
// backtracking step
if((arr[i]-1)&(arr[i]) != 0)
arr[i] = 0;
}
int main()
{
int n;
int val;
cin >> n >> val;
vector<int> arr(n);
fillArray(arr,0,arr.size(),val);
cout << endl;
pirntArray(arr);
}
Result: n :10, val :3
3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
3, 0, 5, 0, 7, 0, 9, 0, 11, 0,
기본적으로 C++의 STL은 passed by value다. 만약 이를 고려하지않고 위의 코드를 수행한다면 재귀호출이 일어나고 종료되는 시점에서 element를 가진 vector는 삭제되고 이전에 적재된 vector가 call stack에 존재하게 된다.
// 레퍼런스 타입으로 가져오지 않을 경우
n :5, val : 1
1, 2, 3, 4, 5,
0, 0, 0, 0, 0,
그러니 Vector Container를 사용할 때 call stack의 현재 위치에 따라 vector의 요소가 변하는 것이다.
이를 방지하기위해선 레퍼런스 타입으로 매개변수를 가져오는것을 잊지말자.
모든 부분 문자열을 출력하는 코드이다. 기존 Recursion Tree와 동일해보이나, 근본적인 차이점으로는 역추적을 통해 선택지를 제거하고 해결책을 제시할 가능성이 있는 모든 선택지를 출력한다는 것이다.
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
bool compare(string a, string b)
{
if(a.length() == b.length())
return a<b ; // 사전순 정렬
return a.length() < b.length();
}
void PrintSubsets(vector<string> &list)
{
int sum = 0;
for (auto &&i : list)
{
cout << i << endl;
sum++;
}
cout << sum;
}
void FindSubsets(char *arr,char *temp,int i,int j,vector<string> &list)
{
//base case
if(arr[i] == '\0')
{
temp[j] = '\0';
string s_temp(temp);
list.push_back(temp);
cout << endl;
return;
}
//recursion case
//Include the ith letter
temp[j] = arr[i];
FindSubsets(arr,temp,i+1,j+1,list);
//Exclude the ith letter
//OverWriting
temp[j] = '\0';
FindSubsets(arr,temp,i+1,j,list);
}
int main()
{
char arr[100];
cin >> arr;
int n = sizeof(arr)/sizeof(char);
char temp[n];
vector<string> list;
FindSubsets(arr,temp,0,0,list);
sort(list.begin(),list.end(),compare);
PrintSubsets(list);
}
위의 코드는 기존 Recursion에서 Backtracking의 조건이 추가되었다.
일련의 과정을 거쳐 수행되는데, 중요한 점은 recursion의 callstack과 그에 따른 backtracking시 절차를 잘 이해하는것 이다.
체스 퀸을 두어 정해진 공간내에서 서로간에 간섭하지 못하도록 하는 경우의 수를 구한다.
로직을 간단히 정리해보자면
bool solveQueen(int n, int board[][20], int i)
{
//base case
if(i==n)
{
printBoard(n,board);
return true;
}
// rec case
// try to place a queen in every row
for (int j = 0; j < n; j++)
{
// whether the current i,j is safe or not
if(CanPlace(board,n,i,j))
{
board[i][j] = 1;
//성공할시 다음 행 체크, 실패시 다음 열 체크 및 백트래킹 수행
bool success = solveQueen(n,board,i+1);
if(success)
return true;
// bactracking
board[i][j] = 0;
}
}
return false;
}
base case와 recursion case를 다룰 함수이다. 인덱스 i는 행, j를 열로 하여 각 인덱스마다 판별과정을 거쳐 퀸을 배치한다.
체스 퀸을 배치하려면 대각선 좌,우 그리고 위 아래를 판별하여야 한다. 아래는 배치할 곳의 인덱스를 기준으로 판단하는 로직이다. 행의 경우 배치된 곳 이후는 채워지지 않은 (not filled) 영역이므로 생략한다.
bool CanPlace(int board[][20], int n, int x, int y)
{
//column
for (int k = 0; k < x; k++)
{
if(board[k][y] == 1)
return false;
}
//Left Diag
int i = x;
int j = y;
while (i>=0 and j>=0)
{
if (board[i][j] == 1)
return false;
i--;
j--;
}
//Right Diag
i = x;
j = y;
while (i>=0 and j<n)
{
if (board[i][j] == 1)
return false;
i--;
j++;
}
return true;
}
이번 문제에서는 재귀호출을 통해 부모에게 메세지를 전달하는것이 포인트라 생각한다.
bool success = solveQueen(n,board,i+1);
해당 로직을 수행하며 Parent로부터 Child까지 모두 true를 반환하면 queen의 배치가 완성되는 것이고, 이중 하나라도 false를 반환하면 backtrack을 수행하며 부모에게 전달하는 방식이다.
마치 심사위원들의 만장일치를 받아야 통과가 가능한것 처럼.. 말이다.
정해진 목적지까지 이동하는 방식을 재귀로 구현하는 문제이다. 왼쪽 가장자리 위를 기준으로 목적지까지 오른쪽 또는 아래로만 움직일 수 있다.
아래 코드를 살펴보자.
#include<iostream>
using namespace std;
int gridWays(int i, int j,int m,int n)
{
//base case
//if destination arrived
if(i == m-1 and j ==n-1)
{
return 1;
}
//if point is out of grid
if(i >= m or j >=n)
{
return 0;
}
// f(x,y) = f(x+1,y) + f(x,y+1);
// 나누어진 sub problem을 기준으로
int ans = gridWays(i+1,j,m,n) + gridWays(i,j+1,m,n);
return ans;
}
int main()
{
int m,n;
cin >>m>>n;
cout << gridWays(0,0,m,n);
}
가장 핵심 부분은 재귀이다.
int ans = gridWays(i+1,j,m,n) + gridWays(i,j+1,m,n);
gridWays를 반복 호출하며 1또는 0을 반환한다. 0을 반환하면 재귀 스택에 따라 gridWays(i,j+1,m,n) 를 수행하고 base case에서 해당 조건이 맞는지 다시 확인한다.
이러한 과정을 거쳐 반환 값을 모두 더하면 경우의 수를 출력할 수 있다.
if(i >= m or j >=n)
{
return 0;
}
마찬가지로 grid의 위치를 벗어날 경우 0을 반환하는 즉, bounds를 지정해주어야 한다.
스도쿠 해결 문제이다. 해당 문제는 이전에 풀이한 queens와 닮았다 순서로 확인해보자.
해당 조건을 만족하도록 조건식을 작성하여야 하는데 크게 3가지 case로 나눌 수 있다.
세로에 중복된 숫자를 금지한다.
가로에 중복된 숫자를 금지한다.
subgrid 간에 중복된 숫자를 금지한다.
해당 recursion case에 맞도록 base case 또한 고려하여야 한다. base case는 누적된 bool 값을 바탕으로 column의 값이 그리드와 동일해지면 true를 반환하고 call stack을 순차적으로 부르게 한다.
//배치 가능여부 판단
bool CanPlaced(int board[9][9],int i,int j, int number,int n)
{
for (int k = 0; k < 9; k++)
{
if(board[k][j] ==number || board[i][k] == number)
return false;
}
int sx = (i/3) *3;
int sy = (j/3) *3;
for (int x = sx; x < sx + 3; x++)
{
for (int y = sy; y < sy + 3; y++)
{
if(board[x][y] == number)
return false;
}
}
return true;
}
// Sudoku 출력
void PrintSudoku(int board[9][9])
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
cout << board[i][j] << " ";
if((j+1) % 3 == 0)
{
cout << "| ";
}
}
if((i+1) % 3 == 0 && i >0)
{
cout << endl <<" _____________________ " << endl;
}
cout << endl;
}
}
// Sudoku 함수 재귀를 통한 해결 및 BackTracking 수행
bool Sudoku(int board[9][9],int i,int j,int n)
{
if(i == n-1 && j == n-1)
{
cout << "done" << endl;
PrintSudoku(board);
return true;
}
if(j == n)
{
return Sudoku(board,i+1,0,n);
}
if(board[i][j] != 0)
{
return Sudoku(board,i,j+1,n);
}
for (int k = 1; k <= n; k++)
{
if(CanPlaced(board,i,j,k,n))
{
board[i][j] = k;
bool success = Sudoku(board,i,j+1,n);
if(success)
{
return true;
}
}
}
//backtracking
board[i][j] = 0;
return false;
cout << "there is no answer sudoku" << endl;
}
위와 같이 총 3개의 기능으로 나눠 구현할 수 있다.
tags: Algorithm, Data Structure