#include <bits/stdc++.h>
using namespace std;
using i64 = int_fast64_t;
#define repeat(i, a, b) for(int i = (a); (i) < (b); ++(i))
#define rep(i, n) repeat(i, 0, n)

// BEGIN CUT HERE
#define mygc(c) (c) = getchar_unlocked()
#define mypc(c) putchar_unlocked(c)
// usage: i64 x=rd()
template <typename T = i64>
inline T rd() {
    T x = 0, m = 0, k;
    for(;;) {
        mygc(k);
        if(k == '-') {
            m = 1;
            break;
        }
        if('0' <= k && k <= '9') {
            x = k - '0';
            break;
        }
    }
    for(;;) {
        mygc(k);
        if(k < '0' || '9' < k) break;
        x = x * 10 + k - '0';
    }
    return x;
}
// usage: wr(x);
template <typename T = i64>
void wr(T x, char c = '\n') {
    int s = 0, m = 0;
    char b[64];
    if(x < 0) m = 1, x = -x;
    for(; x; x /= 10) b[s++] = x % 10;
    if(!s) b[s++] = 0;
    if(m) mypc('-');
    for(; s--;) mypc(b[s] + '0');
    mypc(c);
}
template <typename T = i64, class... Args>
void wr(T &&a, Args &&... args) {
    wr(a, ' ');
    wr(std::forward<Args>(args)...);
    return;
}
template <typename T = i64>
void wr(vector<T> &a) {
    for(int i = 0; i < a.size(); i++) {
        i != a.size() - 1 ? wr(a[i], ' ') : wr(a[i], '\n');
    }
    return;
}
// END CUT HERE

template <class Info>
struct edge {
    int from, to;
    Info info;
    edge(int to, Info info) : to(to), info(info) {}
    edge(int from, int to, Info info) : from(from), to(to), info(info) {}
};

struct normal_edge {
    normal_edge() {}
};

//! graphは基本クラス！使うときはundirected_graphかdirected_graphを使う。
template <class Info>
class graph {
   protected:
    using edge_type = edge<Info>;
    ::std::size_t N;
    ::std::vector<::std::vector<edge_type>> edges;

   public:
    graph(::std::size_t N_) : N(N_), edges(N_) {}
    virtual void add_edge(int x, int y, Info info = static_cast<Info>(1)) = 0;
    const ::std::vector<edge_type> &delta(int v) const {
        assert(v < N);
        return edges[v];
    }
    const ::std::vector<edge_type> &operator[](int v) const {
        return this->delta(v);
    }
    ::std::size_t size() const { return N; }
};

template <class Info>
class undirected_graph : public graph<Info> {
   public:
    undirected_graph(::std::size_t N) : graph<Info>(N) {}
    void add_edge(int x, int y, Info info = static_cast<Info>(1)) {
        this->edges[x].push_back(edge<Info>(x, y, info));
        this->edges[y].push_back(edge<Info>(y, x, info));
    }
};

template <class Info>
class directed_graph : public graph<Info> {
   public:
    directed_graph(::std::size_t N) : graph<Info>(N) {}
    void add_edge(int x, int y, Info info = static_cast<Info>(1)) {
        this->edges[x].push_back(edge<Info>(x, y, info));
    }
};

template <class Info>
struct bipartite_graph : public undirected_graph<Info> {
    ::std::size_t A;
    ::std::size_t B;

    /* UnionFind */
    template <typename T>
    struct UnionFind {
        vector<T> data;
        stack<pair<int, T>> history;

        UnionFind() = default;
        UnionFind(int sz) { data.assign(sz, -1); }

        bool unite(int x, int y) {
            x = root(x), y = root(y);
            history.emplace(x, data[x]);
            history.emplace(y, data[y]);
            if(x == y) return (false);
            if(data[x] > data[y]) swap(x, y);
            data[x] += data[y];
            data[y] = x;
            return (true);
        }

        int root(int k) {
            if(data[k] < 0) return (k);
            return (root(data[k]));
        }

        bool same(int x, int y) { return root(x) == root(y); }

        int size(int k) { return (-data[root(k)]); }

        void undo() {
            data[history.top().first] = history.top().second;
            history.pop();
            data[history.top().first] = history.top().second;
            history.pop();
        }

        void snapshot() {
            while(history.size()) history.pop();
        }

        void rollback() {
            while(history.size()) undo();
        }
    };

    UnionFind<Info> uf;

    bipartite_graph(::std::size_t A_, ::std::size_t B_)
        : A(A_), B(B_), undirected_graph<Info>(A_ + B_), uf(A_ + B_) {}
    void add_edge(int x, int y, Info info = static_cast<Info>(1)) {
        undirected_graph<Info>::add_edge(x, y + A, info);
        this->uf.unite(x, y + A);
    }
    int size(int x, bool is_left = true) {
        return (is_left ? uf.size(x) : uf.size(x + A));
    };
    inline bool same(int left, int right) { return uf.same(left, right + A); };
    inline int root(int x, bool is_left = true) {
        return (is_left ? uf.root(x) : uf.root(x + A));
    }
};

// BFS
void ABC132_E() {
    i64 N, M;
    cin >> N >> M;
    directed_graph<i64> g(3 * N);
    while(M--) {
        i64 u, v;
        cin >> u >> v;
        u--, v--;
        g.add_edge(u, v + N);
        g.add_edge(u + N, v + 2 * N);
        g.add_edge(u + 2 * N, v);
    }
    i64 S, T;
    cin >> S >> T;
    S--, T--;
    vector<i64> dist(3 * N, -1);
    queue<i64> q;
    q.push(S);
    dist[S] = 0;
    while(!q.empty()) {
        i64 v = q.front();
        q.pop();
        for(auto e : g[v]) {
            if(dist[e.to] != -1) continue;
            dist[e.to] = dist[e.from] + e.info;
            q.push(e.to);
        }
    }
    cout << (dist[T] == -1 ? -1 : dist[T] / 3) << endl;
};

// 二部グラフ
void ABC131_F() {
    i64 N = rd();
    bipartite_graph<i64> g(110000, 110000);
    rep(i, N) {
        i64 a = rd(), b = rd();
        g.add_edge(a, b);
    }
    vector<i64> mx(2 * 110000, 0), my(2 * 110000, 0);
    for(int i = 0; i < 110000; i++) {
        mx[g.root(i, true)]++;
        my[g.root(i, false)]++;
    }
    i64 ret = 0;
    rep(i, 2 * 110000) { ret += mx[i] * my[i]; }
    wr(ret - N);
}

int main() {
    // ABC132_E();
    ABC131_F();
    return 0;
};

Measured memory_bytes: 19200