티스토리 뷰

슬슬 문제를 푸는 것이 귀찮아지고 있다. D4-D3권에서 남은 문제들을 푸는 것이 쉽지 않아 보인다. 그래도 아직 문제가 꽤 남았으니 이 부분에서 조금 더 풀어 볼 생각이다.

 

뭔가 문제를 푼 사람이 많을수록 더 풀기 쉬울 것 같으니, 이번에는 푼 사람 순서대로 정렬을 해 봤다.

 

남은 문제: 60 → 55문제

BOJ 20558. 쿼리와 수열

$K_i$의 합을 $K$라고 쓰자. 다음과 같이 상태가 $O(NK)$개인 DP를 만드는 것은 쉽다.

  • $DP[i][j][v]$: 구간 $(i, j)$에서 최댓값이 $v$인 경우 답의 최댓값
    이때 $(i, v)$ 또는 $(j, v)$의 쌍이 $K$개 이하이므로 전체 $(i, j, k)$의 쌍이 $O(NK)$개 이하가 된다. 다만 저렇게 하면 transition의 가짓수가 $K$개까지 될 수 있어서 실제 시간 복잡도는 $O(NK^2)$이 된다.

여기까지 생각해 놓고 한동안 진전이 없어서 에디토리얼을 봤다. 에디토리얼이 굉장히 대충 써 있어서, 이해하는 데 시간이 많이 걸렸다.

 

에디토리얼을 조금 더 자세히 살펴보며 어떤 방향의 풀이인지 정리해 보자.

 

$DP[l][r]$을 구간 $[l, r]$만 고려했을 때의 답이라고 정의하자. 여기서 구간 $[l, r]$만 고려했을 때의 답은, $[l, r]$에 포함되는 구간의 최댓값 쿼리 합에서 $[l, r]$ 구간을 만드는 비용을 뺀 것을 말한다.

 

에디토리얼의 첫 번째 장에 의하면 $DP[l][r] = \max_{l \le k \le r}{(DP[l][k-1] + DP[k+1][r] + cost(l, r, k))}$라고 한다. 이때 $cost(l, r, k)$는 구간 $(l, r)$에서 $A_k$를 최댓값이라 가정했을 때, $k$를 포함하는 구간에 해당하는 최댓값 합에서 $A_k$를 지정하는 비용을 뺀 값 중 가능한 최댓값이라고 생각할 수 있다.

 

정의를 보고 나면, $A_l, \cdots, A_r$이 주어졌을 때 그 중 최대인 $k \in [l, r]$이 반드시 존재하기 때문에, 저러한 $k$를 고르고 나면 $DP[l][r]$이 반드시 그 값과 일치하는 게 가능함을 알 수 있다. 따라서 (좌변) >= (우변)이 성립한다. 또한, 최댓값이 반드시 성립하므로, 그때의 $k$를 골랐을 때 (좌변) > (우변)이면 그것도 그것대로 문제임을 알 수 있다. 우변의 일부 항이 최적이 아니라는 뜻이기 때문이다. 따라서 저렇게 정의하고 나면 (좌변) = (우변)이 성립해야 한다.

 

다만 이렇게 읽고 나면 한 가지 의문이 들 수 있다. 지금 이 DP는 최댓값이 무엇인지에 대한 데이터를 전혀 포함하지 않고 있기 때문이다. 예를 들어, $DP[l][r]$은 $A_k$가 최대임을 가정하는데, $DP[l][k-1]$에서 고른 최댓값이 $A_k$보다 더 큰 경우 모순이 생긴다. 이런 경우 어떻게 처리되는가? 사실 이 풀이의 핵심은 이렇게 DP 전이 과정에서 최댓값을 잘못 고를 경우 오히려 손해를 본다는 데에서 온다. 최댓값을 잘못 골랐다고 생각해 보자. 그렇다면 일부 구간 $[s, e] \subset [l, r]$에 대해, $[s, e]$ 구간의 최댓값이 저평가된 구간들이 생기게 된다. 이런 경우, 실제로 얻을 수 있는 답의 최댓값보다 최댓값 합 부분에서 손해를 보게 되므로, 그냥 DP를 진행해도 문제가 없다는 것이다. 어차피 최적의 답은 구해질 것이고, 조건에 맞지 않는 답들이 구해져도 최적의 답보다 작기 때문에 영향을 주지 않는다.

 

여기까지 왔으면 저 DP를 어떻게 처리하는가가 핵심이 된다. 요점은 $[l, r]$ 구간에서 적당한 $A_k = v$를 골라, $[l, r]$ 구간에서 $k$를 포함하는 쿼리가 $Q_{l, r, k}$개라 할 때, $Q_{l, r, k} v_{k, j} - C_{k, j}$의 최댓값을 모든 빠르게 구할 수 있어야 한다. $(l, r)$이 주어지면 저런 $(k, v)$ 쌍 개수가 총 $K$개라서, 나이브하게 하면 $O(n^2 K)$가 되어 너무 느리다.

 

핵심은 저 상황을 직선 형태 (CHT 꼴)로 보는 것이다. 결국 한 $k$에 대해서 저걸 전부 모아보면 $ax-b$ 꼴로 쓸 수 있는데, $Q_{l, r, k} = x$에 무엇이 들어오냐에 따라서 최적인 직선이 바뀔 것이다. 이렇게 하면 $K$를 $n$으로 바꿔 대략 $O(n^3)$ 또는 $O(n^3 \log n)$ 정도의 시간 복잡도에 풀 수 있게 된다.

 

CHT를 오랜만에 짜서, while문 조건을 잘못 쓰는 바람에 오랫동안 맞왜틀했다.

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

int n;
ll S[302][302];
ll cost[302][302][302];
ll DP[302][302];

inline ll ssum(int l, int r, int k){
    return S[k][k] - S[l-1][k] - S[k][r+1] + S[l-1][r+1];
}

inline ll divCeil(ll a, ll b){
    if(a >= 0) return (a+b-1)/b;
    else return -((-a)/b);
}

struct Line{
    ll a, b, st;
    Line(ll a, ll b): a(a), b(b){}
    Line(ll a, ll b, ll st): a(a), b(b), st(st){}

    inline ll get(ll x){
        return a*x+b;
    }

    friend ll intersect(Line &l1, Line &l2){ // l1이 더 기울기가 작음. l1 < l2가 되는 최소 정수 x좌표
        assert(l1.a < l2.a);
        return divCeil(l1.b - l2.b, l2.a - l1.a);
    }

    bool operator<(const Line &r)const{
        return st < r.st;
    }
};
vector<Line> lines[302];

int main(){
    #ifndef LOCAL
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    #endif

    cin >> n;
    for(int i=1; i<=n; i++) for(int j=i; j<=n; j++) cin >> S[i][j];
    for(int i=1; i<=n; i++) for(int j=n; j>=i; j--){
        S[i][j] += S[i-1][j] + S[i][j+1] - S[i-1][j+1];
    }

    for(int i=1; i<=n; i++){
        int k;
        cin >> k;
        vector<pair<ll, ll> > vec (k);
        for(int i=0; i<k; i++){
            cin >> vec[i].first >> vec[i].second;
            vec[i].second *= -1;
        }

        sort(vec.begin(), vec.end());

        for(auto [a, b]: vec){
            Line line (a, b);
            while(lines[i].size() >= 2 &&
                  intersect(lines[i][lines[i].size()-2], lines[i].back()) >= intersect(lines[i].back(), line)){
                lines[i].pop_back();
            }
            line.st = lines[i].empty() ? LLONG_MIN : intersect(lines[i].back(), line);
            lines[i].push_back(line);
        }
    }

    for(int l=1; l<=n; l++) for(int r=l; r<=n; r++){
        for(int m=l; m<=r; m++){
            ll s = ssum(l, r, m);
            auto it = upper_bound(lines[m].begin(), lines[m].end(), Line(0, 0, s)) - 1;
            cost[l][r][m] = it->get(s);
        }
    }

    for(int i=1; i<=n; i++) DP[i][i] = cost[i][i][i];
    for(int d=1; d<n; d++){
        for(int l=1; l+d<=n; l++){
            int r = l+d;
            DP[l][r] = LLONG_MIN;
            for(int m=l; m<=r; m++){
                DP[l][r] = max(DP[l][r], (l==m ? 0 : DP[l][m-1]) + (r==m ? 0 : DP[m+1][r]) + cost[l][r][m]);
            }
            assert(DP[l][r] != LLONG_MIN);
        }
    }
    cout << DP[1][n];
}

BOJ 31951. Comparator

일단 파싱을 잘 해야 한다. 스택에서 연산자 우선순위로 잘 비교하고 enum 등을 사용하면 구현을 최대한 간단히 할 수 있다.

 

다음으로 파싱 결과에서 $2^k \times 2^k$ 크기의 비교 테이블을 얻어야 한다. 그런데 유의미한 조건은 사실 최대 $4k^2$개뿐이라는 사실을 알 수 있다. 각 $(x, y, xv, yv)$별로 한 번씩만 해도 충분하기 때문이다. 저런 tuple이 총 $4k^2$개 있고, 각 tuple에 대해 유효한 수 쌍이 $(2^{k-1})^2$개 있으니, 이 경우들에 대해서만 나이브하게 돌아봐도 충분히 빠르게 table을 만들 수 있다.

 

table을 만들고 나면 세 가지 속성을 세어야 한다. 앞 두 개는 자명하고, 나머지 하나도 bitset을 쓰면 빠르게 셀 수 있다.

 

결과적으로 $O(n+2^{3k}/w)$ 시간에 문제를 해결할 수 있다.

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

void input();
void makeTable();
void solve();

int main(){
    #ifndef LOCAL
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    #endif

    input();
    makeTable();
    solve();
}

int n, k;
int A[200002], B[200002], S[200002], R[200002];

enum Op {
    ZERO = 0, ONE = 1,
    LP = 2, RP = 3,
    XOR = 4, OR = 5, AND = 6, EQ = 7, NOT = 8
};

inline int opc(Op l, Op m, Op r){
    assert((l == ZERO || l == ONE) && (r == ZERO || r == ONE));
    if(m == XOR) return l ^ r;
    if(m == OR) return l | r;
    if(m == AND) return l & r;
    if(m == EQ) return l == r;
    if(m == NOT) return !r;
    exit(1);
}

int real_evaluate(string expr){
    expr = '(' + expr + ')';

    vector<Op> stk;
    for(char c: expr){
        if(c == '0') stk.push_back(ZERO);
        else if(c == '1') stk.push_back(ONE);
        else if(c == '(') stk.push_back(LP);
        else if(c == ')'){
            while(stk[stk.size() - 2] != LP){
                Op r = stk.back(); stk.pop_back();
                Op m = stk.back(); stk.pop_back();
                Op l = stk.back(); stk.pop_back();
                stk.push_back((Op)opc(l, m, r));
            }
            Op save = stk.back();
            stk.pop_back(); stk.pop_back();
            stk.push_back(save);
        }
        else{
            Op cur = (c == '^' ? XOR : c == '|' ? OR : c == '&' ? AND : c == '=' ? EQ : NOT);
            if(cur == NOT) stk.push_back(ZERO); // 단항 연산자 처리

            while((int)stk.size() >= 3 && stk[stk.size()-2] > cur){
                Op r = stk.back(); stk.pop_back();
                Op m = stk.back(); stk.pop_back();
                Op l = stk.back(); stk.pop_back();
                stk.push_back((Op)opc(l, m, r));
            }
            stk.push_back(cur);
        }
    }

    assert((int)stk.size() == 1 && (stk[0] == ZERO || stk[0] == ONE));
    return stk[0] == ONE ? 1 : 0;
}

int evaluate(string expr){
    int ret = 0;
    for(int a=0; a<2; a++){
        for(int b=0; b<2; b++){
            string newExpr = expr;
            for(auto &c: newExpr){
                if(c == 'x') c = '0' + a;
                if(c == 'y') c = '0' + b;
            }
            int val = real_evaluate(newExpr);
            ret += val << (a*2+b);
        }
    }
    return ret;
}

void input(){
    cin >> n >> k;
    for(int i=1; i<=n; i++){
        int a, b, r;
        string expr;
        cin >> a >> b >> expr >> r;
        int bs = evaluate(expr);
        A[i] = a-1, B[i] = b-1, S[i] = bs, R[i] = r;
        // cout << S[i] << '\n';
    }
}

int table[1<<10][1<<10]; // 0, 1: 결과, 2: 결정 x
int did[10][10][2][2];
int numbers[10][2][1<<9];

void makeTable(){
    int MX = (1<<k);
    for(int i=0; i<MX; i++) for(int j=0; j<MX; j++) table[i][j] = 2;

    for(int i=0; i<k; i++) for(int j=0; j<2; j++){
        int cnt = 0;
        for(int s=0; s<MX; s++){
            int b = (s>>i) & 1;
            if(b == j) numbers[i][j][cnt++] = s;
        }
    }

    for(int i=1; i<=n; i++){
        int a = A[i], b = B[i], s = S[i], r = R[i];
        for(int x=0; x<2; x++) for(int y=0; y<2; y++){
            if((s & (1<<(x*2+y))) == 0 || did[a][b][x][y]) continue;
            did[a][b][x][y] = 1;
            for(int ia=0; ia<(1<<(k-1)); ia++){
                int na = numbers[a][x][ia];
                for(int ib=0; ib<(1<<(k-1)); ib++){
                    int nb = numbers[b][y][ib];
                    if(table[na][nb] == 2) table[na][nb] = r;
                }
            }
        }
    }
    int ret;
    cin >> ret;

    for(int i=0; i<MX; i++) for(int j=0; j<MX; j++) if(table[i][j] == 2) table[i][j] = ret;
}

bitset<1024> out[1<<10], in[1<<10];

void solve(){
    const int MX = (1<<k);
    { // reflexive
        int cnt = 0;
        for(int i=0; i<MX; i++) if(table[i][i]) cnt++;
        cout << cnt << ' ';
    }
    { // symmetric
        int cnt = 0;
        for(int i=0; i<MX; i++) for(int j=0; j<MX; j++){
            if(table[i][j] && table[j][i]) cnt++;
        }
        cout << cnt << ' ';
    }
    { // transitive
        for(int i=0; i<MX; i++) for(int j=0; j<MX; j++){
            if(table[i][j]){
                out[i][j] = 1;
                in[j][i] = 1;
            }
        }

        ll ans = 0;
        for(int i=0; i<MX; i++) for(int k=0; k<MX; k++){
            if(table[i][k]) continue;
            ans += (out[i] & in[k]).count();
        }
        cout << ans;
    }
}

BOJ 17376. 룰렛

문제 자체를 봤을 때는 수식 전개하기가 조금 힘들 것 같았고, 구현도 조금 복잡할 것 같았다.

일단 문제의 상황을 몇 가지 적당한 확률로 나타내는 것이 중요하다. 아래와 같이 정의해 보자.

  • $S[x]$: 놀이기구 $x$에서 출발했을 때, 마지막으로 놀이기구 $x$를 타고 집으로 돌아갈 확률
  • $T[e]$: 간선 $e = x \rightarrow y$에 대해, 놀이기구 $x$에서 출발했을 때, 한 번이라도 간선 $e$를 탈 확률
  • $C[e]$: 간선 $e = x \rightarrow y$에 대해, 놀이기구 $x$에서 출발했을 때, 도착점이 간선 $e$ 기준으로 $y$ 쪽 서브트리에 있을 확률

이제 이 확률 사이의 관계를 나타내어 보자. 다음을 알 수 있다. 여기서 $Pr[e]$는 $e = (x, y)$일 때 $x$에서 바로 간선 $e$를 탈 확률, $Pr[v]$는 $v$에 있을 때 해당 지점에서 바로 멈출 확률, $e'$는 $e$의 역방향 간선이라고 정의한다.

 

$$
\begin{aligned}
S[x] &= 1 - \sum_{e = (x, y) \in E} C[e] \\
S[x] &= Pr[x] + \left(\sum_{e = (x, y) \in E} Pr[e] T[e'] \right) S[x] \\
&= \frac{Pr[x]}{1 - \sum_e Pr[e] T[e']} \\
C[e] &= T[e] (1 - T[e']) + T[e]T[e']T[e](1-T[e'])+ \cdots \\
&= \frac{T[e](1-T[e'])}{1 - T[e]T[e']}
\end{aligned}
$$

 

이렇게 식을 쓰다 보면 결국 $T[e]$를 모두 구해 두는 것이 굉장히 중요함을 알 수 있다. $T[e]$를 $T$에 대해 나타내 보면

 

$$
\begin{aligned}
T[e] &= Pr[e] + \left( \sum_{f = (x, y) \neq e} Pr[f] T[f'] \right) T[e] \\
&= \frac{Pr[e]}{1 - \sum_{f \neq e} Pr[f] T[f']}
\end{aligned}
$$


그런데 여기서 잘 생각해보면 저 $T[e]$ 값은 rerooting DP가 되는 꼴로 나온다. 따라서 저 $T[e]$를 사전에 모두 구해둘 수 있다. 이제 $T$로부터 $C$를 만들고, $C$로부터 $S$를 만들어 두면 각 쿼리는 단순히 트리 위의 구간 쿼리 처리가 된다. $O(N + Q \log N)$에 문제를 해결할 수 있다.

#include <bits/stdc++.h>
#include <atcoder/modint>

using namespace std;
using namespace atcoder;

typedef modint1000000007 ll;

istream& operator>>(istream &in, ll &m){
    long long x;
    in >> x;
    m = ll(x);
    return in;
}

int n, q;
ll A[100002]; int par[100002];
int depth[100002];
vector<pair<int, ll> > edge[100002];

ll Prup[100002], Prdown[100002];
ll Tup[100002];

void dfsT(int x, int p=-1){
    if(p!=-1){
        par[x] = p;
        auto it = edge[x].begin();
        while(it->first != p) ++it;
        Prup[x] = it->second / A[x];
        edge[x].erase(it);
    }

    ll a = Prup[x], b = 1;

    for(auto [y, p1]: edge[x]){
        Prdown[y] = p1 / A[x];
        depth[y] = depth[x] + 1;
        dfsT(y, x);
        b -= Prdown[y] * Tup[y];
    }
    Tup[x] = a / b;
}

ll Tdown[100002];

void dfsT2(int x, int p=-1){
    ll sum = 0;
    for(auto [y, p1]: edge[x]) sum += Prdown[y] * Tup[y];
    if(p != -1) sum += Prup[x] * Tdown[x];
    for(auto [y, p1]: edge[x]){
        Tdown[y] = Prdown[y] / (1 - sum + Prdown[y] * Tup[y]);
        dfsT2(y, x);
    }
}

ll Cup[100002], Cdown[100002], S[100002];
int LCADB[100002][20];

ll TupS[100002], TdownS[100002];

void dfsSum(int x){
    for(auto [y, p1]: edge[x]){
        TupS[y] = TupS[x] * Tup[y];
        TdownS[y] = TdownS[x] * Tdown[y];
        dfsSum(y);
    }
}

int getLCA(int x, int y){
    if(depth[x] > depth[y]) swap(x, y);
    for(int d=0; d<20; d++) if((depth[y] - depth[x]) & (1<<d)) y = LCADB[y][d];
    if(x==y) return x;
    for(int d=19; d>=0; d--) if(LCADB[x][d] != LCADB[y][d]) x = LCADB[x][d], y = LCADB[y][d];
    return par[x];
}

int kthParent(int x, int k){
    for(int i=0; i<20; i++) if((k>>i)&1) x = LCADB[x][i];
    return x;
}

int main(){
    #ifndef LOCAL
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    #endif

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

    for(int i=1; i<n; i++){
        int u, v; ll p1, p2;
        cin >> u >> v >> p1 >> p2;
        edge[u].push_back({v, p1});
        edge[v].push_back({u, p2});
    }

    dfsT(1);
    dfsT2(1);

    for(int i=2; i<=n; i++){
        ll tup = Tup[i], tdown = Tdown[i];
        Cup[i] = tup * (1 - tdown) / (1 - tup * tdown);
        Cdown[i] = tdown * (1 - tup) / (1 - tup * tdown);
    }
    for(int i=1; i<=n; i++){
        S[i] = 1;
        for(auto [y, p1]: edge[i]) S[i] -= Cdown[y];
        if(i != 1) S[i] -= Cup[i];
    }

    // debug
    #ifdef LOCAL
    for(int i=1; i<=n; i++){
        cout << i << ": " << S[i].val() << " " << Tup[i].val() << " " << Tdown[i].val() <<
                " " << Cup[i].val() << " " << Cdown[i].val() << "\n";
    }
    #endif

    for(int i=1; i<=n; i++) LCADB[i][0] = par[i];
    for(int d=1; d<20; d++) for(int i=1; i<=n; i++) LCADB[i][d] = LCADB[LCADB[i][d-1]][d-1];

    TupS[1] = TdownS[1] = 1;
    dfsSum(1);

    while(q--){
        int x, y;
        cin >> x >> y;
        int z = getLCA(x, y);

        ll ans;
        if(x == y) ans = S[x];
        else{
            ans = TupS[x] / TupS[z] * TdownS[y] / TdownS[z];
            ans *= S[y];
        }
        cout << ans.val() << '\n';
    }
}

BOJ 10935. BASE64 인코딩

원래는 번외 문제는 다이아 이후로 미뤄 두려고 했는데, 영 의지가 없어서 쉬워 보이는 하나만 풀었다.

import base64
s = input()
b = s.encode('UTF-8')
print(base64.b64encode(b).decode('ascii'))

 

여기서부터 남은 다4/다3 문제들이 너무 답이 없어 보였다. 대부분 감이 잘 안 잡히거나, 어떻게 하는지는 알겠는데 TL/ML 상수 이슈로 풀기 싫은 것들이 너무 많았다. 그래서 여기서부터는 일단 조금 생각해보고 모르겠으면 풀이를 일찍 찾아보기로 했다.

BOJ 23851. Akcija

문제를 간단히 요약하면 다음과 같다.

 

$n$개의 상품이 있고, 각 상품은 가격 $w_i$와 데드라인 $d_i$가 있다. 1분에 최대 한 개의 상품을 살 수 있다. 즉, 어떤 상품 집합 $S$를 살 수 있을 조건은 (홀의 정리에 의해) 모든 $t$에 대해 $d_i \le t$인 상품이 $t$개 이하 있는 것이다. 모든 살 수 있는 집합을 크기 내림차순으로, 크기가 같다면 가격 합 오름차순으로 정렬할 때, $1$번째부터 $k$번째 집합의 크기와 가격 합을 구하면 된다.

Subtask 2

$k = 1$인 경우 최적의 해를 구하면 된다. 일단 개수를 구하는 것은 쉽다. 그냥 비용 무시하고 그리디하게 데드라인 작은 것부터 고르면 된다. 그런데 전체 비용을 구하는 것은 조금 더 어렵다. 여기서도 그리디한 전략을 쓸 수 있다. 최대 개수가 $s$라고 할 때, 처음에는 데드라인이 $s$ 이상인 것 중 최대를 고르고, 다음으로 데드라인이 $s-1$ 이상인 남은 것 중 최대를 고르고, ..., 이런 식으로 반복하면 된다.

Subtask 3

위와 같이 $k = 1$인 solution을 찾았으면, $k=2$인 solution은 이들 중 한 개를 제거하고 만들 수 있는 최적의 solution이 된다. 이들 중 가장 좋은 것을 찾으면 된다.

Subtask 4

$O(2^n)$에 다 해 보면 된다.

Subtask 5

Subtask 3의 접근을 생각해 보면, 일단 최적해를 구한 뒤, 하나를 다른 걸로 대체하는 식으로 다른 후보들을 만들어 나갈 수 있다고 생각할 수 있다. 이걸 적당히 효율적으로 짜면 Subtask 5를 풀 수 있을 것이다.

Full task

저걸 조금 최적화하면 Full task도 어렵지 않게 풀 수 있을 것 같긴 했는데, 옛날에 MLE를 받은 기록이 있어 (지금까지도 여러 번 겪은) TL/ML 상수 이슈에 휘말릴 것 같았다. 정해를 읽고 짜는 쪽이 더 깔끔할 것 같아 그렇게 하기로 했다.

그런데 에디토리얼도 내가 생각한 것 이상의 특별한 정보를 제공해주지 않았다. 결국 Robotic Cow Herd 느낌의 pq질을 해야 한다는 건 자명한데, 거기서 메모리를 어떻게 깎는지가 문제의 핵심 같아 보였다.

 

핵심은 현재 상태를 pq에 그대로 저장하지 말고, 이전 상태에서 어디가 바뀌는지만 저장하는 것이다. 이렇게 하면 $O(nk)$ 메모리로 문제를 해결할 수 있다.

 

디테일적인 면에서 생각할 게 꽤나 많은 문제인 것 같다. 자세한 디테일은 생략한다.

 

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

struct Product{
    int d; ll v;
    Product(){}
    Product(int d, ll v): d(d), v(v){}
};

int n, k;
Product arr[2002];
bool used[2002][2002];
int sz[2002]; ll cost[2002];
unordered_set<ll> usedSets;
ll RAND[2002];

struct Solution{
    int sz; ll cost;
    int base, del, add; // until: 옮길 수 없는 가장 오른쪽 위치

    Solution(){}
    Solution(int sz, ll cost, int base, int del, int add): sz(sz), cost(cost), base(base), del(del), add(add){}

    bool operator<(const Solution &r)const{
        if(sz != r.sz) return sz < r.sz;
        return cost > r.cost;
    }
};
priority_queue<Solution> pq;

void makeAns(int row){
    for(int i=1; i<=n; i++){
        if(used[row][i]) sz[row]++, cost[row] += arr[i].v;
    }
}

struct segTree{
    int minV[1<<12], minIdx[1<<12];
    int lazy[1<<12];

    void merge(int i){
        minV[i] = min(minV[i*2], minV[i*2+1]);
        minIdx[i] = max(minV[i*2] == minV[i] ? minIdx[i*2] : 0,
                        minV[i*2+1] == minV[i] ? minIdx[i*2+1] : 0);
    }

    void init(int i, int l, int r){
        lazy[i] = 0;
        if(l==r){
            minV[i] = l, minIdx[i] = l;
            return;
        }
        int m = (l+r)>>1;
        init(i*2, l, m);
        init(i*2+1, m+1, r);
        merge(i);
    }

    void propagate(int i, int l, int r){
        minV[i] += lazy[i];
        if(l!=r) lazy[i*2] += lazy[i], lazy[i*2+1] += lazy[i];
        lazy[i] = 0;
    }

    void update(int i, int l, int r, int s, int e, int v){
        propagate(i, l, r);
        if(r<s || e<l) return;
        if(s<=l && r<=e){
            lazy[i] += v;
            propagate(i, l, r);
            return;
        }
        int m = (l+r)>>1;
        update(i*2, l, m, s, e, v);
        update(i*2+1, m+1, r, s, e, v);
        merge(i);
    }

    pair<int, int> query(int i, int l, int r){
        propagate(i, l, r);
        return make_pair(minV[i], minIdx[i]);
    }
} tree;

void putPQ(int row){
    tree.init(1, 1, n);
    for(int i=1; i<=n; i++) if(used[row][i]) tree.update(1, 1, n, arr[i].d, n, -1);

    ll hsh = 0;
    for(int i=1; i<=n; i++) if(used[row][i]) hsh ^= RAND[i];
    if(row == 1) usedSets.insert(hsh);

    vector<pair<ll, ll> > nxtCand;
    for(int a=n; a>=1; a--){
        if(!used[row][a]){
            while(!nxtCand.empty() && nxtCand.back().first <= arr[a].d) nxtCand.pop_back();
            nxtCand.push_back({arr[a].d, a});
            continue;
        }
        tree.update(1, 1, n, arr[a].d, n, 1);
        pair<int, int> p = tree.query(1, 1, n);
        tree.update(1, 1, n, arr[a].d, n, -1);

        int minD = (p.first == 0 ? p.second+1 : 1);
        auto it = upper_bound(nxtCand.begin(), nxtCand.end(), make_pair(minD, 0), greater<pair<int, int> >());
        int minLoc = (it == nxtCand.begin() ? -1 : prev(it)->second);

        if(minLoc != -1){ // 대체
            ll newHsh = hsh ^ RAND[a] ^ RAND[minLoc];
            if(usedSets.count(newHsh)) continue;
            pq.push(Solution(sz[row], cost[row] - arr[a].v + arr[minLoc].v, row, a, minLoc));
            usedSets.insert(newHsh);
        }
        else{ // 삭제
            ll newHsh = hsh ^ RAND[a];
            if(usedSets.count(newHsh)) continue;
            pq.push(Solution(sz[row]-1, cost[row] - arr[a].v, row, a, 0));
            usedSets.insert(newHsh);
        }
    }
}

void getFromPQ(int row){
    assert(!pq.empty());
    Solution p = pq.top(); pq.pop();
    int base = p.base;
    copy(used[base]+1, used[base]+n+1, used[row]+1);
    used[row][p.del] = 0;
    if(p.add) used[row][p.add] = 1;
}

int main(){
    #ifndef LOCAL
    ios::sync_with_stdio(false);
    cin.tie(NULL);
    #endif

    uniform_int_distribution<ll> distrib(LLONG_MIN, LLONG_MAX);
    mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());

    cin >> n >> k;
    for(int i=1; i<=n; i++){
        cin >> arr[i].v >> arr[i].d;
        RAND[i] = distrib(rng);
    }

    sort(arr+1, arr+n+1, [](Product &A, Product &B){
        return A.v < B.v;
    });

    // get best solution
    {
        vector<int> cnt (n+2);
        for(int i=1; i<=n; i++){
            cnt[arr[i].d]++;
            int s = 0, able = 1;
            for(int j=1; j<=n; j++){
                s += cnt[j];
                if(s > j){
                    able = 0;
                    break;
                }
            }

            if(able) used[1][i] = 1;
            else cnt[arr[i].d]--;
        }
    }

    makeAns(1);
    putPQ(1);
    for(int turn=2; turn<=k; turn++){
        getFromPQ(turn);
        makeAns(turn);
        putPQ(turn);
    }

    for(int i=1; i<=k; i++){
        cout << sz[i] << ' ' << cost[i] << '\n';
    }
}

'시리즈 > 과거 청산' 카테고리의 다른 글

과거 청산 챌린지 #6  (0) 2026.02.09
과거 청산 챌린지 #5  (0) 2026.01.28
과거 청산 챌린지 #3  (0) 2026.01.17
과거 청산 챌린지 #2  (0) 2026.01.14
과거 청산 챌린지 #1  (0) 2026.01.11
공지사항
최근에 올라온 글
Total
Today
Yesterday