けんちょんの競プロ精進記録

競プロの精進記録や小ネタを書いていきます

CPSCO2019 Session4 F - Lost Tree (800 点設定)

テスターしてました!難しかった。

問題概要

ラスク君は木を持っていましたが、なくしてしまいました。

この木は、頂点に 1 以上頂点数以下の相異なる整数の番号がついていて、各辺には  1 以上  10^{9} 以下の整数の重みが定まっていました。

頂点数は  K 以上 1000 以下であり、 1 \le i, j \le K に対し、頂点  i と頂点  j の距離は  D_{i, j} でした。ただし、頂点  i と頂点  j の距離とは、頂点  i と頂点  j を結ぶ単純パスに含まれる辺の重みの総和のことをいいます。

これらの情報から、ラスク君の持っていた木として考えられるものを一つ出力してください。なお、この問題で与えられる入力に対しては、いずれも条件に整合する木が少なくとも一つ存在することが保証されています。

制約

  •  2 \le K \le 300
  • 条件を満たす木は少なくとも 1 つ存在する

考えたこと

テスターだったので、ちゃんと挑んだ。とりあえず、木の直径となる 2 点を考えることにした (必ずしも直径である必要がなかったけど、最初に直径を考えたことで結果的に実装がやや楽になった)。まず  D_{p, q} の値が最大となるような 2 頂点  p, q を選んできた ( l = D_{p, q} とする)。

そうすると、残りの各頂点  v に対して、次のことがわかる。

  •  a = D_{p, v},  b = D_{q, v} とする
  • このとき、頂点  v
    •  p からの距離が  \frac{l + a - b}{2} であるような  p-q 上の点を経由して
    • 直径 ab から距離  \frac{a + b - l}{2} だけ離れた地点に位置する
    • このとき、経由した  p-q 上の点が存在しないならば新たに追加しておく

これを用いると、点  p, q 以外の各頂点について、「 p からどの程度の距離が離れた地点を経由することになるか」によって分類できることがわかる。そして同一グループについては、その地点を根とする根付き木に関する問題へと帰着できるので、再帰的に解いていけば OK!!

コード

#include <iostream>
#include <vector>
#include <deque>
#include <map>
using namespace std;

template<class T> inline bool chmax(T& a, T b) { if (a < b) { a = b; return 1; } return 0; }
template<class T> inline bool chmin(T& a, T b) { if (a > b) { a = b; return 1; } return 0; }

int K, N;
vector<vector<long long> > D;
vector<vector<long long> > G;

void solve(const vector<int> &nodes) {
    if (nodes.size() <= 1) return;
    if (nodes.size() == 2) {
        int p = nodes[0], q = nodes[1];
        G[p][q] = D[p][q];
        G[q][p] = D[q][p];
        return;
    }
    
    int p = -1, q = -1;
    long long longest = 0;
    for (int i = 0; i < nodes.size(); ++i) 
        for (int j = 0; j < nodes.size(); ++j) 
            if (chmax(longest, D[nodes[i]][nodes[j]]))
                p = nodes[i], q = nodes[j];

    long long d = D[p][q];
    map<long long, vector<int> > bunrui;
    for (int i = 0; i < (int)nodes.size(); ++i) {
        int v = nodes[i];
        if (v == p || v == q) continue;
        long long a = D[v][p], b = D[v][q];
        long long sum = (d + a + b) / 2;
        long long vdis = sum - d;
        long long pos = a - vdis;
        bunrui[pos].push_back(v);
    }
    
    vector<vector<int> > nnodes;
    long long prev_pos = 0;
    int prev_v = p;
    for (auto it : bunrui) {
        long long cur_pos = it.first;
        int nv = -1;
        for (auto v : it.second) if (D[p][v] == cur_pos) nv = v;
        if (nv == -1) nv = N, it.second.push_back(nv), N++;
        nnodes.push_back(it.second);
        
        G[prev_v][nv] = G[nv][prev_v] = cur_pos - prev_pos;
        for (auto v : it.second) {
            if (v == nv) continue;
            D[v][nv] = D[nv][v] = D[p][v] - cur_pos;
        }
        prev_v = nv;
        prev_pos = cur_pos;
    }
    G[prev_v][q] = G[q][prev_v] = D[p][q] - prev_pos;
    for (auto vs : nnodes) solve(vs);
}

int main() {
    while (cin >> K) {
        D.assign(1000, vector<long long>(1000, 0));
        G.assign(1000, vector<long long>(1000, -1));
        for (int i = 0; i < K; ++i) for (int j = 0; j < K; ++j) cin >> D[i][j];

        vector<int> nodes;
        for (int i = 0; i < K; ++i) nodes.push_back(i);
        N = K;
        solve(nodes);

        cout << N << endl;
        for (int i = 0; i < N; ++i) {
            for (int j = i+1; j < N; ++j) {
                if (G[i][j] > 0)
                    cout << i+1 << " " << j+1 << " " << G[i][j] << " ";
            }
        }
    }
}

Codeforces Round #597 (Div. 2) F. Daniel and Spring Cleaning (R2300)

桁 DP 苦手すぎる...

問題概要

整数  l, r が与えられる。整数の組  (x, y) であって、

  •  l \le x, y \le r
  •  x xor  y =  x + y

を満たすものの個数を求めよ。
(というテストケースが  T 個与えられる)

制約

  •  1 \le T \le 100
  •  0 \le l \le r \le 10^{9}

考えたこと

とりあえず、 x xor  y =  x + y という条件は

 x y を二進法表現したときに、ともに 1 が立っている桁はない」

と同値になる。そして、  l \le x, y という条件が嫌なので、次のように考える。

  •  f(a, b) :=  0 \le x \le a,  0 \le y \le b,  x xor  y =  x= y を満たす  (x, y) の個数

とする。このとき、求める値は

 f(r, r) - f(l-1, r) - f(r, l-1) + f(l-1, l-1)

と求められる。さらに、 f(a, b) の値は桁 DP によって求められる。

桁 DP へ

桁 DP は、「最上位桁からやる」のと「1 の位からやる」のとやり方があるけど、今回は 1 の位からやる方が楽そう。

 (x, y) の 1 の位を (0, 0), (1, 0), (0, 1) の 3 パターンのうちのどれにうするかを試した上で、より上位の桁の問題に帰着させていく。

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

using pll = pair<long long, long long>;
map<pll, long long> dp;
long long func(long long a, long long b) {
    if (dp.count(pll(a, b))) return dp[pll(a, b)];
    if (a < 0 || b < 0) return 0;
    if (a == 0) return b+1;
    if (b == 0) return a+1;

    long long res = 0;

    // (0, 0)
    res += func(a/2, b/2);

    // (0, 1)
    if (b % 2 == 0) res += func(a/2, b/2-1);
    else res += func(a/2, b/2);

    // (1, 0)
    if (a % 2 == 0) res += func(a/2-1, b/2);
    else res += func(a/2, b/2);

    return dp[pll(a, b)] = res;
}

int main() {
    int T; cin >> T;
    while (T--) {
        int l, r;
        cin >> l >> r;
        cout << func(r, r) - func(l-1, r) - func(r, l-1) + func(l-1, l-1) << endl;
    }
}

CPSCO2019 Session3 D - Decode RGB Sequence (400 点設定)

最初は文字列  S を文字列  T にできるかを問うつもりだった。てんぷら君のおかげで、 S を真っ白にすることで、シンプルな問題になった!

問題概要

 N マスがあって最初はすべて白く塗られている。ここに "RGB" のスタンプを押していく。スタンプを押すと次にようになる。

  • 連続する 3 マスが「R」「G」「B」に上塗りされる

スタンプは好きな回数だけ好きな場所に好きな順序で押すことができる。最終結果を文字列  S ('R', 'G', 'B' のみからなる) の状態にすることは可能か?

制約

  •  3 \le N \le 10^{5}

解法

まずこの操作が「スタンプが押された箇所の過去の履歴を消して上書きする」というタイプの操作であることに注目しよう。そのような操作をする問題では「後ろから見る」というのが案外有効だったりする。

今回、文字列  S が作れるとしたとき、 S の最後に押された箇所には "RGB" とスタンプされているはずである。よって

  •  S が "RGB" を含まない場合は不可能

ということが直ちに言える。そして操作列を逆から見ると、

「すでに白色でなくなっている箇所は変更せず、白色の部分にはスタンプの該当文字が押される」

という風に表現できることがわかる。

 

解法(1):ちょっと大変なシミュレーション

上に述べたことを実際に行う解法が考えられる。つまり、 S の中のそれぞれの "RGB" に対して

  • 各 "RGB" から左側へと "R" や "RG" で区間ごとに区切っていく
  • 各 "RGB" から右側へと "B" や "GB" で区間ごとに区切っていく

そうしたときに、

  • 左端と右端はすべてくまなく切り取られる
  • それぞれの "RGB" と "RGB" との間には何も残らないか、"G" のみが残る

というふうになれば OK。

 

解法 (2):簡単な特徴づけ

実はもっと簡単な特徴づけもできる!!!実は以下が必要十分!!!


  • 左端は 'R'
  • 右端は 'B'
  • "RB" と "GG" を含まない

まず、これらの条件を違反していたら不可能であることは自明。逆にこれらを満たすとき、可能であることを言おう。

まず "RGB" が存在しないとダメだけど、"RGB" が存在することは次のように言える。

  •  S の中で最も左側にある "B" を考える
  • "RB" は含まないので、"B" の左隣は "G" である ("B" ではない)
  • "GG" は含まないので、その "G" の左隣は "R" である

よって、 S が "RGB" を必ず含むことがわかった。また、次のように操作を復元できるので大丈夫。

  • 先頭から最初の "B" までの区間は、"GG" が存在しないことから、"R" と "RG" で区切れることになるので OK
    • 先頭が "G" でないことに注意
  • 最初の "B" のその次の "B" を考えると、同様にその "B" も "RGB" の一部になっていることが言える。そして最初の "B" とその次の "B" との間の区間については、次のことが言えるので OK
    • 先頭が "G" である可能性はある (問題ない)
    • それ以外は "R" と "RG" のみで区切ることができる
  • 以下同様にできる。

以上から、わかりやすい特徴づけが得られたので容易に判定できる。計算量は  O(N)

コード

#include <iostream>
#include <string>
using namespace std;

bool solve(int N, const string &S) {
    if (S[0] != 'R') return false;
    if (S.back() != 'B') return false;

    for (int i = 0; i < N-1; ++i) {
        if (S[i] == 'R' && S[i+1] == 'B') return false;
        if (S[i] == 'G' && S[i+1] == 'G') return false;
    }
    return true;
}

int main() {
    int N;
    string S;
    cin >> N >> S;

    if (solve(N, S)) cout << "Yes" << endl;
    else cout << "No" << endl;
}

CPSCO2019 Session3 E - Enumerate Xor Sum (500 点設定)

これ、てんぷら君のおかげで随分とシンプルな問題になった!

問題概要

 N 個の 0 以上の整数  A_{1}, \dots, A_{N} が与えられる。各  k = 1, 2, \dots, N に対して、

  •  X = A_{1} xor  A_{2} xor  \dots xor  A_{k} としたときの
  •  (A_{1} xor  X) + (A_{2} xor  X) + \dots + (A_{k} xor  X) の値を求めよ。

制約

  •  1 \le N \le 3 \times 10^{5}
  •  0 \le A_{i} \lt 2^{30}

解法

XOR に関する問題は各桁ごとに考えるのは常套手段ではある。つまり、各桁  d に対して、次のように考える。

  •  A_{1}, \dots, A_{k} の中に  d 桁目が 1 になっているものの個数を  P としたとき、次のようになる。

    •  P が奇数ならば  X d 桁目は 1
    •  P が偶数ならば  X d 桁目は 0
  • これを踏まえて、 (A_{1} xor  X), (A_{2} xor  X), \dots, (A_{k} xor  X) のうち、 d 桁目が 1 となっているものの個数  Q は次のように求められる。

    •  P が奇数のときは  A_{1}, \dots, A_{k} d 桁目が 0-1 反転するので、 Q = k - P
    •  P が偶数のときは、 Q = P

このようにして求めた  Q に対して、 Q \times 2^{d} を合計していけば答えが求められる。

差分更新へ

上記の作業を  k = 1, 2, \dots, N に対して順に実施していく必要がある。しかし、 P, Q の値は、差分だけ見て更新することが可能である。

よって、全体の計算量は、 D を最大の桁数として  O(ND) で抑えられる。

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int N; cin >> N;
    vector<long long> a(N);
    for (int i = 0; i < N; ++i) cin >> a[i];

    const int DIGIT = 35;
    vector<int> ones(DIGIT, 0);
    for (int k = 0; k < N; ++k) {
        long long res = 0;
        for (int d = 0; d < DIGIT; ++d) {
            if (a[k] & (1LL<<d)) ++ones[d];
            res += (1LL<<d) * (ones[d] % 2 == 0 ? ones[d] : k+1 - ones[d]);
        }
        printf("%lld\n", res);
    }
}

CPSCO2019 Session3 G - Grand Election (800 点設定)

資源配分問題とも言われるタイプの問題。均等配分からの摂動で良さそう (嘘解法) だと騙すことを狙った!

問題概要

 N 個の正の整数  a_{1}, \dots, a_{N} (総和を  A とする) が与えられたとき、

  •  x_{1} + \dots + x_{N} = X
  •  |\frac{x_{1}}{a_{1}} - \frac{X}{A}| + \dots + |\frac{x_{1}}{a_{1}} - \frac{X}{A}| が最小値となる

を満たすような非負整数の組  (x_{1}, \dots, x_{N}) を求めよ。複数通り考えられる場合は辞書順最小のものを求めよ。

制約

  •  1 \le N \le 10^{5}
  •  1 \le X \le 10^{9}
  •  1 \le a_{i} \le 100

解法

まず問題の見た目から、次のような嘘解法が多発することを狙った!

  •  \frac{x_{i}}{a_{i}} \le \frac{X}{A} を満たす範囲でギリギリまで  x_{i} を高めた状態をまず作る
    • この段階では  x の総和は  X 以下になるはず
  • そこから少しずつ  x を高めていく
    • それによる増分が最小になるような部分を Greedy に選んでいく

しかしコーナーケースとして、次のようなケースがある!

これらのコーナーケースを作るのに、とても苦労した!!実際テキトーにケースを作ると、上記の嘘解法が大体正しいのだ。

 

正しい解法

この問題を次のような問題と読み替えよう!


初期状態では  x_{1}, \dots, x{N} はすべて 0 であるとする。スコアは初期状態では  N|\frac{X}{A}| である。今から以下の操作をちょうど  K 回行う。

  •  i を一つ選んで  x_{i} を 1 増やす

このとき、スコアは

 |\frac{x_{i}}{a_{i}} - \frac{X}{A}| - |\frac{x_{i} + 1}{a_{i}} - \frac{X}{A}|

だけ減少する。スコアの最小値を求めよ。


たとえば、 a = (2, 3, 5) X = 13 のときは、次のようになる。たとえば  x_{1} = 0 から  x_{1} = 1 へと増やすとき、スコアは  \frac{1}{2} だげ減少する。一方  x_{1} = 2 から  x_{1} = 3 へと増やすとき、スコアは  \frac{1}{10} だげ減少する。

そして、問題は、上のように各  i について、 x_{i} を 1 増やしたときのスコアの減少分を並べて行ったときに、これらの値から  X 個を左詰めで選ぶ方法のうち、総和が最大のものを求める問題ということになる。

ここで注目したいことは、どの  i に対しても、「 x_{i} を 1 増やしたときのスコアの減少分」は広義単調減少だということだ。このことから、次にことがいえる


 i についての系列を、全部まとめたものをソートして、大きい順に  X 個とればよい


実装上は、ソートか priority_queue を使えば OK。

 

コード

精度の問題から、有理数型か、long double 型が必要となることに注意。

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

long long calc_gcd(long long a, long long b) {return b ? calc_gcd(b, a % b) : a;}
struct frac {
    long long first, second;

    using D = long double;
    inline frac normalize() {
        if (second < 0) {first = -first; second = -second;}
        long long d = calc_gcd(abs(first), abs(second));
        if (d == 0) {first = 0; second = 1;}
        else {first /= d; second /= d;}
        return *this;
    }
    frac(long long f = 0, long long s = 1) : first(f), second(s) { normalize(); }
    inline D to_d() const { return D(first) / second; }
    inline frac operator - () { (*this).first *= -1; return (*this); }
    inline const frac& operator = (long long a) { *this = frac(a, 1); return *this; }
    inline const frac& operator += (const frac& a);
    inline const frac& operator += (long long a);
    inline const frac& operator -= (const frac& a);
    inline const frac& operator -= (long long a);
    inline const frac& operator *= (const frac& a);
    inline const frac& operator *= (long long a);
    inline const frac& operator /= (const frac& a);
    inline const frac& operator /= (long long a);
    inline friend ostream& operator << (ostream& s, const frac& f) { 
        s << f.first; if (f.second != 1) s << "/" << f.second; return s;
    }
};
inline bool operator == (const frac &a, const frac&b) {
    return a.first * b.second == a.second * b.first;
}
inline bool operator != (const frac &a, const frac &b) { return !(a == b); }
inline bool operator < (const frac& a, const frac& b) {
    return a.first * b.second < a.second * b.first;
}
inline bool operator > (const frac& a, const frac& b) { return b < a; }
inline bool operator <= (const frac& a, const frac& b) {
    return a.first * b.second <= a.second * b.first;
}
inline bool operator >= (const frac& a, const frac& b) { return b <= a; }
inline frac operator + (const frac& a, const frac& b) {
    frac res;
    res.first = a.first * b.second + a.second * b.first;
    res.second = a.second * b.second;
    res.normalize();
    return res;
}
inline frac operator - (const frac& a, const frac& b) {
    frac res;
    res.first = a.first * b.second - a.second * b.first;
    res.second = a.second * b.second;
    res.normalize();
    return res;
}
inline frac operator * (const frac& a, const frac& b) {
    frac res;
    res.first = a.first * b.first;
    res.second = a.second * b.second;
    res.normalize();
    return res;
}
inline frac operator / (const frac& a, const frac& b) {
    frac res;
    res.first = a.first * b.second;
    res.second = a.second * b.first;
    res.normalize();
    return res;
}
inline frac abs(const frac& a) {
    frac res; res = a; res.normalize(); 
    if (res.first < 0) res.first = res.first * (-1);
    return res;
}
inline const frac& frac::operator += (const frac& x) {*this = *this + x; return *this;}
inline const frac& frac::operator += (long long x) {*this = *this + x; return *this;}
inline const frac& frac::operator -= (const frac& x) {*this = *this - x; return *this;}
inline const frac& frac::operator -= (long long x) {*this = *this + x; return *this;}
inline const frac& frac::operator *= (const frac& x) {*this = *this * x; return *this;}
inline const frac& frac::operator *= (long long x) {*this = *this * x; return *this;}
inline const frac& frac::operator /= (const frac& x) {*this = *this / x; return *this;}
inline const frac& frac::operator /= (long long x) {*this = *this / x; return *this;}



int N;
long long X, A;
vector<long long> a;


frac eval(long long x, int i) {
    return abs(frac(x, a[i]) - frac(X, A));
}

int main() {
    cin >> N >> X;
    a.resize(N);
    A = 0;
    for (int i = 0; i < N; ++i) cin >> a[i], A += a[i];

    for (int i = 0; i < N; ++i) {
        long long lim = X * a[i] / A;
    }

    vector<long long> res(N, 0);
    priority_queue<pair<frac,int> > que;
    for (int i = 0; i < N; ++i) que.push({eval(0, i) - eval(1, i), i});
    long long X2 = X;
    while (X2 > 0) {
        auto p = que.top(); que.pop();
        int i = p.second;
        long long x = res[i];

        frac diff = eval(x, i) - eval(x+1, i);
        frac diff2 = eval(x+1, i) - eval(x+2, i);
        
        if (diff != diff2) {
            res[i] += 1;
            --X2;
            que.push({diff2, i});
        }
        else if (x == 0) {
            long long lim = X * a[i] / A;
            if (X2 >= lim) {
                X2 -= lim;
                res[i] += lim;
                frac diff3 = eval(lim, i) - eval(lim+1, i);
                que.push({diff3, i});
            }
            else {
                res[i] += X2;
                X2 = 0;
            }
        }
        else {
            res[i] += X2;
            X2 = 0;
        }
    }

    for (int i = 0; i < N; ++i) cout << res[i] << endl;
}