#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;



}

Measured memory_bytes: 410968