아이디어
뒤에서부터 보면서 'K', 'C', 'O'를 찾으면서
각 인덱스에서 "OCK"를 만들 수 있는 경우의 수를 구한다
그리고 값이 'R'인 모든 인덱스에서,
그 인덱스에서
1. "OCK"를 만들 수 있는 경우의 수와
그 인덱스의
2. 앞에 있는 문자의 개수만큼 2의 거듭제곱을 한 값을 구한다.
그리고 1과 2를 곱해 나온 값을 모두 더하면 답이 나온다
쉽게 이해하려면 R인 인덱스 기준으로 보면 된다
1. R인 인덱스 뒤에서 "OCK"를 만들 수 있는 경우의 수 -> 그 R을 가지고 ROCK을 만들 수 있는 경우의 수
2. 앞에 있는 문자의 개수만큼 2의 거듭제곱을 한 값 -> R 앞에 있는 문자 모두를 넣거나 넣지 않거나 하는 경우의 수
1과 2를 곱하면 그 R을 기준으로 가능한 ROCK으로 끝나는 문자열의 경우의 수가 나오는 것이다
위의 아이디어를 구현하기 위해서 DP를 이용한다
채워나갈 배열은 다음과 같이 설정했다
DP[0][idx] -> 전체 문자열 S의 부분 문자열 S[idx~N)에 있는 "K"의 경우의 수
DP[1][idx] -> 전체 문자열 S의 부분 문자열 S[idx~N)에 있는 "CK"의 경우의 수
DP[2][idx] -> 전체 문자열 S의 부분 문자열 S[idx~N)에 있는 "OCK"의 경우의 수
우리가 관심있는 배열은 DP[2][idx]이다. DP[0][idx], DP[1][idx]는 얘를 만들기 위한 배열임.
DP[2][idx]를 완성한 뒤
S[idx] == 'R'인 모든 idx에 대하여 (2의 idx제곱) * (DP[2][idx])를 구해 모두 합하면 답이 된다.
물론 경우의 수가 아주 많기 때문에 모듈러 연산에 신경써주어야 한다.
코드
#include <iostream>
#define ll long long
#define MOD 1'000'000'007
using namespace std;
int N;
string S;
ll DP[3][1'000'000] = { 0, }; //DP[k][idx] -> S[idx..N) 중에서 KCOR와 k개 일치하는 부분열의 개수
ll squareTwo(int n) {
ll total = 1;
if (n < 30)
return (total << n);
if (n % 2)
return ((squareTwo(n / 2) * squareTwo(n / 2) * 2) % MOD);
else
return ((squareTwo(n / 2) * squareTwo(n / 2)) % MOD);
}
int main_26156() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> N;
cin >> S;
DP[2][N - 1] = 0;
DP[1][N - 1] = 0;
if (S[N - 1] == 'K')
DP[0][N - 1] = 1;
else
DP[0][N - 1] = 0;
for (int i = N - 2; i >= 0; i--) {
if (S[i] == 'K')
DP[0][i] = ((DP[0][i + 1] + 1) % MOD);
else
DP[0][i] = DP[0][i + 1];
if (S[i] == 'C')
DP[1][i] = ((DP[1][i + 1] + DP[0][i + 1]) % MOD);
else
DP[1][i] = DP[1][i + 1];
if (S[i] == 'O')
DP[2][i] = ((DP[2][i + 1] + DP[1][i + 1]) % MOD);
else
DP[2][i] = DP[2][i + 1];
}
ll total = 0;
for (int i = N - 1; i >= 0; i--) {
if (S[i] == 'R') {
total += ((squareTwo(i) * DP[2][i]) % MOD);
total %= MOD;
}
}
cout << total << '\n';
return 0;
}
DP[0]의 경우 그냥 K가 보일 때마다 1씩 더해주면 되고
DP[1]의 경우 C가 보일 때마다 그 이전 인덱스의 DP[0]의 값을 더해주면 되고
DP[2]도 C와 같이 O가 보일 때마다 그 이전 인덱스의 DP[1]의 값을 더해주면 된다
DP 배열의 모든 값을 구할 때 모듈러 연산을 미리미리 해줘야 한다.
R이 나올 때마다 2의 거듭제곱과 DP 배열의 값을 더해주는 부분에서는
2의 거듭제곱이 나오는 부분을 신경써줘야 한다
최대 2의 1,000,000 거듭제곱을 구해줘야 하는데, 당연히 이 값을 구하는 건 말이 안되고
모듈러의 성질을 이용한 분할정복 코드를 작성해줬다
좀 더 발전된 아이디어
앞서 이용했던 아이디어의 DP 배열을 확인해보면
원하는 문자가 나오지 않았을 때 그 이전 인덱스의 배열 값을 그대로 옮기는 비효율적인 부분을 확인할 수 있다
앞의 아이디어는 각 인덱스에 대해서 OCK의 개수에 2의 거듭제곱을 구하지만
이 아이디어는 OCK를 계속 합하면서 2를 제곱한다
배열 DP[4]가 있다
뒤에서부터 S를 확인하면서
1. DP[3] *= 2
2. 현재 S[i]가 K이면 DP[0]++
3. 현재 S[i]가 C이면 DP[1] += DP[0]
4. 현재 S[i]가 O이면 DP[2] += DP[1]
5. 현재 S[i]가 R이면 DP[3] += DP[2]
를 해주면
DP[3]이 답이된다
어떤 문자가 나오던 이전의 총합인 DP[3]에 2를 곱해주는 것에 주목하자
처음 DP[3]에 포함된 OCK의 경우의 수는 계속 2가 곱해질 것이고
나중에 들어간 OCK의 경우의 수는 몇 번 안 곱해진다
또한 DP 값을 업데이트 할 때마다 모듈러 연산 해주는 것을 잊지 말자
처음에 풀었던 방법은 DP 자체는 O(logN)이지만
2의 거듭제곱을 분할정복기법으로 구하기 때문에 O(NlogN)이다
이 방법은 2의 거듭제곱을 한 번에 구할 필요가 없이 한번에 2씩 곱하기만 하면 되기 때문에
DP만 보기 때문에 O(logN)이다.
'알고리즘' 카테고리의 다른 글
백준 1193 : 분수찾기 (0) | 2024.10.08 |
---|---|
당신의 기울기는 얼마입니까? : 시간복잡도와 공간복잡도 (5) | 2024.10.07 |
취업을 위한 코테 톺아보기 (1) | 2024.10.04 |
코딩 테스트를 여행하는 개발자를 위한 모든 준비법 (4) | 2024.10.01 |
백준 20955 (0) | 2023.03.14 |