hogecoder

tsutaj 競技プログラミングの記録

Codeforces Round #378 C: Epidemic in Monstropolis

問題概要

原文 → Problem - 733C - Codeforces

サイズ nの配列 aと、サイズ kの配列 bが与えられる。配列 aに対して、隣り合う要素が自分より小さい場合、自分にその要素の値を加算し、その要素を削除する操作(つまりマージ)が可能である。この操作を繰り返して配列 aを配列 bと一致させることは可能か。もし可能な場合、操作の過程(何番目の要素を左or右とマージさせたか)も出力すること。

解説

この問題はコーナーケースが多く、気をつけないとWAやTLE祭りになってしまいますね。私もwhileループ抜けずにTLE問題に直面しました。

まず、 \sum_{i=1}^{n} a_{i} = \sum_{i=1}^{k} b_{i}を満たしていなければ、明らかに達成できません。この判定は最初にすべきでしょう。

これを満たしている場合、配列 aを適当な k個の区間に分けることを考えます。具体的に言うと、 i個目の区間の要素の総和が b_iと等しくなるように分けます。(このような分け方が存在しなければ達成不可能です)

区間内でどことどこをマージするかですが、区間内で値が最大の要素で隣とマージ可能な要素を選んでマージするのが最適で、なぜならそうすることでその要素が常に区間内max(しかも単独トップ)を保ち続けるからです。そのような要素がなければ残念ながら達成不可能なため、 \verb$"NO"$を出力する必要があります。ちなみに、区間内の要素が1になるまでマージし続けてから次の区間に行ったほうが簡潔に書けます。

達成不可能な条件が多いのでまとめると、

  • 配列 aの総和と配列 bの総和が異なるとき
  • 配列 aを適当な区間に分け、 i個目の区間総和を b_iと等しくする方法がないとき
  • 区間内最大の要素で、隣とマージ可能な要素がないとき

となります。1つでも当てはまれば達成不可能です。

ソースコード

signed main() {
    int sum_a = 0, sum_b = 0;
    int n, k; cin >> n; vector<int> a(n);
    rep(i,0,n) {cin >> a[i]; sum_a += a[i];}
    cin >> k; vector<int> b(k);
    rep(i,0,k) {cin >> b[i]; sum_b += b[i];}
    if(sum_a != sum_b) {
        cout << "NO" << endl;
        return 0;
    }

    vector< pair<int, char> > ans;
    int x = 0;
    rep(i,0,k) {
        int temp = 0;
        vector<int> v;
        while(x != n && temp < b[i]) {
            v.pb(a[x]);
            temp += a[x++];
        }
        if(temp != b[i]) {
            cout << "NO" << endl;
            return 0;
        }

        while(v.size() != 1) {
            bool ok = false;
            int ma = *max_element(v.begin(), v.end());
            rep(j,0,v.size()) {
                if(v[j] == ma) {
                    if(j != 0 && v[j-1] < v[j]) {
                        v[j-1] += v[j];
                        ans.pb(make_pair(j+i, 'L'));
                        v.erase(v.begin() + j);
                        ok = true; break;
                    }
                    else if(j != v.size()-1 && v[j+1] < v[j]) {
                        v[j+1] += v[j];
                        ans.pb(make_pair(j+i, 'R'));
                        v.erase(v.begin() + j);
                        ok = true; break;
                    }
                }
            }
            if(!ok) {
                cout << "NO" << endl;
                return 0;
            }
        }
    }
    cout << "YES" << endl;
    rep(i,0,ans.size())
        printf("%lld %c\n", ans[i].fr+1, ans[i].sc);
    return 0;
}

ちなみにこれはコンテスト終了後に書きなおしたもので、終了直後にACしたものはもっと汚いです(下のリンク参照、こりゃあ間違い誘発するわって感じ・・・)

バグを誘発させる事のないように書くことって大事ですね。

Submission #21956334 - Codeforces

AtCoder Regular Contest 046 B: 石取り大作戦

デジャヴを感じたので。

問題概要

原文 → B: 石取り大作戦 - AtCoder Regular Contest 046 | AtCoder

高橋くんと青木くんは N個の石の山から交互に石を取るゲームを行う。交互に1個以上の石を山から取っていき、最後の石をとったプレイヤーが勝者となる。先手の高橋くんは一度に最大 A個まで、後手の青木くんは一度に最大 B個までの石を取ることができる。両者が最善を尽くした場合、勝利するのはどちらか判定せよ。

(部分点)

  •  A = Bを満たすデータセットに正解した場合は40点が与えられる。
  •  A \neq Bを満たすデータセットに正解した場合は60点が与えられる。
  • 上記の2つのデータセット両方に正解した場合は100点が与えられる。

解説

前回書いたTopCoder SRM701 Div2 Hardの類題です。
記事はこちら → TopCoder SRM 701 Div 2 - hogecoder

前回と違う点は、ビットが立っている云々の制限がないのと、プレイヤーが一度に取れる石の数の最大値が同じでない場合があることですね。

ぶっちゃけいうとこれらの条件が揃っているのでTopCoderの問題よりも簡単になっています。が、やはりやりかたがわからないと解きにくいと思うので頑張って書いてみます。

まず N \leqq Aの場合は、自明に高橋くんが勝利します。これ以降は N > Aの場合について解説するものとします。

 A = Bのとき

これは前回の記事と同様に、「山に A+1 (= B+1個)の石があったら消費するのに必ず2ターンかかる」ことを利用します。 N (A+1)で割り、余りが0であれば後手(青木くん)が、0でなければ先手(高橋くん)が勝利します。

なぜそれが言えるか簡単に述べます。 N \div (A+1)の商を p、余りを qと置くと、 N = p(A+1) + qです。先ほど述べたように、 (A+1)個の山を消費するために必要なターンは2ターンなので、 p(A+1)個の石を消費するためには 2pターン必要です。余りが0の場合は q = 0であるために偶数回のターンで終了し、青木くんの勝利になります。余りが0でない場合は、 q \leqq Aより q個消費するのに1ターンしかかからないため必ず奇数回で終了し、高橋くんの勝利になります。

 A \neq Bのとき

この場合は、実は A=Bのときよりも簡単です。結論から言うと大きいほうが勝ちます。なぜこれが言えるのでしょうか。2つのパターンで考えてみましょう。

 A > Bのとき

先手が1個の石をとって、後手がどのように取ろうとも再び先手の手番が訪れます。これは N-1 > A-1 \geqq Bより N-1 > Bが言えるため明らかです。ゲームを繰り返していくと、いずれ N \leqq Aの盤面で先手の手番が来るため先手必勝となります。

 B > Aのとき

先ほどの議論と同じく、先手がどのように取ろうとも再び後手の手番が訪れ、いずれ N \leqq Bの盤面で後手の手番が来るため後手必勝となります。

コード

三項演算子は簡潔にかけるから好き。

Submission → Submission #958577 - AtCoder Regular Contest 046 | AtCoder

signed main() {
    int n, a, b; cin >> n >> a >> b;
    int ans;
    if(n <= a) ans = 1;
    else {
        if(a == b) {
            int x = n % (a + 1);
            ans = (x ? 1 : 0);
        }
        else ans = (a > b ? 1 : 0);
    }
    cout << (ans ? "Takahashi" : "Aoki") << endl;
    return 0;
}

山のゲームの問題はちょっと慣れた気がするけど、もうちょっと類題を知りたいなあ・・・。Typical DP ContestのB問題みたいに山が2つになったらまた難しくなるしなあ(あれは類題と言えるかどうか怪しいけど)。

TopCoder SRM 701 Div 2

TopCoder SRM 701に参加。誤読の恐ろしさとチャレンジの重要性を知った回でした。

SquareFreeString (Easy)

問題 → TopCoder Statistics - Problem Statement

SquareFreeStringとは、文字列Sの中で、長さが1以上の部分文字列Tが連続して(つまり、TT と書ける)登場しない文字列のことです。

例えば、"apple"は、長さ1の部分文字列"p"が連続している箇所が存在するので、SquareFreeStringではありません。また、"people"は同じく長さ1の部分文字列"p"が2つ存在しますが、連続していないのでSquareFreeStringであると言えます。

これを解くためには、

  • 長さが 2 〜 (文字列Sの長さ)の偶数である部分文字列を列挙
  • その部分文字列を半分に割ったものをs1, s2とおいたとき、s1 == s2となるものが存在するならば"not square-free"
  • そのような要素が存在しなければ "square-free"

という方針を取ればよいです。文字列の長さは最大50なので計算量的にも余裕です。

どうやら部分文字列の意味を取り違った人が多かったようで(私も最初間違えました)、同じアルファベットがあるかどうか調べている人がけっこういましたね。今回は「部分列」ではなく「部分文字列」を扱っており、連続したものを見なければいけないのでそれだと方針が違います。ちなみにその間違いが多いだろうなと睨んでいた私はチャレンジで2人落としました(チャレンジしたの初めてだったので成功して嬉しかった)

class SquareFreeString {
    public:
    string isSquareFree(string s) {
        int len = s.length();
        bool ok = false;
        repq(i,2,len) {
            if(i % 2 == 1) continue;
            repq(j,0,len-i) {
                string temp, s1, s2;
                temp = s.substr(j,i);
                s1 = temp.substr(0,i/2);
                s2 = temp.substr(i/2);
                if(s1 == s2) ok = true;
            }
        }
        if(ok) return "not square-free";
        else return "square-free";
    }
};

SortingSubsets (Med)

問題 → TopCoder Statistics - Problem Statement

これ、本当にMedか???と思うくらい簡単だった。個人的にはEasyより全然簡単だった。

与えられる配列をソートしたものと元の配列でdiffとる。以上!

class SortingSubsets {
    public:
    int getMinimalSize(vector<int> a) {
        vector<int> t = a;
        sort(t.begin(), t.end());
        int ans = 0;
        rep(i,0,a.size()) if(a[i] != t[i]) ans++;
        return ans;
    }
};

ThueMorseGame (Hard)

問題 → TopCoder Statistics - Problem Statement

提出したけどSystem testで落とされた。通したの1人しかいなかったみたい。

コンテスト中に考察したこととか (嘘解法)

嘘解法なので参考にしないでください、自戒を込めて書いてます。

  • 最善な行動を取るので、even in binaryにしにいくような選択をとるに決まっている
  • ある人が負けるときのパターンは、odd in binaryにしてしまう選択肢しか残されていないときか、前の手番で石を全て取られた時しかない
  • → 前の手番で石を全て取られたら負けなのだから、なるべく多く取るのが最善なのか? (最善ではなかった)
  • → 現在残っている山から石を取る個数のパターンを、大きい方から全通り試せばodd in binaryを選ばざるを得ないかどうか分かる?
  • → サンプル通ったからこれでええやろ(適当)
  • Failed System test

という感じでした。つらいですね。というわけで真面目に解法書きます。

正しい考察

  • mが1であるとき ... 1減らすしか選択肢がないため、nを1ずつ減らしてodd in binaryになった時のステップ数から勝敗を判定。
  • それ以外のとき ... 前の手番で、相手に石を全て取られないように動きたい
  • → どちらにとっても、今の自分の手の2つ前(直前に行った自分の手)を行ったあとに、山に(m+1)個の石が置いてあれば必ずこれを達成できる。なぜなら、相手のターンで1〜m個の石を取っており、その結果山に残るのは1〜m個になり、全て取ることができるからである。
  • → 実際にはodd in binaryにしてはいけない制約もあるため、「山に(m+1)以上でeven in binaryを満たす最小個数の石」が置かれていれば達成できる。
  • → 逆再生のように、山が0の状態から上記の条件を満たす数ずつ増えていくものとして考え、山にある個数, m, nの大小関係で勝敗を判定すればよい。

ちなみにodd in binaryの判定についてですが、gccビルトイン関数で"__builtin_popcount(unsigned int)"というものがあって、これはunsigned intを2進数表記した時に1が立っているビット数を返してくれる関数のようです。知らんかった・・・。まあ普通に自前で関数作っても問題ないですけどね。

ビット関係のビルトイン関数についてはこちらが参考になると思います。

Tech Tips: ビットを操るgccビルトイン関数たち

class ThueMorseGame {
    public:
    string get(int n, int m) {
        // 1度に1個しか取れない時
        if(m == 1) {
            if(n == 1) return "Alice";
            int flag = 0;
            while(1) {
                n--;
                if(__builtin_popcount(n) & 1) {
                    if(flag) return "Alice";
                    else return "Bob";
                }
                flag = 1 - flag;
            }
        }

// pile := 山の数 (逆再生だと思えば良い)
// pileには常に、消費に2ターン必要な数が足されるため、
// pileは常に後攻(Bob)サイドにとって有利な数になる。
// → 足す数を s と置く。
// → pile + s > n であればアウト (山の数オーバー)
// → つまり、pile + s <= n が成り立たなければならない
        int pile = 0;

// 条件を満たす数ずつ山を増やす操作を考える
// 山はnを超えない (pile + (m+1) <= n → pile + m < n)
        while(pile + m < n) {
            pile += m + 1;
            // even in binaryにするための操作
            while(__builtin_popcount(pile) & 1) pile++;
        }

// 最終結果でpileに余裕がある、つまり pile < n である場合、
// Aliceの最初の操作でnからpileになるように
// 適当に取ることができるためAliceの勝ち
// そうでなければBobの勝利になる。
        if(pile < n) return "Alice";
        else return "Bob";
    }
};

結果

解答状況

問題 時間 得点 (満点)
Easy 1:12:23.087 (Resubmit) 75.00 250.00
Medium 0:01:52.010 497.84 500.00
Hard 0:31:49.838 494.900.00 900.00
(Challenge) Success +2 100.00 -
(Sum) - 672.84 1600.00

順位 / レーティング

Ranking: 115th / 376
Rating: 930 -> 961 (+31)

反省

Easy誤読はいただけない。というか絶対やっちゃダメ。早く解くのも大事だけど正確にやらないとダメ。Medが早く解けたのはよかった。取れる問題は確実に取らないとね。Hardは自分の実力は出した感じがあるので、まあ致し方なし。復習はちゃんとする。

あとチャレンジ超大事。チャレンジしていなければ順位が90くらい落ちてたので本当に危なかった。特に今回のように落とし穴が想定できる問題に関しては必ず目を通す。他のRoomの人にもチャレンジできる知見を得たので次回以降は活用していきたい。

大学のWi-Fiだとなぜかアプレット開かないけどWebArenaは使えることが判明し、大学でもTopCoderできるようになったしぼちぼち過去問を解き始めたい。どっかのサイトで読んだとおり、全探索やDPの出題がかなり多い気がするので対策したい。