eff3ct's st0rage Dev Blog

월반멘토링 4주차 set 풀이

방학이 끝나간..드아.. 어째 풀이가 불친절해지는거 같으면 기분탓이 아니에요 더위 먹어서 힘들어..

DP(Interval DP) / LIS / LCS

1. 증가하는 부분 수열의 개수 (Silver II)

tag : lis(\(\scriptsize O(N^{2})\))

\(\scriptsize i\)번째 원소로 끝나는 LIS의 개수를 세는 문제다.

\(\small dp(i) := "i\)번째 원소로 끝나는 LIS 개수”

라고 정의하면 된다. 그러면, 당연하게도 다음과 같은 점화식을 얻는다.

\[\small dp(i) = \sum_{j < i \wedge arr(j) < arr(i)}dp(j)\]

이 점화식은 \(\scriptsize O(N^{2})\)에 계산할 수 있으므로, 시간내에 결과를 다 구할 수 있다. 따라서, DP 테이블을 채우고 출력하면 AC.

코드 보기
#include <bits/stdc++.h>

using namespace std;
using ll = long long;
using pii = pair<int, int>;
using ppii = pair<int, pii>;

ll A[5050];
ll dp[5050];
const ll MOD = 998244353LL;

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

    int N; cin >> N;
    for(int i = 1; i <= N; ++i) cin >> A[i];

    for(int i = 1; i <= N; ++i) {
        dp[i] = 1;
        for(int j = 1; j < i; ++j) {
            if(A[j] < A[i]) {
                dp[i] += dp[j];
                dp[i] %= MOD;
            }
        }
    }

    for(int i = 1; i <= N; ++i) cout << dp[i] % MOD << ' ';

    return 0;
}

2. 전깃줄 (Gold V)

tag : lis

교차하지 않으려면 \(\scriptsize A\)전봇대의 낮은 번호에서부터 볼 때, \(\scriptsize B\)에 연결되어 있는 번호들이 증가하는 모습을 보여야 한다. 연결된 번호가 감소한다면 교차한다는 의미이다.

문제에서 구하는 것은 교차하지 않도록 하기 위해 없애야 하는 최소 전선의 개수이다. 반대로 생각해보자. 가장 긴 증가하는 부분 수열의 길이가 바로 교차하지 않으며 존재할 수 있는 최대 전선의 개수이다. 즉 LIS의 길이를 찾고 (전체 전선 개수 - LIS 길이)을 해주면 정답이다.

이때, \(\scriptsize N\)의 크기가 100으로 작기 때문에, \(\scriptsize O(N^{2})\)짜리 시간복잡도로 LIS 길이를 구해도 된다. 전봇대 \(\scriptsize A\)에 연결된 \(\scriptsize B\) 위치를 차례대로 넣어두고, LIS를 찾으면 된다.

코드 보기
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>

using namespace std;

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

    int N; cin >> N;
    vector<int> dp(N, 1);

    vector<pair<int, int>> seq(N);
    for(int i = 0; i < N; ++i) cin >> seq[i].first >> seq[i].second;
    sort(seq.begin(), seq.end());

    for(int i = 0; i < N; ++i) {
        for(int j = 0; j < i; ++j) {
            if(seq[i].second > seq[j].second) {
                dp[i] = max(dp[i], 1 + dp[j]);
            }
        }
    }

    int lis = *max_element(dp.begin(), dp.end());

    cout << N - lis << '\n';

    return 0;
}

3. 최장 최장 증가 부분 수열 (Gold V)

tag : lis

문제를 잘 읽어보면, 2차원에서 LIS를 구하는 문제라 생각할 수 있다. 이때, 최단경로로 이동하므로 오른쪽과 아래쪽으로만 움직일 수 있다.

1차원에서 LIS를 구할 때 정의했던 식과 비슷하게 dp식을 정의해보자.

\(\small dp(i, j) := "(i, j)\)까지 만들 수 있는 최장 증가 부분 수열의 길이”

\[\small dp(i, j) = \max_{arr(r, c) < arr(i, j)}dp(r, c) + 1\]

단, \(\small r \leq i \wedge c \leq j \wedge (r, c) \neq (i, j)\)

따라서, DP 테이블은 \(\scriptsize O(N^{4})\)에 구할 수 있다. \(\scriptsize N = 100\)이라서, 충분히 구할 수 있다.

여담으로, 원래 출제자는 이걸 이차원 세그먼트 트리로 최적화 하는.. 문제로 내려 했었답니다. 그걸로 풀면 \(\scriptsize O(N^{2}log^{2}N)\)에 풀림.

코드 보기
#include <bits/stdc++.h>

using namespace std;
using ll = long long;
using pii = pair<int, int>;
using ppii = pair<int, pii>;

int dp[101][101];
int m[101][101];
int N; 

int solve(int i, int j) {
    int& ret = dp[i][j];

    if(ret != -1) return ret;

    ret = 1;
    for(int r = 1; r <= i; ++r) {
        for(int c = 1; c <= j; ++c) {
            if(r == i && c == j) continue;
            if(m[r][c] < m[i][j]) ret = max(ret, solve(r, c) + 1);
        }
    }

    return ret;
}

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

    cin >> N;
    for(int i = 1; i <= N; ++i) {
        fill(dp[i], dp[i] + 101, -1);
        for(int j = 1; j <= N; ++j) {
            cin >> m[i][j];
        }
    }

    int ans = 0;
    for(int i = 1; i <= N; ++i) {
        for(int j = 1; j <= N; ++j) {
            ans = max(ans, solve(i, j));
        }
    }

    cout << ans << '\n';

    return 0;
}

4. LCS2 (Gold IV)

tag : LCS

LCS를 구하고, LCS 중 하나를 출력하면 된다. 간단히 말하면 LCS를 역추적하는 문제다.

void traceLCS(string& str, int a, int b) {
    vector<char> ret;
    while(a >= 0 && b >= 0) {
        if(dp[a][b] == dp[a - 1][b]) a--;
        else if(dp[a][b] == dp[a][b - 1]) b--;
        else {
            ret.push_back(str[a]);
            a--; b--;
        }
    }
}

dp[a][b] == dp[a - 1][b]이면, 왼쪽을 감소시키면 된다. 반대로 dp[a][b] == dp[a][b - 1]이면, 오른쪽을 감소시키자. 저 두 경우가 아니면 dp값에 변화가 있어야 했던 부분이니까 둘 다 감소시켜주자. 이런식으로 LCS의 실제값을 구해낼 수 있다. 이해가 안된다면, 직접 dp값을 써보면서 이해하는걸 추천함.

코드 보기
// https://www.acmicpc.net/problem/9251 //
#include <iostream>
#include <string>
#include <vector>

using namespace std;

int dp[1001][1001];

int LCS(string& str1, string& str2, int idx1, int idx2) {
    if(idx1 < 0 || idx2 < 0) return 0;

    int& res = dp[idx1][idx2];
    if(res != -1) return res;

    if(str1[idx1] == str2[idx2]) return res = LCS(str1, str2, idx1 - 1, idx2 - 1) + 1;
    else return res = max(LCS(str1, str2, idx1 - 1, idx2), LCS(str1, str2, idx1, idx2 - 1));
}

void traceLCS(string& str, int a, int b) {
    vector<char> ret;
    while(a >= 0 && b >= 0) {
        if(dp[a][b] == dp[a - 1][b]) a--;
        else if(dp[a][b] == dp[a][b - 1]) b--;
        else {
            ret.push_back(str[a]);
            a--; b--;
        }
    }
    for(int i = ret.size() - 1; i >= 0; --i) cout << ret[i];
}

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

    string str1, str2;
    cin >> str1 >> str2;

    for(int i = 0; i < 1001; ++i) {
        fill_n(dp[i], 1001, -1);
    }

    cout << LCS(str1, str2, str1.size() - 1, str2.size() -1) << '\n';
    traceLCS(str1, str1.size() - 1, str2.size() - 1);

    return 0;
}

5. 파일 합치기 (Gold III)

tag : dp(interval dp), prefix-sum

구간을 dp 정의식에 포함해야하는 dp이다. 이거 처음 배울 때 엄청 어려웠던 기억이.. 다음과 같은 dp 정의식을 떠올려보자.

\(\small dp(i, j) := "[i, j]\)번째 파일을 합치는 데 드는 최소 비용”

라고 하면, 점화식을 하나 생각할 수 있다.

\[\small dp(i, j) = \min_{i \leq k < j} dp(i, k) + dp(k + 1, j) + \sum_{a = i}^{j} cost(a)\]

즉, 중간 지점에서 나누어서 합치는 걸로 점화식을 세울 수 있다. \(\scriptsize [i, j]\) 구간의 파일을 합치는 비용의 최솟값은 합쳐진 \(\scriptsize [i, k]\) 파일과 합쳐진 \(\scriptsize [k + 1, j]\) 파일을 합치는 비용의 최소와 같다.

이때, 두 파일을 합치는데 드는 비용은 고정적이다. 따라서, 해당 파트는 prefix-sum 으로 처리할 수 있다. prefix-sum은 간단히 설명하자면, 구간합을 \(\scriptsize O(1)\)에 구할 수 있도록 하는 기법이고 앞에서부터 누적합을 저장한 뒤 적절히 빼는 방식으로 사용할 수 있다. 자세한 설명은 다른 분들이 자세하게 해놨으니 따로 찾아서 공부하는걸 추천드립니다.

하여튼, 저 점화식에 의하면 테이블을 채우는데, \(\scriptsize O(N^{3})\)이니 잘 계산해주면 맞출 수 있다.

(진짜 TMI로 Knuth Optimization(크누스 최적화)라는 것도 적용할 수 있는데.. 이걸 할 줄 알면 이 문제가 \(\scriptsize O(N^{2})\)에 풀리고, 그 문제는 심지어 플래티넘2짜리 문제다. 관심있으면 찾아보세요.)

코드 보기
#include <iostream>
#include <vector>
#define INF 987654321

using namespace std;

void solve(vector<int>& seq, int K) {
    vector<vector<int>> dp(K, vector<int>(K, 0));
    vector<int> sum(K + 1, 0);

    for(int i = 1; i <= K; ++i) sum[i] = sum[i - 1] + seq[i - 1];

    for(int length = 1; length < K; ++length) {
        for(int start = 0; start <= K - length - 1; ++start) {
            int end = start + length;
            dp[start][end] = INF;
            for(int pivot = start; pivot + 1 <= end; ++pivot) {
                int cost = dp[start][pivot] + dp[pivot + 1][end] + sum[end + 1] - sum[start];
                dp[start][end] = min(cost, dp[start][end]);
            }
        }
    }

    cout << dp[0][K - 1] << '\n';
}

int main() {
    int T; cin >> T;

    while(T --> 0) {
        int K; cin >> K;
        vector<int> seq(K);
        for(int i = 0; i < K; ++i) cin >> seq[i];

        solve(seq, K);
    }

    return 0;
}

6. LCS3 (Gold III)

tag : LCS

아마 스트링 2개짜리 LCS 점화식을 혼자서 구할 수 있으면 진짜 쉽게 풀 수 있는 문제다. 그냥 단지 스트링이 2개에서 3개로 늘어난거기 때문에 점화식도 그에 맞춰서 확장시키면 된다.

정확히 2개짜리 LCS와 유사하게 점화식을 세우고, str1[i] == str2[i] == str3[i]일때 범위를 모두 하나씩 줄여가며 LCS를 찾으면 된다. 위의 경우가 아닐때도 그냥 2개짜리 LCS와 비슷하게 해서 max값을 구하면 됨.

다만, 3개로 늘어났기에 시간복잡도는 \(\scriptsize O(N^{3})\)이 된다.

자세한 건 코드를 참고!

코드 보기
// https://www.acmicpc.net/problem/1958 //
#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

int dp[101][101][101];

int LCS(string& str1, string& str2, string& str3, int idx1, int idx2, int idx3) {
    if(idx1 < 0 || idx2 < 0 || idx3 < 0) return 0;

    int& res = dp[idx1][idx2][idx3];
    if(res != -1) return res;

    if(str1[idx1] == str2[idx2] && str2[idx2] == str3[idx3]) return res = LCS(str1, str2, str3, idx1 - 1, idx2 - 1, idx3 - 1) + 1;
    else return res = max({LCS(str1, str2, str3, idx1 - 1, idx2, idx3), LCS(str1, str2, str3, idx1, idx2 - 1, idx3), LCS(str1, str2, str3, idx1, idx2, idx3 - 1)});
}

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

    string str1, str2, str3;
    cin >> str1 >> str2 >> str3;

    for(int i = 0; i < 101; ++i) {
        for(int j = 0; j < 101; ++j) {
            fill_n(dp[i][j], 101, -1);
        }
    }

    cout << LCS(str1, str2, str3, str1.size() - 1, str2.size() -1, str3.size() - 1);

    return 0;
}

7. Split the GSHS (Gold III)

tag : dp(interval-dp)

풀고 나중에 업데이트 할게요.

edit : 2022-08-17 02:11 풀이 업데이트

파일합치기와 비슷한 냄새가 난다.

\(\small dp(i, j) := "[i, j]\)를 합칠 때 최소 친밀도 값”

이라고 정의하면, 쉽게 관계식을 찾을 수 있다.

\[\small dp(l, r) = \min_{l\leq k < r}{(dp(l, k) + dp(k + 1, r) -\mid xy\mid)}\]

(단, \(\small \mid xy\mid\)는 \(\small x, y\)의 부호가 서로 다를때만 있는 값이다. 부호가 같거나 0일때는 0의 값을 가짐.)

즉, 구간별로 합칠 때 최소값을 찾아서 전체 문제의 최소값을 찾을 수 있다. 이때, \(\scriptsize x, y\)는 prefix-sum으로 \(\scriptsize O(1)\)에 찾을 수 있으므로 총 시간복잡도 \(\scriptsize O(N^{3})\)에 해결 가능하다.

코드 보기
// 아직 없지롱 우헤헤

//*edit : 2022-08-17 02:11 풀이 업데이트*

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
using pii = pair<int, int>;
using ppii = pair<int, pii>;

const ll INF = 1e18;

ll A[81];
ll dp[81][81];
ll pf_sum[81];

int N; 

void init() {
    for(int i = 0; i < 81; ++i)
        fill(dp[i], dp[i] + 81, -INF);
    for(int i = 1; i <= N; ++i)
        pf_sum[i] = pf_sum[i - 1] + A[i];
}

ll get_min(int l, int r) {
    ll& ret = dp[l][r];

    if(ret != -INF) return ret;
    if(l == r) return ret = 0;
    
    ret = 0;
    for(int k = l; k < r; ++k) {
        ll x = pf_sum[k] - pf_sum[l - 1];
        ll y = pf_sum[r] - pf_sum[k];

        if(x < 0 && y > 0 || x > 0 && y < 0) 
            ret = min(ret, x * y + get_min(l, k) + get_min(k + 1, r));
        else
            ret = min(ret, get_min(l, k) + get_min(k + 1, r));
    }

    return ret;
}

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

    cin >> N;
    for(int i = 1; i <= N; ++i)
        cin >> A[i];

    init();
        
    cout << get_min(1, N) << '\n';

    return 0;
}

8. 최대 공통 증가 수열 (Gold I)

tag : dp(interval-dp)

풀고 나중에 업데이트 할게요.

//edit : 2022-08-20 15:50 풀이 업데이트

이왜골?

\(\scriptsize O(N^{4})\) 풀이는 아마 떠올릴 수 있을거라 생각합니다.

당연히, \(\scriptsize O(N^{4})\) 풀이는 Naive하게 짜서 그대로 제출하면 시간초과를 받는다.

\(\small dp(i, j) := "A_{i}, B_{j}\) 부터 시작하는 최대 공통 증가 수열 길이” (단, \(\small A_{i} = B_{j}\))

\[\small dp(i, j) := \max_{x > i \wedge y > j} (dp(x, y) + 1)\]

즉, \(\scriptsize O(N^{2})\)개의 부분문제를 계산하는데, \(\scriptsize O(N^{2})\)번 계산을 해야해서 \(\scriptsize O(N^{4})\)이다.

이대로 제출하면, 2%에서 시간초과를 받고 뻗는다.

음.. 그럼 \(\scriptsize N\)을 하나 떼던가 해야 AC를 받을 수 있을거란 생각을 하게 된다. 같은 학술부 hibye1217님은 정직한 \(\scriptsize O(N^{3})\)에 해결했다. 근데, 난 그 방법은 생각하지 못했고, \(\scriptsize O(N^{4})\) 풀이에서 최적화를 통해 문제를 해결했다.

즉, 이제부터 설명할 풀이는 최악의 시간복잡도가 \(\scriptsize O(N^{4})\)이 맞는데, 가지가 많이 쳐내져서 AC를 받을 수 있는..(?) 최적화 풀이이다. \(\scriptsize O(N^{3})\) 풀이가 궁금한 분들은 hibye1217님의 블로그를 참고합시다.

먼저, 모든 \(\scriptsize (x, y)\) 쌍에 대해 값을 가져올 필요가 없다. \(\scriptsize A\)를 기준으로 생각하면, 현재 값보다 큰 값들 중 하나를 골랐을 때, \(\scriptsize B\)에서는 가장 먼저 등장하는 값의 최대 공통 증가 수열 길이를 들고오면 된다.

같은 값이 여러개가 있으면, 가장 앞에서 시작해야 길이가 더 길 것이라 그렇다. 이 정도의 최적화를 해주면 \(\scriptsize O(N^{4})\)이지만, 시간 내에 놀랍게도 돌아간다.

이제 최대 공통 증가 수열을 트래킹해서 같이 출력해주면, 정답을 받을 수 있다. 트래킹하는 방법은 여러가지가 있겠지만 std::map을 사용하면, 어렵지 않게 할 수 있다. 현재 \(\scriptsize (x, y)\) 쌍의 다음 쌍을 저장해놓고 다음 쌍이 없을 떄까지 출력해주면 된다.

자세한건 코드를 참고하세요~

코드 보기
// 아직 없어 우히히

//*edit : 2022-08-20 15:50 풀이 업데이트*

#include <bits/stdc++.h>

using namespace std;
using ll = long long;
using pii = pair<int, int>;
using ppii = pair<int, pii>;

int dp[501][501];
int A[501];
int B[501];
map<pair<int, int>, pair<int, int>> next_value;

int N, M;

void init() {
    for(int i = 0; i < 501; ++i)
        fill(dp[i], dp[i] + 501, -1);
}

int solve(int x, int y) {
    int& ret = dp[x][y];

    if(ret != -1) return ret;

    ret = 1;

    next_value[{x, y}] = {-1, -1};
    for(int i = x + 1; i <= N; ++i) {
        if(A[x] >= A[i]) continue;

        for(int j = y + 1; j <= M; ++j) {
            if(A[i] == B[j]) {
                if(ret < solve(i, j) + 1) {
                    ret = solve(i, j) + 1;
                    next_value[{x, y}] = {i, j};
                }
                break;
            }
        }
    }

    return ret;
}

void tracking(int a, int b) {
    if(a <= 0 || b <= 0) return;
    cout << A[a] << ' ';
    tracking(next_value[{a, b}].first, next_value[{a, b}].second);
}

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

    cin >> N;
    for(int i = 1; i <= N; ++i) cin >> A[i];
    cin >> M;
    for(int i = 1; i <= M; ++i) cin >> B[i];

    init();

    int ans = 0;
    int a = -1, b = -1;
    for(int i = 1; i <= N; ++i) {
        for(int j = 1; j <= M; ++j) {
            if(A[i] == B[j]) {
                if(ans < solve(i, j)) {
                    ans = solve(i, j);
                    a = i;
                    b = j;
                }
                break;
            }
        }
    }

    cout << ans << '\n';
    tracking(a, b);

    return 0;
}

9. 팰린드롬 분할 (Gold I)

tag : dp

7, 8 해설과 함께 올라올 예정. 코드는 미리 첨부해놨음!

//edit : 2022-08-20 15:50 풀이 업데이트

갑자기 이런 dp식을 떠올려보자.

\(\small dp(n) := "[0, n]\) 구간에서 최소 팰린드롬 분할 개수”

\[\small dp(n) = \min_{0 \leq k \leq n \wedge \text{[k, n] is pldr}} dp(k - 1) + 1\]

즉, \(\scriptsize [0, n] = [0, k - 1] + [k, n]\) 으로 생각하면, 앞 구간과 뒷 구간으로 나눠 생각할 수 있는데, 뒷 구간이 팰린드롬인지 검사하고 앞에서 최소 개수를 얻어오는 것이다. 이때, \(\scriptsize k = 0\)이면 앞 구간 팰린드롬 분할 수가 0인걸로 생각하면 된다.

팰린드롬 전처리는 \(\scriptsize O(N^{2})\)에 할 수 있다. \(\scriptsize N = 2500\)이므로, 시간 내에 전처리 할 수 있다. (스트링 길이 생각안하고, 나이브하게 짜도 시간 내에 돌아감.)

이 점화식을 그대로 짜면 맞출 수 있다. 아마 dp식을 2차원으로만 생각하고 있었다면, 꽤 헤맬 수 있는 문제였다.

코드 보기
#include <iostream>
#include <vector>
#include <string>
#define MAX 2500

using namespace std;

int dp[MAX];
bool isPldr[MAX][MAX];
string str;

void isPalindrome(string& str, int a, int b) {
    for(int i = 0; i <= (b - a) / 2; ++i) {
        if(str[a + i] != str[b - i]) {
            isPldr[a][b] = false;
            return;
        }
    }
    isPldr[a][b] = true;
}

int solve(string& str, int n) {
    if(n < 0) return 0;

    int& ret = dp[n];
    if(ret != -1) return ret;

    ret = MAX + 1;
    for(int k = 0; k <= n; ++k) {
        if(isPldr[k][n]) ret = min(ret, 1 + solve(str, k - 1));
    }

    return ret;
}

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

    for(int i = 0; i < MAX; ++i) dp[i] = -1; //init

    cin >> str;

    for(int i = 0; i < (int)str.size(); ++i) {
        for(int j = i; j < (int)str.size(); ++j) {
            isPalindrome(str, i, j);
        }
    }
    
    cout << solve(str, (int)str.size() - 1);

    return 0;
}