hogecoder

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

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の出題がかなり多い気がするので対策したい。

TopCoder SRM 700 Div 2

記念すべき TopCoder SRM 700 に無理やり参加しました。

今まで Div2ぐらし!どころか、はいいろぐらし! だったんですが、今日でやっと緑色になりました。TopCoderレーティング維持するのは難しそうなのでこれからも頑張ります。

では適当に振り返ります。(載せているコードはコンテスト中に書いたものなので、不必要なものがあったり汚かったりしても怒らないでね)

Xylophone (Easy)

問題 → TopCoder Statistics - Problem Statement

これはほんとにやるだけ。気をつけるのは剰余取ることくらい?

class Xylophone {
    public:
    int countKeys(vector <int> keys) {
        int cnt[7] = {};
        rep(i,0,keys.size()) cnt[keys[i] % 7]++;

        int ans = 0;
        rep(i,0,7) if(cnt[i] != 0) ans++;
        return ans;
    }
};

XMarksTheSpot (Med)

問題 → TopCoder Statistics - Problem Statement

Medにしては親切な問題だった。けどちょっとめんどいかもしれない。

まず、 '?' の出た座標をvectorとかで管理しておきます。次に 'O' についてですが、 'O' が出現した座標の中で「最も左上」にあるものと「最も右下」にあるものを覚えておけば、現時点でお宝がある可能性がある長方形領域を把握できるので、その2点を求められるように処理します。

'?' は最大で19しかないので、 '?' が 'O' であるか '.' であるかを全列挙して問題ありません (最大でも 2^{19} = 524288 通りなので、間に合います)。あとはビット演算の力を借りて長方形領域がどう変わるかを判定して答えを足していきましょう。

class XMarksTheSpot {
    public:
    int countArea(vector <string> board) {
        pii lu = pii(INF,-1), rd = pii(-1, INF);
        vector<pii> question;
        rep(i,0,board.size()) {
            rep(j,0,board[i].size()) {
                if(board[i][j] == 'O') {
                    lu.fr = min(lu.fr, j);
                    lu.sc = max(lu.sc, i);
                    rd.fr = max(rd.fr, j);
                    rd.sc = min(rd.sc, i);
                }
                if(board[i][j] == '?') {
                    question.pb(pii(j,i));
                }
            }
        }

        ll x, y, ans = 0;
        x = (ll)rd.fr - lu.fr + 1;
        y = (ll)lu.sc - rd.sc + 1;

        int s = question.size();
        if(question.size() == 0) ans += x * y;
        else {
            rep(n,0,1 << s) {
                pii lun = lu, rdn = rd;
                ll xn, yn;
                rep(k,0,s) {
                    if(n >> k & 1) {
                        int j = question[k].fr;
                        int i = question[k].sc;
                        lun.fr = min(lun.fr, j);
                        lun.sc = max(lun.sc, i);
                        rdn.fr = max(rdn.fr, j);
                        rdn.sc = min(rdn.sc, i);
                    }
                }
                xn = (ll)rdn.fr - lun.fr + 1;
                yn = (ll)lun.sc - rdn.sc + 1;
                ans += xn * yn;
            }
        }
        return ans;
    }
};

XYZCoder (Hard)

問題 → TopCoder Statistics - Problem Statement

本番で解けず。あとでやります。

結果

解答状況

問題 時間 得点 (満点)
Easy 0:02:29.688 248.08 250.00
Medium 0:48:27.729 195.87 450.00
Hard - 0.00 900.00
(Challenge) - 0.00 -
(Sum) - 443.95 1600.00

順位 / レーティング

Ranking: 42nd / 287
Rating: 805 -> 930 (+125)

反省

Med解くのが遅い (予想以上に点が取れなくてびっくりした) 最初長方形領域を求めるのに四隅全部把握しようとしてたしダメ。自分の部屋の人、けっこうMedで落ちてたのにChallengeで1個も拾えなかったしダメ (Challengeの能力ってどうやって身につけたらいいんだろう・・・)

早解きの力、今まで考えていたより相当大事だなと思い知ったので、今度からは解答時間とかも記録するようにしたい。

あと何故か知らないけど、前にプラグイン入れたはずなのに全部忘れられてて慌てて入れなおした。コンテスト30分前くらいから用意してて本当に良かった・・・。

次出る時までに少しでもいいから過去問を解いておきたい。