https://www.acmicpc.net/problem/1023
서두
내가 풀 문제를 고를때는 다른 분들이 정리하신 문제집에 수록된 문제들을 살펴서 괜찮아 보이는 것을 잡아서 해결한다.
그런데 이 문제는 약간 다르다. 내가 풀지 않은 문제 중 가장 작은 번호(1023)를 가지고 있었기에 이 문제에 관심을 가졌다.
분명 내가 가장 자신있는 알고리즘은 다이나믹 프로그래밍이다. 점화식을 잘 세우면 해결할 수 있기 때문이다.
그런데, 이 문제는 태그가 다이나믹 프로그래밍을 가졌지만, 해결방안이 전혀 떠올려지지 않았다.
결국 이 문제의 해결은 요원한채 끝날 뻔했다.
다른 문제에서 실마리를 얻다
어느날 문제집을 뒤지던 중 문제 하나를 발견했다.
https://www.acmicpc.net/problem/2248
이 문제를 보자마자, 이 문제의 해결책이 위의 문제를 해결할 열쇠인 것은 금방 알아챘다.
이것도 해결책을 전혀 모르겠지만, 다른 문제도 풀 수 있을 것이라 기대해 평소와 달리 바로 구글링을 거쳤다.
2248 문제는 직접으로 DP를 사용하지 않는다. 이항 계수를 구할 때 DP를 통해 계산해서, 이것을 활용하였다.
만약 처음에 0을 선택했을 때, 뒤에서 있을 수 있는 모든 경우의 수를 전부 더했을 때, 이것이 현재의 사전 순보다 크거나 같다면, 0을 고르고 아니라면 1을 고른다. 1을 고른 경우엔, 사전 순을 모든 경우의 수 만큼 빼면 된다.
예를 들어서, N = 6, L = 3이라 가정해서 0을 골랐을 경우, N = 5, L = 3일 때, L개 이하의 비트를 고르는 경우의 수를 전부 더한다. 이를 위한 이항 계수로
(5개 중 0개를 고를 때 + 5개 중 1개를 고를 때 + 5개 중 2개를 고를 때 + 5개 중 3개를 고를 때)
를 전부 더한다. 그렇다면, 현재 위치에서 0을 골랐을 때, 최대의 경우의 수를 구할 수 있다.
만약 현재의 사전 순이, 최대의 경우의 수보다 작다면, 해당 위치에 0을 충분히 고를 수 있다는 의미이고, 아
니라면 0을 선택한 것보다 사전순이 더 뒤에 있다. 따라서 1을 골라야한다는 의미이다.
그렇게 나온 코드는 다음과 같다.
#include <iostream>
using namespace std;
using ll = long long;
int N, L;
ll I, dp[33][33];
string ans = "";
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
cin >> N >> L >> I;
dp[0][0] = 1;
for (int i = 1; i < 33; i++)
{
dp[i][i] = 1;
dp[i][0] = 1;
}
for (int i = 1; i <= N; i++)
for (int j = 1; j <= i; j++)
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
while (N > 0)
{
if (L == 0)
{
while (N-- > 0) ans.push_back('0');
break;
}
ll s = 0;
for (int i = 0; i <= L; i++)
s += dp[N - 1][i];
if (s >= I)
{
ans.push_back('0');
}
else
{
ans.push_back('1');
--L;
I -= s;
}
--N;
}
cout << ans << '\n';
}
다시 본 문제로
이제 다시 문제로 돌아오자, 이 문제는 바로 전에 해결한 문제처럼 비트 수로 해결하는 문제가 아니라, 괄호가 짝지어지 않는 문자열을 사전순으로 나열했을 때, K번째에 오는 문자열을 구하는 문제다.
위 문제에서 얻은 것을 활용한다면, '('을 골랐을 때, (뒤의 모든 경우의 수 - 짝이 이어지는 경우의 수)를 고르면 된다.
(뒤의 모든 경우의 수)는 각 문자열이 '(' 혹은 ')'을 고르는 모든 경우의 수이기에, 2n - 1(n은 현재 길이) 로 바로 구할 수 있다. 그렇다면 (짝이 이어지는 경우의 수)는 어떻게 구할 수 있을까?
그렇다, 위의 문제에 이항계수를 DP를 통해 해결했듯 위 문제도 (짝이 이어지는 경우의 수)를 DP로 구할 수 있다.
DP[현재 남은 길이][짝지어지지 않은 괄호 수] = DP[현재 남은 길이 + 1][ 짝지어지지 않은 괄호 수 + 1]
+ if( 짝지어지지 않은 괄호 수 > 0) DP[ 현재 남은 길이 + 1 ][ 짝지어지지 않은 괄호 수 - 1];
여기서 현재 남은 길이가 N과 동일할 때, 짝지어지지 않은 괄호 수가 0일 때 1이고 아니면 0이다.
이런 방식으로 짝이 이어지는 경우의 수를 구할 수 있다.
ll solve2(int n, int a)
{
ll& ret = dp[n][a];
if (n == 0)
{
if (a == 0) return ret = 1;
return ret = 0;
}
if (ret >= 0) return ret;
ret = 0;
if (a > 0) ret += solve2(n - 1, a - 1);
ret += solve2(n - 1, a + 1);
return ret;
}
이제, 이것을 활용해서 정답을 구해보자.
void solve(int n, int a, ll r)
{
if (n == 0) return;
if (r == 0)
{
while (n--)
{
ans.push_back('(');
}
return;
}
ll s = pow(2, n - 1) - ((a + 1 > 0) ? dp[n - 1][a + 1] : 0);
if (s > r)
{
ans.push_back('(');
solve(n - 1, a + 1, r);
}
else
{
ans.push_back(')');
solve(n - 1, a - 1, r - s);
}
}
위에선 while문을 활용했지만, 이번엔 재귀를 사용해 반복을 하였다.
놀랍게도, 이 코드는 틀렸다. 분명 이론 상으로 맞을텐데 왜 틀렸는지 이해가 되지 않았다.
결국, 예제를 일일히 만들어서, 문제점을 찾으려 노력했다.
1 0에서 부터, 1 1, 1 2, 2 0, 2 1, 2 2, 2 3 ....
이런 식으로 예제를 만들며 반례를 찾았고, 겨우 하나 찾았다.
4 7
답: )(()
오답: )())
이렇게 된 원인은 )(()을 짝이 맞는 괄호로 계산되었기 때문이다.
따라서, 이전에 짝지어지지 않은 괄호가 한 번 음수가 되었다면, 그 뒤는 어떻게 짝지어져도, 그 괄호는 짝이 맞는 괄호가 될 수 없다는 것을 내가 괄시해버렸던거다.
그렇게, 재귀에 매개변수 하나(짝지어지지 않은 괄호가 음수가 된 적이 있는지 여부)를 추가해서 다시 코드를 구성했다.
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
using ll = long long;
ll N, K, dp[53][53];
string ans = "";
ll solve2(ll n, ll a)
{
ll& ret = dp[n][a];
if (n == 0)
{
if (a == 0) return ret = 1;
return ret = 0;
}
if (ret >= 0) return ret;
ret = 0;
if (a > 0) ret += solve2(n - 1, a - 1);
ret += solve2(n - 1, a + 1);
return ret;
}
void solve(ll n, ll a, ll r, bool f)
{
if (n == 0) return;
if (r == 0)
{
while (n--)
{
ans.push_back('(');
}
return;
}
if (a < 0) f = true;
ll s = pow(2, n - 1) - ((!f) ? dp[n - 1][a + 1] : 0);
if (s > r)
{
ans.push_back('(');
solve(n - 1, a + 1, r, f);
}
else
{
ans.push_back(')');
solve(n - 1, a - 1, r - s, f);
}
}
int main()
{
ios_base::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
cin >> N >> K;
memset(dp, -1, sizeof(dp));
solve2(N, 0);
solve(N, 0, K, false);
if (pow(2, N) - dp[N][0] < K + 1) cout << -1 << '\n';
else cout << ans << '\n';
}
문제를 많이 해결하는 것이 중요하다는 것을 느껴짐과 동시에, 스스로 해결해서 뿌듯한 문제였다.
'알고리즘 문제 풀이 일지' 카테고리의 다른 글
백준 19535: ㄷㄷㄷㅈ (0) | 2024.12.19 |
---|---|
백준 3487: Copying Books (0) | 2024.12.15 |
백준 9359: 서로소 (0) | 2024.12.07 |
백준 1044: 팀 선발 (0) | 2024.12.03 |
백준 2258: 정육점 (0) | 2024.11.29 |