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

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

AtCoder ABC 129 F - Takahashi's Basics in Education and Learning (橙色, 600 点)

重たい。。。
でも例えば行列  A に対して

  •  A^{3} + A^{2} + A + E

の計算を行列累乗に帰着させるテクニックは、蟻本中級編の行列累乗のところに載っていたりする。それを膨らませると

  •  x^{5} + 2x^{4} + 3x^{3} + 4x^{2} + 5x + 6 ({\rm mod} M)

みたいな計算も、行列累乗でできそうという気持ちには確かになるよね。。。(なったけど昨日間に合わなかった)

問題へのリンク

問題概要

初項が  A、交差が  B、長さが  L の等差数列を十進法表記で表して連結してできる数を考える。例えば  A = 3、B = 4、L = 5 なら、 3, 7, 11, 15, 19 なので  317111519 になる。

この数を  M で割ったあまりを求めよ。ただし  M は素数とは限らない。

制約

  •  1 \le L, A, B \lt 10^{18}
  •  2 \le M \le 10^{9}
  • 等差数列の要素はすべて  10^{18} 未満

考えたこと

 A = 3, B = 4, L = 5 の例でいうと、

  •  3 \times (10^{7} + 10^{6} + 10^{4} + 10^{2} + 10^{0})
  •  4 \times (10^{6} + 10^{4} + 10^{2} + 10^{0})
  •  4 \times (10^{4} + 10^{2} + 10^{0})
  •  4 \times (10^{2} + 10^{0})
  •  4 \times (10^{0})

の総和をとる問題だと思うことができて、そういう方針で考えることにした。そして、基本的には等差数列において桁数ごとに考えてあげて  d = 18, 17, ..., 1 に対して  d 桁の整数 ( 10^{d-1} 以上  10^{d}-1 以下) が  C_d 個あるときに、 x = 10^{d} として、

  •  d 桁より大きい整数の分について + E_d 桁分あるとして、
    • その分についての項和を  F_{d} として
  • 以下を合算して  B をかけたもの
    •  (x^{C_{d}} + 2x^{C_{d}-1} + 3x^{C_{d}-2} + \dots + C_{d}) \times x^{E_{d}}
    •  C_{d} \times F_{d}

を各  d に対して合計していけばよい。最後に初項  A の分については補正する。

多項式計算

今回

  •  x^{4} + x^{3} + x^{2} + x + 1
  •  x^{4} + 2x^{3} + 3x^{2} + 4x + 5

といった計算を高速に実行する必要性にかられている。これらは逆元を使えるなら明示的な式で書けるのだが、今回は任意 MOD なのでそうはいかない。でもその場合にも行列累乗に帰着させる方法は蟻本中級編にも載っている!!!!!!

前者は

x 1
0 1

後者は

x 1 1
0 1 1
0 0 1

という行列を累乗していけばよい。

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

// px + r (x >= 0) で表せる整数のうち、x 以上となる最小の整数
long long lower_amari(long long p, long long r, long long x) {
    if (r >= x) return r;
    return (x - r + p-1) / p * p + r;
}

// matrix
int MOD;
struct Matrix {
    vector<vector<long long> > val;
    Matrix(int n, int m, long long x = 0) : val(n, vector<long long>(m, x)) {}
    void init(int n, int m, long long x = 0) {val.assign(n, vector<long long>(m, x));}
    size_t size() const {return val.size();}
    inline vector<long long>& operator [] (int i) {return val[i];}
};

Matrix operator * (Matrix A, Matrix B) {
    Matrix R(A.size(), B[0].size());
    for (int i = 0; i < A.size(); ++i) 
        for (int j = 0; j < B[0].size(); ++j)
            for (int k = 0; k < B.size(); ++k) 
                R[i][j] = (R[i][j] + A[i][k] * B[k][j] % MOD) % MOD; 
    return R;
}

Matrix pow(Matrix A, long long n) {
    Matrix R(A.size(), A.size());
    for (int i = 0; i < A.size(); ++i) R[i][i] = 1;
    while (n > 0) {
        if (n & 1) R = R * A;
        A = A * A;
        n >>= 1;
    }
    return R;
}

// mod
long long modpow(long long a, long long n) {
    long long res = 1;
    while (n > 0) {
        if (n & 1) res = res * a % MOD;
        a = a * a % MOD;
        n >>= 1;
    }
    return res;
}

// x^(n-1) + x^(n-2) + ... + 1
long long ser(long long x, long long n) {
    if (n == 0) return 0;
    Matrix M(2, 2);
    M[0][0] = x % MOD; M[0][1] = 1;
    M[1][0] = 0; M[1][1] = 1;
    auto P = pow(M, n-1);
    return (P[0][0] + P[0][1]) % MOD;
}

// x^(n-1) + 2x^(n-2) + 3x^(n-3) + ... + n
long long ser2(long long x, long long n) {
    if (n == 0) return 0;
    Matrix M(3, 3);
    M[0][0] = x % MOD; M[0][1] = 1; M[0][2] = 1;
    M[1][0] = 0; M[1][1] = 1; M[1][2] = 1;
    M[2][0] = 0; M[2][1] = 0; M[2][2] = 1;
    auto P = pow(M, n-1);
    return (P[0][0] + P[0][1] + P[0][2]) % MOD;
}

long long solve(long long L, long long A, long long B, long long M) {
    MOD = M;
    long long C = A + B * (L-1);
    long long res = 0;
    long long curd = 0;
    long long sum = 0;
    long long fac = 1;
    for (long long d = 18; d >= 1; --d) {
        // まずは d 桁の個数を数える
        long long beki = 1;
        for (int i = 0; i < d - 1; ++i) beki *= 10;
        long long low = beki, high = beki * 10 - 1;

        high = min(high, C);
        if (high < A || C < low) continue;

        long long low_kou = lower_amari(B, A, low);
        long long up_kou = lower_amari(B, A, high + 1);
        long long num = (up_kou - low_kou) / B;

        // それを使って計算
        long long x = modpow(10LL, d);
        long long alr = num % MOD * sum % MOD;
        long long add = ser2(x, num) * fac % MOD;
        res = (res + (add + alr) % MOD * (B % MOD) % MOD) % MOD;
        sum = (sum + fac * ser(x, num) % MOD) % MOD;
        fac = fac * modpow(modpow(10LL, d), num) % MOD;
    }
    long long hosei = ((A - B) % M + M) % M * sum % M;
    res = (res + hosei) % M;
    return res;   
}

int main() {
    long long L, A, B, M;
    while (cin >> L >> A >> B >> M) cout << solve(L, A, B, M) << endl;
}

AtCoder ABC 129 E - Sum Equals Xor (水色, 500 点)

まさにこの考え方で解ける問題

drken1215.hatenablog.com

今回の問題も桁 DP でもよいのだが、実は桁 DP しなくても解ける。

問題へのリンク

問題概要

正整数  L が二進数表記で与えられます。 以下の条件を満たす非負整数  a, b の組がいくつ存在するか求めてください (1000000007 で割ったあまり)

  •  a+b \le L
  •  a+b = a XOR  b

制約

  •  L の桁数は  10^{5} 以下

「a + b = a XOR b」の言い換え

 L 以下」という条件がある時点で、桁DP か、それに類する発想を使いそうな問題に思える。

それを念頭に置きつつ、まずは「a + b = a XOR b」という条件をわかりやすく言い換えることから始めてみる。このように「わかりにくい条件をわかりやすく言い換える」というのは AtCoder では定番なイメージがある。

さて、a と b をともに二進法表記したとき

a = 1010
b = 0110

という風だったとき、下から二番目の位に着目すると、a も b も 1 になっている。このとき

  • a + b ではくり上がりが起こる
  • a XOR b ではくり上がりが起こらず、相殺して 0 になる

ということが読み取れる。反対に (a の i 桁目, b の i 桁目) = (1, 0), (0, 1), (0, 0) の場合にはいずれも i 桁目については a+b と a XOR b とは等しくて、しかも他の桁への影響もない。

よって、


a + b >= a XOR b が常に成立していて、等号成立条件は「どの桁についても a と b がともに 1 になることはないこと」である


ということが言える。

元の問題へ

よって問題は

  • a + b <= L
  • どの桁をみても a と b の少なくとも片方は 0 である

というものを数え上げる問題となった。editorial では桁 DP でやっているが、ここでは桁 DP の思想を引き継ぎつつ表立って DP せずにやってみる。

まず、例えば L = 11001010 という整数だったとき、L 以下の整数というのは以下のように場合分けできる。ここで「...」の部分はなんでもよいことを表す。

  • 0...
  • 10...
  • 11000...
  • 1100000...
  • 11001010

それぞれについて、

  • 「...」の部分については、その部分の桁数を d とすると、各桁について (a, b) = (0, 0), (0, 1), (1, 0) の 3 通りがあるので、 3^{d} 通り
  • それ以外の部分については、1 の個数を e とすると、1 が立っている各桁について (a, b) = (0, 1), (1, 0) の 2 通りがあるので、 2^{e} 通り。

ということなので、まとめると  3^{d} \times 2^{e} 通りとなる。これを合算すれば OK。

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


// 1000000007 で割ったあまりを扱う構造体
template<int MOD> struct Fp {
    long long val;
    constexpr Fp(long long v = 0) noexcept : val(v % MOD) {
        if (val < 0) v += MOD;
    }
    constexpr int getmod() { return MOD; }
    constexpr Fp operator - () const noexcept {
        return val ? MOD - val : 0;
    }
    constexpr Fp operator + (const Fp& r) const noexcept { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp& r) const noexcept { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp& r) const noexcept { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp& r) const noexcept { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp& r) noexcept {
        val += r.val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -= (const Fp& r) noexcept {
        val -= r.val;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp& operator *= (const Fp& r) noexcept {
        val = val * r.val % MOD;
        return *this;
    }
    constexpr Fp& operator /= (const Fp& r) noexcept {
        long long a = r.val, b = MOD, u = 1, v = 0;
        while (b) {
            long long t = a / b;
            a -= t * b; swap(a, b);
            u -= t * v; swap(u, v);
        }
        val = val * u % MOD;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr bool operator == (const Fp& r) const noexcept {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp& r) const noexcept {
        return this->val != r.val;
    }
    friend constexpr ostream& operator << (ostream &os, const Fp<MOD>& x) noexcept {
        return os << x.val;
    }
    friend constexpr istream& operator >> (istream &is, Fp<MOD>& x) noexcept {
        return is >> x.val;
    }
    friend constexpr Fp<MOD> modpow(const Fp<MOD> &a, long long n) noexcept {
        if (n == 0) return 1;
        auto t = modpow(a, n / 2);
        t = t * t;
        if (n & 1) t = t * a;
        return t;
    }
};
const int MOD = 1000000007;
using mint = Fp<MOD>;


mint solve(const string &L) {
    long long num = 0;
    mint res = 0;
    for (int i = 0; i < L.size(); ++i) {
        int len = (int)L.size() - i - 1;
        if (L[i] == '1') {
            res += modpow(mint(2), num) * modpow(mint(3), len);
            ++num;
        }
    }
    res += modpow(mint(2), num);
    return res;
}

int main() {
    string L; cin >> L;
    cout << solve(L) << endl;
}

AtCoder ABC 129 D - Lamp (緑色, 400 点)

これも実はよくある典型問題だったりはする。

問題へのリンク

問題概要

下のような  H \times W の二次元グリッドが与えられる。

#..#..
.....#
....#.
#.#...

このグリッドで '#' は壁を表している。ここで '.' マスを 1 つ選んで、そこに光源を置きたい。光源が照らす範囲は

  • 光源から上方向に行って最初に壁に遮られるまでの範囲
  • 光源から下方向に行って最初に壁に遮られるまでの範囲
  • 光源から左方向に行って最初に壁に遮られるまでの範囲
  • 光源から右方向に行って最初に壁に遮られるまでの範囲

である。どのマスに光源を置けば照らすマス数を最大化できるか、その最大値を求めよ。

制約

  •  1 \le H, W \le 2000

方針

各マスに対して

  • 左にどこまで行ったら壁にぶつかるか
  • 右にどこまで行ったら壁にぶつかるか
  • 上にどこまで行ったら壁にぶつかるか
  • 下にどこまで行ったら壁にぶつかるか

を求めることができれば、それを利用して問題を解くことができそうである。ここではまず「左にどこまで行ったら壁にぶつかるか」を効率よく求める方法を考える。

どこまで行ったら壁にぶつかるか

下図は、「各マスについて、そこから左にどこまで行ったら壁にぶつかるか」を表している。実はこれを求めるのはとても典型的な処理だったりする。

これを愚直に求めたいと思ったら

  • 各マスに対して ( O(HW) 通り)
  • 左側に目一杯進む (最悪  O(W))

ということで  O(HW^{2}) の計算時間がかかってしまう。しかしこれは上手くやることで  O(HW) の計算時間で処理することができる。

まず、各行について独立にやっていいことに注意する。よって、下図のような一次元配列の場合ができれば OK。

これは実は簡単で、以下のようにするだけで実装できる。すなわち cur という変数に、「最後の壁から何マス来たのか」という情報を格納しておいて、各 i に対して

  • i マス目が壁だったら、cur = 0 に戻る
  • i マス目が通路だったら、cur をインクリメントする

とするだけで実装できる。

int num[];
int cur = 0;
for (int i = 0; i < W; ++i) {
  if ( i が壁 ) cur = 0;
  else ++cur;
  num[i] = cur;
}

今回の問題

さて、今回の問題では、これをほんの少し応用するだけでできる。今我々は

  • 各マスから左側に何マス行ったら壁にぶつかるか (left[i][j] とする)

を求めたが、同様に頑張れば

  • 各マスから右側に何マス行ったら壁にぶつかるか (right[i][j] とする)
  • 各マスから上側に何マス行ったら壁にぶつかるか (up[i][j] とする)
  • 各マスから下側に何マス行ったら壁にぶつかるか (down[i][j] とする)

を求めることもできる。そうすれば、各マスに光源を置いたときに照らす範囲は

  • left[i][j] + right[i][j] + up[i][j] + down[i][j] - 3

と求めることができる。「-3」をしているのは、left, right, up, down でそれぞれ「光源が置いてあるマスそれ自体」を 4 回足しているから。

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

int H, W;
vector<string> fi;
int Left[2100][2100], Right[2100][2100], Up[2100][2100], Down[2100][2100];

int main() {
    // 入力
    cin >> H >> W;
    fi.resize(H);
    for (int i = 0; i < H; ++i) cin >> fi[i];

    // 前処理
    for (int i = 0; i < H; ++i) {
        int cur = 0;
        for (int j = 0; j < W; ++j) {
            if (fi[i][j] == '#') cur = 0;
            else ++cur;
            Left[i][j] = cur;
        }
    }
    for (int i = 0; i < H; ++i) {
        int cur = 0;
        for (int j = W-1; j >= 0; --j) {
            if (fi[i][j] == '#') cur = 0;
            else ++cur;
            Right[i][j] = cur;
        }
    }
    for (int j = 0; j < W; ++j) {
        int cur = 0;
        for (int i = 0; i < H; ++i) {
            if (fi[i][j] == '#') cur = 0;
            else ++cur;
            Up[i][j] = cur;
        }
    }
    for (int j = 0; j < W; ++j) {
        int cur = 0;
        for (int i = H-1; i >= 0; --i) {
            if (fi[i][j] == '#') cur = 0;
            else ++cur;
            Down[i][j] = cur;
        }
    }

    // 集計
    int res = 0;
    for (int i = 0; i < H; ++i) {
        for (int j = 0; j < W; ++j) {
            if (fi[i][j] == '#') continue;
            res = max(res, Left[i][j] + Right[i][j] + Up[i][j] + Down[i][j] - 3);
        }
    }
    cout << res << endl;
}

AtCoder ABC 129 C - Typical Stairs (茶色, 300 点)

いわゆる本当に最初の入門という感じの DP だね。

問題へのリンク

問題概要

 N 段の階段があって、現在は 0 段目にいる。

高橋君は一歩で 1 段か 2 段上がることができる。ただし  a_1, a_2, \dots, a_M 段目は壊れていて、そこに足を踏み入れることはできない。

高橋君が 0 段目から  N 段目までたどり着くまでの移動方法は何通りあるか、1000000007 で割ったあまりを求めよ。

制約

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

壊れているところがなかったら

もしかしたら高校時代に似たような問題を解いたことのある方も多いかもしれない。すなわち「どこも壊れていないバージョン」なら解いたことある方も多いかもしれない。

下図のように

  • 0 段目は「なにもしない」という 1 通り
  • 1 段目は「0 段目から 1 段のぼり」という 1 通り
  • 2 段目は、その直前がどうだったかが以下の 2 つの場合があって、それらを合計して 2 通り
    • 直前が 0 段目:1 通り
    • 直前が 1 段目:1 通り
  • 3 段目は、その直前がどうだったかが以下の 2 つの場合があって、それらを合計して 3 通り
    • 直前が 1 段目:1 通り
    • 直前が 2 段目:2 通り
  • 4 段目は、その直前がどうだったかが以下の 2 つの場合があって、それらを合計して 5 通り
    • 直前が 2 段目:2 通り
    • 直前が 3 段目:3 通り
  • 5 段目は、その直前がどうだったかが以下の 2 つの場合があって、それらを合計して 8 通り
    • 直前が 3 段目:3 通り
    • 直前が 4 段目:5 通り

という感じになる。

f:id:drken1215:20190610134207p:plain

f:id:drken1215:20190610134229p:plain

これを漸化式で表すと、 n 段目まで登る方法の数を  a_n 通りとすると、

  •  n-2 段目から来る:  a_{n-2} 通り
  •  n-1 段目から来る:  a_{n-1} 通り

となるので、


 a_{n} = a_{n-1} + a_{n-2}


という漸化式がたつことになる。これはフィボナッチ数列の漸化式でもある。

さて、ここでやっているのはまさに dp そのもので、

  • dp[ v ] := v 段目まで登る方法の数

とすると、v 段目まで登る方法は

  • v-2 段目から来る:dp[ v - 2 ] 通り
  • v-1 段目から来る:dp[ v - 1 ] 通り

あるので、結局

  • dp[ v ] = dp[ v - 1 ] + dp[ v - 2 ]

という式が立つことになる。こうしてみると、DP は漸化式だという立場の気持ちもよくわかる。実装もシンプルに

dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
  dp[i] = dp[i-1] + dp[i-2];
}

という雰囲気で書くことができる (ここでは 1000000007 で割ることは無視してる)。

壊れたところもある

壊れたところがあっても、実はほとんど難易度は変わらなくて、同じように

  • dp[ v ] = v 段目までのぼる場合の数

として、v 段目までのぼる方法を

  • v-2 段目から来る:dp[ v - 2 ] 通り
  • v-1 段目から来る:dp[ v - 1 ] 通り

という感じで場合分けしていたところを、

  • v-2 段目から来る:dp[ v - 2 ] 通り (ただし v-2 段目が壊れていたらなし)
  • v-1 段目から来る:dp[ v - 1 ] 通り (ただし v-1 段目が壊れていたらなし)

とするだけである。詳しくは実装をみるとわかると思う (if 文で分岐してる)。あとは 1000000007 で割る というところに注意。

#include <iostream>
#include <vector>
using namespace std;
const int MOD = 1000000007;

int N, M;
vector<bool> issafe; // issafe[v] := v が壊れていないかどうか

int main() {
    cin >> N >> M;
    issafe.assign(N+1, true);
    for (int i = 0; i < M; ++i) {
        int a; cin >> a;
        issafe[a] = false; // a 段目は壊れてる
    }

    // DP テーブル
    vector<int> dp(N+1, 0); // 各段について 0 で初期化しておく

    // 初期条件
    dp[0] = 1;
    if (issafe[1]) dp[1] = 1;

    // ループ
    for (int n = 2; n <= N; ++n) {
        if (issafe[n-1]) dp[n] += dp[n-1]; // n-1 段目が安全なら
        if (issafe[n-2]) dp[n] += dp[n-2]; // n-2 段目が安全なら
        dp[n] %= MOD; // 1000000007 で割る
    }
    cout << dp[N] << endl;
}

参考資料

qiita.com

qiita.com

みんなのプロコン 2017 C - 検索 (400 点)

頑張った

問題概要

 N 個の文字列が与えられて

  • そのうちの指定された  K 個についてについては、その prefix となっている
  • 指定されていない  N-K 個については prefix にはなっていない

ような最短の文字列を求めよ。存在しない場合は -1 とせよ。

制約

  •  1 \le K \le N \le 10^{5}
  •  N 個の文字列の長さの総和が  10^{5} 以下

考えたこと

まず  K 個の共通 prefix として最長のものを求めて、それを残り  N-K 個に対して「どこで最初にくい違うか」を求めてその index の max をとれば OK

atcoder.jp