えびちゃんの日記

えびちゃん(競プロ)の日記です。

Educational Codeforces Round 15 E: Analysis of Pathes in Functional Graph

今日はこれの解説を書きます.

codeforces.com

問題概要

\(n\) 頂点 \(n\) 辺の有向グラフがあり,頂点 \(i\) からは \(f_i\) に辺が伸びています. すなわち,頂点 \(i\) の行き先はちょうど一つに定まります.

頂点 \(i\) から(\(f_i\) に)遷移するときのコストは \(w_i\) です. 各頂点 \(i\) について,「\(i\) から \(k\) 回の遷移を行うときに発生する(重複を含めて \(k\) 個の)コストの総和と最小値」をそれぞれ求めてください.

考察

頂点 \(i\) から \(1\) 回遷移したときのコストは(入力から)わかっています. ここで,\(w_i\) と \(w_{f_i}\) を見て,頂点 \(i\) から \(2\) 回遷移したときのコストも求めることができます. このとき,\(f_i\) と \(f_{f_i}\) も見て,頂点 \(i\) から \(2\) 回遷移したときの遷移先の頂点も求めておきます.

同様にして,\(2^i\) 回遷移したときのコストと遷移先の頂点から,\(2^{i+1}\) 回遷移したときのそれらを求めることが可能です.

各操作は \(O(n)\) で計算でき,この操作は \(O(\log k)\) 回行えば十分なので,全体の計算量は \(O(n\log k)\) です.

繰り返し二乗法の要領で,\(k\) の特定のビットを見ながら更新していき,全体の答えを求めることができます. 今いる頂点と,今までのコストから計算した値を持っておいて,その頂点から \(2^i\) 回遷移したときにどうなるかを考えて更新していけばよいです.

ところで,適当な型の総和も最小値もモノイド*1 なので,抽象化して作っておくと,一回の実装で済ませられて便利です.というか片方だけバグらせるみたいな事故を防げるのがうれしい.

実装

main() はこれだけ.

int main() {
  size_t n;
  uintmax_t k;
  scanf("%zu %ju", &n, &k);

  std::vector<size_t> f(n);
  for (auto& fi: f) scanf("%zu", &fi);
  std::vector<intmax_t> w(n);
  for (auto& wi: w) scanf("%jd", &wi);

  auto s = transition(f, w, add<intmax_t>(), k);
  auto m = transition(f, w, min<intmax_t>(), k);

  for (size_t i = 0; i < n; ++i)
    printf("%jd %jd\n", s[i], m[i]);
}

addmin はこんな感じで作っておきます.

template <typename Tp>
struct add {
  Tp const identity = 0;
  Tp operator ()(Tp const& x, Tp const& y) const { return x + y; }
};

template <typename Tp>
struct min {
  Tp const identity = std::numeric_limits<Tp>::max();
  Tp operator ()(Tp const& x, Tp const& y) const { return std::min(x, y); }
};

これはえびちゃんのライブラリでありがちなんですが,単位元を表す Tp const identity と,適当な演算を行う Tp operator ()(Tp const&, Tp const&) const を持たせておきます.ここら辺はよしなに.

実際にダブリングを行う transition() はこんな感じ.

template <typename Tp, typename Fn>
std::vector<Tp> transition(std::vector<size_t> const& dst,
                           std::vector<Tp> const& value,
                           Fn f, uintmax_t k) {

  size_t n = dst.size();
  std::vector<Tp> res_v(n, f.identity);
  std::vector<size_t> res_d(n);
  std::iota(res_d.begin(), res_d.end(), 0);
  std::vector<Tp> dbl_v = value;
  std::vector<size_t> dbl_d = dst;
  for (; k; k >>= 1) {
    if (k & 1) {
      std::vector<size_t> tmp_d = res_d;
      for (size_t i = 0; i < n; ++i) {
        res_v[i] = f(res_v[i], dbl_v[tmp_d[i]]);
        res_d[i] = dbl_d[tmp_d[i]];
      }
    }
    {
      std::vector<Tp> tmp_v = dbl_v;
      std::vector<size_t> tmp_d = dbl_d;
      for (size_t i = 0; i < n; ++i) {
        dbl_v[i] = f(dbl_v[i], tmp_v[tmp_d[i]]);
        dbl_d[i] = tmp_d[tmp_d[i]];
      }
    }
  }
  return res_v;
}

繰り返し二乗法の要領です. えびちゃん的には繰り返し二乗法は当然非再帰で書くんですが,そうでない人はよしなに.

https://codeforces.com/contest/702/submission/55269442

*1:単位元を持ち,結合律を満たすもの.\(\min\) についてはその型の最大値を単位元と見なせる.