yutasの競技プログラミング勉強帖

競技プログラミングの問題についての解説記事を主に書いています。

E - Stamp / AtCoder Beginner Contest 329

問題

英大文字からなる長さ  n の文字列  s と、長さ  m \ ( \lt n) の文字列  t が与えられる。

ここで、 # のみからなる長さ  n の文字列  x に対して以下の作業を何回でも行えるとき、  x s に一致させられるか判定せよ。

  •  x の中から連続する  m 文字を選び、  t で置き換える。

入力

まず最初の1行目に整数  n, m が与えられる。

次の1行には文字列  s が与えられる。

その次の1行には文字列  t が与えられる。

条件

  •  1 \leq n \leq 2 \times {10}^{5}
  •  1 \leq m \leq \min (n, 5)
  •  s は英大文字からなる長さ  n の文字列
  •  t は英大文字からなる長さ  m の文字列

出力

 x s に一致させられるならば Yes を、一致させられないならば No をそれぞれ出力すること。

解法

まず、問題文の操作の通りに、 ##...# の状態の文字列  x から、 m 文字の  t を順番に変換していき、 s を目指すという方法を愚直に実装することはとても困難です。 というのも、 x t がどんどん上書きされていくため、「どの部分から操作を始めるべきなのか」「次の操作はどのようにすべきなのか」等の情報を手に入れることが非常に難しくなってしまいます。 (もし、そのような方法でACできるものがあればコメントなどで教えてください…!)

一方で、最終目標となる  s が与えられているため、逆に  s から  t を引いていき、  x を目指すという方法があると考えられます。

ここで、問題の例となっている  s = ABCBABC,  t = ABC の場合について考えます。 この文字列  s#...# にするには、

  1. 4文字目から6文字目の ABC### に変換する
  2. 0文字目から2文字目の ABC### に変換する
  3. 2文字目から4文字目の #B#### に変換する

という手順で変換することができます。 (コードを書くために、0-indexで記述しています。)

この手順を図にすると、以下のようになります。

この例から、 0 \leq j \lt m であるすべての整数  j に対して、以下のいずれかが成立すれば、文字列  s m 文字分だけ # に変換することができると言えます。

  •  s (i + j) 文字目と  t j 文字目が一致する
  •  s (i + j) 文字目が # である

以上から、 0 \leq i \leq n - m である整数  i に対して、文字列  s i 文字目から  (i + m - 1) 文字目まで調べ上げ、条件を満たすならば # に変換していく、という方法で判定を行えます。

しかし、この方法については、先頭から最後まで調べ上げていき、 # に変換された文字列についても先頭から最後まで調べ上げ、ということを愚直に繰り返すと、1回のループで約  n 箇所分を調べ、1回の探索で  m 文字分を調べ、最大で約  n 回分のループを行うため、  O({n}^{2} m) の計算量となってしまい、実行時間制限に間に合いません。

そこで、どの文字から探索を始めるのかということを考えます。

最初の1回目の変換については、文字列  s の中には # が含まれないため、 s の中に  t と一致する部分があるかどうかを探します。

例えば、  s i 文字目から始まる部分が  t と一致すると仮定します。

このとき、 s i 文字目から  (i + m - 1) 文字目までが # に変換されるので、  s の「  (i - m + 1) 文字目から始まる部分」「 (i - m + 2) 文字目から始まる部分」……「 (i + m - 1) 文字目から始まる部分」の全  2(m-1) 箇所が、新たに # に変換できる可能性がある場所として生まれます。

これを図にすると以下のようになります。

以上から、 # に変換できた箇所が1箇所ごとに、 2(m-1) 箇所が次の探索候補となります。 最終的には約  n 箇所を # に変えていくため、このように探索箇所を限っていくことにより、  O(n {m}^{2}) の計算量で調べ上げることができます。

この問題では  m の値は最大でも5であるので、これは実行時間制限に十分間に合うと言えます。

ソースコード

まずは、 i 文字目から  (i + m - 1) 文字目までがすべて # に変換できるかどうかをbool値で返す関数 check() を以下のように実装しました。

bool visited[int(2e5 + 5)]; // visited は true ならば i 文字目が # に変換されていることを表す

bool check(int start) { // start が始点となる文字のindexを表す
  if (start < 0 || start + m - 1 >= n) {
    // start が負であったり、(start + m - 1) 文字目が n 以上であれば、s を探索できないので false を返す
    return false;
  }

  // t の 0 文字目から m - 1 文字目までについて調査する
  // このforループを抜けたら変換可能と判定する
  for (int i = 0; i < m; i++) {
    if (visited[start + i]) {
      // もし visited[start + i] が true ならば、#になっているので次の文字について調べる
      continue;
    }

    if (s[start + i] != t[i]) {
      // 文字が一致しないならば、false を返す
      return false;
    }
  }

  // forループを抜けたので、 # に変換可能ということを記録する
  for (int i = 0; i < m; i++) {
    visited[start + i] = true;
  }

  // 変換可能であるので、 true を返す
  return true;
}

この check() が実装されている状態で、main() 関数に以下のように実装して、全て変換可能かどうかを調査する部分を作成しました。

int n, m;
string s, t;

bool visited[int(2e5 + 5)]; // visited は true ならば i 文字目が # に変換されていることを表す
bool started[int(2e5 + 5)]; // started は true ならば i 文字目から (i + m - 1) 文字目までが既に # に変換されていることを表す
queue<int> que; // 探索スタートとなるindexを順に保存するためのqueue

int main() {
  // 値の入力
  cin >> n >> m;
  cin >> s;
  cin >> t;

  // まずは、すべての i に対して部分列が t と一致するかを調べる
  for (int i = 0; i + m - 1 < n; i++) {
    if (check(i)) {
      // もし一致するならば、queueにそのインデックスを追加する
      que.push(i);
      started[i] = true;
    }
  }

  // queueが空になるまで、探索を行う
  while (!que.empty()) {
    int q = que.front();
    que.pop();

    // (q - m + 1) 文字目から (q + m - 1) 文字目までの文字について、それらを先頭とした場合に # に変換できるかを確認する
    // started[i] が true ならば、追加で探索する必要はないので無視する(既に探索されているため)
    for (int i = 1; i < m; i++) {
      // もし check(q + i) が true ならば、queueに (q + i) を追加する
      if (q + i < n && check(q + i) && !started[q + i]) {
        que.push(q + i);
        started[q + i] = true;
      }
      
      // もし check(q - i) が true ならば、queueに (q - i) を追加する
      if (q - i >= 0 && check(q - i) && !started[q - i]) {
        que.push(q - i);
        started[q - i] = true;
      }
    }
  }

  // すべての visited[i] が true ならば Yes を、そうでないならば No を出力する
  for (int i = 0; i < n; i++) {
    if (!visited[i]) {
      No();
      return 0;
    }
  }
  Yes();

  return 0;
}

感想

個人的には文字列に関しての問題に苦手意識があるので、コンテストに出ている際にこの問題が出たら、対応できるか不安になりますね…。