#include <bits/stdc++.h>  // include all standard C++ libraries

using namespace std;

// Loops
#define _overload3(_1,_2,_3,name,...) name
#define _rep(i,n) repi(i,0,n)
#define repi(i,a,b) for(int i=int(a);i<int(b);++i)
#define _rrep(i,n) rrepi(i,n,0)
#define rrepi(i,a,b) for(int i=int(a)-1;i>=int(b);--i)

#define rep(...) _overload3(__VA_ARGS__,repi,_rep,)(__VA_ARGS__)
#define rrep(...) _overload3(__VA_ARGS__,rrepi,_rrep,)(__VA_ARGS__)
#define each(xi, x) for (auto&& xi : x)
// Note: we can use rep(i,N) or rep(i,from,to)


// typedef
using ll = long long;
template <class T> using vec = vector<T>;
using vi   = vector<int>;
using vvi  = vector<vi>;
using vvvi = vector<vvi>;
using pii  = pair<int, int>;


// Constants

// Shorter repr for frequently used terms
#define pb push_back
#define eb emplace_back
#define mp make_pair
#define Fi first
#define Se second


// Algorithms
#define all(x) (x).begin(), (x).end()
#define uniq(v) v.erase( unique(all(v)), v.end() )
#define perm(c) sort(all(c));for(bool c##p=1;c##p;c##p=next_permutation(all(c)))

template<class T> pair<T, size_t> max(vector<T> &x){ auto it = max_element(all(x)); return mp(*it, it-x.begin()); }
template<class T> pair<T, size_t> min(vector<T> &x){ auto it = min_element(all(x)); return mp(*it, it-x.begin()); }

template<class T> inline bool chmax(T &maxval, const T &newval) { if (maxval<newval) { maxval=newval; return 1; } return 0; }
template<class T> inline bool chmin(T &minval, const T &newval) { if (minval>newval) { minval=newval; return 1; } return 0; }

// Utilities
// Grid world utilities
int dx[4]={0,1,0,-1};
int dy[4]={1,0,-1,0};
#define inside(H,W,y,x) 0<=(x) && (x)<(W) && 0<=(y) && (y)<(H)

inline int in() {int x; cin >> x; return x;} // read int from cin
inline  ll IN() {ll x; cin >> x; return x;}  // read ll from cin



// Debug
#ifdef LOCAL
  #include "dump.hpp"
  #define debug(x) cerr << #x << ": " << x << '\n'
#else
  #define dump(...)
  #define debug(x)
#endif

// Paste snippets here!!
struct UnionFind {
  int n;
  std::vector<int> par;
  std::vector<int> rank;
  UnionFind(int n_) : n(n_), par(n_), rank(n_,0) { for(int i=0;i<n;++i) par[i] = i; }
  int root(int x) { return par[x] == x ? x : (par[x]=root(par[x])); }
  bool same(int x, int y) { return root(x)==root(y); }
  void unite(int x, int y) {
    x = root(x); y = root(y);
    if(x==y) return;
    if(rank[x] < rank[y]) par[x] = y;
    else {
      par[y] = x;
      if(rank[x]==rank[y]) rank[x]++;
    }
  }
};

// 
// Bipartite graph
struct BG {
  int nx; int ny; int nedge;
  ll possible(){ return (ll)nx * ny - nedge; }
};

int main(){
  cin.tie(0); ios::sync_with_stdio(false);    // Magic for faster cin
  int N; cin >> N;
  vi x(N), y(N);
  rep(i,N) cin >> x[i] >> y[i];
  unordered_map<int, int> dict_x;
  unordered_map<int, int> dict_y;

  int nx = 0; int ny = 0;
  rep(i,N) if(dict_x.find(x[i])==dict_x.end()) dict_x[x[i]] = nx++;
  rep(i,N) if(dict_y.find(y[i])==dict_y.end()) dict_y[y[i]] = ny++;

  vvi G(nx);
  rep(i,N){
    G[dict_x[x[i]]].pb(dict_y[y[i]]);
    //G[dict_x[x[i]]][dict_y[y[i]]] = 1;
  }

  UnionFind uf(nx+ny);
  rep(i,nx) rep(j,G[i].size()) {
    //if(G[i][j]) uf.unite(i,nx+j);
    uf.unite(i,nx+G[i][j]);
  }

  unordered_map<int, BG> gg;
  rep(i,nx){
    int r = uf.root(i);
    gg[r].nx++;
  }
  rep(i,ny){
    int r = uf.root(nx+i);
    gg[r].ny++;
  }
  rep(i,nx) {
    int r = uf.root(i);
    gg[r].nedge += G[i].size();
  }
  ll ans = 0;
  for(auto kv : gg){
    ans += kv.second.possible();
  }
  cout << ans << endl;

  return 0;
}


Measured memory_bytes: 8716