AtCoder Regular Contest 026 C - 蛍光灯

問題

 西側0:メートル地点から東側へLメートル分の廊下がある。この廊下にはN個の蛍光灯が付いており、i番目の蛍光灯はl_i地点からr_i地点の間を照らすことができ、点けるのにc_i円の費用がかかる。廊下をすべて照らすときの最小費用を求めよ。

解法

 まず蛍光灯を左端の値でソートする。i番目の蛍光灯を使うときは、それまでの蛍光灯でi番目の蛍光灯の左端から右端までの間のどこかに照らすコストの最小値が来ているはずである。これはセグメント木を用いればO(\log{N})で求めることができる。そして最小コストをi番目の蛍光灯の右端の位置に記録すれば、それ以降の蛍光灯でそこを含むものがその最小コストを拾うことができる。

別解

 各地点をノードとし、蛍光灯のエッジを張る。また戻る向きで隣のノードへはコスト0で行けるものとしてさらにエッジを張ると地点0から地点Lまで行く最短経路問題に帰着する。賢い。

 creep04のコード

反省

 1時間17分(3WA)でAC。16分ほどで部分点解法はわかったが、そこから計算量を落とす方法がわからなかった。部分点解法として考えたのが更新を範囲で行い、値の取得を点で行うようなDPであったため、それをそのままセグメント木に落とそうとすると遅延評価セグメント木となり、いろいろなサイトを調べて実装してみたがバグを取り切れなかった。

 結局1時間ほどたってから解説スライドを見たところ、更新を点で行い値の取得を範囲で行っても解けるとのことだった。これであれば素直なセグメント木を使って解くことができるため、実装でもバグりにくいのだろう。その方針で(セグメント木はコピペして)書いて通した。

 たまたまcreep04の提出も見てみたらダイクストラ法で解いていて目から鱗だった。問題で使われるモチーフが橋とかだったらこれにもすぐ気づけたりするんだろうか。本質が見えていない証拠である。

コード

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

constexpr ll INF = LLONG_MAX / 2;

//http://tsutaj.hatenablog.com/entry/2017/03/29/204841 のコピペ(一部改変)
struct SegmentTree {
private:
    ll n;
    vector<ll> node;

public:
    // 元配列 v をセグメント木で表現する
    SegmentTree(vector<ll> v) {
        // 最下段のノード数は元配列のサイズ以上になる最小の 2 冪 -> これを n とおく
        // セグメント木全体で必要なノード数は 2n-1 個である
        ll sz = v.size();
        n = 1; while (n < sz) n *= 2;
        node.resize(2 * n - 1, INF);

        // 最下段に値を入れたあとに、下の段から順番に値を入れる
        // 値を入れるには、自分の子の 2 値を参照すれば良い
        for (ll i = 0; i < sz; i++) node[i + n - 1] = v[i];
        for (ll i = n - 2; i >= 0; i--) node[i] = min(node[2 * i + 1], node[2 * i + 2]);
    }
    void update(ll x, ll val) {
        // 最下段のノードにアクセスする
        x += (n - 1);

        // 最下段のノードを更新したら、あとは親に上って更新していく
        node[x] = min(node[x], val); //ここでminを取る処理を入れないとサンプルが合わない<------------改変部分
        while (x > 0) {
            x = (x - 1) / 2;
            node[x] = min(node[2 * x + 1], node[2 * x + 2]);
        }
    }
    
    // 要求区間 [a, b) 中の要素の最小値を答える
    // k := 自分がいるノードのインデックス
    // 対象区間は [l, r) にあたる
    ll getmin(ll a, ll b, ll k = 0, ll l = 0, ll r = -1) {
        // 最初に呼び出されたときの対象区間は [0, n)
        if (r < 0) r = n;

        // 要求区間と対象区間が交わらない -> 適当に返す
        if (r <= a || b <= l) return INF;

        // 要求区間が対象区間を完全に被覆 -> 対象区間を答えの計算に使う
        if (a <= l && r <= b) return node[k];

        // 要求区間が対象区間の一部を被覆 -> 子について探索を行う
        // 左側の子を vl ・ 右側の子を vr としている
        // 新しい対象区間は、現在の対象区間を半分に割ったもの
        ll vl = getmin(a, b, 2 * k + 1, l, (l + r) / 2);
        ll vr = getmin(a, b, 2 * k + 2, (l + r) / 2, r);
        return min(vl, vr);
    }
};

int main() {
    ll N, L;
    cin >> N >> L;
    struct Light {
        ll l, r, c;
        bool operator<(const Light& rhs) const {
            return l < rhs.l;
        }
    };
    vector<Light> lights(N);
    for (ll i = 0; i < N; i++) {
        cin >> lights[i].l >> lights[i].r >> lights[i].c;
    }

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

    vector<ll> cost(L + 1, INF);
    cost[0] = 0;
    SegmentTree st(cost);

    for (ll i = 0; i < N; i++) {
        ll pre_cost = st.getmin(lights[i].l, lights[i].r + 1);
        ll new_cost = pre_cost + lights[i].c;
        st.update(lights[i].r, new_cost);
    }

    cout << st.getmin(L, L + 1) << endl;
}

AtCoder Regular Contest 025 C - ウサギとカメ

問題

ウサギとカメがレースをする。N個の地点があり、地点間はM本の道で繋がれている。ウサギの速さがRm/s、カメの速さがTm/sであると与えられるとき、カメが先に目的地に到着するような目的地A、ウサギのスタート地点B、カメのスタート地点Cの選び方の個数を求めよ。

解法

 目的地をiであると仮定したとき、ダイクストラ法を用いれば各地点への最短距離がわかる。これをRTで割ったものを一つの配列に入れてソートすれば時間が到着時間が早いものから見ていくことができ、カメが着く時刻について、それ以降にウサギが到着する数をカウントしていけばよい。

反省

 23分19秒(1WA)でAC。まぁ初手はダイクストラを考えるところからで、愚直にやるとO(N^3)になってしまうけどまぁ上手いことやればなんとかなりそうだったので書き始める。書きながら考えたら普通にvectorに押し込んで前から見るだけで良さそうだと思ったのでそれで書いて通した。カメの方が速い場合に削らなければいけない数を勘違いして1回WAを出してしまった。

 少数を使うことになっているので精度が不安だったり、最悪ケースについて実行時間が1361msだったりと、もうちょっと厳しいテストケースがあったらダメだったかもしれない。二分探索でやるのは思いつくべきだった。

コード

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

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

    vector<vector<pair<ll, ll>>> connect(N);
    for (ll i = 0; i < M; i++) {
        ll a, b, c;
        cin >> a >> b >> c;
        a--; b--;
        connect[a].push_back({ b, c });
        connect[b].push_back({ a, c });
    }

    ll ans = 0;
    for (ll i = 0; i < N; i++) {
        //iが目的地とする
        vector<ll> distance(N, INT_MAX);
        distance[i] = 0;
        struct Element {
            ll cost, curr;
            bool operator<(const Element& rhs) const {
                return cost > rhs.cost;
            }
        };
        priority_queue<Element> pq;
        pq.push({ 0, i });
        while (!pq.empty()) {
            auto t = pq.top();
            pq.pop();

            for (auto e : connect[t.curr]) {
                ll new_cost = t.cost + e.second;
                if (new_cost < distance[e.first]) {
                    distance[e.first] = new_cost;
                    pq.push({ new_cost, e.first });
                }
            }
        }
        
        vector<pair<double, ll>> times;
        times.reserve(2 * N - 2);
        for (ll j = 0; j < N; j++) {
            if (j == i) {
                continue;
            }

            times.push_back({ (double)distance[j] / R, 0 });
            times.push_back({ (double)distance[j] / T, 1 });
        }

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

        ll rabbit_num = N - 1;
        for (auto p : times) {
            if (p.second == 0) {
                rabbit_num--;
            } else {
                ans += rabbit_num;
            }
        }
    }

    if (R < T) {
        //カメの方が早いとき同じ地点をカウントしてしまう分を引く
        ans -= N * (N - 1);
    }

    cout << ans << endl;
}

AtCoder Regular Contest 024 C - だれじゃ

問題

 長さNの文字列の中から、長さKの連続な部分文字列を重複が無いように二つ選んだ時、それらがアナグラムになっているようなものが元の文字列の中に存在するかを判定せよ。

解法

 長さKの部分文字列が互いにアナグラムになるかどうかは、その中にどの文字が何回登場するかをカウントすれば求めることができる。

 まず先頭K文字分についてどの文字が何回登場するかをカウントしたあとは、K+1文字目の登場回数を1回増やし、1文字目の登場回数を1回減らすというような操作で順番に2文字目からK+1文字目までのカウント、3文字目からK+2文字目までのカウント\dotsができていく。

 求めらこれらを、先頭の文字の位置と共にmapに入れて、最後に中身が一致し、先頭の文字の位置がK以上離れているものが存在するかどうかを判定すれば良い。全体としてO(N\log{N})で求まる。

反省

 20分25秒(0WA)でAC。sizeが26ののvectorをそのままmapに突っ込むだけという雑な解法でなんか通ってしまった。いや、理屈から言ってそれが可能なら通るのは当たり前なんだけど、vectorがキーとしてどういう風に表されているかを知らないまま書いているのでSTLが優秀であるというだけの話になってしまっている。

 なにか適当なハッシュ関数を作るのかとも思ったが、衝突させない方法がわからなかったので断念。適当な幅を取って開番地法でもやれば通ったかどうか。まぁmapでできるなら別に変なことする必要はないわけだけど……。

コード

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

int main() {
    ll N, K;
    string S;
    cin >> N >> K >> S;

    vector<ll> num(26, 0);
    for (ll i = 0; i < K; i++) {
        num[S[i] - 'a']++;
    }

    map<vector<ll>, ll> max_index;
    map<vector<ll>, ll> min_index;

    max_index[num] = K - 1;
    min_index[num] = K - 1;

    for (ll i = K; i < N; i++) {
        num[S[i - K] - 'a']--;
        num[S[i] - 'a']++;
        max_index[num] = i;
        if (min_index[num] == 0) {
            min_index[num] = i;
        }
    }

    for (auto p : max_index) {
        if (min_index[p.first] != 0) {
            if (p.second - min_index[p.first] >= K) {
                cout << "YES" << endl;
                return 0;
            }
        }
    }
    cout << "NO" << endl;
}

SRM510 Div1 Easy - TheAlmostLuckyNumbersDivOne

問題

 aからbまでの整数の中で4と7以外の数字を多くとも1つしか使わない数字はいくつあるか求めよ。

解法

 桁DPのメモ化が無いようなもの。状態として、

  • 上から何桁目を見ているか
  • 4と7以外の数字が何回出てきたか
  • 今まで見てきた要素でaより大きいことが保証されているか
  • 今まで見てきた要素でbより小さいことが保証されているか
  • 0以外の数字が出てきたか(一番左に並ぶ0はカウントしてはいけないため)

 を保持しながら条件に合わせて遷移していけばよい。

反省

 メモ化再帰、をやろうとしたところメモ化する必要はなかった。実質的に枝刈り全探索ということになるのか。しかし気持ちとしては完全に桁DPで、DP苦手なのでかなり苦しんだ。

 最初は問題文を誤読していて「4と7以外の数字のうち最大のものが一番左に来ているような整数の数を求めよ」かと思ってなんじゃこの問題は! と苦しんでいた。

 誤解に気づいてからも、forループでのDPを書こうとして挫折し、結局再帰関数を書くことに。最初っからこっちで書き始めればよかったのに。

 a以上b以下の条件を満たす数を求める問題で、ある整数n以下について条件を満たす数を求める関数fを作ってf(b)- f(a - 1)として答えを求めるのは常識なはずなのにそれができない。こういうところで無駄に難しい問題を解きに行ってしまうから実装で大変な目に遭うんだ。

 先頭の0は数えないようにするという部分でもかなり苦しんだ。最初はaの桁をbの桁に揃えるときに埋める数字を変えていろいろやろうとしたが、結局状態に0以外の数字が出たかどうか持たせるのが早い。こういうところでも桁DPに慣れているかどうかというのが顕著に表れるのだと思う。慣れていこう。

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

class TheAlmostLuckyNumbersDivOne {
public:
    long long find(long long a, long long b) {
        a_str = to_string(a);
        b_str = to_string(b);
        while (a_str.size() != b_str.size()) {
            a_str.insert(0, "0");
        }

        return solve(0, 0, false, false, false);
    }
private:
    string a_str, b_str;
    ll solve(ll d, ll num, bool bigger_than_a, bool smaller_than_b, bool appear_non0) {
        if (num >= 2) {
            return 0;
        }
        if (d == a_str.size()) {
            return 1;
        }
        ll result = 0;
        ll a_num = a_str[d] - '0';
        ll b_num = b_str[d] - '0';

        ll low  = ( bigger_than_a ? 0 : a_num);
        ll high = (smaller_than_b ? 9 : b_num);

        for (ll i = low; i <= high; i++) {
            ll add = (appear_non0 ? (i != 4 && i != 7) : (i != 0 && i != 4 && i != 7));
            result += solve(d + 1, num + add, i > a_num || bigger_than_a, i < b_num || smaller_than_b, appear_non0 || i != 0);
        }

        return result;
    }
};

SRM509 Div1 Easy - LuckyRemainder

問題

 整数から数字を1つ以上取り除き残った数字を順番はそのままに右へ詰めてできる整数らの和を9で割った余りを求めよ。

解法

 上の桁から順番に9で割った余りがそれぞれ何回出てくるかを記録していく。これをnum_iとする(0 \le i \lt 8)。j桁目の数字がnだったとき、これを使うことによりnum_{(10i + n)\%9}が現在のnum_iだけ増える。またj桁目の数字を一つだけ使うことによりnum_{n \% 9}も1増える。この二つの操作を桁を順番に下りながらやっていき、最後に各要素の和を取っていけばよいのでO(10\times \log_{10}{X})で求まる。

反省

 ちょうどこの前やったARC020のCなどの印象が強く、桁をずらしながら足して余りを求めていくという発想しか出てこなかった。もっと簡単にできるらしい

 多少でも複雑になってしまうとj桁目の数字を一つだけ使うことを忘れたりそこで9の余りを取るのを忘れたりでWAを出してしまう(しまった)ので、できるだけ簡単な解法まで落としてからコードを書き始めるということが重要なんだろうが、まぁ実際はいけそうな解法を思いついたらすぐ書き出してしまうのが人の性である。

コード

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

class LuckyRemainder {
public:
    int getLuckyRemainder(string X) {
        vector<ll> num(9, 0);
        for (char c : X) {
            ll n = c - '0';
            auto before = num;
            for (ll i = 0; i < 9; i++) {
                num[(i * 10 + n) % 9] += before[i];
            }
            num[n % 9]++;
        }

        ll sum = 0;
        for (ll i = 0; i < 9; i++) {
            sum += num[i] * i;
        }
        return sum % 9;
    }
};

AtCoder Regular Contest 023 C - タコヤ木

問題

 広義単調増加列のうち一部分が不明となっている列が与えられる。あり得る組み合わせの数を求めよ。

解法

 不明な部分が連続している一つの塊に注目する。たとえば定数a, b(a \lt b)についてa, -1, -1, -1, bという部分列を考えるとすると、aからbに至るまでの4回の遷移の総和がb-aとなっているということになる。これはb-a個の増加操作が3個の仕切りによって区切られるような場合の数を数えることで計算できる。つまり一般に-1がx個続く場合には{}_{b- a + x}C_{x}を求めればよい。100,000,007についての余りを求めることに注意して累乗計算を高速化すればこの方針で間に合う。

反省

 1時間18分(4WA)でAC。33分で80点の部分点解法を出したが、それ以降分からずうつらうつらしていたら1時間以上経っていたので解説スライドを見た。方針は考えていたままだった。{}_nC_mの求め方として、いつも通りの累乗とその逆元を事前計算する実装でやったためA_iの数が大きいときだめだなーと思っていたのだった。つまり

//fact[i]とinv_fact[i]は事前計算してある
ll combination(ll a, ll b) {
    return fact[a] * inv_fact[a - b] % MOD * inv_fact[b] % MOD;
}

 としていたのだが、別にすべて事前計算する必要はないためその場で必要なものを計算していけばよいのだった。

ll combination(ll a, ll b) {
    ll n = 1, m = 1;
    for (ll i = 1; i <= b; i++) {
        n *= a - i + 1;
        n %= MOD;
        m *= i;
        m %= MOD;
    }
    return n * POW(m, MOD - 2);
}

 その場計算の方が汎用性が高いのだろうか。どうせbが大きかったら事前計算でもダメなわけで、事前計算で可能だがこちらではダメという例があるのかどうか。しかしこういう実装もアドホックで考えていくことが大切そうだ。

コード

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

constexpr ll MOD = 1e9 + 7;

ll POW(ll a, ll b) {
    ll result = 1;
    while (b) {
        if (b & 1) {
            result *= a;
            result %= MOD;
        }
        a *= a;
        a %= MOD;
        b /= 2;
    }
    return result;
}

ll combination(ll a, ll b) {
    ll n = 1, m = 1;
    for (ll i = 1; i <= b; i++) {
        n *= a - i + 1;
        n %= MOD;
        m *= i;
        m %= MOD;
    }
    return n * POW(m, MOD - 2) % MOD;
}

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

    struct Blank {
        ll left, right, num;
    };
    vector<Blank> blanks;

    for (ll i = 0; i < N; i++) {
        if (A[i] == -1) {
            Blank b;
            b.left = A[i - 1];
            for (ll j = i + 1; j < N; j++) {
                if (A[j] != -1) {
                    b.right = A[j];
                    b.num = j - i;
                    i = j;
                    break;
                }
            }
            blanks.push_back(b);
        }
    }

    ll ans = 1;
    for (auto b : blanks) {
        ans *= combination(b.right - b.left + b.num, b.num);
        ans %= MOD;
    }
    cout << ans << endl;
}

AtCoder Regular Contest 022 C - ロミオとジュリエット

問題

 N頂点の木が与えられるので最も距離が長くなるような2つのノードのペアを一つ答えよ。

解法

 ある頂点を根として考え、そこから再帰関数を回す。それぞれの関数内では、そのノードから各リーフノードへの距離と、リーフノードの番号を返すようにする。一つ上のノードは隣接ノードについてこの再帰関数を回し、一番長いところと次に長いところまでの距離を計算して、グローバル変数として置いてある今までの最大値と比較して答えを更新する。そして返り値としては一番長いものを返す。再帰的に行なわれることによってこれで答えが求まる。

f:id:tokumini:20180829103032j:plain

反省

 15分22秒でAC。これは簡単めな問題だったと思う。木の直径を求める方法なんて調べればすぐ出てくるとは思ったが、一応自力で考察して解くことができた。

 解説スライドを読むと木の直径を求める方法として

  • 木DPをする
  • ある頂点から最も遠い頂点を探し、さらにその頂点から最も遠い頂点を探す

 という2つが挙げられていた。自分の解法は上の木DPに相当すると思われるが、書いている最中は木DPという自覚はなかった。後者の方法は木の直径に絞られた解き方ではあるが、覚えておいて損はなさそうだ。

コード

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

vector<vector<ll>> connect;
ll global_max;
pair<ll, ll> ans;

pair<ll, ll> solve(ll curr, ll pre) {
    ll max1 = 0, max2 = 0;
    pair<ll, ll> p = { curr, curr };
    for (ll next : connect[curr]) {
        if (next == pre) {
            continue;
        }

        auto n = solve(next, curr);
        ll length = n.first + 1;

        if (length > max1) {
            max2 = max1;
            max1 = length;

            p.second = p.first;
            p.first = n.second;
        } else if (length > max2) {
            max2 = length;
            p.second = n.second;
        }
    }

    if (max1 + max2 > global_max) {
        ans = p;
        global_max = max1 + max2;
    }
    return { max1, p.first };
}

int main() {
    ll N;
    cin >> N;
    connect.resize(N);
    for (ll i = 0; i < N - 1; i++) {
        ll A, B;
        cin >> A >> B;
        A--; B--;
        connect[A].push_back(B);
        connect[B].push_back(A);
    }

    solve(0, -1);
    cout << ans.first + 1 << " " << ans.second + 1 << endl;
}

AtCoder Regular Contest 021 C - 増築王高橋君

問題

 N個の建物をK回増築することを考える。建物i

  • 最初の価格はA_iである
  • 増築するときには現在の価格に等しい費用がかかる
  • 1回増築すると価格がD_i上昇する

 K回増築するのに必要な価格の合計値の最小値を求めよ。

解法

 解法の流れは、まず最後に増築するときの費用がCであるような場合について、増築回数がK回を超えられるかどうかを判定することでCについての2分探索をする。そして求まったCに基づいてシミュレーションして答えを得る。

 最後に増築するときの費用がCであるとき、各建物iについては価格がC未満であるときはどこかの段階で必ず増築し、Cちょうどであるときは最後に増築するかしないか選択の余地がある。結局、価格がCを超えるような増築回数が可能な増築回数であり、iについてそれらの和を考えればCに対しての可能な増築回数が求まる。それがKを超えていればそのCを最後の増築費用として行うことができ、これは単調性があるので2分探索が可能である。

 シミュレーションはほぼ上と同様であり、Cについて各建物について価格がCを超えるように増築し、費用の総和を求めながら増築回数を数えていく。最後に増築回数の総和がKを超えていれば超過分の操作におけるコストはCなのでそれをかけた値を引いて答えとなる。超過分の操作コストがC未満ということもC超過であるということもないのでそのまま引いてよい。

反省

 1時間34分(8WA)でAC。部分点解法のpriority_queueを使ってO(K\log{N})というのはすぐ見えたし、満点解法としても答えを直接ではなく違う部分を2分探索するというアイデアには至っていたがまとめきれず、1時間過ぎたあたりで解説を読んだ。

 解説のように操作のコストの方に着目する方が良く、最終的な建物の価格で考えていたので混乱してしまった。K回を超えたりすることがあるのでそこを上手く処理しなければならなかったのだが……。

 解説を理解してからも初期化のミスで4WA出してしまった。INT_MAXでは小さすぎるのと、LLONG_MAXでは大きすぎるというのを両方やってしまったが、内部の判定でN回足されるため限界値よりは多少小さめにして[tex:1015]やLLONG_MAX / Nとしなければならなかったのだった。あまりにこういうミスが多いので、constexpr ll INF = (ll)1e15とでもしておいた方がいいかもしれない。using ll = int64_tも抵抗があってなかなか入れなかったが、こういう本質的でないミスを回避できるとすれば多少の気持ち悪さには目をつぶるべきか。

コード

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

ll K, N;
vector<ll> A, D;

bool isOK(ll price) {
    //最後の増築がprice円である場合に増築回数がK回を超えられるか
    ll num = 0;
    for (ll i = 0; i < N; i++) {
        //各建物についてprice円超過となるように増築する回数
        if (A[i] > price) {
            continue;
        }
        num += (price - A[i]) / D[i] + 1;
    }

    return (num >= K);
}

int main() {
    cin >> K >> N;
    A.resize(N);
    D.resize(N);
    for (ll i = 0; i < N; i++) {
        cin >> A[i] >> D[i];
    }

    ll ok = LLONG_MAX / N;
    ll ng = 0;
    while (ng + 1 != ok) {
        ll mid = (ok + ng) / 2;
        assert(ng <= mid && mid <= ok);
        (isOK(mid) ? ok = mid : ng = mid);
    }

    ll ans = 0;
    ll num = 0;
    for (ll i = 0; i < N; i++) {
        //各建物についてprice円以上となるように増築する
        if (A[i] > ok) {
            continue;
        }
        ll time = (ok - A[i]) / D[i] + 1;

        num += time;
        //ans += (A[i] + A[i] + D[i] * (time - 1)) * time / 2;
        ans += A[i] * time + time * (time - 1) / 2 * D[i];
    }

    //足し過ぎた分を引く
    ans -= ok * (num - K);

    cout << ans << endl;
}

AtCoder Regular Contest 020 C - A mod B Problem

問題

 ある整数A, Bを与えるのでABで割った余りを求めなさい。しかしAは非常に大きく、i = 0, 1, \dots, Nについてa_iL_i回繰り返されたものを上の桁から順番に並べたものとして表されるものとする。

解法

 ダブリング。a2^n回並べてできる整数をkとすると、a2^{n + 1}回並べてできる整数は$$ k \times (10^{aの桁数 \times 2^n} + 1)$$である。これは123を2回並べた123123から4回並べた123123123123を作る過程など具体例を考えるとわかりやすい。

 このようにすれば各nについてa2^n回並べてできる整数は小さいほうから順番に求めていくことができるので、L2の累乗の和として表現しなおせば上で求めたもののみを使ってaL回並べた整数の余りを求めることができる。

 各a_i, L_iについて余りが求まれば、それを順番に桁数分シフトながら余りとして足していけば最終的な答えが得られる。

反省

 2時間6分(3WA)でAC。1時間ほど考えて20点分の解答しかわからなかったので解答を見てまず99点分の解答を書いて、そのあと満点解法を書いた。解法が難しいのもそうだけど桁をずらしながら足していく操作も慣れていなくて実装が大変だった。

 手元のメモを見返すと思いっきり等比数列っぽいものは書いているのに99点解法が思い浮かばなかったのは良くない。しかし結局桁をずらしながら足していけば求まるというコア部分が見えていないのでこういう発想に至らなかったともいえるので、これは慣れによって何とかなる気もする。

 ダブリングは必須テクニックなので敏感になっていきたい。どういうときに思い浮かべばいいのかまだよくわかっていないけど、同じ操作を大きな数やる場合などか。

コード

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

ll B;

ll POW(ll n, ll m) {
    ll result = 1;
    while (m) {
        if (m & 1) {
            result *= n;
            result %= B;
        }

        m /= 2;
        n *= n;
        n %= B;
    }
    return result;
}

ll MOD(ll a, ll L) {
    //a[i]の桁数
    const ll d = to_string(a).size();

    //aがpow(2, i)回繰り返されたものの余りをf[i]に格納していく
    vector<ll> f;
    f.push_back(a);
    for (ll i = 1; pow(2, i) <= L; i++) {
        f.push_back(f[i - 1] * ((POW(10, pow(2, i - 1) * d) + 1) % B) % B);
    }

    ll result = 0;
    ll curr_d = 0;
    for (ll i = 0; pow(2, i) <= L; i++) {
        if (L & (1 << i)) {
            //ビットが立っていたら足す
            result += f[i] * POW(10, curr_d) % B;
            result %= B;

            //桁を更新
            curr_d += d * pow(2, i);
        }
    }

    return result;
}

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

    ll ans = 0;
    for (ll i = 0; i < N; i++) {
        //a[i]の桁数
        const ll d = to_string(a[i]).size();

        ll m = MOD(a[i], L[i]);

        //前回までの余りを今回の桁数分シフトして今回の余りと足す
        ans = (ans * POW(10, d * L[i]) % B + m) % B;
    }

    cout << ans << endl;
}

AtCoder Regular Contest 019 C - 最後の森

問題

 縦R行、横C列のマス目で構成されているマップについて各マスは

  • 平地……平地のマスは自由に通行できる。
  • 木……木のあるマスは通行できない。
  • 強敵のいるマス……凶暴な強敵のいるマスである。後述の条件を満たさなければ通行できない。
  • プレイヤーの村……プレイヤーの初期位置である。このマスは自由に通行できる。
  • ほこら……伝説の剣 Link-Cut Sword があるほこらである。このマスは自由に通行できる。
  • 魔王の城……魔王の城があるマスである。このマスは自由に通行できる。

 のいずれかとなっている。村から出発し、ほこらを通って魔王の城に行くことを考える。強敵は一度倒すと消滅しそのマスは平地と同じとなる。倒す強敵の数がK以下となるように進むときの最短経路長を求めよ。

解法

 あるマス(i, j)へ倒す強敵の数がkとなるように進んだときの最短経路を、出発点として村、ほこら、城の3つを選んだ場合についてそれぞれ幅優先探索で求める。

 全てのマスについて村からそのマス、そのマスからほこら、ほこらからそのマス、そのマスから城という経路についてコストを計算し、一番小さいものが答えとなる。

 調べるマスに敵がいる場合は注意が必要であり、3回同じ敵を数えることになるのでそのマスから城へ行くときの余裕分は+2される。

反省

 3時間3分(9WA)でAC。きつかった……。

 最初は村からほこら、ほこらから城へと2回ダイクストラをやるだけかなと思っていくらか実装していたが、どうも一つだけサンプルが合わない。いろいろ考えていると、

6 4 2
S..T
.TET
.TCT
..ET
..ET
TTGT

 のようなケースでダメだということに気が付いた。つまりSからCへ戦闘回数が2回以下で行くときの最短経路は、Cの上にあるEを2回踏むことで達成できてしまうが、それでは最終的な経路は長くなってしまう。ダイクストラでは途中の敵の遭遇具合を管理できないのでこの方法ではうまくいかないと悟ったのが1時間30分ごろ。

 おそらく深さ優先探索的なことをしていくのだろうと思いつつわからなかったので解説スライドを読んだら全然考えてもないことばかりが書いてあった。

 苦しみつつ実装をしたところ、ダイクストラのときにダメだったサンプルがやはり合わない。調べるマスに敵がいる場合についてマスから城へ行くときの余裕分に1しか足していなかったのが原因だったようだ。村からそのマスで1回、ほこらへと往復するから2回で、同じ敵を3回数えた計算になることがわかっていなかった。

コード

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

ll R, C, K;
vector<string> s;
enum {
    START, CENTER, GOAL, NUM
};

constexpr ll dx[] = { 0, 1, 0, -1 };
constexpr ll dy[] = { 1, 0, -1, 0 };
bool isOK(ll x, ll y) {
    return (0 <= x && x < C && 0 <= y && y < R && s[y][x] != 'T');
};

void bfs(ll sx, ll sy, vector<vector<vector<ll>>>& cost) {
    queue<tuple<ll, ll, ll>> q;
    q.push(make_tuple(sx, sy, 0));

    while (!q.empty()) {
        ll x, y, k;
        tie(x, y, k) = q.front();
        q.pop();

        for (ll i = 0; i < 4; i++) {
            ll nx = x + dx[i];
            ll ny = y + dy[i];
            if (!isOK(nx, ny)) {
                continue;
            }
            ll nk = k + (s[ny][nx] == 'E');
            if (nk > K) {
                continue;
            }
            ll nc = cost[y][x][k] + 1;
            if (nc < cost[ny][nx][nk]) {
                cost[ny][nx][nk] = nc;
                q.push(make_tuple(nx, ny, nk));
            }
        }
    }
}

int main() {
    cin >> R >> C >> K;
    s.resize(R);

    vector<ll> pos_x(NUM), pos_y(NUM);

    for (ll i = 0; i < R; i++) {
        cin >> s[i];
        for (ll j = 0; j < C; j++) {
            if (s[i][j] == 'S') {
                pos_x[START] = j;
                pos_y[START] = i;
            } else if (s[i][j] == 'C') {
                pos_x[CENTER] = j;
                pos_y[CENTER] = i;
            } else if (s[i][j] == 'G') {
                pos_x[GOAL] = j;
                pos_y[GOAL] = i;
            }
        }
    }

    vector<vector<vector<vector<ll>>>> cost(NUM, vector<vector<vector<ll>>>(R, vector<vector<ll>>(C, vector<ll>(K + 1, INT_MAX))));

    for (ll i = 0; i < NUM; i++) {
        cost[i][pos_y[i]][pos_x[i]][0] = 0;
        bfs(pos_x[i], pos_y[i], cost[i]);
    }

    ll ans = INT_MAX;
    for (ll i = 0; i < R; i++) {
        for (ll j = 0; j < C; j++) {
            if (s[i][j] == 'T') {
                continue;
            }

            //戦闘回数k回以下で行けるときk+1回以下でも行ける
            for (ll n = 0; n < NUM; n++) {
                for (ll k = 0; k < K; k++) {
                    cost[n][i][j][k + 1] = min(cost[n][i][j][k + 1], cost[n][i][j][k]);
                }
            }

            //sから(i, j)へk1回の戦闘で行き、(i, j)からcへk2回の戦闘で行って戻ってきて、(i, j)からgへ残りの戦闘回数で行く
            for (ll k1 = 0; k1 <= K; k1++) {
                for (ll k2 = 0; k2 <= K; k2++) {
                    ll k3 = K - k1 - k2;
                    if (s[i][j] == 'E') {
                        k3 += 2;
                    }
                    if (k3 < 0 || K < k3) {
                        continue;
                    }
                    ans = min(ans, cost[0][i][j][k1] + 2 * cost[1][i][j][k2] + cost[2][i][j][k3]);
                }
            }
        }
    }

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