좀 시간이  지났지만 심심해서 써본다.

 

1차 코딩 테스트는 5솔로 가볍게 통과했다.

6, 7번 문제는 내 기억이 맞다면 점 쿼리와 게임 이론 문제였는데.. 여기는 내가 모르는 유형이라 풀지 못했다.

6번 문제는 사실 그냥 구현만 하면 정확성은 통과할 수 있는 것으로 알고 있는데, 나는 정확성만 맞아도 0.5솔로 친다는 사실을 몰라서 그냥 문제 푸는 것을 포기했었다.


2차 코테는.. api를 사용해야하는 처음 해보는 유형의 코테였다.

api를 이용하기 때문에, 많은 사람들이 파이썬을 사용하는 것을 추천하였고... 나도 일단 파이썬으로 테스트에 응했다.

하지만.. 음 모르겠다 만약 돌아간다면 C++로 2차 코테에 도전할 것 같다.

 

확실히 파이썬은 나에게 익숙치 않은 언어이기 때문에 머리에 떠오르는 아이디어가 바로바로 구현이 안되어서 시간을 많이 소비했다..

그래도 어찌저찌 구현했는데... 리더보드가 프리징 되기 전 내 등수가 700등대였던 것으로 기억한다.

물론 남은 시간동안 더 최적화해서 점수를 올리긴 했지만, 잘 쳐줘야 600등대였을 것이라고 생각한다.

300등 안쪽이 합격선으로 예측되는 상황이었고 그래서.. 맘 비우고 있었는데..

이게 왜 합격이.. ?

2차 코딩 테스트 팁은.. 꼭 전년도와 전전년도 2차 코테 문제 풀어보는 것 밖에 없다. API 통신에서 애먹는 시간을 최대한 줄여야 한다.

그리고 kakao tech 블로그에 들어가면 해설이 있으니까 어떤 방식으로 접근해서 풀어야 하는지 꼭 한번 읽어보길 바란다.


아직 학생이었던 나는.. 뭔가 개발 쪽 보다 인프라 계열에 관심이 많아서 1지망으로 인프라를 선택했다.

1차 면접이 기술 면접이라 OS, DB, 네트워크 쪽을 다시 공부했다.

말이 공부지.. 사실 그냥 전공 강의 때 진행했던 PPT들 쭉 한번 돌려봤다.

자료구조와 알고리즘은 자신 있는 분야라 따로 공부하지 않고, Shell sort같은 좀 특이한 정렬 방법들만 검색해봤다.

 

그리고 면접날......... 개털렸다 ㅋㅋㅋ전공 공부를 꽤 열심히 한 편이라, 나름 좀 자신있게 임했는데 쉽지 않다

인프라를 지원해서 그런지 OS, DB, 네트워크 등 모든 분야의 질문이 들어왔고.. 기본적인 질문은 다 어찌저찌 대답했는데 꼬리 질문들을 만족할만큼 대답하지 못했다. 

전공 공부를 꽤 열심히 한 편이라 가벼운 마음으로 임했는데, 카카오의 벽은 높았다.

 

혹시 인프라를 지원하셨다면, 깊이 있게 공부하고 면접에 임하는 것을 추천한다 !

이 문제에 나온 코드 그대로 적어서 내면 시간초과 나온다.

왜 그런지 알아보자.

 

fibonacci(10)을 호출했다고 가정해보자.

fibonacci(10) -> 1 * fibonacci(9) + 1 * fibonacci(8)

fibonacci(10) -> fibonacci(8) + fibonacci(7) + fibonacci(7) + fibonacci(6)

                              = 1 * fibonacci(8) + 2 * fibonacci(7) + 1 * fibonacci(6)

fibonacci(10) -> fibonacci(7) + fibonacci(6) + 2 * (fibonacci(6) + fibonacci(5)) + fibonacci(5) + fibonacci(4)

                             = 1 * fibonacci(7) + 3 * fibonacci(6) +  3 * fibonacci(5) + 1 * fibonacci(4)

 

대략 이런 식으로 계속 호출이 일어난다. 

혹시, 붉은색 숫자들을 보고 떠오르는 것이 없나 ?

나는 이게 떠올랐다.

파스칼 삼각형

결국, fibonacci(10)을 호출하면 재귀 호출에 의해 함수들의 총 호출 횟수가 \((1 + 9 + 36 + 84 + ... + 36 + 9 + 1)\) 이 될 것이다.

일반화해보자.

\(n > 0\)일 때, fibonacci(n)을 호출하면 총 함수 호출 횟수는 \(_{n - 1}C_0 + _{n - 1}C_1 + ... + _{n - 1}C_{n - 2} + _{n - 1}C_{n - 1}\)이 된다.

또한 \(_{n - 1}C_0 + _{n - 1}C_1 + ... + _{n - 1}C_{n - 2} + _{n - 1}C_{n - 1} = 2^{n - 1}\) 임을 우린 알고 있다.

따라서, 문제에서 제공하는 소스의 시간복잡도는 \(O(2^{n})\)이다.

ㅋㅋ 지수승의 시간복잡도는.. 알고리즘 문제에서 절대 피해야하는 친구이다.


시간복잡도를 개선해보기 전에, 사실 이 문제.. 설명이 좀 틀린 부분이 있다. 

호출 순서에 대한 것인데, 

  • fibonacci(3)은 fibonacci(2)와 fibonacci(1) (첫 번째 호출)을 호출한다.
  • fibonacci(2)는 fibonacci(1) (두 번째 호출)과 fibonacci(0)을 호출한다.

이게 아니라

  • fibonacci(3)은 fibonacci(2)을 호출한다.
  • fibonacci(2)는 fibonacci(1) (첫번째 호출)을 호출한다.
  • fibonacci(1) 이 1 출력 후, 리턴한다.
  • fibonacci(2)가 fibonacci(0)을 호출한다.
  • fibonacci(0)이 0 출력 후, 리턴한다.
  • fibonacci(2)가 1을 출력 후, 리턴한다.
  • fibonacci(3)이 fibonacci(1) (두번째 호출)을 호출한다.
  • fibonacci(1) 이 1 출력 후, 리턴한다.
  • fibonacci(2)가 fibonacci(0)을 호출한다.

이렇게 된다.

왜 WHY? 함수 하나가 동시에 두 개의 재귀함수를 호출할 수 없다. (멀티 쓰레드 프로그램에서는 가능.. 하지만 여기선 논외)

따라서, fibonacci(3)은 fibonacci(2)를 먼저 호출해서 모든 것을 다 처리하고 나서야 fibonacci(1)을 호출할 수 있다.

실제로 직접 코딩해서 출력해보면,

fibonacci(3)이 fibonacci(1)을 호출하는 게 가장 느린 것을 확인할 수 있다.

 

자, 그래서 뭐가 문제냐 ? 

fibonacci(n)을 호출하면, fibonacci(n - 1)을 먼저 호출해서 모든 것을 처리한 이후에 fibonacci(n - 2)를 호출한다.

생각해보자. fibonacci(n - 1)을 처리하면서 이미 fibonacci(n - 2)가 처리됐을 것이다.

근데 한번 더 finbonacci(n - 2)를 처리하는 건 너무 비효율적이라는 생각이 든다.

 

이 때 필요한게, Memorization이다.

먼저, 1 출력 횟수를 구해보자.

\(n == 0\)일 때, 0

\(n == 1\)일 때, 1

\(n == 2\)일 때, 1

\(n == 3\)일 때, 2

...

이 값들을 미리 어딘가에 저장해놓자.

예를 들어 fibonacci(5)의 값은 fibonacci(5) = fibonacci(4) + fibonacci(3)이 되고,

fibonacci(4)를 구하기 위해 fibonacci(3), fibonacci(2), fibonacci(1), fibonacci(0)을 계산할 것이다.

미리 계산해놓은 것이 있다면, fibonacci(3)을 한방에 구하여 바로 fibonacci(5)의 값을 구할 수 있다.

 

백문이 불여일견, 피보나치 수를 구하는 코드를 Memorization을 사용하여 구현한 코드를 한번 보자.

#include <bits/stdc++.h>
using namespace std;

int fibo[41];

int fibonacci(int n) {

	if (fibo[n] != -1) return fibo[n];

	if (n == 0) return fibo[0] = 0;
	else if (n == 1) return fibo[1] = 1;
	else return fibo[n] = fibonacci(n - 1) + fibonacci(n - 2);
	
}
int main() {
	for (int i = 0; i < 41; ++i) fibo[i] = -1;
	fibonacci(40);
    
	return 0;
}

 요로코롬 코딩하면 fibo의 모든 원소들이 채워질 것이다 !

혹시.. DP의 첫번째 글에서 DP에 Top-Down방식과 Bottom-Up방식이 있다라고 얘기했던 것을 기억하는가 ?

설명이 길었지만 이게 재귀를 이용한 Top-Down 방식이다. 

한마디로, 40(꼭대기)에서 시작해서 밑으로 내려가기 때문에 이렇게 이름을 붙인 것 같다.

 

그렇다면 반대로 Bottom-Up은 ? 당연히 0부터 시작해서 위로 올라가는 것이다.

운이 좋게도 우리는 피보나치의 수의 점화식을 알고 있다.

n번째 수는 n - 1번째 수와 n - 2번째 수의 합이라는 아주 아름다운 점화식 말이다.

이걸 그대로 for문을 사용해 구현하여 40번째 피보나치 수까지 구해보자.

#include <bits/stdc++.h>
using namespace std;

int fibo[41];

int main() {
	fibo[0] = 0;
	fibo[1] = 1;
	for(int i = 2; i < 41; ++i) fibo[i] = fibo[i - 1] + fibo[i - 2];
    
	return 0;
}

fibo행렬 채우기 끝!


Memorization이 뭔지 알았으니, 0 출력 횟수와 1 출력 횟수를 담을 배열이 두개 필요하다 ? 사실 아니다.

0 출력 횟수는 0부터 시작했을 때 나오는 피보나치 수이고,

1 출력 횟수는 1부터 시작했을 때 나오는 피보나치 수이다.

0부터 시작했을 때 : 0, 1, 1, 2, 3, 5, 8, 13, ...

1부터 시작했을 때 : 1, 1, 2, 3, 5, 8, 13, 21, ...

 

따라서, fibonacci(n)의

0 출력 횟수 : 0부터 시작했을 때 n번째 피보나치 수

1 출력 횟수 : 0부터 시작했을 때 (n + 1)번째 피보나치 수

가 된다.

아, 한가지 예외 케이스가 있는데 \(n == 0\)일 때는 따로 0 1을 출력하게 처리해주어야한다.

 

정답코드

#include <bits/stdc++.h>
using namespace std;

int fibo[41];

int fibonacci(int n) {
	if (fibo[n] != -1) return fibo[n];

	if (n == 0) return fibo[0] = 0;
	else if (n == 1) return fibo[1] = 1;
	else return fibo[n] = fibonacci(n - 1) + fibonacci(n - 2);
	
}
int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
    
	for (int i = 0; i < 41; ++i) fibo[i] = -1;
	
	int cnt;
	cin >> cnt;
	while (cnt--) {
		int num;
		cin >> num;
		if (num == 0) cout << 1 << ' ' << 0 << '\n';
		else cout << fibonacci(num - 1) << ' ' << fibonacci(num) << '\n';
	}
    
	return 0;
}

 

내가 가장 못하는 DP가 나왔다.

 

동적 계획법의 의의는

  1. 점화식을 통해 최적화된 풀이 가능
  2. Memorization을 사용하여, 답들을 미리 계산

정도로 볼 수 있다.

 

개인적으로 점화식을.. 정말 못 세우는 편이라 난이도 있는 DP문제를 많이 버거워한다.

 

DP문제 풀이의 감을 잡으면 DP만큼 쉬운 문제가 없다고들 하는데, 나는 감이 아직도 안온다;;

 

앞으로 단순 점화식만 소개하는 것이 아니라, 어떻게 그 점화식을 떠올리게 되었는지도 적을 예정이니 재미삼아 읽어보는 것도 좋을 것 같다.

 

DP를 구현하는 방법은 크게 두 가지가 있다. 재귀를 이용한 Top-Down 방식과 for문을 이용한 Bottom-Up방식이 있다.자세한 내용은 동적 계획법1의 첫번째 문제인 피보나치 문제에서 설명해보겠다.

 

아 ! 한가지 Tip을 드리자면, 가끔 문제를 풀 때 \(1,000,000,009\)같이 매우 큰 소수로 나눈 나머지를 답으로 내라는 문제들이 있다.이럴 땐, 꼭!!!! DP나 Memorization쪽을 고려해보자. 왜냐하면, 저렇게 큰 소수로 나눈 나머지가 답으로 나오는 문제들은 적어도 저 소수보다 많은 경우가 나온다는 말인데... 그 경우를 하나하나 세면 무조건 시간초과일 것이다.

 

두 팀으로 나누기만 하면 되는 문제다.

스타트 팀이 될 사람만 체크하고, 스타트 팀과 링크 팀이 완성되면 두 팀의 전력차를 계산하자.

 

여기서, 한가지 재미있는 점은 4명이 있다고 가정했을 때,

  1. 스타트 팀 : (0 ,1), 링크 팀 : (2, 3)
  2. 스타트 팀 : (2, 3), 링크 팀 : (0, 1)

이 두 가지 경우는 사실상 같은 경우이다.

왜냐하면, (0, 1)이 스타트 팀인지 링크 팀인지 상관없다. 그냥 (0, 1)이 같은 팀이기만 하면 되기 때문이다.

이 사실을 이용하면 시간 복잡도를 줄일 수 있다.

 

1. naive하게 구현

#include <bits/stdc++.h>
using namespace std;

const int INF = 2147483647;

int n;
bool isStartTeam[21]; // 어떤 사람이 스타트 팀이지 체크
int sTeam[21], lTeam[21]; // 각 팀의 팀원 목록
int power[21][21]; // 능력치 저장
int ans = INF; // 답

// cur은 몇번부터 뽑을 차례인지, cnt은 몇명을 뽑았는지
void recur(int cur, int cnt) {

	if (cnt == n / 2) {
		int curStart = 0, curLine = 0;
		// 스타트팀과 링크팀을 나눔.
		for (int i = 0; i < n; ++i) {
			if (isStartTeam[i]) sTeam[curStart++] = i;
			else lTeam[curLine++] = i;
		}
		// 전력 계산
		int sPower = 0, lPower = 0;
		for (int i = 0; i < n / 2; ++i) for (int j = 0; j < n / 2; ++j) {
			sPower += power[sTeam[i]][sTeam[j]];
			lPower += power[lTeam[i]][lTeam[j]];
		}
		
		// 답 갱신
		ans = min(ans, abs(sPower - lPower));
		
		return;
	}

	for (int i = cur; i < n; ++i) {
		isStartTeam[i] = true;
		
		recur(i + 1, cnt + 1);

		isStartTeam[i] = false;
	}
}


int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	cin >> n;
	for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) cin >> power[i][j];

	recur(0, 0);

	cout << ans;
	return 0;
}

 

2. 같은 경우를 제거

#include <bits/stdc++.h>
using namespace std;
const int INF = 2147483647;

int n;
bool isStartTeam[21];
int sTeam[21], lTeam[21];
int power[21][21];
int ans = INF;

void recur(int cur, int cnt) {

	if (cnt == n / 2) {
		int curStart = 0, curLine = 0;

		for (int i = 0; i < n; ++i) {
			if (isStartTeam[i]) sTeam[curStart++] = i;
			else lTeam[curLine++] = i;
		}

		int sPower = 0, lPower = 0;
		for (int i = 0; i < n / 2; ++i) for (int j = 0; j < n / 2; ++j) {
			sPower += power[sTeam[i]][sTeam[j]];
			lPower += power[lTeam[i]][lTeam[j]];
		}

		ans = min(ans, abs(sPower - lPower));

		return;
	}
	
	// for문의 범위가 바뀜
	for (int i = cur; i < n / 2 + cnt; ++i) {
		isStartTeam[i] = true;

		recur(i + 1, cnt + 1);

		isStartTeam[i] = false;
	}
}


int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	cin >> n;
	for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) cin >> power[i][j];

	recur(0, 0);

	cout << ans;
	return 0;
}

확실히 빨라졌다.

'BOJ_단계별로 풀어보기(9단계~) > [13단계] 백트래킹' 카테고리의 다른 글

[백준 14888] 연산자 끼워넣기  (0) 2022.03.16
[백준 2580] 스도쿠  (0) 2022.03.16
[백준 9663] N-Queen  (0) 2022.03.11
[백준 15651] N과 M (4)  (0) 2022.03.11
[백준 15651] N과 M (3)  (0) 2022.03.11

브루트포스 문제이다.

백트래킹을 사용하여 모든 경우를 탐색해주자. 

연산자 개수가 10개 밖에 안돼서 next_permutation으로 해결하는 방법도 있다.

1. 백트래킹

#include <bits/stdc++.h>
using namespace std;

int n;
int num[12];
int cal[4];
int maxANS = -1'000'000'001; // 답의 범위는 -10억 ~ 10억
int minANS = 1'000'000'001;

void recur(int sum, int cur) {
	// 한번의 탐색이 끝나면, 최댓값 최솟값 갱신
	if (cur == n) {
		maxANS = max(maxANS, sum);
		minANS = min(minANS, sum);
		return;
	}

	for (int i = 0; i < 4; ++i) {
		// i번 연산자가 남아있지 않은 경우 다음 연산자로 넘어감
		if (cal[i] == 0) continue;
        
		// i번 연산자를 1개 사용
		--cal[i];
		if (i == 0) recur(sum + num[cur], cur + 1);
		else if (i == 1) recur(sum - num[cur], cur + 1);
		else if (i == 2) recur(sum * num[cur], cur + 1);
		else if (i == 3) recur(sum / num[cur], cur + 1);
        
		// 사용이 끝났으니 사용한 연산자 반납
		++cal[i];
	}
}

int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	cin >> n;
	for (int i = 0; i < n; ++i) cin >> num[i];
	for (int i = 0; i < 4; ++i) cin >> cal[i];
	
	recur(num[0], 1);
	cout << maxANS << '\n' << minANS;
	return 0;
}

2. next_permutation()

#include <bits/stdc++.h>
using namespace std;

int num[11];
int maxANS = -1'000'000'001;
int minANS = 1'000'000'001;
vector<int> calc;

int main()
{
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	int n, numOfCases;
	cin >> n;

	for (int i = 0; i < n; i++)
		cin >> num[i];
        
	// 연산자 나열
	for (int i = 0; i < 4; i++)
	{
		cin >> numOfCases;
		while (numOfCases--) calc.push_back(i);
	}

	// 나올 수 있는 경우의 수 계산 == 팩토리얼 연산
	numOfCases = 1;
	for (int i = 1; i <= n - 1; i++) numOfCases *= i; 
	
	while(numOfCases--) {
		int tmp = num[0];
		for (int j = 1; j < n; j++)
		{
			switch (calc[j - 1])
			{
			case 0:
				tmp += num[j];
				break;
			case 1:
				tmp -= num[j];
				break;
			case 2:
				tmp *= num[j];
				break;
			case 3:
				tmp /= num[j];
				break;
			}
		}
		minANS = min(minANS, tmp);
		maxANS = max(maxANS, tmp);
		next_permutation(calc.begin(), calc.end()); // 다음 경우
	}

	cout << maxANS << '\n' << minANS;

	return 0;
}

가능하다고 했지 빠르다곤 안했다.. ㅎㅎ

백트래킹은 중간 연산 결과를 이용할 수 있어서 속도가 빠르지만,

이 풀이는 매 경우마다 계산을 다 해주어야해서 당연히 속도가 느리다.

'BOJ_단계별로 풀어보기(9단계~) > [13단계] 백트래킹' 카테고리의 다른 글

[백준 14889] 스타트와 링크  (0) 2022.03.16
[백준 2580] 스도쿠  (0) 2022.03.16
[백준 9663] N-Queen  (0) 2022.03.11
[백준 15651] N과 M (4)  (0) 2022.03.11
[백준 15651] N과 M (3)  (0) 2022.03.11

백트래킹으로 빈칸마다 들어갈 수 있는 수를 찾아 넣어주자.

그러다보면 빈칸임에도 불구하고 넣을 수 있는 숫자가 없는 경우가 있다.

이 경우엔 앞에서 잘못된 숫자가 들어간 것이기 때문에, 다시 뒤로 돌아가자.

 

\((x, y)\)번째 칸에 넣을 수를 찾는 방법은 1부터 9까지 돌면서

  1. \(x\)번째 행에 어떤 수 \(n\)이 존재하는지
  2. \(y\)번째 열에 어떤 수 \(n\)이 존재하는지
  3. \(((x / 3), (y / 3))\)번 정사각형 안에 어떤 수 n이 존재하는지 (하단 그림 참조)

확인해주면 된다.

 

1. bool 배열로 구현

#include <bits/stdc++.h>
using namespace std;

int sudoku[9][9];
bool row[9][10]; // 행
bool col[9][10]; // 열
bool block[3][3][10]; // 3x3 정사각형
vector<pair<int, int>> blank; // 빈칸들의 좌표

void recur(int cur) {
	// 모든 칸이 채워졌다면 출력 후, exit(0)을 통해 바로 프로그램 종료
	if (blank.size() == cur) {
		for (int i = 0; i < 9; ++i) {
			for (int j = 0; j < 9; ++j) cout << sudoku[i][j] << ' ';
			cout << '\n';
		}
		exit(0);
	}
    
	int x = blank[cur].first;
	int y = blank[cur].second;

	for (int i = 1; i < 10; ++i) {
		// i라는 숫자가 현재 칸이 속한 행, 열, 3x3 정사각형 안에 존재하는 지 확인
		// 한 곳이라도 존재하면 다음 숫자로 넘어감
		if (row[x][i] || col[y][i] || block[x / 3][y / 3][i]) continue;

		// (x, y)에 i라는 숫자를 채워넣음
		row[x][i] = true;
		col[y][i] = true;
		block[x / 3][y / 3][i] = true;
		sudoku[x][y] = i;
        
		recur(cur + 1); // 다음 칸으로 넘어가기
		
		// 호출한 재귀함수가 리턴됐다는 것은, 현재 빈칸에 i라는 숫자가 들어가면 안된다는 의미
		// 다른 숫자를 채워넣기 위해, (x, y)에서 i라는 숫자를 제거
		row[x][i] = false;
		col[y][i] = false;
		block[x / 3][y / 3][i] = false;
		sudoku[x][y] = 0;
	}
}

int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	for (int i = 0; i < 9; ++i) for (int j = 0; j < 9; ++j) {
		cin >> sudoku[i][j];
		// 입력받은 수를 행, 열, 3x3 정사각형에 추가
		row[i][sudoku[i][j]] = true;
		col[j][sudoku[i][j]] = true;
		block[i / 3][j / 3][sudoku[i][j]] = true;

		if (sudoku[i][j] == 0) blank.push_back({ i, j });
	}

	recur(0);

	return 0;
}

 

2. 비트 연산으로 구현

#include <bits/stdc++.h>
using namespace std;

int sudoku[9][9];
int row[9];
int col[9];
int block[3][3];
vector<pair<int, int>> blank;

void recur(int cur) {

	if (blank.size() == cur) {
		for (int i = 0; i < 9; ++i) {
			for (int j = 0; j < 9; ++j) cout << sudoku[i][j] << ' ';
			cout << '\n';
		}
		exit(0);
	}
    
	int x = blank[cur].first;
	int y = blank[cur].second;

	for (int i = 1; i < 10; ++i) {
		int bit = (1 << i);

		if ((row[x] & bit) || (col[y] & bit) || (block[x / 3][y / 3] & bit)) continue;

		row[x] |= bit;
		col[y] |= bit;
		block[x / 3][y / 3] |= bit;
		sudoku[x][y] = i;
		recur(cur + 1);
		
		row[x] &= (~bit);
		col[y] &= (~bit);
		block[x / 3][y / 3] &= (~bit);
		sudoku[x][y] = 0;
	}
}

int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);

	for (int i = 0; i < 9; ++i) for (int j = 0; j < 9; ++j) {
		cin >> sudoku[i][j];
		row[i] |= (1 << sudoku[i][j]);
		col[j] |= (1 << sudoku[i][j]);
		block[i / 3][j / 3] |= (1 << sudoku[i][j]);

		if (sudoku[i][j] == 0) blank.push_back({ i, j });
	}

	recur(0);

	return 0;
}

bit연산으로 구현하면 빨라질까 해서 구현해봤지만.. 아무래도 메모리접근 한번보다는 비트연산의 비용이 더 비싼 것 같다.

너무나도 유명한 문제이다.

세상에 수많은 풀이 방법이 공개되어있으니 한번 검색해보는 것을 추천한다.

 

이 문제는 널널한 시간을 줬음에도 불구하고, 아무생각 없이 모든 경우를 탐색하면 시간초과이다.

여기서 내가 말하는 모든 경우 탐색이란, Queen을 같은 칸이 아닌 곳에 일단 박아놓고 Queen끼리 서로 공격할 수 있는지 확인하는 무지성 방법이다.

대충 시간복잡도 계산해보면,

  • 퀸을 놓는 경우 = \(O( _{225}P_{15})\)
  • 서로 공격할 수 있는지 확인하는 데 걸리는 시간 = \(O(15^2)\)

매 경우마다 공격할 수 있는지 확인해야하니 둘을 곱하면.... 대충 생각해봐도 어질어질한 수치이다.

 

그래서 이 문제는 처음부터 서로 공격하지 못하게 놓는게 포인트이다.

 

그렇게 하기 위해 Queen은 8방향 모두 움직일 수 있다는 특성을 이용하자.

Queen이 8방향을 다 갈 수있다 == 어떤 Queen 하나를 놓았을 때, 이 Queen이 속하는 행, 열, 대각선 2개 위에 더 이상 다른 Queen이 올라오면 안된다는 것이다.

 

그럼 어떻게 코딩할 것인가 ?

재귀함수는 x번 행에서 y번 열에 Queen을 놓을 수 있는지 확인한다. y번 열에 놓을 수 있는지 판단하기 위해서, (x, y)의 값을 통해

  • x번 행 위에 Queen이 존재하는지
  • y번 열 위에 Queen이 존재하는지
  • (x, y)를 포함하고 왼쪽 위에서 오른쪽 아래로 떨어지는 대각선 위에 Queen이 존재하는지
  • (x, y)를 포함하고 오른쪽 위에서 왼쪽 아래로 떨어지는 대각선 위에 Queen이 존재하는지

를 보면 된다.

만약 놓을 수 있다면, (x, y)가 속한 모든 선을 true로 바꿔주고 다른 행으로 넘어가면 된다.

 

대각선을 판단하는게 좀 어려울 수도 있는데, 이것만 알아두면 된다.

  • 왼쪽 위에서 오른쪽 아래로 떨어지는 "같은" 대각선에 속한 칸들의 (행 - 열) 값은 모두 동일하다.
  • 오른쪽 위에서 왼쪽 아래로 떨어지는 "같은" 대각선에 속한 칸들의 (행 + 열) 값은 모두 동일하다.

코드를 보자.

 

#include <bits/stdc++.h>
using namespace std;

bool row[16]; // 행
bool col[16]; // 열
bool crossFromRight[31]; // 대각선 오른쪽 위에서 왼쪽 아래
bool crossFromLeft[31]; // 대각선 왼쪽 위에서 오른쪽 아래
int n;

void setFlag(int& line, int& i, bool flag) {
	row[line] = col[i] = crossFromRight[line + i] = crossFromLeft[i - line + 15] = flag;
}

void recur(int line, int& cnt) {
	// n개의 라인에 Queen을 모두 놓은 경우
	if (line == n) {
		cnt++;
		return;
	}

	for (int i = 0; i < n; ++i) {
		if (!row[line] && !col[i] && !crossFromRight[line + i] && !crossFromLeft[i - line + 15]) {
			setFlag(line, i, true);
			recur(line + 1, cnt);
			setFlag(line, i, false);
		}
	}
}


int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	int cnt = 0;
	cin >> n;

	recur(0, cnt);

	cout << cnt;

	return 0;
}

내 코드는.. 좀 많이 느린 편이다. 구글링을 통해 더 빠른 코드들을 구경해보는 것도 좋을 것 같다.

0ms 걸리는 코드들은 그냥 https://oeis.org/A000170 이걸보고 바로 답을 출력한 것 같다.

 

비내림차순.. 별 거 없다. 

이전 재귀함수에서 뽑힌 숫자부터 for문을 돌려주면 된다.

#include <bits/stdc++.h>
using namespace std;

int N, M;
string ans;

void recur(int prev, int cnt) {

	if (cnt == M) {
		ans.back() = '\n';
		cout << ans;
		return;
	}

	for (int cur = prev; cur <= N; ++cur) {
		ans += cur + '0';
		ans += ' ';
		recur(cur, cnt + 1);
		ans.pop_back();
		ans.pop_back();
	}
}

int main() {
	ios::ios_base::sync_with_stdio(false);
	cin.tie(nullptr);
	cout.tie(nullptr);
	cin >> N >> M;

	recur(1, 0);

	return 0;
}

'BOJ_단계별로 풀어보기(9단계~) > [13단계] 백트래킹' 카테고리의 다른 글

[백준 2580] 스도쿠  (0) 2022.03.16
[백준 9663] N-Queen  (0) 2022.03.11
[백준 15651] N과 M (3)  (0) 2022.03.11
[백준 15650] N과 M (2)  (0) 2022.03.11
[백준 15649] N과 M (1)  (0) 2022.03.11

+ Recent posts