깊이 우선 탐색.
DFS는 문제가 따로 없다.
다차원 배열 순회 문제는 DFS 보다 BFS로 풀어야 함.
나중에 그래프와 트리에선 DFS가 필요하다.
귀납적 사고방식이 돼야 한다..
"1번 도미노가 쓰러진다', 'k번 도미노가 쓰러지면 k+1번 도미노도 쓰러진다'가 참이니까 모든 도미노가 쓰러진다"
base case가 있어야 한다. 모든 입력은 base case로 수렴해야 한다.
재귀는 모두 반복문으로 바꿀 수 있다. 재귀는 반복문에 비해 코드가 간결해질 수는 있지만 함수 호출 비용 때문에 메모리와 시간에서 손해를 본다.
재귀 함수가 자기 자신을 부를 때 스택 영역에 함수 정보가 누적된다. 지역 변수도 스택 메모리에 할당된다. 스택 메모리가 제한적일 경우 재귀를 너무 깊게 들어가거나 지역 변수로 너무 큰 크기의 배열을 잡으면 메모리 초과가 된다.
이 문제는 단순히 A^B % C를 구하면 되는 것처럼 보이지만,
두 가지 이슈를 반드시 고려해야 한다.
오버플로우
A^B는 너무 커져서 long long으로도 못 담는다.
중간중간에 % C를 적용해줘야 한다. (모듈러 연산은 분배법칙이 적용된다)
시간복잡도
A를 B번 곱하면서 계산하면 시간복잡도는 O(B)
B는 최대 2,147,483,647
(약 2 * 10⁹)
약 20억번 연산.
시간제한은 0.5초. c++ 기준 일반적으로 1초에 약 1억번(10⁸) 연산.
시간제한 0.5초면 약 5천만 번 연산 가능.
이 문제는 지수를 절반씩 줄여가며 계산하는 분할 정복 방식으로 해결할 수 있다.
즉, 빠른 거듭제곱 (Fast Exponentiation) 을 사용해야 한다.
3^10 → 3^9 → 3^8 → ... → 3^1
처럼→ B가 커지면 시간초과 발생
3^10 → 3^5 → 3^2 → 3^1
처럼
지수를 반씩 줄이면서 재귀적으로 계산
계산 방식은 아래와 같다:
지수가 짝수라면,
→ 이전 결과를 제곱:
지수가 홀수라면,
→ 제곱한 결과에 밑(a)을 한 번 더 곱해줌:
곱셈 연산은 모듈러 연산의 분배법칙이 성립하기 때문에,
중간중간에 mod C
를 적용해도 결과는 유지된다.
즉, 계산할 때마다
(a * b) % C
를 해주면,
오버플로우를 방지하면서도 정답은 변하지 않는다.
O(log B)
로 줄어든다.예를 들어:
즉, 20억이라는 큰 수라도 31번만 계산하면 된다
자리 수로 log₂를 대충 추정할 수 있다:
공식:
예시:
→ 실제 log₂ 값 31과 거의 비슷!
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll func(ll a, ll b, ll c) {
if (b == 1) return a % c;
ll result = func(a, b / 2, c);
result = (result * result) % c;
if (b % 2 == 0) return result;
return (a % c) * result % c;
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
ll a, b, c;
cin >> a >> b >> c;
cout << func(a, b, c);
}
그냥 제곱을 한다면
ll func(ll a, ll b, ll c) {
if (b == 1) return a;
ll result = func(a, b / 2, c);
if (b % 2 == 0) return result;
return result;
}
근데 모듈러 연산을 적용하면
ll func(ll a, ll b, ll c) {
if (b == 1) return a % c;
ll result = func(a, b / 2, c);
result = (result * result) % c;
if (b % 2 == 0) return result;
return (a % c) * result % c;
}
또는
ll func(ll a, ll b, ll c) {
if (b == 1) return a % c;
ll result = func(a, b / 2, c);
result = (result * result) % c;
if (b % 2 == 0) return result;
return a * result % c;
}
적당히 값이 오버플로가 날 거 같은 부분에 %c로 제한 걸어주면 됨.
수학적으로는 곱셈 결과에만 % c를 적용해도 정답은 동일하지만,
중간 계산에서 값이 long long 범위를 초과할 수 있기 때문에,
보통은 곱셈하기 전에 a % c, b % c를 먼저 해주는 것이 안전하다.
그니까 (a % c) * result % c
나 a * result % c
나 계산 결과는 동일한데 a * result
가 overflow가 발생할 것 같다면 a % c * result
로 제한해주면 되는 거
이제 진짜 이해했다...
#include <bits/stdc++.h>
using namespace std;
void print_hanoi(int n, int from, int to) {
if (n == 1) {
cout << from << ' ' << to << '\n';
return;
}
// f(n-1) + 1 + f(n-1): n-1개를 빈 곳에 옮기고 + n번째를 to에 옮기고 + n-1을 빈 곳에서 to로 옮김
print_hanoi(n - 1, from, 6 - from - to);
cout << from << ' ' << to << '\n';
print_hanoi(n - 1, 6 - from - to, to);
}
int hanoi(int n) {
if (n == 1) return 1;
return 2 * hanoi(n - 1) + 1;
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
cout << hanoi(n) << '\n';
print_hanoi(n, 1, 3);
}
이동 횟수와 이동 과정을 둘다 재귀로 문제를 풀었는데, 사실 이동 횟수는 굳이 재귀로 계산하지 않아도 된다!
#include <bits/stdc++.h>
using namespace std;
void print_hanoi(int n, int from, int to) {
if (n == 1) {
cout << from << ' ' << to << '\n';
return;
}
// f(n-1) + 1 + f(n-1): n-1개를 빈 곳에 옮기고 + n번째를 to에 옮기고 + n-1을
// 빈 곳에서 to로 옮김
print_hanoi(n - 1, from, 6 - from - to);
cout << from << ' ' << to << '\n';
print_hanoi(n - 1, 6 - from - to, to);
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
cout << (1 << n) - 1 << '\n';
print_hanoi(n, 1, 3);
}
하노이 탑 이동 횟수는 다음 점화식을 따른다:
이 점화식은 아래와 같이 변형할 수 있다:
여기서 로 두면:
즉, 공비가 2인 등비수열
따라서 일반항은:
이제 원래 수열로 다시 바꿔주면:
하노이 탑에서 n개의 원판을 옮기는 최소 이동 횟수는
그래서 이동 횟수는 재귀로 구하지 않아도 됨!
이동 과정만 재귀로 잘 구성해주면 된다.
cout << (1 << n) - 1 << '\n';
이렇게 하면 바로 값을 계산할 수 있음
#include <bits/stdc++.h>
using namespace std;
int cnt, ans;
int N, r, c;
void func(int x, int y, int k) {
if (k == 1) {
if (x == r && y == c) ans = cnt;
++cnt;
return;
}
func(x, y, k / 2);
func(x, y + k / 2, k / 2);
func(x + k / 2, y, k / 2);
func(x + k / 2, y + k / 2, k / 2);
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> N >> r >> c;
func(0, 0, 1LL << N);
cout << ans;
}
처음엔 각 사분면을 모두 들어가서 계산을 했다. 시간초과.
최악의 경우 2^15 * 2^15 연산. 2^30
2^10(1024)이 약 10^3이니까 (2^10)^3 ≈ (10^3)^3 = 10^9. 약 10억번.
시간제한 0.5초 c++기준 1초당 약 1억번 연산. 5천만번이 최대.
그니까 시간 초과 당연.
#include <bits/stdc++.h>
using namespace std;
int N, r, c, ans;
void func(int x, int y, int k, int cnt) {
if (k == 1) {
if (x == r && y == c) ans = cnt;
return;
}
k = k / 2;
if (x + k <= r && y + k <= c) return func(x + k, y + k, k, cnt + k * k * 3);
if (x + k <= r && y <= c) return func(x + k, y, k, cnt + k * k * 2);
if (x <= r && y + k <= c) return func(x, y + k, k, cnt + k * k);
return func(x, y, k, cnt);
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> N >> r >> c;
func(0, 0, 1LL << N, 0);
cout << ans;
}
개선 버전에서는 사분면 4개를 모두 재귀적으로 호출하지 않고,
(r, c)가 포함된 단 하나의 사분면만 재귀 호출한다.
이로 인해 시간 복잡도는 다음과 같이 O(N)으로 줄어든다:
k는 처음에 2^N이고, 한 번 호출할 때마다 k = k / 2로 절반씩 줄어듦.
그러니 k^(n-1)로 줄어드는 것.
따라서 전체 재귀 호출 깊이는 log₂(2^N) = N
각 호출은 상수 시간 연산만 하므로 총 연산 횟수는 O(N)
즉 최악의 경우 15번
간단
#include <bits/stdc++.h>
using namespace std;
int board[2200][2200];
int cnt[3];
bool check(int x, int y, int n) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (board[x][y] != board[x + i][y + j]) return false;
}
}
cnt[board[x][y] + 1] += 1;
return true;
}
void func(int x, int y, int n) {
if (check(x, y, n)) return;
n /= 3;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) func(x + i * n, y + j * n, n);
}
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int N;
cin >> N;
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N; ++j) cin >> board[i][j];
}
func(0, 0, N);
for (int i = 0; i < 3; ++i) cout << cnt[i] << '\n';
}
int(pow(3, 7))
해보면 2187 나옴.
배열 넉넉하게 int board[2200][2200] 정도 잡고
2200 * 2200 * 4 / pow(10, 6)
해보면
19.36. 약 19.36MB. 메모리 충분.
위 문제와 유형 똑같. 간단.
#include <bits/stdc++.h>
using namespace std;
int board[130][130];
int cnt[2];
bool check(int x, int y, int n) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (board[x][y] != board[x + i][y + j]) return false;
}
}
return true;
}
void func(int x, int y, int n) {
if (check(x, y, n)) {
cnt[board[x][y]] += 1;
return;
}
n /= 2;
func(x, y, n);
func(x, y + n, n);
func(x + n, y, n);
func(x + n, y + n, n);
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int N;
cin >> N;
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j) cin >> board[i][j];
func(0, 0, N);
cout << cnt[0] << '\n' << cnt[1];
}
위 유형들이랑 똑같. 간단.
간단.
#include <bits/stdc++.h>
using namespace std;
char board[2200][2200];
void f(int r, int c, int n) {
if (n == 1) {
board[r][c] = '*';
return;
}
n /= 3;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
if (i == 1 && j == 1) continue;
f(r + n * i, c + n * j, n);
}
}
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
for (int i = 0; i < n; ++i) fill(board[i], board[i] + n, ' ');
f(0, 0, n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) cout << board[i][j];
cout << '\n';
}
}
공간 안 쓰는 방법도 있음
#include <bits/stdc++.h>
using namespace std;
void print(int r, int c, int n) {
if ((r / n) % 3 == 1 && (c / n) % 3 == 1) {
cout << ' ';
return;
}
if (n == 1) {
cout << '*';
return;
}
print(r, c, n / 3);
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int n;
cin >> n;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
print(i, j, n / 3);
}
cout << '\n';
}
}
좌표 하나하나마다 재귀로 해당 좌표가 공백인지 아닌지 판단하는 거임. 근데 이렇게 생각하는 것보다 첫 번째 버전이 훨씬 직관적이고 좋은 듯..
그래도 아이디어 이해해놓으면 좋을 것 같아서 끝까지 이해해봤다.
블록의 크기를 점점 줄여가면서(n / 3),
그때의 블록 크기 기준으로
내 좌표 (r, c)
가 가운데 블록에 해당하는지를
if ((r / n) % 3 == 1 && (c / n) % 3 == 1)
으로 판단한다.
블록 단위로 생각하기
- r / size : 현재 size 크기의 블록 기준으로 세로 몇 번째 블록인지
- c / size : 현재 size 크기의 블록 기준으로 가로 몇 번째 블록인지
- 3×3 블록 중 가운데인지 확인
(r / size) % 3 == 1 && (c / size) % 3 == 1
→ 현재 좌표가 3×3 블록 중 가운데 칸에 위치함
아... 머리 아파....
못 풀었다
#include <bits/stdc++.h>
using namespace std;
char board[4000][8000];
void f(int r, int c, int n) {
if (n == 3) {
board[r][c] = '*';
board[r + 1][c - 1] = board[r + 1][c + 1] = '*';
for (int i = -2; i <= 2; ++i) board[r + 2][c + i] = '*';
return;
}
n /= 2;
f(r, c, n);
f(r + n, c - n, n);
f(r + n, c + n, n);
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int N;
cin >> N;
for (int i = 0; i < N; ++i) fill(board[i], board[i] + N * 2 - 1, ' ');
f(0, N - 1, N);
for (int i = 0; i < N; ++i) {
for (int j = 0; j < N * 2 - 1; ++j) cout << board[i][j];
cout << '\n';
}
}
챗지피티한테 아이디어 얻어서 다시 풀었다...
풀고 나니까 쉽네
*
* *
*****
이 모양이 기본이다!
그니까 n==3이 베이스 컨디션임.
그 전에는 가운데 삼각형 꼭짓점을 기준으로
(r, c)
, (r + n/2, c - n/2)
, (r + n/2, c + n/2)
로 타고타고 들어가면 됨!