// Arup Guha
// 11/17/2014
// Solution to 2014 Southeast Regional D1 Problem: Containment

import java.util.*;

public class containment {

	final public static int DIM = 10;
	final public static int TOTALCUBES = DIM*DIM*DIM;

	public static void main(String[] args) {

		// Read in pts.
		Scanner stdin = new Scanner(System.in);
		int n = stdin.nextInt();
		boolean[] pts = new boolean[TOTALCUBES];
		for (int i=0; i<n; i++) {
			int x = stdin.nextInt();
			int y = stdin.nextInt();
			int z = stdin.nextInt();
			pts[100*x+10*y+z] = true;
		}

		// Start forming flow graph - source is v = TOTALCUBES, sink = TOTALCUBES+1.
		Edge[][] graph = new Edge[TOTALCUBES+2][];

		// From source to all boundary cubes - some flows are > 1, since we are counting surfaces...
		graph[TOTALCUBES] = new Edge[TOTALCUBES-(DIM-2)*(DIM-2)*(DIM-2)];
		int eIndex = 0;
		for (int i=0; i<TOTALCUBES; i++) {
			int myCap = countSurfaceFaces(i);
			if (myCap > 0)
				graph[TOTALCUBES][eIndex++] = new Edge(myCap, i);
		}

		// Form edges from each internal vertex - squares that are on go to sink with capacity 6 (max for any cube).
		for (int i=0; i<TOTALCUBES; i++) {
			ArrayList<Integer> next = getNext(i);
			int cnt = pts[i] ? next.size()+1 : next.size();
			graph[i] = new Edge[cnt];
			for (int j=0; j<next.size(); j++)
				graph[i][j] = new Edge(1, next.get(j));
			if (pts[i]) graph[i][cnt-1] = new Edge(6, TOTALCUBES+1);
		}

		// Need this for flow code.
		graph[TOTALCUBES+1] = new Edge[0];

		// Set up graph and output maximum flow (same as minimum cut), which is what the question wants.
		netflowdinic myFlowNetwork = new netflowdinic(graph, TOTALCUBES, TOTALCUBES+1);
		System.out.println(myFlowNetwork.getMaxFlow());
	}

	public static int[] unPack(int v) {
		// Unpack pt.
		int[] pt = new int[3];
		for (int i=2; i>=0; i--) {
			pt[i] = v%DIM;
			v /= DIM;
		}
		return pt;
	}

	// Assumes DIM > 1, so no cube has more than 3 surface faces.
	public static int countSurfaceFaces(int v) {

		// Convert to pt form.
		int[] pt = unPack(v);

		// Count faces on surface.
		int cnt = 0;
		for (int i=0; i<3; i++)
			if (pt[i] == 0 || pt[i] == DIM-1)
				cnt++;
		return cnt;
	}

	public static ArrayList<Integer> getNext(int v) {

		ArrayList<Integer> ans = new ArrayList<Integer>();

		// Convert to pt form.
		int[] pt = unPack(v);

		// Being lazy - try all six ways.
		if (pt[0] > 0)     ans.add(100*(pt[0]-1)+10*pt[1]+pt[2]);
		if (pt[0] < DIM-1) ans.add(100*(pt[0]+1)+10*pt[1]+pt[2]);
		if (pt[1] > 0)     ans.add(100*pt[0]+10*(pt[1]-1)+pt[2]);
		if (pt[1] < DIM-1) ans.add(100*pt[0]+10*(pt[1]+1)+pt[2]);
		if (pt[2] > 0)     ans.add(100*pt[0]+10*pt[1]+pt[2]-1);
		if (pt[2] < DIM-1) ans.add(100*pt[0]+10*pt[1]+pt[2]+1);

		return ans;
	}

}

/*** From here on is my network flow code for Dinic's ***/
class Edge {

	public int dest;
	public int capacity;
	public int flow;

	public Edge(int cap, int d) {
		capacity = cap;
		flow = 0;
		dest = d;
	}

	public String toString() {
		return "("+dest+" "+capacity+", "+flow+") ";
	}

	public int maxPushForward() {
		return capacity - flow;
	}

	public int maxPushBackward() {
		return flow;
	}

	public boolean pushForward(int moreflow) {

		// We can't push through this much flow.
		if (flow+moreflow > capacity)
			return false;

		// Push through.
		flow += moreflow;
		return true;
	}

	public boolean pushBack(int lessflow) {

		// Not enough to push back on.
		if (flow < lessflow)
			return false;

		flow -= lessflow;
		return true;
	}
}

class direction {

	public int prev;
	public boolean forward;

	public direction(int node, boolean dir) {
		prev = node;
		forward = dir;
	}

	public String toString() {
		if (forward)
			return "" + prev + "->";
		else
			return "" + prev + "<-";
	}
}

class pair {

	public int vertex;
	public int distance;

	public pair(int v, int d) {
		vertex = v;
		distance = d;
	}
}

class netflowdinic {

	private Edge[][] adjMat;
	private int source;
	private int dest;
	private HashMap[] lookup;
	private LinkedList[] backEdgeLookup;
	private int[] distance;

	public netflowdinic(Edge[][] matrix, int start, int end) {

		// Set up easy stuff.
		adjMat = matrix;
		source = start;
		dest = end;
		lookup = new HashMap[matrix.length];
		distance = new int[matrix.length];

		// Allocate empty LLs.
		backEdgeLookup = new LinkedList[matrix.length];
		for (int i=0; i<matrix.length; i++)
			backEdgeLookup[i] = new LinkedList<Integer>();

		// Fill these in.
		for (int i=0; i<adjMat.length; i++) {

			lookup[i] = new HashMap<Integer,Integer>();
			for (int j=0; j<adjMat[i].length; j++) {
				backEdgeLookup[adjMat[i][j].dest].offer(i);
				lookup[i].put(adjMat[i][j].dest,j);
				adjMat[i][j].flow = 0;
			}
		}

	}

	// Wrapper function for dfs that finds an augmenting path of a specific length.
	public ArrayList<direction> findAugmentingPath() {
		boolean[] used = new boolean[adjMat.length];
		ArrayList<direction> path = new ArrayList<direction>();
		path.add(new direction(source, true));
		path = findAugmentingPathRec(source, used, path);
		if (path == null) return null;
		return fix(path);
	}

	// Recursive function that builds the augmenting path from next to dest.
	public ArrayList<direction> findAugmentingPathRec(int next, boolean[] used, ArrayList<direction> path) {

		// We're done.
		if (next == dest) return path;

		// Mark this node.
		used[next] = true;
		int curDist = distance[next];

		// Find all neighbors and add into the queue. These are forward edges.
		for (int i=0; i<adjMat[next].length; i++) {

			int item = adjMat[next][i].dest;
			if (!used[item] && adjMat[next][i].maxPushForward() > 0 && distance[item] == curDist+1) {
				path.add(new direction(item, true));
				ArrayList<direction> temp = findAugmentingPathRec(item, used, path);
				if (temp != null)
					return temp;
				else
					path.remove(path.size()-1);
			}
		}

		// Now look for back edges.
		for (int i=0; i<backEdgeLookup[next].size(); i++) {

			int item = (Integer)backEdgeLookup[next].pollFirst();
			if (!used[item] && lookup[item].containsKey(next) && adjMat[item][(Integer)(lookup[item].get(next))].maxPushBackward() > 0 && distance[item] == curDist+1) {
				path.add(new direction(item, false));
				ArrayList<direction> temp = findAugmentingPathRec(item, used, path);
				if (temp != null)
					return temp;
				else
					path.remove(path.size()-1);
			}
			backEdgeLookup[next].offer(item);
		}

		return null;
	}

	// This is to fix the faulty output of the recursive functions. The directions are "off by one".
	public static ArrayList<direction> fix(ArrayList<direction> list) {
		for (int i=0; i<list.size()-1; i++)
			list.get(i).forward = list.get(i+1).forward;
		return list;
	}

	// This is the BFS that labels all of the distances from the source.
	public boolean labelDistances() {

		Arrays.fill(distance, -1);

		// Set up BFS.
		boolean[] inQueue = new boolean[adjMat.length];
		for (int i=0; i<inQueue.length; i++)
			inQueue[i] = false;

		LinkedList<pair> bfs_queue = new LinkedList<pair>();
		bfs_queue.offer(new pair(source, 0));
		inQueue[source] = true;

		// Our BFS will go until we clear out the queue.
		while (bfs_queue.size() > 0) {

			// Add all the new neighbors of the current node.
			pair nextItem = bfs_queue.poll();
			int next = nextItem.vertex;

			// Store distance from source.
			distance[next] = nextItem.distance;

			// Find all neighbors and add into the queue. These are forward edges.
			for (int i=0; i<adjMat[next].length; i++) {

				int item = adjMat[next][i].dest;
				if (!inQueue[item] && adjMat[next][i].maxPushForward() > 0) {
					bfs_queue.offer(new pair(item, nextItem.distance+1) );
					inQueue[item] = true;
				}
			}

			// Now look for back edges.
			for (int i=0; i<backEdgeLookup[next].size(); i++) {

				int item = (Integer)backEdgeLookup[next].pollFirst();
				if (!inQueue[item] && lookup[item].containsKey(next) && adjMat[item][(Integer)(lookup[item].get(next))].maxPushBackward() > 0) {
					bfs_queue.offer(new pair(item, nextItem.distance+1));
					inQueue[item] = true;
				}
				backEdgeLookup[next].offer(item);
			}
		}

		// No augmenting path found.
		return inQueue[dest];
	}


	// Run the Max Flow Algorithm here.
	public int getMaxFlow() {

		int flow = 0;

		// Outer level - do BFS to label nodes.
		while (labelDistances()) {

			ArrayList<direction> nextpath = findAugmentingPath();

			// Run adjusted DFS here.
			while (nextpath != null) {



				// Check what the best flow through this path is.
				int this_flow = Integer.MAX_VALUE;
				for (int i=0; i<nextpath.size()-1; i++) {

					if (nextpath.get(i).forward) {
						this_flow = Math.min(this_flow, adjMat[nextpath.get(i).prev][(Integer)lookup[nextpath.get(i).prev].get(nextpath.get(i+1).prev)].maxPushForward());
					}
					else {
						this_flow = Math.min(this_flow, adjMat[nextpath.get(i+1).prev][(Integer)lookup[nextpath.get(i+1).prev].get(nextpath.get(i).prev)].maxPushBackward());
					}
				}

				// Now, put this flow through.
				for (int i=0; i<nextpath.size()-1; i++) {

					if (nextpath.get(i).forward) {
						adjMat[nextpath.get(i).prev][(Integer)lookup[nextpath.get(i).prev].get(nextpath.get(i+1).prev)].pushForward(this_flow);
					}
					else {
						adjMat[nextpath.get(i+1).prev][(Integer)lookup[nextpath.get(i+1).prev].get(nextpath.get(i).prev)].pushBack(this_flow);
					}
				}

				// Add this flow in and then get the next path.
				flow += this_flow;
				nextpath = findAugmentingPath();
			}
		}

		return flow;
	}
}