// Arup Guha
// 2/10/2017
// Solution to 2017 FHSPS Playoff Problem: Defeating Dragons

import java.util.*;

public class dragons {


	public static void main(String[] args) {

		Scanner stdin = new Scanner(System.in);
		int numCases = stdin.nextInt();

		// Process each case.
		for (int loop=0; loop<numCases; loop++) {

			// Set up our hash maps for both weapons and poisons.
			int n = stdin.nextInt();
			HashMap<String,Integer> weaponMap = new HashMap<String,Integer>();
			HashMap<String,Integer> poisonMap = new HashMap<String,Integer>();

			// Stores each edge for our flow graph.
			// Indexes will ultimately be changed as both items (weapon, poison
			// are initially stored 0-based.
			int[][] edges = new int[n][2];

			// Read in the weapon, poison combinations.
			int wI = 0, pI = 0;
			for (int i=0; i<n; i++) {

				// Don't need this.
				stdin.next();

				String weapon = stdin.next();

				// We have this one already, just get its index.
				if (weaponMap.containsKey(weapon))
					edges[i][0] = weaponMap.get(weapon);

				// Add this to our list and update wI.
				else {
					edges[i][0] = wI++;
					weaponMap.put(weapon, edges[i][0]);
				}

				String poison = stdin.next();

				// We have this one already, just get its index.
				if (poisonMap.containsKey(poison))
					edges[i][1] = poisonMap.get(poison);

				// Add this to our list and update pI.
				else {
					edges[i][1] = pI++;
					poisonMap.put(poison, edges[i][1]);
				}
			}

			// Annoying - figure out vertex out degrees for flow graph.
			int[] weaponSize = new int[wI];
			for (int i=0; i<n; i++)
				weaponSize[edges[i][0]]++;

			// Start forming flow graph.
			int len = wI + pI + 2;
			Edge[][] graph = new Edge[len][];

			// Edges from source to weapons.
			graph[len-2] = new Edge[wI];
			for (int i=0; i<wI; i++)
				graph[len-2][i] = new Edge(1, i);

			// Silly.
			graph[len-1] = new Edge[0];

			// Edges from poisons to sink.
			for (int i=wI; i<wI+pI; i++) {
				graph[i] = new Edge[1];
				graph[i][0] = new Edge(1, len-1);
			}

			// Set up each edge from weapons to poisons.
			for (int i=0; i<wI; i++)
				graph[i] = new Edge[weaponSize[i]];

			// Edges between weapons and poisons.
			int[] curIndex = new int[wI];
			for (int i=0; i<n; i++) {
				int myW = edges[i][0];
				int myP = edges[i][1] + wI;
				graph[myW][curIndex[myW]++] = new Edge(1, myP);
			}

			// Set up the flow network. The max flow represents the fact that for each of the pairings, you MUST
			// cover at least one of the two. It turns out that since no more pairings can be added, if you grab
			// one of these two, you can cover everything.
			netflowdinic flow = new netflowdinic(graph, len-2, len-1);
			int maxflow = flow.getMaxFlow();
			System.out.println(maxflow);
		}
	}

}

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);
			}
		}

		ListIterator<Integer> itr = backEdgeLookup[next].listIterator(0);

		// Now look for back edges.
		while (itr.hasNext()) {
			Integer item = itr.next();
			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);
			}
		}

		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;
				}
			}

			ListIterator<Integer> itr = backEdgeLookup[next].listIterator(0);

			// Now look for back edges.
			while (itr.hasNext()) {
				Integer item = itr.next();
				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;
				}
			}
		}

		// 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;
	}
}
