class UnionFind {
 public:
  UnionFind(int n) : count(n), id(n), sz(n, 1) {
    iota(id.begin(), id.end(), 0);
  }
  void unionBySize(int u, int v) {
    const int i = find(u);
    const int j = find(v);
    if (i == j)
      return;
    if (sz[i] < sz[j]) {
      sz[j] += sz[i];
      id[i] = j;
    } else {
      sz[i] += sz[j];
      id[j] = i;
    }
    --count;
  }
  int getCount() const {
    return count;
  }
  int getMaxSize() const {
    return ranges::max(sz);
  }
 private:
  int count;
  vector<int> id;
  vector<int> sz;
  int find(int u) {
    return id[u] == u ? u : id[u] = find(id[u]);
  }
};
class Solution {
 public:
  vector<int> groupStrings(vector<string>& words) {
    UnionFind uf(words.size());
    unordered_map<int, int> maskToIndex;
    unordered_map<int, int> deletedMaskToIndex;
    for (int i = 0; i < words.size(); ++i) {
      const int mask = getMask(words[i]);
      for (int j = 0; j < 26; ++j)
        if (mask >> j & 1) {
          // Going to delete this bit.
          const int m = mask ^ 1 << j;
          if (const auto it = maskToIndex.find(m); it != maskToIndex.cend())
            uf.unionBySize(i, it->second);
          if (const auto it = deletedMaskToIndex.find(m);
              it != deletedMaskToIndex.cend())
            uf.unionBySize(i, it->second);
          else
            deletedMaskToIndex[m] = i;
        } else {
          // Going to add this bit.
          const int m = mask | 1 << j;
          if (const auto it = maskToIndex.find(m); it != maskToIndex.cend())
            uf.unionBySize(i, it->second);
        }
      maskToIndex[mask] = i;
    }
    return {uf.getCount(), uf.getMaxSize()};
  }
 private:
  int getMask(const string& s) {
    int mask = 0;
    for (const char c : s)
      mask |= 1 << c - 'a';
    return mask;
  }
};