AtCoder Regular Contest 053 C - 魔法使い高橋君

問題

 N(\le 10^5)個の魔法があり、i番目の魔法を唱えると気温がa_i(\le 10^9)度上がってb_i(\le 10^9)度下がる。全ての魔法を1回ずつ唱えるとき、この間の気温の最大値を最小化せよ。

解法

 まずa_i \lt b_iであるものについてa_iの昇順に、次にa_i = b_iであるものについて任意の順番に、最後にa_i \gt b_iであるものについてb_iの降順に使用していけば良い。

 a_i \lt b_iであるものについてa_iの昇順に使用すれば良いことはわかりやすいが、a_i \gt b_iであるものについてb_iの降順に使用すれば最善となることについては、逆から見た状況を考えると良い。最後の温度から考え始めてa_i \gt b_iであるものを用いて気温の最大値を最小化するのは、最初の気温からa_i \lt b_iであるものを用いて最小化するのと変わらない状況である。

反省

 1時間12分(0WA)でAC。1時間考察したがコードを書く段階には到達せず、結局1行も書かないまま解説を見ることになった。

 これが解けないのはちょっとつらいところ。a_ib_iの大小関係で場合分けして考えることはしていたが、a_i \gt b_iであるものについてどういう順番で使用すれば最善なのかがわからなかった。逆から見るという発想が完全に抜けてしまっていたとは……。そうでなくとも大きいbが来ればそれ以降はその中に変化が収まってしまうみたいなところは考えていたのになぁ。残念。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    ll N;
    cin >> N;
    vector<pair<ll, ll>> lesser, equal, greater;
    for (ll i = 0; i < N; i++) {
        ll a, b;
        cin >> a >> b;
        if (a > b) {
            greater.emplace_back(a, b);
        } else if (a == b) {
            equal.emplace_back(a, b);
        } else {
            lesser.emplace_back(a, b);
        }
    }

    sort(lesser.begin(), lesser.end(), [&](pair<ll, ll>& lhs, pair<ll, ll>& rhs) {
        return lhs.first < rhs.first;
    });
    sort(greater.begin(), greater.end(), [&](pair<ll, ll>& lhs, pair<ll, ll>& rhs) {
        return lhs.second > rhs.second;
    });

    ll ans = 0, curr = 0;
    for (auto p : lesser) {
        curr += p.first;
        ans = max(ans, curr);
        curr -= p.second;
    }
    for (auto p : equal) {
        curr += p.first;
        ans = max(ans, curr);
        curr -= p.second;
    }
    for (auto p : greater) {
        curr += p.first;
        ans = max(ans, curr);
        curr -= p.second;
    }

    cout << ans << endl;
}

AtCoder Regular Contest 061E - すぬけ君の地下鉄旅行 / Snuke's Subway Trip

問題

 N(\le 10^5)個の駅がM(\le 2\times 10^5)個の路線で繋がれている。各路線は一つの会社によって運営されており、同じ会社の路線を使い続ける限り料金は1である一方、違う会社の路線に乗り換える場合は新たに料金が1かかる。駅1から駅Nまで移動するのにかかる料金の最小値を求めよ。

解法

 頂点に「最後に使った路線の会社」の情報も加えてダイクストラ法を行う。このとき普通に行うと頂点数がO(M)、一つの頂点から出ている辺の数がO(M)となり、全体で辺の数がO(M^2)となって間に合わない。

 各頂点について違う会社に乗り換えるときはある特殊な部分を通過するということにする。これによって辺の総数はO(N + M)となり解けた。

反省

 2時間19分(8WA)でAC。最初は会社ごとにノードを作ってそれでダイクストラ法をやれば解けると勘違いしてWA(TLE, MLE含む)。これは同じ会社が管理しているものでも飛び石になったりしている場合があるし、計算量も全然十分ではなくてんでダメ。

 次はダイクストラ法の各更新部分でdfsして同じ会社の路線を埋めていくという方針でやったがWA。最短で行ける場合ではなくとも、その後に同じ会社の路線が多く続いていて有利になるパターンというのはあるので最短だけ取ってもダメ。

 この二つをやって1時間20分くらい経ってしまったので諦めて解説PDFを見たが、辺の数がO(M^2)になるという説明が理解できず(辺の総数と、ある頂点における辺の数とを混同していた!)愚直な辺の構築をやってTLEというのを2回ほど。

 テストケースが公開されているのを思い出してTLEになった部分をやってみると、priority_queueのサイズがO(M)になって各頂点でO(M)の辺の数を調べていくのでそれはそうだということにようやく気付いた。

 あとは解説で書かれているように、乗り換えるパターンを書いてAC。あまりきれいなコードにはならなかった。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);

    ll N, M;
    cin >> N >> M;

    struct Edge {
        ll to, c;
    };
    //edges[i][j] := 頂点iに路線jでいるときコスト0で行ける次の頂点
    vector<map<ll, vector<ll>>> edges(N);
    for (ll i = 0; i < M; i++) {
        ll p, q, c;
        cin >> p >> q >> c;
        p--; q--; c--;
        edges[p][c].push_back(q);
        edges[q][c].push_back(p);
    }

    constexpr ll INF = INT_MAX;
    vector<map<ll, pair<bool, ll>>> cost(N);
    struct Element {
        ll node, cost, c;
        bool operator>(const Element& rhs) const {
            return cost > rhs.cost;
        }
    };
    priority_queue<Element, vector<Element>, greater<Element>> pq;

    cost[0][-1] = { true, 0 };
    pq.push({ 0, 0, -1 });

    while (!pq.empty()) {
        Element t = pq.top();
        pq.pop();

        if (cost[t.node][t.c].first && t.cost > cost[t.node][t.c].second) {
            continue;
        }

        if (t.c == -1) {
            for (auto p : edges[t.node]) {
                ll new_cost = t.cost + 1;
                for (auto next : p.second) {
                    if (!cost[next][p.first].first || new_cost < cost[next][p.first].second) {
                        cost[next][p.first].first = true;
                        cost[next][p.first].second = new_cost;
                        pq.push({ next, new_cost, p.first });
                    }
                }
            }
        } else {
            ll new_cost = t.cost;
            for (auto next : edges[t.node][t.c]) {
                if (!cost[next][t.c].first || new_cost < cost[next][t.c].second) {
                    cost[next][t.c].first = true;
                    cost[next][t.c].second = new_cost;
                    pq.push({ next, new_cost, t.c });
                }
            }
            //-1への遷移
            if (!cost[t.node][-1].first || new_cost < cost[t.node][-1].second) {
                cost[t.node][-1].first = true;
                cost[t.node][-1].second = new_cost;
                pq.push({ t.node, new_cost, -1 });
            }

        }
    }

    ll ans = (cost[N - 1][-1].first ? cost[N - 1][-1].second : INF);

    cout << (ans == INF ? -1 : ans) << endl;
}

SRM519 Div1 Easy - BinaryCards

問題

 なんだっけ。

解法

 なるほどなぁ。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

class BinaryCards {
public:
    long long largestNumber(long long A, long long B) {
        if (A == B) {
            return B;
        }
        ll ans = 0;
        while (true) {
            ll a = f(A);
            ll b = f(B);
            if (a != b) {
                return ans + b * 2 - 1;
            }
            ans += a;
            A -= a;
            B -= b;
        }
    }
private:
    ll f(ll x) {
        for (ll i = 1; ; i *= 2) {
            if (i > x) {
                return i / 2;
            }
        }
    }
};

SRM518 Div1 Easy - LargestSubsequence

問題

 辞書順最大の部分文字列を求めよ。

解法

 大きい文字から取っていって、一番最後に取った位置を記録しておく。次の文字はそこからスタート。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

class LargestSubsequence {
public:
    string getLargest(string s) {
        string ans;
        ll start = 0;
        for (ll i = 25; i >= 0; i--) {
            for (ll j = start; j < s.size(); j++) {
                if (s[j] - 'a' == i) {
                    ans += s[j];
                    start = j;
                }
            }
        }
        return ans;
    }
};

AtCoder Beginner Contest 112

結果

 36分22秒(3WA)で全完。89位。WAが多すぎた。

A問題

 場合を分ける。

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    ll N;
    cin >> N;
    if (N == 1) {
        cout << "Hello World" << endl;
    } else {
        ll A, B;
        cin >> A >> B;
        cout << A + B << endl;
    }
}

 提出

B問題

 t_i \le Tであるものの中での最小を取る。

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    ll N, T;
    cin >> N >> T;
    ll ans = INT_MAX;
    for (ll i = 0; i < N; i++) {
        ll c, t;
        cin >> c >> t;
        if (t <= T) {
            ans = min(ans, c);
        }
    }

    if (ans == INT_MAX) {
        cout << "TLE" << endl;
    } else {
        cout << ans << endl;
    }
}

 提出

C問題

 C_xC_yについて全探索する。そのときにまずHをどれかで決定しておき、その他について全部試してみて矛盾が生じないかを確認する。確認の式が間違っていて2回WAを出した。

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    ll N;
    cin >> N;
    vector<ll> x(N), y(N), h(N);
    for (ll i = 0; i < N; i++) {
        cin >> x[i] >> y[i] >> h[i];
    }

    for (ll cx = 0; cx <= 100; cx++) {
        for (ll cy = 0; cy <= 100; cy++) {
            ll H = -1;
            for (ll i = 0; i < N; i++) {
                if (h[i] != 0) {
                    H = h[i] + abs(cx - x[i]) + abs(cy - y[i]);
                    break;
                }
            }
            assert(H != -1);
            bool ok = true;
            for (ll i = 0; i < N; i++) {
                if (h[i] != max(H - abs(cx - x[i]) - abs(cy - y[i]), (ll)0)) {
                    ok = false;
                    break;
                }
            }

            if (ok) {
                cout << cx << " " << cy << " " << H << endl;
                return 0;
            }
        }
    }
}

 提出

D問題

 a,bは正の整数としてM = a \times bという形に分解できたとき、a \ge Nなら少なくともN - 1個をb、残り1個をM - b(N - 1) = b(a - N + 1)とすれば全てbの倍数となるから最大公約数はbになり得る。a,bを逆にしたものについても同じことが成り立つ。

 a2から\sqrt{M}まで回していって割り切れたら上の検証を行えばよい。計算量はO(\sqrt{M})なので間に合う。N = 1というコーナーケースにハマって1WA。

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    ll N, M;
    cin >> N >> M;

    if (N == 1) {
        cout << M << endl;
        return 0;
    }

    ll ans = 1;
    for (ll i = 2; i * i <= M; i++) {
        if (M % i == 0) {
            if (i >= N) {
                ans = max(ans, M / i);
            } else if (M / i >= N) {
                ans = max(ans, i);
            }
        }
    }
    cout << ans << endl;
}

 提出

AtCoder Regular Contest 052 C - 高橋くんと不思議な道

問題

 N個の町がM個の道で結ばれている。道はタイプA,Bの二種類が存在し、タイプAのコストは1、タイプBのコストは(今まで通ったタイプBの道の本数) + 1である。全ての町について町0からの最小コストを求めよ。

解法

 タイプBの道はあまり多く使わない方が良い。具体的には解説PDFの通り最小の使用本数+\sqrt{N}程度である。頂点をこれらのタイプBの使用本数で拡張してダイクストラ法を行えばよいわけだが、そもそもこの使用回数がそこまで大きくならないので使用本数が小さい順にpriority_queueから出てくるようにして普通にダイクストラ法をやれば通る。

反省

 1時間53分(5WA)でAC。愚直にダイクストラ法をやってみたり(TLE)、セグメント木を使って高速化(?)してみたり(MLE)、最小全域木問題に帰着したりしないのかなーと思っていろいろ考えていたが1時間経ってしまったので解説を見た。

 なにやら前処理が面倒くさそうだなと思って、同実装するのかcreep04の提出を見てみたらなんか普通のダイクストラ法をやっているようにしか見えない。しばらく考えてから、tupleの順番がb, costの順番になっているのでそれで枝刈り相当のことが実現されているのだと気づいた。

 これで本当に上手くいくのかはきっちり証明できないが、制約の関係から枝刈りっぽいダイクストラ法をやるんだろうなというのは気づきたかったところ。ダイクストラ法は応用が結構あって難しい。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

int main() {
    ll N, M;
    cin >> N >> M;

    vector<vector<vector<ll>>> edges(2, vector<vector<ll>>(N));
    for (ll i = 0; i < M; i++) {
        ll C, A, B;
        cin >> C >> A >> B;
        edges[C][A].push_back(B);
        edges[C][B].push_back(A);
    }

    constexpr ll INF = INT_MAX;
    vector<ll> cost(N, INF);
    struct Element {
        ll node, cost, b_cnt;
        bool operator<(const Element& rhs) const {
            return b_cnt != rhs.b_cnt ? b_cnt < rhs.b_cnt : cost < rhs.cost;
        }
        bool operator>(const Element& rhs) const {
            return b_cnt != rhs.b_cnt ? b_cnt > rhs.b_cnt : cost > rhs.cost;
        }
    };
    priority_queue<Element, vector<Element>, greater<Element>> pq;

    cost[0] = 0;
    pq.push({ 0, 0, 0 });
    while (!pq.empty()) {
        Element t = pq.top();
        pq.pop();
        if (t.cost > cost[t.node]) {
            continue;
        }

        cost[t.node] = t.cost;

        for (ll next : edges[0][t.node]) {
            ll new_cost = t.cost + 1;
            if (new_cost < cost[next]) {
                pq.push({ next, new_cost, t.b_cnt });
            }
        }
        for (ll next : edges[1][t.node]) {
            ll new_cost = t.cost + t.b_cnt + 1;
            if (new_cost < cost[next]) {
                pq.push({ next, new_cost, t.b_cnt + 1 });
            }
        }
    }

    for (ll i = 0; i < N; i++) {
        cout << cost[i] << endl;
    }
}

AtCoder Regular Contest 051 C - 掛け算

問題

 N(\le 50)個の整数a_1, \dots, a_N(\le 10^9)が与えられる。「一番小さいものをA(\le10^9)倍する」という操作をB(\le 10^9)回繰り返した後の整数たちを昇順に並べ出力せよ。

解法

 解説PDFの通り。

 a_1, \dots, a_Nがソートしてあるとして、a_1 \times A \ge a_Nならばそのあと操作される整数はa_1, a_2, \dots, a_N, a_1, a_2, \dots, a_N, a_1, a_2, \dotsとなる。したがってそれ以降Aが何回かけられるかは簡単に求めることができる。

 a_1 \times A \ge a_Nになるまでは愚直にシミュレーションするとすると、これは最大値以外のa_iA倍していって最大値を超えるまで操作をするということなので、高々O(N\log{(\max{a_i})})程度の計算量である。最小値の探索にO(N)かかったとしてもO(N^2\log{(\max{a_i})})

 シミュレーションが終わったあとは高速な累乗を用いてO(N\log{B}) (ここは解説PDFではO(N\log{A})とあったが違う気がする。自分の勘違いだろうか)で計算できるので、これでこの問題が解けた。

反省

 1時間20分(2WA)でAC。

 最初のころは\log_A{a_i}を考えていけば解けるのかと思ったが、最終的に高速累乗を使わなければならないことは目に見えていて、それだと整数型でかける回数を持っていないとダメだということに気づいて頓挫。

 よってこれを商と余り、みたいな感じでA^na_iを超えない最大のnと、そのときの残り部分を持って、a_i = A^n + pというように分解すれば解けそうと思って実装していく。全操作後の最小なnについて二分探索をして、必要な最小操作回数がBとなるように求めていく方針でいけると思っていた。

 1時間経ったくらいタイミングでサンプル1と2は合うものができたが、サンプル3がどうしても合わなず、しばらく格闘したが諦めて解説を見た。

 各iについてnの最大と最小が高々1しか差がないことはサンプルの3のデバッグをしているときに気づいたのに、それでシミュレーションすればいいという発想に至らなかった。これはなかなか気づきが必要で難しいように思えるが、問題の考察が足りなかったということだろう。すぐ知っているアルゴリズムに結び付けようとしてしまうが、その前にもっと問題自体の性質を考えなくてはならないなぁ。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

constexpr ll MOD = (ll)1e9 + 7;

ll MODpow(ll n, ll m) {
    ll result = 1;
    while (m) {
        if (m % 2 == 1) {
            result *= n;
            result %= MOD;
        }

        m /= 2;
        n *= n;
        n %= MOD;
    }

    return result;
}

int main() {
    ll N, A, B;
    cin >> N >> A >> B;
    vector<ll> a(N);
    for (ll i = 0; i < N; i++) {
        cin >> a[i];
    }

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

    if (A == 1) {
        for (ll e : a) {
            cout << e % MOD << endl;
        }
        return 0;
    }

    while (true) {
        auto min_itr = min_element(a.begin(), a.end());
        if (*min_itr * A >= *max_element(a.begin(), a.end())) {
            break;
        }
        *min_itr *= A;
        if (--B == 0) {
            sort(a.begin(), a.end());
            for (ll e : a) {
                cout << e % MOD << endl;
            }
            return 0;
        }
    }

    sort(a.begin(), a.end());
    for (ll i = B % N; i < N; i++) {
        cout << a[i] * MODpow(A, B / N) % MOD << endl;
    }
    for (ll i = 0; i < B % N; i++) {
        cout << a[i] * MODpow(A, B / N + 1) % MOD << endl;
    }
}

AtCoder Regular Contest 050 C - LCM 111

問題

 1A個並べてできる整数をxB個並べてできる整数をyとしたとき、xyの最小公倍数をMで割った余りを求めよ。

解法

 1n個並べてできる整数をf(n)としたとき\mathrm{gcd}(f(A), f(B)) = f(\mathrm{gcd}(A, B))であることがわかる。最小公倍数は\mathrm{LCM}(x, y) = \frac{xy}{\mathrm{gcd}(x, y)}であるので、\frac{f(A)f(B)}{f(\mathrm{gcd}(A, B))}Mで割った余りが求まれば良い。\mathrm{gcd}(A, B) = gとする。ここで\frac{f(B)}{f(g)}は「0g - 1個、1が1個」という整数を\frac{B}{g}個並べてできる整数である。

 例. A = 12, B = 8のとき、f(A) = 111111111111, f(B) = 11111111であり、\frac{f(B)}{f(g)} = \frac{11111111}{1111} = 00010001

 結局f(A), \frac{f(B)}{f(g)}はそれぞれ「ある整数をある回数並べてできる整数」という形で表現できる。これをMで割った余りは並べる回数をnとしたときダブリングによってO(\log{n})で求めることができるので、それぞれ個別に余りを求めた後掛け合わせて最後にMで割った余りを取ればこの問題が解ける。

反省

 51分31秒(0WA)でAC。久しぶりにしっかりと考察して自力で解けたという感じだったが、おそらくこれもコンテストに当時参加していたようなので解説スライドをチラ見したことがあるのだろう。2年半前のコンテストらしいのではっきりと記憶に残っているわけではないが、何となくの方向性などはどこか覚えていて考察が変な方向にいかないというくらいの影響はありそうだ。

 あまりコンテストは休んでないはずなのでここから先は一応見たことがある問題が多いのではないか。B問題で詰まってC問題まで読んでないということはいくらかありそうだが。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

ll A, B, M;

ll MODpow(ll n, ll m) {
    ll result = 1;
    while (m) {
        if (m % 2 == 1) {
            result *= n;
            result %= M;
        }

        m /= 2;
        n *= n;
        n %= M;
    }

    return result;
}

ll f(ll n, ll c) {
    //「0がc - 1個,1が1個」をn回繰り返し並べた数を返す
    ll result = 0;
    ll m = 1;
    ll d = c;
    while (n) {
        if (n & 1) {
            result *= MODpow(10, d);
            result += m;
            result %= M;
        }

        m = m * MODpow(10, d) + m;
        m %= M;
        d *= 2;
        n /= 2;
    }
    return result;
}

ll gcd(ll a, ll b) {
    return (b ? gcd(b, a % b) : a);
}

int main() {
    cin >> A >> B >> M;
    if (A < B) {
        swap(A, B);
    }

    ll g = gcd(A, B);
    cout << f(A, 1) * f(B / g, g) % M << endl;
}

AtCoder Regular Contest 049 C - ぬりまーす

問題

 N(\le 100)頂点のグラフについて、二つのタイプの条件の下に色を塗っていくことを考える。

  • タイプ1:ある頂点xに色を塗るとき、既に頂点yに色が塗られてなければならない。
  • タイプ2:ある頂点uに色を塗るとき、既に頂点vに色が塗られていてはいけない。

 タイプ1の制約はA(\le 100)個、タイプ2の制約はB(\le 10)個である。条件を守る限り任意の順番でグラフの頂点を塗れる場合に、最大で何個の頂点を塗ることができるか求めよ。

解法

 解説スライドはスライド中の文字が消えているため下のテキスト部分から解読した。

 タイプ1の制約だけの場合は、塗れるものを塗っていって変化がなくなるまでループを回し続けることでO(N(N + A))で解ける。タイプ2の制約は「頂点uを塗らないか、あるいは頂点vに色を塗るときには既に頂点uに色が塗られてなければならない」と言い換えることができ、後者はタイプ1の制約である。頂点uを塗らないという制約は簡単に実現できるため、つまりタイプ2の制約については2^B通り試せばよい。Bは小さいのでこれでこの問題が解けた。

反省

 1時間8分(4WA)でAC。最初はタイプ1の制約もタイプ2の制約もそのまま愚直に考えて、変化がなくなるまでループするという解答を書いて出したところ当然WA。サンプルは合うというのがいやらしい。

 頂点を選択する順番をランダムに選択して何度か実行し最大値を出力するという完全な嘘解法を書いてみて出してみたがダメ。WAは確実に減っているが……。これが解き始めてから30分ほどの時点。

 ちゃんと考察しようと考えなおして強連結成分分解とかdfsでの自己ループ検出とか考えてみたが、結局サンプルが合う解法すらひねり出せず1時間が経ったので解説スライドを見た。

 こういう問題を言い換えるタイプの解法が全然思いつかない。結局考察がほとんどできていないんじゃないか。確信を持っていない解法を書き始めるのはいい加減やめよう。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

ll N, A, B;

ll solve(vector<vector<ll>> edge, vector<bool> forbid) {
    vector<bool> colored(N, false);
    ll result = 0;
    while (true) {
        bool change = false;

        for (ll i = 0; i < N; i++) {
            //i個目の頂点が塗れるか
            if (colored[i] || forbid[i]) {
                continue;
            }

            bool ok = true;
            for (ll to : edge[i]) {
                if (!colored[to]) {
                    ok = false;
                    break;
                }
            }
            if (!ok) {
                continue;
            }
            colored[i] = true;
            change = true;
            result++;
        }

        if (!change) {
            break;
        }
    }
    return result;
}

int main() {
    cin >> N;

    vector<vector<ll>> edgeA(N);

    cin >> A;
    for (ll i = 0; i < A; i++) {
        ll x, y;
        cin >> x >> y;
        x--; y--;
        edgeA[x].push_back(y);
    }
    cin >> B;
    vector<ll> u(B), v(B);
    for (ll i = 0; i < B; i++) {
        cin >> u[i] >> v[i];
        u[i]--; v[i]--;
    }

    ll ans = 0;

    for (ll i = 0; i < (1LL << B); i++) {
        auto edge = edgeA;
        vector<bool> forbid(N, false);
        for (ll j = 0; j < B; j++) {
            if ((1LL << j) & i) {
                edge[v[j]].push_back(u[j]);
            } else {
                forbid[u[j]] = true;
            }
        }

        ans = max(ans, solve(edge, forbid));
    }
    
    cout << ans << endl;
}

AtCoder Regular Contest 048 C - 足の多い高橋君

問題

 高橋君には足がN(\le 10^5)本あり、i番目の足はL_i(\le10^9)本の骨が一列に繋がった構造をしている。全ての骨に0または1を書き込むことにし、任意の足2本について片方の足先から胴体を通ってもう一方の足先まで辿って読むと回文になるようにしたとき、文字の書き込み方としてあり得る数を求めよ。

解法

 まずL_iはソートされているものとして考える。次の3つのことが言える。

  • 全ての文字列について先頭L_1文字は等しく、これは任意に0, 1を選べる。
  • 各文字列に対して先頭L_1文字を取り除いた文字列は回文
  • 2つの異なる文字列に対し「1つ目の文字列の先頭L_i文字を取り除いたもの + 2つ目の文字列の先頭L_i文字を取り除いたものの反転」が回文

 ここで3つ目の条件は、組み合わせる2つの文字列は2つ目の条件から回文であることがわかっているので、結局「回文と回文を繋げると回文ができる」ということを示している。ここで「長さXの回文Aと長さYの回文Bを繋げて回文ができるとき、AもBも周期\mathrm{gcd}(X, Y)を持ち、さらにその1周期は回文」である(証明は解説スライドを参照のこと)

 この条件が満たされるようにL_i - L_1たちのgcdを取っていけばこれが最小の周期となる。これが回文となるように選べる部分は2で割った切り上げ。先頭L_1文字分自由に選べるのと合わせて、2をこれらの数の和で累乗したものが答えとなる。

反省

 48分13秒(2WA)でAC。いろいろ考察していたら(足の長さでソートしてuniqueを取ったとして)一番長い足について、一番短い足の分は任意に取れて、あとは残りの部分がL_N - L_1文字の回文かつL_N - L_2文字の回文かつ\dotsかつL_N - L_{N - 1}の回文になればいいというところまではわかった。つまりだいたいans = 2^{L_0 + x}という形になっていてxが特定できればいいと考えた。

 サンプルの3つ目をこの形で解くとx = 5 \times 10^5になることがわかって、これはいかにも10^6 / 2っぽい。そして10^6というのはL_iの差分の最小公倍数っぽいなーというのをエスパーして、あとはサンプル1,2も合うように奇数の場合は+1してから2で割るということをして(まぁ回文だしそれっぽい操作だと思った)適当に投げたらAC。完全に考察ではなくエスパーで解いてしまった。多分これはコンテスト後かなんかで一度解説を読んだことがある気がする。

 同じ文字列長のものがあったときの処理が不安でuniqueを取ってしまったが別にこれは必要なかったか。gcd(0, x) = xになるから問題ないんだな。

コード

#include"bits/stdc++.h"
using namespace std;
using ll = int64_t;

constexpr ll MOD = (ll)1e9 + 7;

ll gcd(ll a, ll b) {
    return (b ? gcd(b, a % b) : a);
}

ll MODpow(ll n, ll m) {
    ll result = 1;
    while (m) {
        if (m % 2 == 1) {
            result *= n;
            result %= MOD;
        }

        m /= 2;
        n *= n;
        n %= MOD;
    }

    return result;
}

int main() {
    ll N;
    cin >> N;
    vector<ll> L(N);
    for (ll i = 0; i < N; i++) {
        cin >> L[i];
    }
    sort(L.begin(), L.end());
    L.erase(unique(L.begin(), L.end()), L.end());

    ll diff_gcd = L[1] - L[0];
    for (ll i = 2; i < L.size(); i++) {
        diff_gcd = gcd(diff_gcd, L[i] - L[i - 1]);
    }

    cout << MODpow(2, L[0]) * MODpow(2, (diff_gcd + 1) / 2) % MOD << endl;
}