Algorithm/코딩 테스트 준비 - 기초
[코딩 테스트 준비 - 기초] 4. 브루트 포스 - N과 M & NM과 K
반응형
1. 재귀함수 작성 방법
- 재귀함수는 i 번째 방법과 i+1 번째 방법의 패턴이 동일한 경우 사용한다.
- 다음으로 넘어갈 때 변화하는 것에 집중해 이를 함수의 인자로 만들거나 따로 관리해야 한다.
구현 Point
- 재귀함수 호출 전, 그 함수를 호출하기 위한 상태를 미리 준비해둬야 한다.
- 재귀함수 호출이 끝난 것은 해당 경우가 끝난 것으로, 원 상태로 돌려줄 필요가 있다.
- 재귀의 종료 조건을 설정하는 것이 중요하다.
2. N과 M
[백준 15649 N과 M(1)]
- index번 위치에 올 수 있는 수를 결정하는 함수를 재귀함수로 작성한다.
- 1번 위치에서 2번 위치로 넘어갈 때 변화하는 것은 위치와 앞에서 1번 위치에서 사용한 숫자 i의 사용 여부이다.
- 벡터를 사용하기 때문에 배열의 현재 위치를 따로 관리하지 않아도 되며, i의 사용 여부는 전역으로 관리한다.
#include <iostream>
#include <vector>
using namespace std;
vector<int> v;
bool visited[10];
void backtrack(int cur, int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=1; i<=n; i++){ //1부터 n까지 수 중 선택
if(!visited[i]){ //i가 앞에서 사용되지 않았다면
//재귀 호출 전 상태 준비
visited[i] = true;
v.push_back(i);
backtrack(cur + 1, n, m);
//재귀 호출 후 원 상태로 돌리기
visited[i] = false;
v.pop_back();
}
}
}
int main(){
int n, m;
cin>>n>>m;
backtrack(0, n, m);
return 0;
}
[백준 15650 N과 M(2)]
- 위 소스코드와 매우 유사하다. 1번 위치에서 2번 위치로 넘어갈 때 변화하는 것에 범위의 시작 값을 추가하면 된다.
- 범위의 시작을 관리하기 위해 cur을 사용했고 이는 현재 선택한 숫자를 의미한다. (사실 위 코드에서 cur인자는 필요하지 않다. 지금 코드와 비교하기 위해 넣어두었다.)
- 참고) 선택의 문제로도 풀 수 있다. -> O(2^n)
void backtrack(int cur, int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=cur; i<=n; i++){ //cur부터 n까지 수 중 선택
if(!visited[i]){
//재귀 호출 전 상태 준비
visited[i] = true;
v.push_back(i);
backtrack(i + 1, n, m); //다음 위치에서는 방금 선택한 i보다 큰 수 중에 선택 가능
//재귀 호출 후 원 상태로 돌리기
visited[i] = false;
v.pop_back();
}
}
}
//backtrack(1, n, m)
- 오름차순 출력의 특성 상 배열 visited없이 작성할 수도 있다. 앞에서 결정된 숫자보다 큰 범위에서 다음 숫자를 결정해야 하기 때문이다. 따라서 아래처럼 써도 된다.
void backtrack(int cur, int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=cur; i<=n; i++){ //cur부터 n까지 수 중 선택 가능
v.push_back(i);
backtrack(i + 1, n, m);
v.pop_back();
}
}
// backtrack(1, n, m);
[백준 15651 N과 M(3)]
- 각 자리수가 가질 수 있는 숫자는 모두 1~N의 값이다.
- 앞에서 선택한 수는 다음 위치에서 선택할 값에 영향을 미치지 않기 때문에 visited가 필요없다.
void backtrack(int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=1; i<=n; i++){ //1부터 n까지 수 중 선택 가능
v.push_back(i);
backtrack(n, m);
v.pop_back();
}
}
[백준 15652 N과 M(4)]
- 2번과 유사하다. 앞서 선택한 값이 다음 선택에 영향을 미치므로 visited는 필요없다.
- 재귀함수를 호출할 때 현재 선택한 값보다 크거나 같은 수의 범위에서 선택하도록 i를 넘긴다.
void backtrack(int cur, int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=cur; i<=n; i++){ //cur부터 n까지 수 중 선택 가능
v.push_back(i);
backtrack(i, n, m); //다음 위치에서는 방금 선택한 i보다 크거나 같은 수 중에 선택 가능
v.pop_back();
}
}
[백준 15652 N과 M(5)]
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> v;
vector<int> arr;
bool visited[10]; //arr의 idx를 기준으로 방문여부 판단
void backtrack(int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=0; i<n; i++){
if(!visited[i]){
visited[i] = true;
v.push_back(arr[i]);
backtrack(n, m);
visited[i] = false;
v.pop_back();
}
}
}
int main(){
int n, m;
cin>>n>>m;
int x;
for(int i=0; i<n; i++){
cin>>x;
arr.push_back(x);
}
sort(arr.begin(), arr.end());
backtrack(n, m);
return 0;
}
[백준 15652 N과 M(6)]
void backtrack(int idx, int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=idx; i<n; i++){
v.push_back(arr[i]);
backtrack(i+1, n, m);
v.pop_back();
}
}
//sort(arr.begin(), arr.end());
//backtrack(0, n, m);
[백준 15652 N과 M(7)]
void backtrack(int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=0; i<n; i++){
v.push_back(arr[i]);
backtrack(n, m);
v.pop_back();
}
}
//sort(arr.begin(), arr.end());
//backtrack(n, m);
[백준 15652 N과 M(8)]
void backtrack(int idx, int n, int m){
if((int)v.size() == m){ //재귀 종료 조건
for(int i=0; i<m; i++)
cout<<v[i]<<" ";
cout<<'\n';
return;
}
for(int i=idx; i<n; i++){
v.push_back(arr[i]);
backtrack(i, n, m);
v.pop_back();
}
}
//sort(arr.begin(), arr.end());
//backtrack(0, n, m);
3. NM과 K
[백준 18290 NM과 K(1)]
- cnt는 선택한 칸의 수, sum 선택한 값의 총합이다. 이를 재귀 함수의 인자로 사용한다.
- px와 py는 중복을 피하기 위한 추가적인 인자이다. 이전에 확인한 칸을 다음 재귀에서 선택하지 않도록 한다.
#include <iostream>
#include <vector>
using namespace std;
bool visited[11][11];
int arr[11][11];
int n, m, k;
int ans = -40000;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
void backtrack(int px, int py, int cnt, int sum){
if(cnt == k){ //재귀 종료 조건
ans = max(ans, sum);
return;
}
for(int x=px; x<n; x++){
for(int y=(x == px ? py : 0); y<m; y++){
if(!visited[x][y]){
bool flag = true;
for(int i=0; i<4; i++){ //인접 여부 판단
int nx = x + dx[i];
int ny = y + dy[i];
if(0 <= nx && nx < n && 0 <= ny && ny < m){ //범위 내에 존재 -> 선택 가능한 경우
if(visited[nx][ny]) flag = false; //이미 방문한 칸이면 선택 불가
}
}
if(flag){
visited[x][y] = true;
backtrack(x, y, cnt + 1, sum + arr[x][y]);
visited[x][y] = false;
}
}
}
}
}
int main(){
cin>>n>>m>>k;
for(int i=0; i<n; i++){
for(int j=0; j<m; j++)
cin>>arr[i][j];
}
backtrack(0, 0, 0, 0);
cout<<ans<<'\n';
return 0;
}
재귀 함수의 이중 for문에서 x=0, y=0으로 시작해도 되지만, 이 경우 재귀함수가 호출될 때마다 중복된 값을 검사하기 때문에 시간복잡도가 매우 커진다. 아래 그림은 x=0, y=0으로 했을 때의 시간과 위 코드의 시간 차이로 윗 부분이 위 코드의 결과이다.
반응형
'Algorithm > 코딩 테스트 준비 - 기초' 카테고리의 다른 글
[코딩 테스트 준비 - 기초] 6. 브루트 포스 - 순열 (0) | 2022.01.10 |
---|---|
[코딩 테스트 준비 - 기초] 5. 브루트 포스 - 재귀 & 백트래킹 (0) | 2022.01.05 |
[코딩 테스트 준비 - 기초] 3. 브루트 포스 - 그냥 다 해보기 (0) | 2021.12.23 |
[코딩 테스트 준비 - 기초] 2. 수학 (나머지 연산, 약수, 최대공약수와 최소공배수, 소수) (0) | 2021.11.29 |
[코딩 테스트 준비 - 기초] 1. 시간 복잡도와 언어별 유의사항 (0) | 2021.11.29 |