Official

J - 図形のシフト/Shift the Pattern Editorial by MMNMM


まず、次の問題を考えてみましょう。

長さ \(N\) の文字列 \(S,T\) が与えられる。 \(T\) を何回か巡回シフト(先頭の文字を末尾に移動させる)することで \(S\) と一致させることができるか判定せよ。

文字列 \(Q\) を \(Q=S+{}\)$\({}+T+T\) と定めると(ここで、$ は \(S\) にも \(T\) にも含まれない文字です)、\(l[i]\coloneqq (Q\) と \(Q[i:]\) の最長共通接頭辞の長さ\()\) を用いて「\(l[i]=N\) となる \(i\gt0\) が存在する」ことと同値になります(具体的に何回巡回シフトを行うことで一致させることができるかも求められます)。

\(l[i]\) は Z-algorithm を使って求められます。 文字列の長さを \(L\) として、Z-algorithm は文字の等値比較をたかだか \(L\) 回行い、\(O(L)\) 回の四則演算や整数の大小比較を行います。

元の問題に戻りましょう。 それぞれの列を一つの文字だと思うことで、図形 \(S,T\) を長さ \(W\) の文字列だと思うことができます。 これらに対して上の問題と同様の操作を行うことでこの問題を解くことができます。

一般的な文字列と異なり、文字どうしが等しいか比較するのに最悪で \(\Theta(H)\) 時間かかりますが、文字の比較はたかだか \(3W+1\) 回しか行われないため、十分高速です。

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

#include <iostream>
#include <vector>
#include <string>

// Z-algorithm
// seq[0:l[i]] == seq[i:i+l[i]] かつ seq[0:l[i]+1] != seq[i:i+l[i]+1] を満たす配列 l を返す。
// 入力の seq の長さ N に対して、T どうしの比較をたかだか N 回、std::size_t の四則演算や大小比較を O(N) 回行う。
template<typename T>
std::vector<std::size_t> z_algorithm(const std::vector<T> &seq) {
    const auto n = std::size(seq);
    std::vector<std::size_t> result(n);
    result[0] = n;
    std::size_t L = 1, R = 1;
    for (std::size_t i = 1; i < n; ++i)
        if (i + result[i - L] < R)
            result[i] = result[i - L];
        else {
            L = i, R = std::max(R, i);
            while (R < n && seq[R] == seq[R - i])
                R++;
            result[i] = R - i;
        }
    return result;
}

int main() {
    using namespace std;
    
    size_t H, W;
    cin >> H >> W;
    
    // Q = S + 2 + T + T
    // S, T は長さ H の {0, 1} 列の列
    // 2 は長さ H の {2} 列
    vector<vector<int>> Q(3 * W + 1);
    for(size_t i = 0; i < H; ++i){
        string s;
        cin >> s;
        for(size_t j = 0; j < W; ++j)Q[j].emplace_back(s[j] == '#');
    }
    Q[W] = vector<int>(H, 2);
    for(size_t i = 0; i < H; ++i){
        string t;
        cin >> t;
        for(size_t j = 0; j < W; ++j)Q[W + 1 + j].emplace_back(t[j] == '#');
        for(size_t j = 0; j < W; ++j)Q[W + 1 + W + j].emplace_back(t[j] == '#');
    }
    
    // z_algorithm で計算された最長共通接頭辞の長さが W と一致するものがあるとき、かつそのときに限り答えは Yes
    const auto l = z_algorithm(Q);
    for(auto i = W + 1; i < W + 1 + W; ++i)if(l[i] == W){
        cout << "Yes" << endl;
        return 0;
    }
    cout << "No" << endl;
    return 0;
}

文字列のみに対応する Z-algorithm のライブラリを持っている場合、それぞれの行で「何回シフトさせれば一致するか」を計算し、すべての行でそれに含まれているものがあれば Yes とする解法の実装のほうが楽かもしれません。

#include <iostream>
#include <vector>
#include <string>
#include <atcoder/string>

int main() {
    using namespace std;

    size_t H, W;
    cin >> H >> W;
    vector<string> S(H), T(H);
    for (auto &&s : S)cin >> s;
    for (auto &&t : T)cin >> t;
    
    auto L = 3 * W + 1;

    const auto ok_count{
        transform_reduce(
            begin(S),
            end(S),
            begin(T),
            vector<int>(L),
            [L](auto &&lhs, auto &&rhs) {
                for (size_t i = 0; i < L; ++i)lhs[i] += rhs[i];
                return lhs;
            },
            [W](auto &&s, auto &&t) {
                return [W](auto&& seq) {
                    for (auto&& v : seq)v = (v == W);
                    return seq;
                } (atcoder::z_algorithm(s + '$' + t + t));
            }
        )
    };

    cout << (any_of(begin(ok_count), end(ok_count), [H](auto c) { return c == H; }) ? "Yes" : "No") << endl;

    return 0;
}

posted:
last update: