Problem p02589 (C++) — buggy submission, verdict: Runtime Error

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

using ll = long long;
using ld = long double;
using pll = pair<ll, ll>;
using pld = pair<ld, ld>;
const int INF = 1e9 + 7;
const ll LINF = 9223372036854775807;
const ll MOD = 1e9 + 7;
const ld PI = acos(-1);
const ld EPS = 1e-10; // 微調整用（EPSより小さいと0と判定など）

int ii() {
  int x;
  if (scanf("%d", &x) == 1)
    return x;
  else
    return 0;
}
long long il() {
  long long x;
  if (scanf("%lld", &x) == 1)
    return x;
  else
    return 0;
}
string is() {
  string x;
  cin >> x;
  return x;
}
char ic() {
  char x;
  cin >> x;
  return x;
}
void oi(int x) { printf("%d ", x); }
void ol(long long x) { printf("%lld ", x); }
void od_nosp(double x) { printf("%.15f", x); } // 古い問題用
void od(double x) { printf("%.15f ", x); }
// long doubleで受け取り、fをLfなどに変えて出力すると、変な数値が出る
// それをなんとかするには独自の出力を作らなければならなそう
void os(const string &s) { printf("%s ", s.c_str()); }
void oc(const char &c) { printf("%c ", c); }
#define o_map(v)                                                               \\
  {                                                                            \\
    cerr << #v << endl;                                                        \\
    for (const auto &xxx : v) {                                                \\
      cout << xxx.first << " " << xxx.second << "\\n";                          \\
    }                                                                          \\
  } // 動作未確認
void br() { putchar('\\n'); }

// #define gcd __gcd //llは受け取らない C++17~のgcdと違うので注意
// int lcm(int a, int b){return a / gcd(a, b) * b;}
#define begin_end(a) a.begin(), a.end() // sort(begin_end(vec));
#define REP(i, m, n) for (ll i = (ll)(m); i < (ll)(n); i++)
#define DREP(i, m, n) for (ll i = (ll)(m); i > (ll)(n); i--)
#define rep(i, n) REP(i, 0, n)
#define m_p(a, b) make_pair(a, b)
#define SORT_UNIQUE(c)                                                         \\
  (sort(c.begin(), c.end()),                                                   \\
   c.resize(distance(c.begin(), unique(c.begin(), c.end()))))
#define p_b push_back
#define SZ(x) ((ll)(x).size()) // size()がunsignedなのでエラー避けに
#define endk '\\n'

// coutによるpairの出力（空白区切り）
template <typename T1, typename T2>
ostream &operator<<(ostream &s, const pair<T1, T2> &p) {
  return s << "(" << p.first << " " << p.second << ")";
}
// coutによるvectorの出力（空白区切り）
template <typename T> ostream &operator<<(ostream &s, const vector<T> &v) {
  int len = v.size();
  for (int i = 0; i < len; ++i) {
    s << v[i];
    if (i < len - 1)
      s << " "; //"\\t"に変えるとTabで見やすく区切る
  }
  return s;
}
// coutによる多次元vectorの出力（空白区切り）
template <typename T>
ostream &operator<<(ostream &s, const vector<vector<T>> &vv) {
  int len = vv.size();
  for (int i = 0; i < len; ++i) {
    s << vv[i] << endl;
  }
  return s;
}

// 最大値、最小値の更新。更新したor等しければtrueを返す
template <typename T> bool chmax(T &a, T b) { return (a = max(a, b)) == b; }
template <typename T> bool chmin(T &a, T b) { return (a = min(a, b)) == b; }

// 4近傍（上下左右） rep(i, 2) にすると右・下だけに進む
vector<int> dx_4 = {1, 0, -1, 0};
vector<int> dy_4 = {0, 1, 0, -1};

// -------- template end - //

// - library ------------- //

ll ans = 0;

// existence[i][j][k] := S[i]の [j]文字目以降に アルファベット[k:a~z]
// が何個あるか
vector<vector<vector<ll>>> existence;

// Trie 木
// O(|S|) で文字列を挿入する
// O(max(Trie木に入っている文字列 & 検索キーとなる文字列) )
// なので、検索キー文字列が長くても、辞書内の文字列が短ければすぐ終わる
// ex)天下一2016-C たんごたくさん メモリ領域は最悪でも
// max(|S|)*(文字種数)*(文字種数)+(文字列数) か？ Trie<26, 'a'> trie;
// などとして構築 void insert(str word);              文字列の挿入 bool
// search(str word);              指定した文字列そのものがあるか bool
// search_prefix(str prefix);     指定したprefixを持つ文字列があるか int
// word_count();                   挿入した単語の数 int node_count();
// 頂点の総数（根を含む） accept
// 変数を使えば、条件に沿う文字列が「いくつあるか」への変更は容易と思われる
template <int char_size, int base> struct Trie {
  struct Node {       // 頂点の構造体を定義
    vector<int> next; // 子の頂点番号を格納。存在しなければ-1
                      // next[c] := 次の文字が c となる子頂点の頂点番号
    vector<int> accept; // 末端がこの頂点になる文字列の str_id を保存
                        // str_id := その文字列が何番目に入ったか(0-indexed)
    int c;      // base からの間隔をint型で表現したもの
    int common; // いくつの文字列がこの頂点を共有しているか（根は全員が共有）
    Node(int c_) : c(c_), common(0) { // <- なぜ0で生成？ 1では？
      next.assign(char_size, -1); // 生成時は子がいないので、next[全文字種] = -1
    }
  };
  vector<Node> nodes; // trie 木本体
  int root;           // 根（の頂点番号か？
            // と思いきや、Node(root)としているから、頂点の文字種になっちゃってないか？
            // どういうこと？）
  Trie() : root(0) {
    // 初期化。はじめは根しか無い
    nodes.push_back(Node(root)); // rootノード.cが0とはどういうことか？
                                 // 文字種'a'ってことになっちゃわないか？
  }

  // 単語を追加 ※使う時は、void insert(const string &word) の方を使う
  void insert(const string &word,
              int word_id) { // 単語:str と 単語の番号: str_id
    int node_id = 0;         // 今見ている頂点番号 最初は根(0)
    // 今の頂点から、次の頂点（子）を探す
    // 深さ1辿るごとに必ず1文字進むので、文字数と同じ回数ループすれば良い
    for (int i = 0; i < (int)word.size(); i++) {
      int c = (int)(word[i] - base); // i文字目(0-indexed)を探す
      // 今の頂点の子の中で、次に文字種cが来る頂点番号（-1なら更新したいので参照渡し）
      int &next_id = nodes[node_id].next[c];
      if (next_id == -1) { // 次の頂点が存在しなければ追加
        next_id = (int)nodes.size(); // 現在ある頂点数 = 新しい頂点の頂点番号
        nodes.push_back(Node(c)); // 文字種 c の頂点を追加
      }
      ++nodes[node_id].common; // 今の頂点を共有する文字列を1つ増やす
      node_id = next_id;       // 次の頂点（子）に移動
    }
    // 最後の頂点（最後の文字）の処理を忘れずに行う
    ++nodes[node_id].common;
    nodes[node_id].accept.push_back(
        word_id); // 単語の終端なので、頂点に番号を入れておく
  }
  void insert(const string &word) {
    insert(word,
           nodes[0]
               .common); // 根を共有する文字列の数 = 挿入済みの文字列数 = str_id
  }

  // 単語 もしくは prefixの検索
  // 第二引数として 今回見始めるのは何文字目（0-indexed）からかを渡す
  vector<int> search(const string &word, int s_idx) {
    vector<int> res; // 見つけた単語のstr_id
    int node_id = 0; // 今見ている頂点番号 最初は根(0)
    // 今の頂点から、次の頂点（子）を探す
    // 深さ1辿るごとに必ず1文字進むので、文字数と同じ回数ループすれば良い
    for (int i = s_idx; i < (int)word.size(); i++) {
      int c = (int)(word[i] - base);
      int &next_id = nodes[node_id].next[c];
      if (next_id == -1) { // 次の頂点が存在しなければ終了
        return res;
      }
      node_id = next_id;
      // ここで次の頂点に移っているので、ここで文字列が見つかったか判定する
      for (int id_found : nodes[node_id].accept)
        res.p_b(id_found);
    }
    return res;
  }

  // 挿入した単語の数
  int word_count() const { return (nodes[0].common); }

  // Trie木のノード数
  int node_count() const { return ((int)nodes.size()); }
};

// --------- library end - //

int main() {

  ll N = il();

  // existence[i][j][k] := S[i]の [j]文字目以降に アルファベット[k:a~z]
  // が何個あるか [i][len] の部分には番兵が入っている
  existence = vector<vector<vector<ll>>>(N, vector<vector<ll>>());

  vector<string> ss;
  rep(i, N) {
    ss.push_back(is());
    reverse(begin_end(ss[i]));
    existence[i] = vector<vector<ll>>(SZ(ss[i]) + 1, vector<ll>(26, 0));
  }

  // existence の前計算
  rep(i, N) {
    DREP(j, SZ(ss[i]) - 1, -1) {
      // j : SZ(ss[i])-1 ~ 0
      rep(k, 26) { existence[i][j][k] = existence[i][j + 1][k]; }
      existence[i][j][ss[i][j] - 'a'] += 1;
    }
  }

  // Trie木を構築
  Trie<26, 'a'> trie;
  rep(i, N) trie.insert(ss[i]);

  // S[i] を用いて作れる S[i] 以外の文字列がいくつあるか を各iについて見ていく
  // Trie木の search() を今回用に改造するのは大変そうなので、データだけ利用する
  rep(i, N) {
    int node = 0; // 今見ている prefix の末尾
    rep(j, SZ(ss[i])) {
      // j : 0 ~ Siの長さ - 1
      // 「1文字はprefixから離れていてもいい」という1文字について判定するために、「Siのj文字目以降に存在する文字」について判定してループするが、その
      // j 文字目 を表す（0-indexed）
      rep(k, 26) {
        // k : 0 ~ 25 (a ~ z)
        // trie.nodes[node].next[k]が-1
        // でなければ、S[i]以外に、j文字目に文字種kが入っているものがあることが分かる
        // また、その頂点の.acceptが1つ以上あれば、そこで単語が終わっている＝一致する単語があるということが分かる
        if (existence[i][j][k] > 0 && trie.nodes[node].next[k] != -1 &&
            SZ(trie.nodes[trie.nodes[node].next[k]].accept) > 0) {
          ans++;
        }
      }
      // 次の文字に移る（次はj文字目をprefixに入れる）
      if (trie.nodes[node].next[ss[i][j] - 'a'] == -1)
        break;
      else
        node = trie.nodes[node].next[ss[i][j] - 'a'];
    }
  }

  ans -= N; // 各ループで自分自身も検出しているので、それを引く

  cout << ans << endk;
}

Error: WA

Fixed submission:

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

using ll = long long;
using ld = long double;
using pll = pair<ll, ll>;
using pld = pair<ld, ld>;
const int INF = 1e9 + 7;
const ll LINF = 9223372036854775807;
const ll MOD = 1e9 + 7;
const ld PI = acos(-1);
const ld EPS = 1e-10; // 微調整用（EPSより小さいと0と判定など）

int ii() {
  int x;
  if (scanf("%d", &x) == 1)
    return x;
  else
    return 0;
}
long long il() {
  long long x;
  if (scanf("%lld", &x) == 1)
    return x;
  else
    return 0;
}
string is() {
  string x;
  cin >> x;
  return x;
}
char ic() {
  char x;
  cin >> x;
  return x;
}
void oi(int x) { printf("%d ", x); }
void ol(long long x) { printf("%lld ", x); }
void od_nosp(double x) { printf("%.15f", x); } // 古い問題用
void od(double x) { printf("%.15f ", x); }
// long doubleで受け取り、fをLfなどに変えて出力すると、変な数値が出る
// それをなんとかするには独自の出力を作らなければならなそう
void os(const string &s) { printf("%s ", s.c_str()); }
void oc(const char &c) { printf("%c ", c); }
#define o_map(v)                                                               \\
  {                                                                            \\
    cerr << #v << endl;                                                        \\
    for (const auto &xxx : v) {                                                \\
      cout << xxx.first << " " << xxx.second << "\\n";                          \\
    }                                                                          \\
  } // 動作未確認
void br() { putchar('\\n'); }

// #define gcd __gcd //llは受け取らない C++17~のgcdと違うので注意
// int lcm(int a, int b){return a / gcd(a, b) * b;}
#define begin_end(a) a.begin(), a.end() // sort(begin_end(vec));
#define REP(i, m, n) for (ll i = (ll)(m); i < (ll)(n); i++)
#define DREP(i, m, n) for (ll i = (ll)(m); i > (ll)(n); i--)
#define rep(i, n) REP(i, 0, n)
#define m_p(a, b) make_pair(a, b)
#define SORT_UNIQUE(c)                                                         \\
  (sort(c.begin(), c.end()),                                                   \\
   c.resize(distance(c.begin(), unique(c.begin(), c.end()))))
#define p_b push_back
#define SZ(x) ((ll)(x).size()) // size()がunsignedなのでエラー避けに
#define endk '\\n'

// coutによるpairの出力（空白区切り）
template <typename T1, typename T2>
ostream &operator<<(ostream &s, const pair<T1, T2> &p) {
  return s << "(" << p.first << " " << p.second << ")";
}
// coutによるvectorの出力（空白区切り）
template <typename T> ostream &operator<<(ostream &s, const vector<T> &v) {
  int len = v.size();
  for (int i = 0; i < len; ++i) {
    s << v[i];
    if (i < len - 1)
      s << " "; //"\\t"に変えるとTabで見やすく区切る
  }
  return s;
}
// coutによる多次元vectorの出力（空白区切り）
template <typename T>
ostream &operator<<(ostream &s, const vector<vector<T>> &vv) {
  int len = vv.size();
  for (int i = 0; i < len; ++i) {
    s << vv[i] << endl;
  }
  return s;
}

// 最大値、最小値の更新。更新したor等しければtrueを返す
template <typename T> bool chmax(T &a, T b) { return (a = max(a, b)) == b; }
template <typename T> bool chmin(T &a, T b) { return (a = min(a, b)) == b; }

// 4近傍（上下左右） rep(i, 2) にすると右・下だけに進む
vector<int> dx_4 = {1, 0, -1, 0};
vector<int> dy_4 = {0, 1, 0, -1};

// -------- template end - //

// - library ------------- //

ll ans = 0;

// existence[i][j][k] := S[i]の [j]文字目以降に アルファベット[k:a~z]
// が何個あるか
vector<vector<vector<ll>>> existence;

// Trie 木
// O(|S|) で文字列を挿入する
// O(max(Trie木に入っている文字列 & 検索キーとなる文字列) )
// なので、検索キー文字列が長くても、辞書内の文字列が短ければすぐ終わる
// ex)天下一2016-C たんごたくさん メモリ領域は最悪でも
// max(|S|)*(文字種数)*(文字種数)+(文字列数) か？ Trie<26, 'a'> trie;
// などとして構築 void insert(str word);              文字列の挿入 bool
// search(str word);              指定した文字列そのものがあるか bool
// search_prefix(str prefix);     指定したprefixを持つ文字列があるか int
// word_count();                   挿入した単語の数 int node_count();
// 頂点の総数（根を含む） accept
// 変数を使えば、条件に沿う文字列が「いくつあるか」への変更は容易と思われる
template <int char_size, int base> struct Trie {
  struct Node {       // 頂点の構造体を定義
    vector<int> next; // 子の頂点番号を格納。存在しなければ-1
                      // next[c] := 次の文字が c となる子頂点の頂点番号
    vector<int> accept; // 末端がこの頂点になる文字列の str_id を保存
                        // str_id := その文字列が何番目に入ったか(0-indexed)
    int c;      // base からの間隔をint型で表現したもの
    int common; // いくつの文字列がこの頂点を共有しているか（根は全員が共有）
    Node(int c_) : c(c_), common(0) { // <- なぜ0で生成？ 1では？
      next.assign(char_size, -1); // 生成時は子がいないので、next[全文字種] = -1
    }
  };
  vector<Node> nodes; // trie 木本体
  int root;           // 根（の頂点番号か？
            // と思いきや、Node(root)としているから、頂点の文字種になっちゃってないか？
            // どういうこと？）
  Trie() : root(0) {
    // 初期化。はじめは根しか無い
    nodes.push_back(Node(root)); // rootノード.cが0とはどういうことか？
                                 // 文字種'a'ってことになっちゃわないか？
  }

  // 単語を追加 ※使う時は、void insert(const string &word) の方を使う
  void insert(const string &word,
              int word_id) { // 単語:str と 単語の番号: str_id
    int node_id = 0;         // 今見ている頂点番号 最初は根(0)
    // 今の頂点から、次の頂点（子）を探す
    // 深さ1辿るごとに必ず1文字進むので、文字数と同じ回数ループすれば良い
    for (int i = 0; i < (int)word.size(); i++) {
      int c = (int)(word[i] - base); // i文字目(0-indexed)を探す
      // 今の頂点の子の中で、次に文字種cが来る頂点番号（-1なら更新したいので参照渡し）
      int &next_id = nodes[node_id].next[c];
      if (next_id == -1) { // 次の頂点が存在しなければ追加
        next_id = (int)nodes.size(); // 現在ある頂点数 = 新しい頂点の頂点番号
        nodes.push_back(Node(c)); // 文字種 c の頂点を追加
      }
      ++nodes[node_id].common; // 今の頂点を共有する文字列を1つ増やす
      node_id = next_id;       // 次の頂点（子）に移動
    }
    // 最後の頂点（最後の文字）の処理を忘れずに行う
    ++nodes[node_id].common;
    nodes[node_id].accept.push_back(
        word_id); // 単語の終端なので、頂点に番号を入れておく
  }
  void insert(const string &word) {
    insert(word,
           nodes[0]
               .common); // 根を共有する文字列の数 = 挿入済みの文字列数 = str_id
  }

  // 単語 もしくは prefixの検索
  // 第二引数として 今回見始めるのは何文字目（0-indexed）からかを渡す
  vector<int> search(const string &word, int s_idx) {
    vector<int> res; // 見つけた単語のstr_id
    int node_id = 0; // 今見ている頂点番号 最初は根(0)
    // 今の頂点から、次の頂点（子）を探す
    // 深さ1辿るごとに必ず1文字進むので、文字数と同じ回数ループすれば良い
    for (int i = s_idx; i < (int)word.size(); i++) {
      int c = (int)(word[i] - base);
      int &next_id = nodes[node_id].next[c];
      if (next_id == -1) { // 次の頂点が存在しなければ終了
        return res;
      }
      node_id = next_id;
      // ここで次の頂点に移っているので、ここで文字列が見つかったか判定する
      for (int id_found : nodes[node_id].accept)
        res.p_b(id_found);
    }
    return res;
  }

  // 挿入した単語の数
  int word_count() const { return (nodes[0].common); }

  // Trie木のノード数
  int node_count() const { return ((int)nodes.size()); }
};

// --------- library end - //

int main() {

  ll N = il();

  // existence[i][j][k] := S[i]の [j]文字目以降に アルファベット[k:a~z]
  // が何個あるか [i][len] の部分には番兵が入っている
  existence = vector<vector<vector<ll>>>(N, vector<vector<ll>>());

  vector<string> ss;
  rep(i, N) {
    ss.push_back(is());
    reverse(begin_end(ss[i]));
    existence[i] = vector<vector<ll>>(SZ(ss[i]) + 1, vector<ll>(26, 0));
  }

  // existence の前計算
  rep(i, N) {
    DREP(j, SZ(ss[i]) - 1, -1) {
      // j : SZ(ss[i])-1 ~ 0
      rep(k, 26) { existence[i][j][k] = existence[i][j + 1][k]; }
      existence[i][j][ss[i][j] - 'a'] += 1;
    }
  }

  // Trie木を構築
  Trie<26, 'a'> trie;
  rep(i, N) trie.insert(ss[i]);

  // S[i] を用いて作れる S[i] 以外の文字列がいくつあるか を各iについて見ていく
  // Trie木の search() を今回用に改造するのは大変そうなので、データだけ利用する
  rep(i, N) {
    int node = 0; // 今見ている prefix の末尾
    rep(j, SZ(ss[i])) {
      // j : 0 ~ Siの長さ - 1
      // 「1文字はprefixから離れていてもいい」という1文字について判定するために、「Siのj文字目以降に存在する文字」について判定してループするが、その
      // j 文字目 を表す（0-indexed）
      rep(k, 26) {
        // k : 0 ~ 25 (a ~ z)
        // trie.nodes[node].next[k]が-1となって、SZ(...)の部分が配列外参照を起こすことはない（S[i]自身が入っているため
        // & S[i]に存在しないパターンは existence[i][j][k]>0
        // の判定で除外されるため）
        if (trie.nodes[node].next[k] != -1) {
          cout << "aaaa" << endk;
          return 0;
        }
        if (existence[i][j][k] > 0 &&
            SZ(trie.nodes[trie.nodes[node].next[k]].accept) > 0) {
          ans++;
        }
      }
      // 次の文字に移る（次はj文字目をprefixに入れる）
      if (trie.nodes[node].next[ss[i][j] - 'a'] == -1)
        break;
      else
        node = trie.nodes[node].next[ss[i][j] - 'a'];
    }
  }

  ans -= N; // 各ループで自分自身も検出しているので、それを引く

  cout << ans << endk;
}