본문 바로가기

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조차 접근을 하나도 못 했습니다. 실망스러운 결과가 나온 만큼 공부를 좀 더 해봐야 될 것 같습니다.

'Problem Solving' 카테고리의 다른 글

[BOJ] 20093 Coins  (0) 2021.05.26
(임시) 2020 ICPC Seoul Regional 후기  (2) 2020.11.17
2020 숭고한 Marathon 참가 후기  (2) 2020.07.20
Google Hash Code 2020 후기  (1) 2020.02.21
Goodbye, BOJ 2019! 후기  (5) 2020.01.05