use std::io::*;
use std::str::FromStr;
use std::collections::HashMap;
use std::collections::HashSet;

#[allow(dead_code)]
fn read<T: FromStr>() -> T {
    let stdin = stdin();
    let stdin = stdin.lock();
    let token: String = stdin
        .bytes()
        .map(|c| c.expect("failed to read char") as char)
        .skip_while(|c| c.is_whitespace())
        .take_while(|c| !c.is_whitespace())
        .collect();
    token.parse().ok().expect("failed to parse token")
}

#[allow(dead_code)]
fn pow_mod(base: i64, power: i64, modulo: i64) -> i64 {
    let mut base = base;
    let mut power = power;
    let mut ans = 1;
    while power > 0 {
        if power & 1 == 1 {
            ans = ans * base % modulo;
        }
        power >>= 1;
        base = base * base % modulo;
    }
    ans
}

#[allow(dead_code)]
fn inv_mod(a: i64, modulo: i64) -> i64 {
    pow_mod(a, modulo - 2, modulo)
}

const MOD: i64 = 1e+9 as i64 + 7;
const FACT_MAX: usize = 200100;
static mut FACTORIAL: [i64; FACT_MAX + 1] = [0; FACT_MAX + 1];
static mut RFACTORIAL: [i64; FACT_MAX + 1] = [0; FACT_MAX + 1];
fn init_fact() {
    unsafe {
        if FACTORIAL[0] == 0 {
            FACTORIAL[0] = 1;
            for i in 1..FACT_MAX + 1 {
                FACTORIAL[i] = (FACTORIAL[i - 1] * i as i64) % MOD;
            }
            RFACTORIAL[FACT_MAX] = inv_mod(FACTORIAL[FACT_MAX], MOD);
            for i in (0..FACT_MAX).rev() {
                RFACTORIAL[i] = RFACTORIAL[i + 1] * (i as i64 + 1) % MOD;
            }
        }
    }
}
#[allow(dead_code)]
fn fact(n: usize) -> i64 {
    init_fact();
    unsafe { FACTORIAL[n] }
}
#[allow(dead_code, unused_comparisons)]
fn combi(n: usize, m: usize) -> i64 {
    if n < m || m < 0 {
        return 0;
    }
    init_fact();
    unsafe { FACTORIAL[n] * RFACTORIAL[m] % MOD * RFACTORIAL[n - m] % MOD }
}

#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
struct Edge {
    index: usize,
    src: usize,
    dest: usize,
}
#[allow(dead_code)]
impl Edge {
    fn new(index: usize, src: usize, dest: usize) -> Edge {
        Edge {
            index: index,
            src: src,
            dest: dest,
        }
    }
}
impl std::cmp::Ord for Edge {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        (self.src, self.dest).cmp(&(other.src, other.dest))
    }
}
impl std::cmp::PartialOrd for Edge {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}
type Edges = Vec<Edge>;
type Graph = Vec<Edges>;
#[allow(dead_code)]
fn graph_new(n: usize) -> Graph {
    vec![vec![]; n]
}
#[allow(dead_code)]
fn add_bi_edge(graph: &mut Graph, index: &mut usize, src: usize, dest: usize) {
    graph[src].push(Edge::new(*index, src, dest));
    graph[dest].push(Edge::new(*index, dest, src));
    *index += 1;
}

fn merge(lhs: (usize, usize), rhs: (usize, usize)) -> (usize, usize) {
    let cnt = lhs.1 + rhs.1;
    let v = lhs.0 as i64 * rhs.0 as i64 % MOD * combi(cnt, lhs.1) % MOD;
    (v as usize, cnt)
}

struct Solver {
    n: usize,
    g: Graph,
}

impl Solver {
    fn new() -> Solver {
        let n: usize = read();
        let mut g = graph_new(2 * n);
        let mut e = 0;
        for _ in 0..n * 2 {
            let x = read::<usize>() - 1;
            let y = read::<usize>() - 1;
            add_bi_edge(&mut g, &mut e, y, x + n);
        }
        Solver { n: n, g: g }
    }
    fn usable(&self, constraint: &Edge, edge: &Edge) -> bool {
        edge.dest < constraint.dest
    }
    fn dfs1(&mut self, from: usize, connected: &mut HashSet<usize>) -> usize {
        if connected.contains(&from) {
            return 0;
        }
        let mut ret = self.g[from].len();
        connected.insert(from);
        for edge in self.g[from].clone().iter() {
            ret += self.dfs1(edge.dest, connected);
        }
        ret
    }
    fn set_direction(&self, connected: &HashSet<usize>, dir: usize) -> HashMap<usize, Edge> {
        let mut edge_cnt = HashMap::<usize, usize>::new();
        let mut stack = vec![];
        for &node in connected.iter() {
            let cnt = self.g[node].len();
            edge_cnt.insert(node, cnt);
            if cnt == 1 {
                stack.push(node);
            }
        }
        let mut ret = HashMap::<usize, Edge>::new();
        let mut used_edge = HashSet::<usize>::new();
        let mut second_start = false;
        for _ in 0..2 {
            while let Some(from) = stack.pop() {
                for edge in self.g[from].clone().iter() {
                    // println!("{:?}", edge);
                    if used_edge.contains(&edge.index) {
                        continue;
                    }
                    if second_start {
                        second_start = false;
                        continue;
                    }
                    used_edge.insert(edge.index);
                    assert!(!ret.contains_key(&from));
                    ret.insert(from, edge.clone());
                    *edge_cnt.get_mut(&edge.dest).unwrap() -= 1;
                    if edge_cnt[&edge.dest] == 1 {
                        stack.push(edge.dest);
                    }
                    break;
                }
            }
            if dir == 1 {
                second_start = true;
            }
            // println!("{:?}", ret);
            for &node in connected.iter() {
                if edge_cnt[&node] == 2 {
                    stack.push(node);
                    break;
                }
            }
        }
        ret
    }
    fn calc(&self, from: usize, matching: &HashMap<usize, Edge>) -> (usize, usize) {
        let mut ret = (1, 0);
        for edge in self.g[from].iter() {
            if !self.usable(&matching[&from], edge) {
                continue;
            }
            // println!("{:?} {:?}", matching[&from], edge);
            let nret = self.calc(edge.dest, matching);
            ret = merge(ret, nret);
        }
        ret.1 += 1;
        ret
    }
    fn solve(&mut self) {
        let mut ans = (1, 0);
        let mut visited = vec![false; 2 * self.n];
        for node in 0..2 * self.n {
            if visited[node] {
                continue;
            }
            let mut connected = HashSet::<usize>::new();
            let edge_cnt = self.dfs1(node, &mut connected) / 2;
            if edge_cnt != connected.len() {
                ans = (0, 0);
                break;
            }
            for &node in connected.iter() {
                visited[node] = true;
            }
            let mut sum = 0;
            for dir in 0..2 {
                // println!("calc connect: {} {}", node, dir);
                let matching = self.set_direction(&connected, dir);
                assert!(matching.len() == connected.len());
                let mut lans = (1, 0);
                // println!("{:?}", matching);
                for &node in connected.iter() {
                    let dest = matching[&node].dest;
                    if !self.usable(&matching[&dest], &Edge::new(0, dest, node)) {
                        let mut nret = self.calc(node, &matching);
                        // println!("{:?}", nret);
                        lans = merge(lans, nret);
                    }
                }
                sum = (sum + lans.0) % MOD as usize;
            }
            ans = merge(ans, (sum, connected.len()));
        }
        println!("{}", ans.0);
    }
}

fn main() {
    let mut solver = Solver::new();
    solver.solve();
}


Measured memory_bytes: 67876