#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <fstream>
#include <iostream>
#include <algorithm>
#include <bitset>
#include <array>
#include <vector>
#include <queue>
#include <set>
#include <cmath> // 変数名にy1が使えなくなるかも…。
#include <map>
#include <unordered_map>
#include <unordered_set>
#include <string>

typedef __int128_t int128_t;
std::istream &operator>>(std::istream& input, int128_t& value) { // int128_tの入力。入力が64bitに収まる前提。
    long long tmp; input >> tmp; value = tmp;
    return input;
}
std::ostream &operator<<(std::ostream& output, const int128_t value) { // int128_tの出力。出力が64bitに収まる前提。
    output << (long long)value;
    return output;
}

template<template <typename...> class Tm, typename Tv>
Tm<Tv, int> coordinate_compress(const std::vector<Tv>& vec, const bool reverse = false) {
    // 座標圧縮。要素の小さい順に1-indexで値を割り振る。
    // reverse = trueの場合、大きい順に割り当てるが、mapでの保存順序は常に小さい順である。
    // auto compress = coordinate_compress<Tm>(vec); で利用可能。
    // Tvにhashが設定されていれば、Tmとしてstd::unordered_mapが利用可能で高速。std::pairや、int128_tにはデフォルトでは使えない。
    // Tvに順序が設定されていれば、Tmとしてstd::mapが利用可能だが低速。
    std::vector<Tv> tmp = vec;
    std::sort(tmp.begin(), tmp.end());
    tmp.erase(std::unique(tmp.begin(), tmp.end()), tmp.end());
    Tm<Tv, int> mp;
    for (int i = 0; i < tmp.size(); i++) {
        mp[tmp[i]] = reverse ? tmp.size() - i : i + 1;
    }
    return mp;
}

template<template <typename...> class Tm, typename Tv>
Tm<Tv, int> coordinate_compress(const Tv* A, const int L, const int R, const bool reverse = false) {
    // 上の同名の関数の配列引数版。同じように
    // auto compress = coordinate_compress<Tm>(a, l, r); で利用可能。
    std::vector<Tv> vec;
    for (int i = L; i <= R; i++) {
        vec.push_back(A[i]);
    }
    return coordinate_compress<Tm, Tv>(vec, reverse);
}

template<template <typename...> class Tm, typename Tv>
std::vector<Tv> coordinate_expand(const Tm<Tv, int>& mp) {
    // 座標展開。coordinate_compressで圧縮した座標を元の値に戻すための配列を与える。
    // coordinate_compress の戻り値 compressに対して
    // auto expand = coordinate_expand(compress); で利用可能。圧縮を1-indexで行っているため、ret[0]は意味を持たない
    std::vector<Tv> ret; ret.resize(mp.size()+1);
    for (auto itr = mp.begin(); itr != mp.end(); ++itr) {
        assert(itr->second < ret.size());
        ret[itr->second] = itr->first;
    }
    return ret;
}

class Union_Find {
    public:
    std::vector<int> par;
    Union_Find() {}
    Union_Find(int n){
        par.resize(n, 0);
        reset();
    }

    void reset() {
        // サイズは変えずに初期状態に戻す。
        for (int i = 0; i < par.size(); i++) {
            par[i] = i;
        }
    }

    void simplify(int x) {
        std::vector<int> path;
        while (par[x] != x) {
            path.push_back(x);
            x = par[x];
        }
        for (int p : path) {
            par[p] = x;
        }
    }
    
    int find(const int x){
        simplify(x);
        return par[x];
    }
    
    void unite(int x, int y){
        x = find(x);
        y = find(y);
        if (x != y) {
            par[y] = x;
            // 改造する時は「根になっているノードの親を変更する」という原則が守られていることに注意。
        }
    }

    std::unordered_map<int, std::vector<int>> get_group(const int begin, const int end) {
        assert(0 <= begin && end < par.size());
        std::unordered_map<int, std::vector<int>> ret;
        for (int i = begin; i <= end; i++) {
            ret[find(i)].push_back(i);
        }
        return ret;
    }
};

const int MAX_N = 100050;
int N;
int128_t x[MAX_N], y[MAX_N];

Union_Find uf = Union_Find(MAX_N);

int main(int argc, char **argv) {
    std::cin >> N;
    for (int i = 1; i <= N; i++) {
        std::cin >> x[i] >> y[i];
    }

    std::vector<int> vec;
    for (int i = 1; i <= N; i++) {
        vec.push_back(i);
    }

    std::sort(vec.begin(), vec.end(), [&](const int i, const int j){
        return y[i] < y[j];
    });

    for (int i = 0; i < N - 1; i++) {
        if (y[vec[i]] == y[vec[i+1]]) {
            uf.unite(vec[i], vec[i+1]);
        }
    }

    std::sort(vec.begin(), vec.end(), [&](const int i, const int j){
        return x[i] < x[j];
    });

    for (int i = 0; i < N - 1; i++) {
        if (x[vec[i]] == x[vec[i+1]]) {
            uf.unite(vec[i], vec[i+1]);
        }
    }

    auto group = uf.get_group(1, N);

    int128_t ret = 0;
    for (auto itr = group.begin(); itr != group.end(); ++itr) {
        std::unordered_set<int> set_x, set_y;
        for (int i : itr->second) {
            set_x.insert(x[i]);
            set_y.insert(y[i]);
        }
        ret += (int128_t)set_x.size() * set_y.size();
    }

    std::cout << ret - N << std::endl;

    return 0;
}

Measured memory_bytes: 8696