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

// dataは0以上のとき親のindex
// 負のときは根であり、集合のサイズを表す
struct UnionFind {
  vector<int> data;
  VII num;
  UnionFind(int n) : data(n, -1) {}
  int root(int x) {
    return data[x] < 0 ? x : data[x] = root(data[x]);
  }
  void unite(int x, int y) {
    x = root(x); y = root(y);
    if (x == y) return;
    if (data[y] < data[x]) swap(x, y);
    // yをxにマージ
    data[x] += data[y];
    num[x].fi += num[y].fi;
    num[x].se += num[y].se;
    data[y] = x;
  }
  bool is_same(int x, int y) {
    return root(x) == root(y);
  }
  int size(int x) {
    return -data[root(x)];
  }
};

const VI x = {0, 1, 0, -1};
const VI y = {1, 0, -1, 0};
signed main() {
  int H, W; cin >> H >> W;
  vector<string> S(H);
  REP (i, H) cin >> S[i];

  UnionFind uf(H * W);
  uf.num = VII(H * W);
  REP (i, H) {
    REP (j, W) {
      (S[i][j] == '.' ? uf.num[W * i + j].fi : uf.num[W * i + j].se) = 1;
    }
  }
  auto valid = [&](int i, int j) {
    return 0 <= i && i < H && 0 <= j && j < W;
  };
  REP (i, H) {
    REP (j, W) {
      REP (k, 4) {
        int i2 = i + x[k];
        int j2 = j + y[k];
        if (!valid(i2, j2) || S[i][j] == S[i2][j2]) continue;
        uf.unite(W * i + j, W * i2 + j2);
      }
    }
  }
  int ans = 0;
  REP (i, H) {
    REP (j, W) {
      if (uf.root(W * i + j) == W * i + j) {
        II tmp = uf.num[W * i + j];
        ans += tmp.fi * tmp.se;
      }
    }
  }
  cout << ans << endl;
}


Measured memory_bytes: 4224