본문 바로가기

Problem Solving

[SCPC] 2020 1차 예선 풀이

Intro

대충 4시간 정도 때려 박아서 1~4번에서 만점을 받았고, 5번은 문제만 읽었습니다. 예전 SCPC 기출에 비해서는 확실히 난이도가 균일해진 것 같습니다(https://newdeal123.tistory.com/61 참조 요망). 제가 예상하는 Solved.ac 티어 난이도는 다음과 같습니다.

  • 다이어트 (Silver 3)
  • 카드 게임 (Platinum 3)
  • 사다리 게임 (Platinum 3)
  • 범위 안의 숫자 (Platinum 2)
  • 우범 지역 (Diamond 2~3?)

1. 다이어트

다음과 같이 생각해봅시다. 일단 자명히 두 식단에서 칼로리가 낮은 순으로 K개만 먹어야 한다는 것을 알 수 있습니다. 이때 식단 A의 가장 큰 칼로리를 가지는 식사는 B의 어떤 식사랑 매칭시켜야 할까요? 당연히 B의 가장 작은 칼로리의 식사와 매칭시켜야 합니다. 이런 접근을 나머지 배열에 대해서 반복해 주면 A의 오름차순 배열과 B의 내림차순 배열을 보고 그 중 더한 값의 최소를 출력해야 한다는 것을 알 수 있습니다. 시간복잡도는 $O(N \lg N)$입니다.

#include <bits/stdc++.h>
#include <random>
#include <cassert>
#define all(x) (x).begin(), (x).end()
#define endl "\n"
#define ends " "
#define pb(x) push_back(x)
#define fio() ios_base::sync_with_stdio(0); cin.tie(0)
#define fileio() ifstream file_in("input.txt");ofstream file_out("output.txt")
/*#define cin file_in
#define cout file_out*/

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef tuple<int, int, int> tpi;
typedef tuple<ll, ll, ll> tpl;
typedef pair<double, ll> pdl;

const int MOD = 1000000007;
const ll LMOD = 1000000007;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double pi = acos(-1);
const double eps = 1e-10;
const int dx[] = { 0,1,0,-1 };
const int dy[] = { 1,0,-1,0 };

void solve() {
    int n, k; cin >> n >> k;
    vector<int> a(n), b(n);
    for (int i = 0; i < n; i++) cin >> a[i];
    for (int i = 0; i < n; i++) cin >> b[i];
    sort(all(a));
    sort(all(b));

    int ma = 0;
    for (int i = 0; i < k; i++)
        ma = max(ma, a[i] + b[k - i - 1]);
    cout << ma << endl;
}

int main() {
    fio();
    int tc; cin >> tc;
    for (int i = 1; i <= tc; i++) {
        cout << "Case #" << i << endl;
        solve();
        cout.flush();
    }
    return 0;
}

2. 카드 게임

게임 위에서 DP하는 건 흔히 알려진 접근이니, 그 접근만 잘 최적화해보도록 합시다. 구체적으로는 $DP[x][y]$를 왼쪽 카드 더미에 $x$장, 오른쪽 카드 더미에 $y$장이 남아 있을 때 선공이 이기면 1, 후공이 이기면 0으로 두고 상태 전이를 시도합니다. $DP[0][0]=1$입니다.

WLOG을 걸어 왼쪽 카드 더미를 제거한다고 합시다. 내가 카드를 적당히 가져가서 $x$장을 $j$장으로 만들 수 있다고 할 때, $\sum_{i=j}^{x-1} DP[j][y]$가 전부 1이라면 내가 무슨 짓을 해도 다음 턴의 선공, 즉 다른 사람이 이기게 됩니다. 오른쪽 카드 더미에 대해서도 같습니다. 이 두 경우를 따져줬을 때 모든 값이 1이라면 $DP[x][y]=0$이고 그렇지 않다면 $DP[x][y]=1$입니다.

그러면 이제 남은 것은 이 과정을 효율적으로 하는 방법입니다. 특정한 $x$에 대해 $j$를 찾아주는 것은 미리 투 포인터 느낌으로 $O(N^2)$에 전처리 해주면 $O(1)$에 할 수 있습니다. 마찬가지로 DP 배열이 모두 1인지 검사하는 것은 DP 배열을 채우는 과정에서 누적합을 같이 저장해주면, $O(1)$에 할 수 있습니다. 따라서 총 시간복잡도는 $O(N^2)$입니다.

#include <bits/stdc++.h>
#include <random>
#include <cassert>
#define all(x) (x).begin(), (x).end()
#define endl "\n"
#define ends " "
#define pb(x) push_back(x)
#define fio() ios_base::sync_with_stdio(0); cin.tie(0)
#define fileio() ifstream file_in("input.txt");ofstream file_out("output.txt")
/*#define cin file_in
#define cout file_out*/

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef tuple<int, int, int> tpi;
typedef tuple<ll, ll, ll> tpl;
typedef pair<double, ll> pdl;

const int MOD = 1000000007;
const ll LMOD = 1000000007;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double pi = acos(-1);
const double eps = 1e-10;
const int dx[] = { 0,1,0,-1 };
const int dy[] = { 1,0,-1,0 };

int dp[3005][3005], pr[3005][3005], qr[3005][3005], px[3005], qx[3005], p[3005], q[3005];
void solve() {
    int n, k; cin >> n >> k;
    for (int i = 0; i <= n; i++) for (int j = 0; j <= n; j++) dp[i][j] = pr[i][j] = qr[i][j] = 0;
    for (int i = 0; i <= n; i++) px[i] = qx[i] = 0;
    for (int i = 1; i <= n; i++) cin >> p[i];
    for (int i = 1; i <= n; i++) cin >> q[i];

    dp[0][0] = 1;

    int l = 1, ls = 0;
    for (int i = 1; i <= n; i++) {
        ls += p[i];
        while (ls > k) ls -= p[l++];
        px[i] = l - 1;
    }
    l = 1, ls = 0;
    for (int i = 1; i <= n; i++) {
        ls += q[i];
        while (ls > k) ls -= q[l++];
        qx[i] = l - 1;
    }

    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            int pt = 0, qt = 0;
            if (i) pt = pr[i - 1][j] - (px[i] ? pr[px[i] - 1][j] : 0);
            if (j) qt = qr[i][j - 1] - (qx[j] ? qr[i][qx[j] - 1] : 0);
            if (pt + qt != i - px[i] + j - qx[j]) dp[i][j] = 1;
            pr[i][j] = (i ? pr[i - 1][j] : 0) + dp[i][j];
            qr[i][j] = (j ? qr[i][j - 1] : 0) + dp[i][j];
        }
    }
    int s = 0;
    for (int i = 0; i <= n; i++)
        for (int j = 0; j <= n; j++)
            s += dp[i][j];
    cout << s << ends << (n + 1) * (n + 1) - s << endl;
}

int main() {
    fio();
    int tc; cin >> tc;
    for (int i = 1; i <= tc; i++) {
        cout << "Case #" << i << endl;
        solve();
        cout.flush();
    }
    return 0;
}

3. 사다리 게임

문제를 그래프에서의 최단거리 문제로 바꿔봅시다. 가로선 개수가 적다는 점에 주목해서 정점을 X번째 세로선에 존재하는 Y번째 교점을 통과한 직후의 위치로 잡습니다. 이러면 정점과 간선의 개수가 각각 $N$과 $k$에 비례하게 됩니다. 모든 $1 \le i \le N$에 대해서 $i$번째 세로선에서 시작해 다른 세로선으로 도착하는 최단거리를 구해봅시다. 그냥 정상적으로 가로선을 타고 가면 비용은 0이고, 가로선을 제거하면 비용은 1입니다. 다익스트라를 사용하면 한 번 수행에 $O(k\lg N)$정도가 나오겠지만 가중치가 0 또는 1이라는 점에 주목해 0-1 BFS를 사용해주면 $O(N+k)$에 할 수 있습니다. 이걸 $N$번 반복하니 총 복잡도는 $O(N^2 + Nk)$입니다.

#include <bits/stdc++.h>
#include <random>
#include <cassert>
#define all(x) (x).begin(), (x).end()
#define endl "\n"
#define ends " "
#define pb(x) push_back(x)
#define fio() ios_base::sync_with_stdio(0); cin.tie(0)
#define fileio() ifstream file_in("input.txt");ofstream file_out("output.txt")
/*#define cin file_in
#define cout file_out*/

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef tuple<int, int, int> tpi;
typedef tuple<ll, ll, ll> tpl;
typedef pair<double, ll> pdl;

const int MOD = 1000000007;
const ll LMOD = 1000000007;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double pi = acos(-1);
const double eps = 1e-10;
const int dx[] = { 0,1,0,-1 };
const int dy[] = { 1,0,-1,0 };

int cnt[1505], ans[1505][1505];
vector<int> dist[1505];
vector<pii> adj[1505];

void solve() {
    int n, k, m;
    cin >> n >> k >> m;
    for (int i = 0; i <= n; i++) adj[i].clear(), cnt[i] = 0;
    for (int i = 0; i < k; i++) {
        int t1, t2; cin >> t1 >> t2;
        adj[t1].emplace_back(t2, cnt[t2]);
        adj[t2].emplace_back(t1, cnt[t1]);
        cnt[t1]++; cnt[t2]++;
    }
    for (int i = 0; i <= n; i++) for (int j = 0; j <= n; j++) ans[i][j] = -1;

    for (int i = 1; i <= n; i++) {
        deque<pii> d;
        d.emplace_back(i, -1);
        for (int j = 1; j <= n; j++) dist[j].assign(cnt[j], INF);
        while (!d.empty()) {
            auto [x, y] = d.front(); d.pop_front();
            int bf = y == -1 ? 0 : dist[x][y];
            if (cnt[x] == y + 1) {
                ans[i][x] = bf;
                continue;
            }

            auto [tx,ty] = adj[x][y + 1];
            if (dist[tx][ty] > bf) {
                dist[tx][ty] = bf;
                d.emplace_front(tx, ty);
            }
            if (dist[x][y + 1] > bf + 1) {
                dist[x][y + 1] = bf + 1;
                d.emplace_back(x, y + 1);
            }
        }
    }
    int s = 0;
    for (int i = 0; i < m; i++) {
        int t1, t2; cin >> t1 >> t2;
        s += ans[t1][t2];
    }
    cout << s << endl;
}

int main() {
    fio();
    int tc; cin >> tc;
    for (int i = 1; i <= tc; i++) {
        cout << "Case #" << i << endl;
        solve();
        cout.flush();
    }
    return 0;
}

4. 범위 안의 숫자

어떤 수열에서 임의의 위치를 1로 바꾸면 영향을 받는 수는 최대 18개입니다. 모든 경우에서 매번 바꿔가면서 테스트해주면 됩니다. 범위 안의 숫자 최대를 구하는 것은 세그먼트 트리로 해 주면 되는데, 수 $x$가 추가될 경우 $[x-m,x]$에서 시작해 $[x,x+m]$에서 끝나는 구간에 숫자가 하나 늘어나게 됩니다. 따라서 $[x-m,x]$로 각 구간을 대표하게 한 다음 여기에 lazy propagation으로 수가 추가되면 1, 제거되면 -1을 더해주고 max 쿼리를 날려주면 됩니다. 수가 커서 Dynamic Segment Tree를 사용해야 겠다고 생각할 수 있는데, 요즘 트렌드를 따라가지 못하는 대회인지는 몰라도 메모리 제한이 꼴랑 256MB입니다. 좌표압축해줍시다.

#include <bits/stdc++.h>
#include <random>
#include <cassert>
#define all(x) (x).begin(), (x).end()
#define endl "\n"
#define ends " "
#define pb(x) push_back(x)
#define fio() ios_base::sync_with_stdio(0); cin.tie(0)
#define fileio() ifstream file_in("input.txt");ofstream file_out("output.txt")
/*#define cin file_in
#define cout file_out*/

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef tuple<int, int, int> tpi;
typedef tuple<ll, ll, ll> tpl;
typedef pair<double, ll> pdl;

const int MOD = 1000000007;
const ll LMOD = 1000000007;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3f;
const double pi = acos(-1);
const double eps = 1e-10;
const int dx[] = { 0,1,0,-1 };
const int dy[] = { 1,0,-1,0 };

int CM[10];
int n, k, m;

struct seg {
    vector<int> a, lz;
    seg(int _n) : a(_n * 4 + 4), lz(_n*4+4) {}
    void push(int nd, int s, int e) {
        if (!lz[nd]) return;
        a[nd] += lz[nd];
        if (s != e) {
            lz[nd << 1] += lz[nd];
            lz[nd << 1 | 1] += lz[nd];
        }
        lz[nd] = 0;
    }
    void update(int nd, int s, int e, int l, int r, int v) {
        push(nd, s, e);
        if (l <= s && e <= r) {
            lz[nd] += v;
            push(nd, s, e);
            return;
        }
        if (e < l || r < s) return;
        int mid = s + e >> 1;
        update(nd << 1, s, mid, l, r, v);
        update(nd << 1 | 1, mid + 1, e, l, r, v);
        a[nd] = max(a[nd << 1], a[nd << 1 | 1]);
    }
    int query(int nd, int s, int e) {
        push(nd, s, e);
        return a[nd];
    }
};

vector<int> f(vector<int>& a, int x) {
    int cnt = 0, r = 0;
    vector<int> t;
    for (int i = x - k + 1; i <= x + k - 1; i++) {
        if (i < 0 || i >= n) continue;
        if (cnt >= k) r -= a[i - k] * CM[k - 1];
        r *= 10; r += a[i];
        if (cnt >= k - 1) t.push_back(r);
        cnt++;
    }
    return t;
}

vector<int> p[50005], q[50005];

void solve() {
    cin >> n >> k >> m;
    string str; cin >> str;
    vector<int> a(n), v;
    for (int i = 0; i < n; i++) a[i] = str[i] - '0';
    for (int i = 0; i < n; i++)
        p[i].clear(), q[i].clear();

    int r = 0;
    for (int i = 0; i < k - 1; i++) {
        r *= 10; r += a[i];
    }
    for (int i = k - 1; i < n; i++) {
        if (i != k - 1) r -= a[i - k] * CM[k - 1];
        r *= 10; r += a[i];
        v.push_back(r);
        v.push_back(r - m);
    }
    v.push_back(0);

    for (int i = 0; i < n; i++) {
        p[i] = f(a, i);
        int sto = a[i];
        a[i] = 1;
        q[i] = f(a, i);
        a[i] = sto;

        for (int x : p[i]) v.push_back(x), v.push_back(x-m);
        for (int x : q[i]) v.push_back(x), v.push_back(x-m);
    }
    sort(all(v), greater<int>());
    while (v.back() < 0) v.pop_back();
    reverse(all(v));
    v.erase(unique(all(v)), v.end());

    seg S(v.size() + 1);
    int N = v.size();

    r = 0;
    for (int i = 0; i < k - 1; i++) {
        r *= 10; r += a[i];
    }
    for (int i = k - 1; i < n; i++) {
        if (i != k - 1) r -= a[i - k] * CM[k - 1];
        r *= 10; r += a[i];
        int i1 = lower_bound(all(v), max(0,r - m)) - v.begin();
        int i2 = lower_bound(all(v), r) - v.begin();

        S.update(1, 0, N, i1, i2, 1);
    }

    int ma = S.query(1, 0, N);
    for (int i = 0; i < n; i++) {
        for (auto x : p[i]) {
            int i1 = lower_bound(all(v), max(0, x - m)) - v.begin();
            int i2 = lower_bound(all(v), x) - v.begin();
            S.update(1, 0, N, i1, i2, -1);
        }
        for (auto x : q[i]) {
            int i1 = lower_bound(all(v), max(0, x - m)) - v.begin();
            int i2 = lower_bound(all(v), x) - v.begin();
            S.update(1, 0, N, i1, i2, 1);
        }
        ma = max(ma, S.query(1, 0, N));
        for (auto x : p[i]) {
            int i1 = lower_bound(all(v), max(0, x - m)) - v.begin();
            int i2 = lower_bound(all(v), x) - v.begin();
            S.update(1, 0, N, i1, i2, 1);
        }
        for (auto x : q[i]) {
            int i1 = lower_bound(all(v), max(0, x - m)) - v.begin();
            int i2 = lower_bound(all(v), x) - v.begin();
            S.update(1, 0, N, i1, i2, -1);
        }
    }
    cout << ma << endl;
}

int main() {
    fio();
    int tc; cin >> tc;
    CM[0] = 1;
    for (int i = 1; i <= 9; i++) CM[i] = CM[i - 1] * 10;
    for (int i = 1; i <= tc; i++) {
        cout << "Case #" << i << endl;
        solve();
        cout.flush();
    }
    return 0;
}

Outro

5번 풀이는 알아내는 대로 추가하겠습니다. Point in Triangle이나 대충 기하적 방법에 적절하게 확률론을 섞으면 되는 거 같은데 확률 계산에 매우 취약해서 Subtask 1조차 접근을 하나도 못 했습니다. 실망스러운 결과가 나온 만큼 공부를 좀 더 해봐야 될 것 같습니다.