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

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

yukicoder No.254 文字列の構成

yukicoder で「構築」練習することにした!

問題概要

英小文字からなる文字列  S であって、次の条件を満たすものを求めよ。

  • 文字数は  10^{5} 以下である
  • どの隣接する文字も相異なる
  •  S の連続する部分文字列であって、回文であるものがちょうど  N 個存在する

制約

  •  N \le 10^{9}

考えたこと

基本的に


"abababab...aba"


のような文字列を、文字を変えて繋げていけばよさそうだと考えた。

まず、上の文字列において、文字 'a' が  x 個あるときに、回文である連続文字列が何個あるかを求めてみた。そうすると......

  • 長さ  1 のもの: 2x - 1
  • 長さ  3 のもの: 2x - 3
  • 長さ  5 のもの: 2x - 5
  • ...
  • 長さ  2x - 1 のもの: 1

これらを足すと、なんと綺麗に  x^{2} となるのである。

よって、もとの問題は「整数  N を平方数の和として表せ」という問題となったのだ。

基本的には Greedy で良いと考えた。つまり、「 N 以下の最大の平方数を  s として、 N から  s を引く」を繰り返せばよいと。

最後に、この方針で文字列の長さが  10^{5} 以下になるかどうかを確かめた。 N = 10^{9} でも、文字列の長さは 63725 であったので大丈夫だった。

コード

#include <bits/stdc++.h>
using namespace std;

// a -> 1
// aba -> 4
// ababa -> 9
// abababa -> 16

const long long MAX = 100000;

int main() {
    string res = "";
    char letter = 'a';
    
    long long N;
    cin >> N;
    while (N > 0) {
        long long low = -1, high = MAX;
        while (high - low > 1) {
            long long x = (low + high) / 2;
            if (x * x <= N) low = x;
            else high = x;
        }
        res += letter;
        for (int i = 0; i < low - 1; ++i) {
            res += (char)(letter + 1);
            res += letter;
        }
        N -= low * low;
        letter += 2;
    }
    cout << res << endl;
}

AtCoder ARC 176 E - Max Vector (赤色, 800 点)

面白かった!! 上手に変数変換することで「2 変数劣モジュラ関数の和の最小化」になるタイプの問題だった。

問題概要

考えたこと

一目見て、2 変数劣モジュラ関数の最小化 (燃やす埋める) っぽいと感じた。値が 500 以下というのも怪しい。

次のような変数  x_{i,j}, y_{i,j}, z_{k} を考えたくなった。ここで、 K 値をとる整数変数を  K-1 個の 0-1 変数で表現するテクを用いている。

 x_{i,j} = \left\{
\begin{array}{ll}
1 & (X_{i} \gt j のとき)\\
0 & (X_{i} \le j のとき)
\end{array}
\right.
 y_{i,j} = \left\{
\begin{array}{ll}
1 & (Y_{i} \gt j のとき)\\
0 & (Y_{i} \le j のとき)
\end{array}
\right.
 z_{k} = \left\{
\begin{array}{ll}
1 & (数列 k を X 側に操作するとき)\\
0 & (数列 k を Y 側に操作するとき)
\end{array}
\right.

そして、次のような制約条件が成り立つ。

  •  z_{j} = 1 かつ  X_{i, A_{j,i}-1} = 0 は禁止する
  •  z_{j} = 0 かつ  Y_{i, A_{j,i}-1} = 0 は禁止する

しかし、上はともかく、下は劣モジュラ関数にはならない。そこで、変数  y の定義をビット反転して、以下のように変更することにした。

 y_{i,j} = \left\{
\begin{array}{ll}
0 & (Y_{i} \gt j のとき)\\
1 & (Y_{i} \le j のとき)
\end{array}
\right.

これで、制約条件は次のように記述できることとなった。

  •  z_{j} = 1 かつ  X_{i, A_{j,i}-1} = 0 は禁止する
  •  z_{j} = 0 かつ  Y_{i, A_{j,i}-1} = 1 は禁止する

総合すると、各制約条件は次のように書ける (これは実装中のメモ)。

コード

#include <bits/stdc++.h>
using namespace std;


// 2-variable submodular optimization
template<class COST> struct TwoVariableSubmodularOpt {
    // constructors
    TwoVariableSubmodularOpt() : N(2), S(0), T(0), OFFSET(0) {}
    TwoVariableSubmodularOpt(int n, COST inf = 0)
    : N(n), S(n), T(n + 1), OFFSET(0), INF(inf), list(n + 2) {}
    
    // initializer
    void init(int n, COST inf = 0) {
        N = n, S = n, T = n + 1;
        OFFSET = 0, INF = inf;
        list.assign(N + 2, Edge());
        pos.clear();
    }

    // add 1-Variable submodular functioin
    void add_single_cost(int xi, COST false_cost, COST true_cost) {
        assert(0 <= xi && xi < N);
        if (false_cost >= true_cost) {
            OFFSET += true_cost;
            add_edge(S, xi, false_cost - true_cost);
        } else {
            OFFSET += false_cost;
            add_edge(xi, T, true_cost - false_cost);
        }
    }
    
    // add "project selection" constraint
    // xi = T, xj = F: strictly prohibited
    void add_psp_constraint(int xi, int xj) {
        assert(0 <= xi && xi < N);
        assert(0 <= xj && xj < N);
        add_edge(xi, xj, INF);
    }
    
    // add "project selection" penalty
    // xi = T, xj = F: cost C
    void add_psp_penalty(int xi, int xj, COST C) {
        assert(0 <= xi && xi < N);
        assert(0 <= xj && xj < N);
        assert(C >= 0);
        add_edge(xi, xj, C);
    }
    
    // add both True profit
    // xi = T, xj = T: profit P (cost -P)
    void add_both_true_profit(int xi, int xj, COST P) {
        assert(0 <= xi && xi < N);
        assert(0 <= xj && xj < N);
        assert(P >= 0);
        OFFSET -= P;
        add_edge(S, xi, P);
        add_edge(xi, xj, P);
    }
    
    // add both False profit
    // xi = F, xj = F: profit P (cost -P)
    void add_both_false_profit(int xi, int xj, COST P) {
        assert(0 <= xi && xi < N);
        assert(0 <= xj && xj < N);
        assert(P >= 0);
        OFFSET -= P;
        add_edge(xj, T, P);
        add_edge(xi, xj, P);
    }
    
    // add general 2-variable submodular function
    // (xi, xj) = (F, F): A, (F, T): B
    // (xi, xj) = (T, F): C, (T, T): D
    void add_submodular_function(int xi, int xj, COST A, COST B, COST C, COST D) {
        assert(0 <= xi && xi < N);
        assert(0 <= xj && xj < N);
        assert(B + C >= A + D);  // assure submodular function
        OFFSET += A;
        add_single_cost(xi, 0, D - B);
        add_single_cost(xj, 0, B - A);
        add_psp_penalty(xi, xj, B + C - A - D);
    }
    
    // add all True profit
    // y = F: not gain profit (= cost is P), T: gain profit (= cost is 0)
    // y: T, xi: F is prohibited
    void add_all_true_profit(const vector<int> &xs, COST P) {
        assert(P >= 0);
        int y = (int)list.size();
        list.resize(y + 1);
        OFFSET -= P;
        add_edge(S, y, P);
        for (auto xi : xs) {
            assert(xi >= 0 && xi < N);
            add_edge(y, xi, INF);
        }
    }
    
    // add all False profit
    // y = F: gain profit (= cost is 0), T: not gain profit (= cost is P)
    // xi = T, y = F is prohibited
    void add_all_false_profit(const vector<int> &xs, COST P) {
        assert(P >= 0);
        int y = (int)list.size();
        list.resize(y + 1);
        OFFSET -= P;
        add_edge(y, T, P);
        for (auto xi : xs) {
            assert(xi >= 0 && xi < N);
            add_edge(xi, y, INF);
        }
    }
    
    // solve
    COST solve() {
        return dinic() + OFFSET;
    }
    
    // reconstrcut the optimal assignment
    vector<bool> reconstruct() {
        vector<bool> res(N, false), seen(list.size(), false);
        queue<int> que;
        seen[S] = true;
        que.push(S);
        while (!que.empty()) {
            int v = que.front();
            que.pop();
            for (const auto &e : list[v]) {
                if (e.cap && !seen[e.to]) {
                    if (e.to < N) res[e.to] = true;
                    seen[e.to] = true;
                    que.push(e.to);
                }
            }
        }
        return res;
    }
    
    // debug
    friend ostream& operator << (ostream& s, const TwoVariableSubmodularOpt &G) {
        const auto &edges = G.get_edges();
        for (const auto &e : edges) s << e << endl;
        return s;
    }
    
private:
    // edge class
    struct Edge {
        // core members
        int rev, from, to;
        COST cap, icap, flow;
        
        // constructor
        Edge(int r, int f, int t, COST c)
        : rev(r), from(f), to(t), cap(c), icap(c), flow(0) {}
        void reset() { cap = icap, flow = 0; }
        
        // debug
        friend ostream& operator << (ostream& s, const Edge& E) {
            return s << E.from << "->" << E.to << '(' << E.flow << '/' << E.icap << ')';
        }
    };
    
    // inner data
    int N, S, T;
    COST OFFSET, INF;
    vector<vector<Edge>> list;
    vector<pair<int,int>> pos;
    
    // add edge
    Edge &get_rev_edge(const Edge &e) {
        if (e.from != e.to) return list[e.to][e.rev];
        else return list[e.to][e.rev + 1];
    }
    Edge &get_edge(int i) {
        return list[pos[i].first][pos[i].second];
    }
    const Edge &get_edge(int i) const {
        return list[pos[i].first][pos[i].second];
    }
    vector<Edge> get_edges() const {
        vector<Edge> edges;
        for (int i = 0; i < (int)pos.size(); ++i) {
            edges.push_back(get_edge(i));
        }
        return edges;
    }
    void add_edge(int from, int to, COST cap) {
        if (!cap) return;
        pos.emplace_back(from, (int)list[from].size());
        list[from].push_back(Edge((int)list[to].size(), from, to, cap));
        list[to].push_back(Edge((int)list[from].size() - 1, to, from, 0));
    }
    
    // Dinic's algorithm
    COST dinic(COST limit_flow) {
        COST current_flow = 0;
        vector<int> level((int)list.size(), -1), iter((int)list.size(), 0);
        
        // Dinic BFS
        auto bfs = [&]() -> void {
            level.assign((int)list.size(), -1);
            level[S] = 0;
            queue<int> que;
            que.push(S);
            while (!que.empty()) {
                int v = que.front();
                que.pop();
                for (const Edge &e : list[v]) {
                    if (level[e.to] < 0 && e.cap > 0) {
                        level[e.to] = level[v] + 1;
                        if (e.to == T) return;
                        que.push(e.to);
                    }
                }
            }
        };
        
        // Dinic DFS
        auto dfs = [&](auto self, int v, COST up_flow) {
            if (v == T) return up_flow;
            COST res_flow = 0;
            for (int &i = iter[v]; i < (int)list[v].size(); ++i) {
                Edge &e = list[v][i], &re = get_rev_edge(e);
                if (level[v] >= level[e.to] || e.cap == 0) continue;
                COST flow = self(self, e.to, min(up_flow - res_flow, e.cap));
                if (flow <= 0) continue;
                res_flow += flow;
                e.cap -= flow, e.flow += flow;
                re.cap += flow, re.flow -= flow;
                if (res_flow == up_flow) break;
            }
            return res_flow;
        };
        
        // flow
        while (current_flow < limit_flow) {
            bfs();
            if (level[T] < 0) break;
            iter.assign((int)iter.size(), 0);
            while (current_flow < limit_flow) {
                COST flow = dfs(dfs, S, limit_flow - current_flow);
                if (!flow) break;
                current_flow += flow;
            }
        }
        return current_flow;
    };
    COST dinic() {
        return dinic(numeric_limits<COST>::max());
    }
};

int main() {
    const int VAL = 500;
    
    int N, M;
    cin >> N >> M;
    vector<int> X(N), Y(N);
    for (int i = 0; i < N; ++i) cin >> X[i];
    for (int i = 0; i < N; ++i) cin >> Y[i];
    vector A(M, vector(N, 0));
    for (int i = 0; i < M; ++i) for (int j = 0; j < N; ++j) cin >> A[i][j];
    
    /*
     j = 0, 1, ..., 499 について
     x[i,j] = 1 (Xi が j より大きい) -> cost 1; 0 (Xi が j 以下である) -> cost 0
     y[i,j] = 0 (Yi が j より大きい) -> cost 1; 1 (Yi が j 以下である) -> cost 0
     z[i] = 1 (X 側を選ぶとき), 0 (Y 側を選ぶとき)
     
     x[i,j] = 0, x[i,j+1] = 1 -> cost INF
     y[i,j] = 1, y[i,j+1] = 0 -> cost INF
     
     x[i,X[i]-1] = 1 である -> 0 に cost INF
     y[i,Y[i]-1] = 0 である -> 1 に cost INF
     
     z[j] = 1 かつ X[i,A[j,i]-1] = 0 -> cost INF
     z[j] = 0 かつ Y[i,A[j,i]-1] = 1 -> cost INF
     */
    const long long INF = 1LL<<50;
    TwoVariableSubmodularOpt<long long> tvs(N * VAL * 2 + M, INF);
    for (int i = 0; i < N; ++i) {
        for (int j = 0; j < VAL; ++j) {
            tvs.add_single_cost(i * VAL + j, 0, 1);
            tvs.add_single_cost(N * VAL + i * VAL + j, 1, 0);
            if (j + 1 < VAL) {
                tvs.add_psp_constraint(i * VAL + j + 1, i * VAL + j);
                tvs.add_psp_constraint(N * VAL + i * VAL + j, N * VAL + i * VAL + j + 1);
            }
        }
        tvs.add_single_cost(i * VAL + X[i] - 1, INF, 0);
        tvs.add_single_cost(N * VAL + i * VAL + Y[i] - 1, 0, INF);
    }
    for (int j = 0; j < M; ++j) {
        for (int i = 0; i < N; ++i) {
            tvs.add_psp_constraint(N * VAL * 2 + j, i * VAL + A[j][i] - 1);
            tvs.add_psp_constraint(N * VAL + i * VAL + A[j][i] - 1, N * VAL * 2 + j);
        }
    }
    cout << tvs.solve() << endl;
}

AtCoder ARC 176 D - Swap Permutation (橙色, 700 点)

行列累乗した。デバッグに手こずった。

問題概要

 1, 2, \dots, N の順列  P_{1}, P_{2}, \dots, P_{N} が与えられる。以下の操作を  M 回行う。

  •  i, j を選んで  P_{i} P_{j} を swap する

操作列は  (\frac{N(N-1)}{2})^{M} 通り考えられるが、それぞれについての  \displaystyle \sum_{i=1}^{N-2} |P_{i} - P_{i+1}| の総和を 998244353 で割った余りを求めよ。

制約

  •  N, M \le 2 \times 10^{5}

考えたこと

 \displaystyle \sum_{i=1}^{N-2} |P_{i} - P_{i+1}| の期待値を求めて、最後に  (\frac{N(N-1)}{2})^{M} をかけることにした。

このとき、期待値の線形性から、操作後の  |P_{i} - P_{i+1}| の値の期待値を求めて、各  i について合算すれば十分である。ここで、値の組  (P_{i}, P_{i+1}) が操作によってどのように変化していくかを考えると、以下の 4 パターンを考えれば十分であることに気づく。なお、初期状態で  P_{i} = a,  P_{i+1} = b であるとする。

  •  P_{i}, P_{i+1} a, b である状態 (順序は問わない)
  •  P_{i}, P_{i+1} a b 以外である状態 (順序は問わない)
  •  P_{i}, P_{i+1} b a 以外である状態 (順序は問わない)
  •  P_{i}, P_{i+1} がいずれも  a でも  b でもない状態 (順序は問わない)

各グループについて  2, N-2, N-2, {}_{N}\rm{C}_{2} 通りあるわけだが、同じグループ内では確率は等しいため、この 4 通りの状態に潰してよいわけだ。

この 4 通りの状態を移り変わる推移図を求めて、行列累乗することによって、 M 回操作後の、上記 4 状態になっている確率を求めることができる。これを用いて、操作後の  |P_{i} - P_{i+1}| の値の期待値が求められる。

なお、この求めた確率は  i の値によらないので、他の  i にも再利用できる。

全体として計算量は  O(N \log M) となる。

コード

#include <bits/stdc++.h>
using namespace std;

// matrix
template<class mint> struct MintMatrix {
    // inner value
    vector<vector<mint>> val;
    
    // constructors
    MintMatrix(int H, int W, mint x = 0) : val(H, vector<mint>(W, x)) {}
    MintMatrix(const MintMatrix &mat) : val(mat.val) {}
    void init(int H, int W, mint x = 0) {
        val.assign(H, vector<mint>(W, x));
    }
    void resize(int H, int W) {
        val.resize(H);
        for (int i = 0; i < H; ++i) val[i].resize(W);
    }
    
    // getter and debugger
    constexpr int height() const { return (int)val.size(); }
    constexpr int width() const { return (int)val[0].size(); }
    vector<mint>& operator [] (int i) { return val[i]; }
    constexpr vector<mint>& operator [] (int i) const { return val[i]; }
    friend constexpr ostream& operator << (ostream &os, const MintMatrix<mint> &mat) {
        os << endl;
        for (int i = 0; i < mat.height(); ++i) {
            for (int j = 0; j < mat.width(); ++j) {
                if (j) os << ", ";
                os << mat.val[i][j];
            }
            os << endl;
        }
        return os;
    }
    
    // comparison operators
    constexpr bool operator == (const MintMatrix &r) const {
        return this->val == r.val;
    }
    constexpr bool operator != (const MintMatrix &r) const {
        return this->val != r.val;
    }
    
    // arithmetic operators
    constexpr MintMatrix& operator += (const MintMatrix &r) {
        assert(height() == r.height());
        assert(width() == r.width());
        for (int i = 0; i < height(); ++i) {
            for (int j = 0; j < width(); ++j) {
                val[i][j] += r.val[i][j];
            }
        }
        return *this;
    }
    constexpr MintMatrix& operator -= (const MintMatrix &r) {
        assert(height() == r.height());
        assert(width() == r.width());
        for (int i = 0; i < height(); ++i) {
            for (int j = 0; j < width(); ++j) {
                val[i][j] -= r.val[i][j];
            }
        }
        return *this;
    }
    constexpr MintMatrix& operator *= (const mint &v) {
        for (int i = 0; i < height(); ++i)
            for (int j = 0; j < width(); ++j)
                val[i][j] *= v;
        return *this;
    }
    constexpr MintMatrix& operator *= (const MintMatrix &r) {
        assert(width() == r.height());
        MintMatrix<mint> res(height(), r.width());
        for (int i = 0; i < height(); ++i)
            for (int j = 0; j < r.width(); ++j)
                for (int k = 0; k < width(); ++k)
                    res[i][j] += val[i][k] * r.val[k][j];
        return (*this) = res;
    }
    constexpr MintMatrix operator + () const { return MintMatrix(*this); }
    constexpr MintMatrix operator - () const { return MintMatrix(*this) *= mint(-1); }
    constexpr MintMatrix operator + (const MintMatrix &r) const { return MintMatrix(*this) += r; }
    constexpr MintMatrix operator - (const MintMatrix &r) const { return MintMatrix(*this) -= r; }
    constexpr MintMatrix operator * (const mint &v) const { return MintMatrix(*this) *= v; }
    constexpr MintMatrix operator * (const MintMatrix &r) const { return MintMatrix(*this) *= r; }
    
    // pow
    constexpr MintMatrix pow(long long n) const {
        assert(height() == width());
        MintMatrix<mint> res(height(), width()),  mul(*this);
        for (int row = 0; row < height(); ++row) res[row][row] = 1;
        while (n > 0) {
            if (n & 1) res *= mul;
            mul *= mul;
            n >>= 1;
        }
        return res;
    }
    friend constexpr MintMatrix<mint> pow(const MintMatrix<mint> &mat, long long n) {
        return mat.pow(n);
    }
    
    // gauss-jordan
    constexpr int find_pivot(int cur_rank, int col) const {
        int pivot = -1;
        for (int row = cur_rank; row < height(); ++row) {
            if (val[row][col] != 0) {
                pivot = row;
                break;
            }
        }
        return pivot;
    }
    constexpr void sweep(int cur_rank, int col, int pivot) {
        swap(val[pivot], val[cur_rank]);
        auto ifac = val[cur_rank][col].inv();
        for (int col2 = 0; col2 < width(); ++col2) {
            val[cur_rank][col2] *= ifac;
        }
        for (int row = 0; row < height(); ++row) {
            if (row != cur_rank && val[row][col] != 0) {
                auto fac = val[row][col];
                for (int col2 = 0; col2 < width(); ++col2) {
                    val[row][col2] -= val[cur_rank][col2] * fac;
                }
            }
        }
    }
    constexpr int gauss_jordan(int not_sweep_width = 0) {
        int rank = 0;
        for (int col = 0; col < width(); ++col) {
            if (col == width() - not_sweep_width) break;
            int pivot = find_pivot(rank, col);
            if (pivot == -1) continue;
            sweep(rank++, col, pivot);
        }
        return rank;
    }
    friend constexpr int gauss_jordan(MintMatrix<mint> &mat, int not_sweep_width = 0) {
        return mat.gauss_jordan(not_sweep_width);
    }
    friend constexpr int linear_equation
    (const MintMatrix<mint> &mat, const vector<mint> &b, vector<mint> &res) {
        // extend
        MintMatrix<mint> A(mat.height(), mat.width() + 1);
        for (int i = 0; i < mat.height(); ++i) {
            for (int j = 0; j < mat.width(); ++j) A[i][j] = mat.val[i][j];
            A[i].back() = b[i];
        }
        int rank = A.gauss_jordan(1);
        
        // check if it has no solution
        for (int row = rank; row < mat.height(); ++row) if (A[row].back() != 0) return -1;

        // answer
        res.assign(mat.width(), 0);
        for (int i = 0; i < rank; ++i) res[i] = A[i].back();
        return rank;
    }
    friend constexpr int linear_equation(const MintMatrix<mint> &mat, const vector<mint> &b) {
        vector<mint> res;
        return linear_equation(mat, b, res);
    }
    
    // determinant
    constexpr mint det() const {
        MintMatrix<mint> A(*this);
        int rank = 0;
        mint res = 1;
        for (int col = 0; col < width(); ++col) {
            int pivot = A.find_pivot(rank, col);
            if (pivot == -1) return mint(0);
            res *= A[pivot][rank];
            A.sweep(rank++, col, pivot);
        }
        return res;
    }
    friend constexpr mint det(const MintMatrix<mint> &mat) {
        return mat.det();
    }
};

// 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 Fp(const Fp &v) : val(v.get()) { }
    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();
    }
};


int main() {
    const int MOD = 998244353;
    using mint = Fp<MOD>;
    
    long long N, M;
    cin >> N >> M;
    vector<long long> P(N);
    for (int i = 0; i < N; ++i) cin >> P[i], --P[i];
    
    long long NC = N * (N - 1) / 2, N2C = (N - 2) * (N - 3) / 2;
    mint all = mint(NC).pow(M);
    
    if (N == 2) {
        cout << all << endl;
        return 0;
    }
    
    auto Q = P;
    sort(Q.begin(), Q.end());
    vector<long long> left(N+1, 0), right(N+1, 0);
    for (int i = 0; i < N; ++i) {
        left[i+1] = left[i] + Q[i];
        right[i+1] = right[i] + Q[N-i-1];
    }
    
    auto calc_sum = [&](long long x) -> long long {
        long long l = lower_bound(Q.begin(), Q.end(), x) - Q.begin();
        return (x * l - left[l]) + (right[N - l] - x * (N - l));
    };
    
    vector<mint> f(N, 0);
    mint S = 0;
    for (int i = 0; i < N; ++i) {
        f[i] = calc_sum(P[i]);
        S += f[i];
    }
    
    MintMatrix<mint> A(4, 4);
    A[0][0] = mint(N2C + 1); // / NC;
    A[0][1] = A[0][2] = A[1][2] = A[2][1] = mint(1); // / NC;
    A[1][0] = A[2][0] = mint(N - 2); // / NC;
    A[1][1] = A[2][2] = mint(N2C + N - 2); // / NC;
    A[1][3] = A[2][3] = mint(2); // / NC;
    A[3][1] = A[3][2] = mint(N - 3); // / NC;
    A[3][3] = mint(N2C + N * 2 - 7); // / NC;
    auto AM = pow(A, M);
    
    mint res = 0;
    for (int i = 0; i + 1 < N; ++i) {
        mint diff = abs(P[i] - P[i+1]);
        res += AM[0][0] * diff;
        res += AM[1][0] * (f[i] - diff) / mint(N - 2);
        res += AM[2][0] * (f[i+1] - diff) / mint(N - 2);
        if (N > 3) res += AM[3][0] * (mint(S) / 2 - f[i] - f[i+1] + diff) / mint(N2C);
    }
    cout << res << endl;
}

AtCoder ABC 176 B - Simple Math 4 (水色, 400 点)

 M - K = 1 というコーナーケースにやられた!

問題概要

整数  N, M, K が与えられる。

 2^{N} 2^{M} - 2^{K} で割った余りの一の位の値を求めよ。

(マルチテストケース)

制約

  •  1 \le N \le 10^{18}
  •  1 \le K \lt M \le 10^{18}

 

考えたこと

まず最初に考えたのは「全体を  2^{K} で割った世界」で考えれば良いということ。

このことを正当化しよう。一般に、次のことが成り立つ。


整数  a, b, k と正の整数  m に対して

 a \equiv b \pmod m

であることと、

 ka \equiv kb \pmod{km}

であることは同値である。


つまり、 2^{N-K} 2^{M-K} - 1 で割った余りを  r とすると、

 2^{N-K} \equiv r \pmod{2^{M-K} - 1}

と表せるが、これを全体に  2^{K} 倍して得られる

 2^{N} \equiv 2^{K}r \pmod{2^{M} - 2^{K}}

は同値だということだ。

よって、この問題の答えは


 2^{N-K} 2^{M-K} - 1 で割った余りを求めて、 2^{K} をかけた値


となることがわかった。

例外処理

ただし、 N \lt K の場合は例外なので例外扱いする。

この場合は、 2^{N} \lt 2^{K} \le 2^{M} - 2^{K} であるから、答えは  2^{N} である。

 

本題

以降、 N \ge K とする。そして、「 2^{N-K} 2^{M-K} - 1 で割った余り」を頑張って求める。

ここで、今度は次のことに注目しよう。


相異なる整数 a, b と、整数多項式  f(x) に対して、次のことが成り立つ。

 f(a) \equiv f(b) \pmod{a - b}


つまり、 \pmod{a - b} についての合同式を考えるとき、 a の部分をそのまま  b に置き換えてよいということだ!!!

そうすると、たとえば  N - K = 14, M - K = 3 のとき、次のように式変形できる。

 2^{14} = (2^{3})^{4} \times 2^{2} \equiv 1^{4} \times 2^{2} = 2^{2} \pmod{2^{3} - 1}

より一般に、 N - K M - K で割った余りを  r とすると、次のようになる。

 2^{N - K} \equiv 2^{r} \pmod{2^{M-K} - 1}

よって、この問題の答えは  2^{r+K} である。あとは、この値を 10 で割った余りを求めれば OK。

計算量は  O(\log N) となる。

 

コード

#include <bits/stdc++.h>
using namespace std;

template<class T> T mod_pow(T a, T n, T m) {
    T res = 1;
    while (n > 0) {
        if (n % 2 == 1) res = res * a % m;
        a = a * a % m;
        n >>= 1;
    }
    return res;
};

void solve() {
    long long N, M, K;
    cin >> N >> M >> K;
    
    if (N < K) {
        cout << mod_pow(2LL, N, 10LL) << endl;
    } else {
        if (M - K == 1)
            cout << 0 << endl;
        else
            cout << mod_pow(2LL, (N - K) % (M - K) + K, 10LL) << endl;
    }
}

int main() {
    int T;
    cin >> T;
    while (T--) solve();
}

AtCoder ABC 350 C - Sort (灰色, 300 点)

 O(N^{2}) の計算量で良いなら簡単。

「どこに値  i の要素があるのか」を管理するというテクニックをここで習得しよう!

問題概要

 1, 2, \dots, N の並び替えである順列  A_{1}, A_{2}, \dots, A_{N} が与えられる。これをソートしたい。以下の操作を  N-1 回まで実施できる。

  •  i, j を選んで、 A_{i} A_{j} を swap する

ソートするための操作列を 1 つ求めよ (必ず可能であることは示せる)。

制約

  •  N \le 2 \times 10^{5}

考えたこと

この手の問題では、最初は計算量を気にせずに考えるとよい。最後に高速化を試みよう。

この問題に対しては、トランプなどでカードが配られて、それを見やすい順に並び替えるときのことを思い出すと、次のような解法が自然に思い浮かぶ。


  • まず、1 を探して、それを左から 1 番目に持ってくる
    • ただし、もともと左から 1 番目が 1 のときは何もしない
  • 次に、2 を探して、それを左から 2 番目に持ってくる
    • ただし、もともと左から 2 番目が 2 のときは何もしない
  • ...
  • 最後に、 N-1 を探して、それを左から  N-1 番目に持ってくる
    • ただし、もともと左から  N-1 番目が  N-1 のときは何もしない

最後を  N ではなく、 N-1 までにしているのは、「左から  1, 2, \dots, N-1 までが揃った時点で、 N の位置も自動的に揃っているため」だ。

さて、値  i (= 1, 2, \dots, N-1) について考えているとき、愚直にやると、以下の処理に  O(N) の計算量を要する。


 A_{i+1}, A_{i+2}, \dots, A_{N} の中から、値が  i であるものを探す


そのため、全体の計算量は  O(N^{2}) となってしまう。上記の処理を高速化しよう。

 

高速化

上記の処理を高速化するためには、次の情報を管理すればよい!! この「どこにその要素があるのかを管理するテク」はより高難易度の問題では頻出する!!


  • where[v] A_{i} = v であるような  i

この値を管理すれば、上記の「 A_{v+1}, A_{v+2}, \dots, A_{N} の中から、値が  v であるものを探す」という処理を  O(1) の計算量でこなすことができて、全体の計算量も  O(N) となるのである。

where の更新

最後に、 A_{i} A_{j} を swap するときに、この配列 where も更新する必要があることに注意しよう。慣れないと難しいかもしれない。次のようにできる (これは憶えてしまおう)。

swap(where[A[i]], where[A[j]]);
swap(A[i], A[j]);

ここで、1 行目と 2 行目の順序を間違えないように注意しよう。2 行目を先にやってしまうと、1 行目の処理において、更新されてしまった配列 A の情報を活用することとなるためだ。

 

コード

全体的に 0-indexed で実装した。

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

int main() {
    int N;
    cin >> N;
    vector<int> A(N), where(N);
    for (int i = 0; i < N; ++i) {
        cin >> A[i];
        --A[i];
        
        // 値 A[i] があるのは i 番目
        where[A[i]] = i;
    }
    
    // 左から i 番目に、値 i を持ってくる処理をする
    vector<pint> res;
    for (int i = 0; i <= N - 2; ++i) {
        // i 番目がすでに値 i なら何もしない
        if (A[i] == i) continue;
        
        // 値 i がある場所
        int j = where[i];
        
        // A[i] と A[j] を swap する
        swap(where[A[i]], where[A[j]]);
        swap(A[i], A[j]);
        res.emplace_back(i, j);
    }
    
    // 出力
    cout << res.size() << endl;
    for (auto [i, j] : res) {
        cout << i+1 << " " << j+1 << endl;
    }
}