재귀에는 어느정도 익숙해진듯 하다.
재귀를 풀었을 때 특유의 쾌감도 있는듯..
하지만 갈 길은 아직 멀다....
정리 시작!
-
이전에 풀었던 문제를 재귀를 사용해서 풀어보자.
* 1, 2, 3 더하기 (low) : 정수 n을 1,2,3의 합으로 나타내는 방법의 수 구하기
9095번: 1, 2, 3 더하기
문제 정수 4를 1, 2, 3의 합으로 나타내는 방법은 총 7가지가 있다. 합을 나타낼 때는 수를 1개 이상 사용해야 한다. 1+1+1+1 1+1+2 1+2+1 2+1+1 2+2 1+3 3+1 정수 n이 주어졌을 때, n을 1, 2, 3의 합으로 나타내는
www.acmicpc.net
#include <iostream>
using namespace std;
int ans = 0;
void go(int left, int n) {
if (left == 0) {
ans += 1;
return;
}
if (left >= 1) go(left - 1, n);
if (left >= 2) go(left - 2, n);
if (left >= 3) go(left - 3, n);
return;
}
int main() {
int k;
cin >> k;
while (k--) {
int n;
cin >> n;
go(n, n);
cout << ans << '\n';
ans = 0;
}
return 0;
}
- 직관적으로 풀린다.
* 암호 만들기 (mid) : 조건을 만족하는 암호를 출력하면 된다. n과 m 문제처럼 생각하면 수월하게 풀린다.
1759번: 암호 만들기
첫째 줄에 두 정수 L, C가 주어진다. (3 ≤ L ≤ C ≤ 15) 다음 줄에는 C개의 문자들이 공백으로 구분되어 주어진다. 주어지는 문자들은 알파벳 소문자이며, 중복되는 것은 없다.
www.acmicpc.net
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
char p[15];
void go(int l, int c, int m, int j, int idx, int selected, string &s) {
if (selected == l && m >= 1 && j >= 2) {
cout << s << '\n';
return;
}
if (idx >= c) return;
s += p[idx];
if (p[idx] == 'a' || p[idx] == 'e' || p[idx] == 'i' ||
p[idx] == 'o' || p[idx] == 'u') {
go(l, c, m + 1, j, idx + 1, selected + 1, s);
}
else {
go(l, c, m, j + 1, idx + 1, selected + 1, s);
}
if(s.size()) s.pop_back();
go(l, c, m, j, idx + 1, selected, s);
}
int main() {
int l, c;
cin >> l >> c;
for (int i = 0; i < c; i++)
cin >> p[i];
sort(p, p + c);
string s;
go(l, c, 0, 0, 0, 0, s);
return 0;
}
- selected 된 수를 세는 n과 m 문제의 방법을 활용했다.
- 입력 받은 수를 꼭 정렬하고 재귀를 돌리자. 원치 않은 결과를 얻을 수도 있다...
- go의 인수 중 m은 모음, j는 자음이다. 길이 조건을 만족해도, 모음과 자음의 조건이 충족되지 않으면 출력되지 않는다.
- 더 깔끔하게 푼 코드는 백준에서 참조하자.. (코드는 너무 올리면 안 될 것 같다)
* 퇴사 (mid) : 퇴사 전까지 최대한 상담으로 많은 수익을 내는 문제다.
14501번: 퇴사
첫째 줄에 백준이가 얻을 수 있는 최대 이익을 출력한다.
www.acmicpc.net
#include <iostream>
#include <algorithm>
using namespace std;
int n, t[16], p[16], ans = 0;
void go(int day, int sum) {
if (day == n + 1) {
if (ans < sum) ans = sum;
return;
}
if (day > n + 1) return;
go(day + t[day], sum + p[day]);
go(day + 1, sum);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> t[i] >> p[i];
go(1, 0);
cout << ans << '\n';
return 0;
}
- 퇴사일 이전에 모든 상담이 끝나야 한다는 것에 주의해서 차근차근 하다보면 어렵지 않게 구할 수 있다.
* 백트래킹 : 의미 없는 경우는 제외하고 구하는 방법이다. 더이상 진행하는 게 의미 없는 조건을 찾는 게 관건이다.
문제 풀어보자.
* 스타트와 링크 (high) : 슬슬 어려워진다. 두 팀의 능력치 차이가 최소가 되도록 팀을 짜고, 능력치 차이의 최솟값을 구하는 문제다. 좀 많이 헤맸다.
14889번: 스타트와 링크
예제 2의 경우에 (1, 3, 6), (2, 4, 5)로 팀을 나누면 되고, 예제 3의 경우에는 (1, 2, 4, 5), (3, 6, 7, 8)로 팀을 나누면 된다.
www.acmicpc.net
- a팀으로 가거나 b팀으로 가거나 둘 중에 하나니까, 2^n 가지의 경우를 생각할 수 있다. 그렇게 하다보면 딱 절반으로 안 나눠지는 경우도 있으므로 예외를 잘 생각해야한다.
- 재귀는 리턴값을 잘 활용해야 더 어려운 문제도 잘 풀 수 있을 것 같다. 리턴값을 활용하는 풀이법에 익숙해지자.
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int n, s[20][20];
int go(int idx, vector<int> &a, vector<int> &b) {
if (idx == n) {
if (a.size() != n / 2) return -1;
if (b.size() != n / 2) return -1;
int t1 = 0, t2 = 0;
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < n / 2; j++) {
if (i == j) continue;
t1 += s[a[i]][a[j]];
t2 += s[b[i]][b[j]];
}
}
int diff = abs(t1 - t2);
return diff;
}
if (a.size() > n / 2) return -1;
if (b.size() > n / 2) return -1;
int ans = -1;
a.push_back(idx);
int t1 = go(idx + 1, a, b);
if (ans == -1 || t1 != -1 && ans > t1) {
ans = t1;
}
a.pop_back();
b.push_back(idx);
int t2 = go(idx + 1, a, b);
if (ans == -1 || t2 != -1 && ans > t2) {
ans = t2;
}
b.pop_back();
return ans;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cin >> s[i][j];
}
}
vector <int> a, b;
cout << go(0, a, b) << '\n';
return 0;
}
- 딱 절반으로 나눠야 하므로, 한 팀이 절반 이상을 가져가는 것도 예외에 포함된다. 이를 추가해주면 실행 속도를 높일 수 있다. 재귀는 정말 예외를 어떻게 설정하느냐에 따라 실행시간이 천차만별이다.
- 부등호 (high) : 풀릴듯 너무 안 풀렸다. 한번 풀었는데도 재귀로 풀려고 하니 좀 까다롭게 느껴졌다.
2529번: 부등호
두 종류의 부등호 기호 ‘<’와 ‘>’가 k개 나열된 순서열 A가 있다. 우리는 이 부등호 기호 앞뒤에 서로 다른 한 자릿수 숫자를 넣어서 모든 부등호 관계를 만족시키려고 한다. 예를 들어, 제��
www.acmicpc.net
- 부등호가 k개이면 수는 k+1개다. 이를 활용해서 부등호에 따라 수가 잘 놓였는지 확인 해야한다.
- 이 문제 또한 n과 m 문제를 활용해서 풀었다.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector <string> ans;
char boo[10];
bool check[10];
int n;
bool ok(string num) {
for (int i = 0; i < n; i++) {
if (boo[i] == '<') {
if (num[i] > num[i + 1]) return false;
}
else {
if (num[i] < num[i + 1]) return false;
}
}
return true;
}
void go(int index, string num) {
if (index == n + 1) {
if (ok(num)) {
ans.push_back(num);
}
return;
}
for (int i = 0; i <= 9; i++) {
if (check[i]) continue;
check[i] = true;
go(index + 1, num + to_string(i));
check[i] = false;
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
cin >> boo[i];
string num;
go(0, num);
string u = "", d = "";
for (int i = 0; i < ans.size(); i++) {
if (u == "" || u < ans[i]) u = ans[i];
if (d == "" || d > ans[i]) d = ans[i];
}
cout << u << '\n' << d << '\n';
return 0;
}
- ok 함수에서는 입력된 수가 부등호에 따라 잘 배열 되어 있는지 확인한다. 조건에 부합하면 벡터에 저장하고, 이중 최댓값과 최솟값을 구하면 된다.
- 여기서도 예외를 따로 설정해주면 속도가 굉장히 빨라진다. 다음 재귀로 넘어갈 때, 방금 사용한 부등호가 올바른지 바로 확인하고 틀렸다면 뒤에 부등호를 검사할 필요가 없으므로 바로 넘어가면 된다.
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
vector <string> ans;
char a[10];
bool check[10];
int n;
bool good(char num, char n, char a) {
if (a == '<') {
if (num > n) return false;
}
else {
if (num < n) return false;
}
return true;
}
void go(int index, string num) {
if (index == n + 1) {
ans.push_back(num);
return;
}
for (int i = 0; i <= 9; i++) {
if (check[i]) continue;
if (index == 0 || good(num[index - 1], i + '0', a[index - 1])) {
check[i] = true;
go(index + 1, num + to_string(i));
check[i] = false;
}
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
string num;
go(0, num);
string u = ans[0], d = ans[0];
for (int i = 1; i < ans.size(); i++) {
u = max(u, ans[i]);
d = min(d, ans[i]);
}
cout << u << '\n' << d << '\n';
return 0;
}
- 새로운 함수를 설정해서 방금 사용한 부등호가 올바르게 사용됐는지 확인하고, 틀렸다면 그 뒤의 경우는 생각하지 않고 바로 다음으로 넘어가면 된다.
* 맞춰봐 (hard) : 열심히 풀어서 답에 근접하긴 했지만 자꾸 시간초과가 떴다. 백트래킹을 사용하지 않으면 못 푸는 문제였다. 예외를 생각하는 연습이 더 필요할 것 같다. (내가 본 백준 문제 중에 가장 서론이 길었다)
1248번: 맞춰봐
문제 규현이는 멍청하다. 왜냐하면, 1~10까지 수 밖에 모르기 때문이다. 어느 날 규현이 옆을 지나가던 태석이가 규현이를 보고 이렇게 외쳤다. "빵빵!!" 규현이는 "아하!" 하면서 세상에는 빵이란
www.acmicpc.net
- (-)10부터 10까지를 생각하면 양수, 음수, 0 으로 나눌 수 있다. 여기서 모든 경우를 구하려면 양수는 1~10, 음수는 -10 ~ -1, 0은 0 하나를 생각할 수 있다. 양수와 음수는 굳이 부등호를 붙여가면서 따로 계산하지 말고 각각 1, -1을 저장해 1에서 10까지 반복문을 돌려 각각 곱해주면 된다. 그럼 굳이 char 형으로 배열을 안 잡고 int형으로 계산을 할 수 있다. 이 아이디어는 다른 문제에서도 써먹을 수 있으니까 꼭 기억해두자.
- 얘도 백트래킹을 사용하지 않으면 시간초과가 뜬다. 불필요한 경우를 제외하고 반복문을 돌도록 예외를 설정해줘야 한다. 각각의 수를 넣어줄 때마다, 조건을 만족하는지 확인하고 그렇지 않다면 그 뒤의 경우는 건너 뛰도록 해준다.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int n;
int s[10][10], ans[10];
bool check(int idx) {
int sum = 0;
for (int i = idx; i >= 0; i--) {
sum += ans[i];
if (s[i][idx] == 0) {
if (sum != 0) return false;
}
else if (s[i][idx] > 0) {
if (sum <= 0) return false;
}
else if (s[i][idx] < 0) {
if (sum >= 0) return false;
}
}
return true;
}
bool go(int idx) {
if (idx == n) {
return true;
}
if (s[idx][idx] == 0) {
ans[idx] = 0;
return check(idx) && go(idx + 1);
}
for (int i = 1; i <= 10; i++) {
ans[idx] = s[idx][idx] * i;
if (check(idx) && go(idx + 1)) return true;
}
return false;
}
int main()
{
cin >> n;
string sign;
cin >> sign;
int cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
if (sign[cnt] == '0')
s[i][j] = 0;
if (sign[cnt] == '+')
s[i][j] = 1;
if (sign[cnt] == '-')
s[i][j] = -1;
cnt += 1;
}
}
go(0);
for (int i = 0; i < n; i++)
cout << ans[i] << ' ';
return 0;
}
- 재귀도 많이 풀어보는 게 답인 것 같다. 이제 어느정도는 감이 오는데, 연습이 더 필요할듯 하다.
-
브루트 포스가 끝나간다. 이 챕터가 기초에 잘 어울리는 챕터가 아닌가 하는 생각이 든다.
진짜 말 그대로 다 해보는 거니까.. 근데 이제 머리를 좀 써서 안 해도 될 거는 안 하는 것까지 배웠다.
다음 단원에서 조금 더 업그레이드 해보자.
정리 끝!
'알고리즘 > 백준 알고리즘 강의' 카테고리의 다른 글
백준 알고리즘 기초 - 그래프1 (BFS, DFS) (0) | 2020.07.21 |
---|---|
백준 알고리즘 기초 - 브루트 포스 (비트마스크) (0) | 2020.07.21 |
백준 알고리즘 기초 - 브루트 포스 (순열) (0) | 2020.07.17 |
백준 알고리즘 기초 - 브루트 포스 (예제 + N과 M) (0) | 2020.07.16 |
백준 알고리즘 기초 - 브루트 포스 (0) | 2020.07.15 |