#include <bits/stdc++.h>
#define REP(i, n) for (int i = 0, max_i = (n); i < max_i; i++)
#define REPI(i, a, b) for (int i = (a), max_i = (b); i < max_i; i++)
#define ALL(obj) (obj).begin(), (obj).end()
#define RALL(obj) (obj).rbegin(), (obj).rend()
#define fi first
#define se second
#define int long long
using namespace std;
using ll = long long;
using II = pair<int, int>; using VII = vector<II>; using VVII = vector<VII>;
using VI = vector<int>; using VVI = vector<VI>; using VVVI = vector<VVI>;
template <class T = int> inline bool chmax(T &a, const T &b) { if (a < b) { a = b; return true; } return false; }
template <class T = int> inline bool chmin(T &a, const T &b) { if (a > b) { a = b; return true; } return false; }
template <class T> ostream& operator<<(ostream &s, const vector<T>& d) { REP (i, d.size()) s << d[i] << (i == d.size() - 1 ? "" : " "); return s; }
template <class T> ostream& operator<<(ostream &s, const vector<vector<T>>& d) { REP (i, d.size()) s << d[i] << (i == d.size() - 1 ? "" : "\n"); return s; }
template <class T, class S> ostream& operator<<(ostream &s, const pair<T, S>& p) { s << "{" << p.first << ", " << p.second << "}"; return s; }
template <class T, class S> ostream& operator<<(ostream& s, const map<T, S> m) { for (auto it = m.begin(); it != m.end(); it++) { s << *it << (next(it) == m.end() ? "" : "\n"); } return s; }
#ifdef _MY_DEBUG
  #define dump(...) cerr << "/* " << #__VA_ARGS__ << " :[" << __LINE__ << ":" << __FUNCTION__ << "]" << endl, dump_func(__VA_ARGS__), cerr << "*/\n\n";
#else
  #define dump(...)
#endif
void dump_func() { cerr << endl; }
template <class Head, class... Tail> void dump_func(Head&& h, Tail&&... t) { cerr << h << (sizeof...(Tail) == 0 ? "" : ", "), dump_func(forward<Tail>(t)...); }
struct Fast { Fast() { cin.tie(0); ios::sync_with_stdio(false); } } fast;
constexpr int MOD = 1e9 + 7;

// class UnionFind {
// public:
//   VI par;
//   VI num;
//   UnionFind(int n) : {
//     par = VI(n); iota(par.begin(), par.end(), 0);
//     num = VI(n, 0);
//   }
//   int root(int x) {
//     if (par[x] == x) {
//       return x;
//     } else {
//       num[par[x]] += num[x];
//       num[x] = 0;
//       return par[x] = root(par[x]);
//     }
//   }
//   void unite(int x, int y, bool add) {
//     x = root(x);
//     y = root(y);
//     if (x == y) { return; }
//     par[x] = y;
//     if (add) num[y]++;
//   }
//   bool same(int x, int y) {
//     return root(x) == root(y);
//   }
// };
class QuickFind {
public:
  int N;
  VI i2g; // 各頂点のグループ
  VVI g2i; // 各グループの頂点集合
  QuickFind(int n) : N(n) {
    i2g.resize(N);
    g2i = VVI(N, VI(1));
    REP (i, N) {
      i2g[i] = i;
      g2i[i][0] = i;
    }
  }
  // iを含むグループとjを含むグループをmerge
  void merge(int i, int j) {
    int gi = i2g[i], gj = i2g[j];
    if (gi == gj) return;
    if (g2i[gi].size() < g2i[j].size()) swap(gi, gj);
    // jをiにmerge
    for (int v: g2i[gj]) i2g[v] = gi;
    g2i[gi].insert(g2i[gi].end(), ALL(g2i[gj]));
    g2i[gj].clear();
  }
  bool same(int i, int j) {
    return i2g[i] == i2g[j];
  }
};


VI x = {0, 1};
VI y = {1, 0};

signed main() {
  int H, W; cin >> H >> W;
  vector<string> S(H);
  REP (i, H) cin >> S[i];
  QuickFind qf(H * W);
  REP (i, H) {
    REP (j, W) {
      REP (k, 2) {
        int i2 = i + x[k];
        int j2 = j + y[k];
        if (i2 < 0 || i2 >= H || j2 < 0 || j2 >= W) continue;
        if (S[i2][j2] != S[i][j]) {
          qf.merge(W * i + j, W * i2 + j2);
        }
      }
    }
  }
  int ans = 0;
  for (auto& g: qf.g2i) {
    int p = 0, q = 0;
    for (int x: g) {
      int i = x / W;
      int j = x % W;
      if (S[i][j] == '.') {
        p++;
      } else {
        q++;
      }
    }
    ans += p * q;
  }
  cout << ans << endl;
}


Measured memory_bytes: 797328