C - Rotate and Palindrome Editorial by MMNMM

より高速な解法

この問題において、\(S _ i=S _ j\) かつ \(i\lt j\) なる \((i,j)\) の組があると、その \((i,j)\) を対称な位置になるまで移動させたときの \(2\) 番目の操作を \(1\) 回節約できます。 そのような移動は、文字列の中心を \(\dfrac{i+j}2\) 文字目にするものです。 より厳密には、

  • \(N\) が奇数のとき、中央を \(\dfrac{i+j}2\bmod N\) 文字目にするような移動
  • \(N\) が偶数のとき、
    • \(i+j\equiv0\bmod 2\) のときはそのような移動は存在しない
    • \(i+j\equiv1\bmod 2\) のときは中央を \(\dfrac{i+j-1}2\) 文字目と \(\dfrac{i+j+1}{2}\) 文字目の間にするような移動および中央を \(\dfrac{i+j-1+N}2\bmod N\) 文字目と \(\dfrac{i+j+1+N}{2}\bmod N\) 文字目の間にするような移動

です。

よって、\(S _ i=S _ j\) かつ \(i\lt j\) なる \((i,j)\) の組に対する \(i+j\) の値にのみ興味があります。 特に、各 \(k\) に対して \(i+j=k\) かつ \(S _ i=S _ j\) かつ \(i\lt j\) なる \((i,j)\) の組の個数 \(C _ k\) がわかるとこの問題が解けることがわかります。

文字 \(c\) に対して、\(T _ c=(T _ {c,i}) _ {1\leq i\leq N}\) を \(T _ {c,i}=\begin{cases}1\quad(S _ i=c)\\0\quad(\text{otherwise})\end{cases}\) で定めます。 すると、\(i+j=k\) かつ \(S _ i=S _ j=c\) なる \((i, j)\) の組の個数は \(T _ c\) の自身との畳み込みによって得られます。 畳み込みによって得る可能性のある値はたかだか \(N\) なので、適切な素数 \(P\) に対する \({}\bmod P\) における値を用いることで畳み込みを \(O(N\log N+\log P)\) で計算することができます。

すべての \(c\) にわたって畳み込みの結果を足した結果 \(T ^ 2\) から、\(i=j\) なる組(これはすべての \(T ^ 2 _ {2i}\) から \(1\) を引くことに対応します)を除いて \(2\) で割ることで求める組の個数 \(C _ k\) が得られます。

これを用いて計算を行うと、全体で計算量は \(O(\sigma(N\log N+\log P))\) 時間となります(\(\sigma=26\) はアルファベットサイズです)。 \(\sigma=26,N=2\times10^5\) でも比較的高速に答えを求めることができます。

実装例は以下のようになります。

#include <iostream>
#include <string>
#include <vector>
#include <limits>
#include <atcoder/modint>
#include <atcoder/convolution>

int main() {
    using namespace std;
    // constexpr int MOD{469762049}; // N が大きいとき
    constexpr int MOD{65537}; // 2^d | MOD - 1 かつ 2^d >= 2*N が必要 今回は N <= 5000 なので d >= 14 であればよい
    using modint = atcoder::static_modint<MOD>;

    unsigned N;
    unsigned long A, B;
    cin >> N >> A >> B;
    string S;
    cin >> S;

    vector<modint> C(2 * N); // C[k] = (i + j == k かつ S[i] == S[j] かつ i < j なる組 (i, j) の個数)

    const auto char_vector{[&N, &S](const char c) {
        vector<modint> ret;
        ret.reserve(N);
        transform(begin(S), end(S), back_inserter(ret), [&c](const auto i) { return i == c; });
        while (!empty(ret) && ret.back() == 0) ret.pop_back();
        return ret;
    }};
    for (char c{'a'}; c <= 'z'; ++c) {
        // T[i] = (S[i] == c なら 1, そうでなければ 0)
        auto &&T{char_vector(c)};
        // T_sq[k] = (i + j == k かつ S[i] == c かつ S[j] == c なる組 (i, j) の個数)
        auto &&T_sq{atcoder::convolution(T, T)};
        for (unsigned i{}; i < size(T_sq); ++i) C[i] += T_sq[i]; // C[i] = ∑_c T_sq[i]
    }

    for (unsigned i{}; i < N; ++i) --C[2 * i]; // i == j の組を除く
    for (unsigned i{}; i < N; ++i) C[i] += C[i + N]; // index を mod N
    C.resize(N);
    for (auto &&c : C) c *= (MOD + 1) / 2; // i > j の組を除く

    unsigned long ans{numeric_limits<unsigned long>::max()};
    for (unsigned i{}; i < N; ++i)
        // 1 つめの操作を i 回行うと、2 つめの操作は N / 2 - (節約できる個数) 回
        ans = min(ans, A * i + B * (N / 2 - C[(2 * i + N - 1) % N].val()));
    cout << ans << endl;

    return 0;
}

posted:
last update: