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

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

AtCoder ARC 171 B - Chmax (水色, 600 点)

順列をどうのこうのする系、最近多いかもしれない。

問題文

考えたこと

操作の内容を解釈するのに少し苦労した。

順列  P から誘導される Functional Graph を考えた。このグラフ上で、頂点  i から出発して頂点番号が大きくなる限り進んでいったときの終点を  T_{i} とすると、操作によって、最終的に数列  B F(B) = T となることがわかった。

ここで、サンプル 3 を具体的に見てみよう (0-indexed にします)。 N = 8 A = (5, 5, 7, 3, 4, 5, 7, 7) である。このサンプルから読み取れるのは


  • 頂点 0, 1, 5 は最終的に頂点 5 に行き着く
    • よって、Functional Graph は「1 → 2 → 5」を含む
  • 頂点 2, 6, 7 は最終的に頂点 7 に行き着く
    • よって、Functional Graph は「3 → 6 → 7」を含む
  • 頂点 3 は頂点 3 に行き着く
  • 頂点 4 は頂点 4 に行き着く

さて、上記の 4 つの要素は、次の 4 本のパスであると再解釈できる。

  • 頂点 3 から頂点 3 へのパス (1 頂点)
  • 頂点 4 から頂点 4 へのパス (1 頂点)
  • 頂点 0 から頂点 5 へのパス (3 頂点)
  • 頂点 2 から頂点 7 へのパス (3 頂点)

この時点で Functional Graph において、「行き先」の決まっていない頂点は、パスの終点をなす頂点 3, 4, 5, 7 のみである。これらを頂点番号が小さい順に行き先を決めていくと、次のように考えられる。

  • 頂点 3 の行き先になれる頂点は、パスの始点をなす頂点 0, 2, 3, 4 のうち、3 以下である 3 個である
  • 頂点 4 の行き先になれる頂点は、パスの始点をなす頂点 0, 2, 3, 4 のうち、4 以下である 4 個の中で、頂点 3 の行き先として選ばれた 1 個を除いた 4 - 1 = 3 個である
  • 頂点 5 の行き先になれる頂点は、パスの始点をなす頂点 0, 2, 3, 4 のうち、5 以下である 4 個の中で、頂点 3, 4 の行き先として選ばれた 2 個を除いた 4 - 2 = 2 個である
  • 頂点 7 の行き先になれる頂点は、パスの始点をなす頂点 0, 2, 3, 4 のうち、7 以下である 4 個の中で、頂点 3, 4, 5 の行き先として選ばれた 3 個を除いた 4 - 3 = 1 個である

よって、これらをかけて

 3 \times 3 \times 2 \times 1 = 18 通り

である。

一般化

上記の議論を一般化しよう。まず、そもそも Functional Graph が存在するためには、

  • 任意の  i に対して  A_{i} \ge i であること
  •  A_{j} = i となる  j の最大値が  i であること

が必要であることに注意しよう。これさえ満たせば、条件を満たす Functional Graph は存在する。このとき、サンプル 3 と同様に、数列  A の値で分類することで

  • 頂点  s_{0} から頂点  t_{0} へのパス
  • 頂点  s_{1} から頂点  t_{1} へのパス
  •  \dots
  • 頂点  s_{M-1} から頂点  t_{M-1} へのパス

を抽出する。ただし、 t_{0} \lt t_{1} \lt \dots \lt t_{M-1} とする。このとき、各  i に対して


( s_{0}, s_{1}, \dots, s_{M-1} のうち  t_{i} 以下であるものの個数)  - i


をかけていけばよい。計算量は  O(N) などとなる。

コード

#include <bits/stdc++.h>
using namespace std;
using pint = pair<int, int>;

// modint
template<int MOD> struct Fp {
    // inner value
    long long val;
    
    // constructor
    constexpr Fp() : val(0) { }
    constexpr Fp(long long v) : val(v % MOD) {
        if (val < 0) val += MOD;
    }
    constexpr long long get() const { return val; }
    constexpr int get_mod() const { return MOD; }
    
    // arithmetic operators
    constexpr Fp operator + () const { return Fp(*this); }
    constexpr Fp operator - () const { return Fp(0) - Fp(*this); }
    constexpr Fp operator + (const Fp &r) const { return Fp(*this) += r; }
    constexpr Fp operator - (const Fp &r) const { return Fp(*this) -= r; }
    constexpr Fp operator * (const Fp &r) const { return Fp(*this) *= r; }
    constexpr Fp operator / (const Fp &r) const { return Fp(*this) /= r; }
    constexpr Fp& operator += (const Fp &r) {
        val += r.val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -= (const Fp &r) {
        val -= r.val;
        if (val < 0) val += MOD;
        return *this;
    }
    constexpr Fp& operator *= (const Fp &r) {
        val = val * r.val % MOD;
        return *this;
    }
    constexpr Fp& operator /= (const Fp &r) {
        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 Fp pow(long long n) const {
        Fp res(1), mul(*this);
        while (n > 0) {
            if (n & 1) res *= mul;
            mul *= mul;
            n >>= 1;
        }
        return res;
    }
    constexpr Fp inv() const {
        Fp res(1), div(*this);
        return res / div;
    }

    // other operators
    constexpr bool operator == (const Fp &r) const {
        return this->val == r.val;
    }
    constexpr bool operator != (const Fp &r) const {
        return this->val != r.val;
    }
    constexpr Fp& operator ++ () {
        ++val;
        if (val >= MOD) val -= MOD;
        return *this;
    }
    constexpr Fp& operator -- () {
        if (val == 0) val += MOD;
        --val;
        return *this;
    }
    constexpr Fp operator ++ (int) const {
        Fp res = *this;
        ++*this;
        return res;
    }
    constexpr Fp operator -- (int) const {
        Fp res = *this;
        --*this;
        return res;
    }
    friend constexpr istream& operator >> (istream &is, Fp<MOD> &x) {
        is >> x.val;
        x.val %= MOD;
        if (x.val < 0) x.val += MOD;
        return is;
    }
    friend constexpr ostream& operator << (ostream &os, const Fp<MOD> &x) {
        return os << x.val;
    }
    friend constexpr Fp<MOD> pow(const Fp<MOD> &r, long long n) {
        return r.pow(n);
    }
    friend constexpr Fp<MOD> inv(const Fp<MOD> &r) {
        return r.inv();
    }
};

const int MOD = 998244353;
using mint = Fp<MOD>;

void solve() {
    int N;
    cin >> N;
    vector<int> A(N);
    for (int i = 0; i < N; ++i) {
        cin >> A[i];
        --A[i];
        if (A[i] < i) {
            cout << 0 << endl;
            return;
        }
    }

    // Functional Graph のうち、すでに行き先のわかっている部分をまとめることで、パスに分解する
    map<int, vector<int>> ma;
    for (int i = 0; i < N; ++i) ma[A[i]].push_back(i);
    vector<pint> paths;  // パスの終点, 始点
    for (auto [val, vec] : ma) {
        sort(vec.begin(), vec.end());
        if (vec.back() != val) {
            cout << 0 << endl;
            return;
        }
        paths.emplace_back(vec.back(), vec[0]);
    }
    
    // パスの始点のうち、頂点番号がある値以下であるものの個数を累積和で求める
    vector<int> num(N + 2, 0);
    for (auto [x, y] : paths) ++num[y+1];
    for (int i = 0; i <= N; ++i) num[i+1] += num[i];

    // 集計
    mint res = 1;
    for (int i = 0; i < paths.size(); ++i) {
        res *= num[paths[i].first + 1] - i;
    }
    cout << res << endl;
}

int main() {
    solve();
}