ckw1140's profile image

ckw1140

May 7, 2019 12:00

알고리즘 문제 풀이2

알고리즘 문제 풀이

4월에 푼 재미있는 문제들의 풀이를 작성해보았습니다.

문자열 장식

N개의 문자열이 주어지면 이 문자열들을 합쳐서 만들수 있는 사전순으로 가장 앞서는 문자열을 구하는 문제입니다. 단, 각 문자열 안에서의 상대적인 순서는 유지해야합니다.

우선 상대적인 순서를 유지해야 하니까 주어진 문자열들의 앞글자부터 하나씩 때어 온다고 생각합시다.

그러면, 매순간 각 문자열의 앞글자들 중에 사전순으로 가장 작은 알파벳이 있다면 그것을 때어 오는 것이 이득이라는 것을 쉽게 알 수 있습니다.

그런데 이러한 문자열이 여러개가 있으면 어떤 문자열에서 때어오는 것이 이득일까요?

이러한 문자열들의 뒷부분을 쭉 비교해나가면서 처음으로 다른 것이 나타나는 위치를 찾아봅시다. 이러한 위치에서 사전순으로 가장 작은 알파벳 $x$가 있다면 이 알파벳이 속한 문자열의 맨 앞을 때어 오는 것이 이득입니다.

왜냐하면, 다른 문자열에서 때어왔을 때 사전순으로 가장 작은 문자열을 만들었다고 가정하면, $x$가 속한 문자열의 맨 앞을 때어온 뒤, 이 문자열과 다른 문자열의 공통 부분을 같은 방식으로 소비하므로써 사전순으로 같거나 더 좋은 문자열을 항상 만들 수 있기 때문입니다.

이러한 $x$ 또한 여러개가 등장한다면, 어떻게 해야할까요? 마찬가지로 그 뒤의 문자열을 계속해서 비교해 나가면 됩니다. 이 과정을 반복하다보면 결국 남아 있는 문자열이 사전순으로 가장 작은 문자열의 맨 앞을 때어오는 것이 이득이라는 것과 같다는 것을 알 수 있습니다. 다만 위에서 $x$를 택한 것이 더 이득이라는 논리를 만족시키기 위해서는 $null$ 문자의 우선순위를 가장 크게 해주어야합니다. (빈칸이 알파벳보다 우선 순위가 높다는 뜻)

따라서 아래와 같은 코드로 해결할 수 있습니다. 시간 복잡도는 $O(N * MAXLEN^2)$ 입니다.

#include<bits/stdc++.h>
using namespace std;

int N;
string S[22];

int main() {
    std::ios::sync_with_stdio(false);

    cin >> N;

    int sum = 0;
    for(int i = 0; i < N; i++) {
        cin >> S[i];
        sum += S[i].size();
        S[i] += 'a';
    }

    for(int i = 0; i < sum; i++) {
        int a = min_element(S, S + N) - S;
        cout << S[a][0];
        S[a].erase(S[a].begin());
    }
}

P-수열

서로 다른수로 이루어진 수열의 모든 permutation 중 인접한 원소의 차이가 P로 나누어 떨어지지 않으면 P-수열이라고 할 때, P-수열의 개수를 묻는 문제입니다.

즉, P로 나눈 나머지가 같은 두 수가 인접해 있지 않은 permutation의 개수를 구하는 것입니다.

P로 나눈 나머지가 같은 두 수가 인접해 있다면, 그 둘 사이에 간선이 생긴다고 표현해보겠습니다.

그렇다면, 구하고자 하는 값은 ‘간선이 0개인 경우의 수’ 이고, 이는 포함과 배제의 법칙을 통해 아래와 같이 구할 수 있습니다.

(전체 경우의 수) - (특정 간선 1개가 있는 경우의 수) + (특정 간선 2개가 있는 경우의 수) - (특정 간선 3개가 있는 경우의 수) + …

따라서 우선 나머지가 같은 수들끼리 그룹을 나눈 다음 다음과 같은 dynamic programming 을 사용하여 해결할 수 있습니다.

dp1(x, n) := x번째 그룹~마지막 그룹까지 포함될 간선을 특정해 줄 것이고, x번째 이전 그룹들에서 만들어진 컴포넌트의 개수가 n개일 때, 위의 포함과 배제의 식에 더해지는 항의 합

transition 유용하게 계산하기 위하여 아래와 같은 dynamic programming 의 도움을 받을 수 있습니다.

dp2(n, m) := n개의 서로다른 공을 m개의 그룹으로 나누는 경우의 수. 단, 각 그룹 내에서의 순서를 고려해야한다.

위의 두 dynamic programming 을 떠올렸다면, 아래의 코드와 같은 방식으로 풀 수 있습니다. 시간 복잡도는 $O(N^3)$ 입니다.

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;

const int mod = 1234567891;

int exp(int x, int n) {
    int ret = 1;
    while(n) {
        if(n & 1) ret = 1LL * ret * x % mod;
        x = 1LL * x * x % mod;
        n >>= 1;
    }
    return ret;
}
int inv(int x) {
    return exp(x, mod - 2);
}
int fact[33], invf[33];

int N, P;
int cnt[1010];
vector<int> V;

ll cc2[33][33];
ll dp2(int n, int m) {
    ll &ret = cc2[n][m];
    if(ret != -1) return ret;
    if(n == 0) return ret = m == 0;

    ret = 0;
    if(m) {
        ret += 1LL * n * dp2(n - 1, m - 1) % mod;
        ret %= mod;
    }
    ret += 1LL * n * dp2(n - 1, m) % mod;
    ret %= mod;
    return ret;
}

ll cc1[33][33];
ll dp1(int x, int n) {
    if(x == V.size()) return (N - n) % 2? mod - fact[n] : fact[n];
    ll &ret = cc1[x][n];
    if(ret != -1) return ret;

    ret = 0;
    for(int i = 1; i <= V[x]; i++) {
        ret += 1LL * V[x] * dp2(V[x] - 1, i - 1) % mod * invf[i] % mod * dp1(x + 1, n + i) % mod;
        ret %= mod;
    }
    return ret;
}

void main2() {
    scanf("%d %d", &N, &P);

    memset(cnt, 0, sizeof(cnt));
    for(int i = 0; i < N; i++) {
        int t; scanf("%d", &t);
        t = (t % P + P) % P;
        cnt[t]++;
    }
    V.clear();
    for(int i = 0; i < P; i++) if(cnt[i]) V.push_back(cnt[i]);

    memset(cc1, -1, sizeof(cc1));
    printf("%lld\n", dp1(0, 0));
}

int main() {
    fact[0] = 1;
    for(int i = 1; i < 33; i++) {
        fact[i] = 1LL * fact[i - 1] * i % mod;
    }
    for(int i = 0; i < 33; i++) {
        invf[i] = inv(fact[i]);
    }
    memset(cc2, -1, sizeof(cc2));
    for(int i = 0; i < 2; i++) main2();
}