import java.io.*;
import java.util.*;

/**
 * Solution for NEERC'2013 Problem C: Cactus Automorphisms
 * This solution checks correctness of the input.
 *
 * @author Roman Elizarov
 */
public class cactus_re implements Runnable {
	public static final boolean DEBUG = false;
	private static final int MAX_N = 50000;

	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(new cactus_re());
		t.start();
		t.join();
	}

	@Override
	public void run() {
		long time = System.currentTimeMillis();
		try {
			read();
			solve();
			write();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		System.out.printf("Done in %d ms%n", System.currentTimeMillis() - time);
	}

	// -------------------------- INPUT --------------------------

	int n;
	int m;

	List<List<Integer>> adj = new ArrayList<>();
	List<Set<Integer>> adjSets = new ArrayList<>(); // for validation only

	private void read() throws FileNotFoundException {
		Scanner in = new Scanner(new File("cactus.in"));
		n = in.nextInt();
		m = in.nextInt();
		in.nextLine();

		assert n >= 1 && n <= MAX_N;
		assert m >= 0 && m <= MAX_N;

		adj.add(null);
		adjSets.add(null);
		for (int i = 1; i <= n; i++) {
			adj.add(new ArrayList<Integer>());
			adjSets.add(new HashSet<Integer>());
		}

		for (int i = 0; i < m; i++) {
			int k = in.nextInt();
			assert k >= 2 && k <= 1000;
			int u = 0;
			for (int j = 0; j < k; j++) {
				int v = in.nextInt();
				assert v >= 1 && v <= n;
				if (u > 0) {
					assert u != v;
					add(u, v);
					add(v, u);
				}
				u = v;
			}
			in.nextLine();
		}
		in.close();
	}

	void add(int a, int b) {
		assert !adjSets.get(a).contains(b);
		adj.get(a).add(b);
		adjSets.get(a).add(b);
	}

	// -------------------------- SOLUTION --------------------------

	static int[] primes = new int[MAX_N];
	static int nPrimes;

	static void generatePrimes() {
		primes[0] = 2;
		nPrimes = 1;
		for (int i = 3;; i += 2) {
			boolean isPrime = true;
			for (int j = 0; primes[j] * primes[j] <= i; j++) {
				if (i % primes[j] == 0) {
					isPrime = false;
					break;
				}
			}
			if (isPrime) {
				primes[nPrimes++] = i;
				if (i > MAX_N)
					break;
			}
		}
	}

	static void mul(int[] answer, int num) {
		for (int j = 0; primes[j] <= num; j++)
			while (num % primes[j] == 0) {
				answer[j]++;
				num /= primes[j];
			}
	}

	static class Cycle implements Comparable<Cycle> {
		final List<Vertex> v = new ArrayList<>(); // list of vertices in a cycle, v(0) is up, v(1) is its key in up's next map

		int depth; // "cyclic" depth of subgraph at this cycle
		Vertex vDeepest; // deepest vertex

		Vertex[] ordered; // ordered vertices
		boolean symmetricAsHangs; // true when ordered is symmetric (against hang point!) and size() > 2 (order method fills it)

		@Override
		public String toString() {
			return v.toString();
		}

		int size() {
			return v.size();
		}

		int cmp(int i, int j) {
			return ordered[i].compareTo(ordered[j]);
		}

		void order() {
			if (ordered != null)
				return;
			ordered = v.toArray(new Vertex[size()]);
			int d = 0;
			for (int i = 1; i < (size() + 1) / 2; i++) {
				d = cmp(i, size() - i);
				if (d != 0)
					break;
			}
			symmetricAsHangs = size() > 2 && d == 0;
			if (d < 0) // then flip around
				for (int i = 1; i < (size() + 1) / 2; i++) {
					Vertex t = ordered[i];
					ordered[i] = ordered[size() - i];
					ordered[size() - i] = t;
				}
		}

		@Override
		public int compareTo(Cycle o) {
			if (this == o)
				return 0;
			int d = o.depth - depth;
			if (d != 0)
				return d;
			d = o.size() - size();
			if (d != 0)
				return d;
			order();
			o.order();
			for (int i = 1; i < size(); i++) {
				d = o.ordered[i].compareTo(ordered[i]);
				if (d != 0)
					return d;
			}
			return 0;
		}

		void countAutomorphismsAsHangs(int answer[]) {
			order();
			for (int i = 1; i < size(); i++)
				v.get(i).countAutomorphisms(answer);
			if (symmetricAsHangs)
				mul(answer, 2);
		}

		void countAutomorphismsFree(int[] answer) {
			// count symmetries of everything hanging here
			for (int i = 0; i < size(); i++)
				v.get(i).countAutomorphisms(answer);
			// count rotational symmetry
			mul(answer, countRotations());
			// can it be also flipped?
			if (size() > 2 && canFlip())
				mul(answer, 2);
		}

		int countRotations() {
			order(); // not really needed, but will init ordered array that cmp is using
			// Use Knuth-Morris-Pratt algorithm to find length of circular rotation m
			int n = size();
			// Build KMP table
			int[] t = new int[n]; // KMP table
			t[0] = -1;
			int i = 2;
			int len = 0;
			while (i < n) {
				if (cmp(i - 1, len % n) == 0) {
					len++;
					t[i] = len;
					i++;
				} else if (len > 0) {
					len = t[len]; // fallback
				} else
					i++; // nowhere to fallback - go next
			}
			// Use KMP table to find first rotated match
			int m = 1; // start searching at offset 1
			i = 0;
			while (m < n) {
				if (i == n)
					break; // match found
				if (cmp(i, (m + i) % n) == 0) {
					i++;
				} else {
					// fallback on mismatch
					m += i - t[i];
					i = Math.max(0, t[i]);
				}
			}
			assert n % m == 0; // match pos must divide string length
			return n / m;
		}

			// NOTE ON POSITIONS: even positions correspond to vertices, odd positions to "in-between" vertices
		boolean eqPos(int p0, int p1) {
			assert p0 % 2 == p1 % 2; // compared positions shall always have the same evenness/oddness
			return p0 % 2 == 1 || cmp(p0 / 2, p1 / 2) == 0;
		}

		boolean canFlip() {
			order(); // not really needed, but will init ordered array that cmp is using
			// Use Manacher's algorithm to find a place where we can flip the string
			int n = size();
			// NOTE ON POSITIONS: even positions correspond to vertices, odd positions to "in-between" vertices
			int[] len = new int[2 * n]; // known max len table around given pos (there are 2*n positions!)
			int c = 0; // position of the current center of the largest palindrome to the left
			int r = 0; // position of the right edge of the current palindrome
			for (int i = 0; i < 2*n; i++) {
				// compute starting len[i] from mirror image if we're inside current palindrome
				if (r > i)
					len[i] = Math.min(r - i, len[(2 * c - i)]);
				// expand palindrome at i while possible
				//   1. we are not expanding way cyclically to the left to maintain O(n) time)
				//   2. but string is considered to extend indefinitely to the right
   				while (len[i] < i && eqPos(i - len[i] - 1, (i + len[i] + 1) % (2 * n))) {
					len[i]++;
					if (len[i] >= n) // len is counted in positions, so it means actual len is >= n/2 which is what we need to declare win
						return true; // found a position (a vertex or an edge between them) to flip around!
				}
				// if palindrome at i extend past r, then switch to new palindrome
				if (i + len[i] > r) {
					c = i;
					r = i + len[i];
				}
			}
			return false; // cannot flip...
		}

		void printTree(int indent) {
			System.out.printf("%" + indent + "s c%s depth=%d%n", "", v, depth);
			for (int i = 1; i < size(); i++)
				v.get(i).printTree(indent + 2);
		}
	}

	static class Vertex implements Comparable<Vertex> {
		final int index; // original index
		final Map<Vertex, Cycle> next = new HashMap<>(); // cycles handing at this vertex (keyed by the next vertex)

		int num;        // number in dfs order
		int nextAdjIdx; // next adj vertex to be scanned
		Vertex vUp;      // uplink in dfs

		int depth;      // "cyclic" depth of subgraph at this vertex
		Cycle cDeepest; // deepest cycle

		Cycle[] ordered; // ordered cycles

		Vertex(int index) {
			this.index = index;
		}

		@Override
		public String toString() {
			return "" + index;
		}

		int size() {
			return next.size();
		}

		void order() {
			if (ordered != null)
				return;
			ordered = next.values().toArray(new Cycle[size()]);
			Arrays.sort(ordered);
		}

		@Override
		public int compareTo(Vertex o) {
			if (this == o)
				return 0;
			int d = o.depth - depth;
			if (d != 0)
				return d;
			d = o.size() - size();
			if (d != 0)
				return d;
			order();
			o.order();
			for (int i = 0; i < size(); i++) {
				d = o.ordered[i].compareTo(ordered[i]);
				if (d != 0)
					return d;
			}
			return 0;
		}

		void countAutomorphisms(int[] answer) {
			order();
			int i = 0;
			while (i < size()) {
				Cycle c0 = ordered[i];
				ordered[i].countAutomorphismsAsHangs(answer);
				int j = i + 1;
				int cnt = 1;
				while (j < size() && ordered[j].compareTo(c0) == 0) {
					cnt++;
					mul(answer, cnt);
					ordered[j].countAutomorphismsAsHangs(answer);
					j++;
				}
				i = j;
			}
		}

		void printTree(int indent) {
			System.out.printf("%" + indent + "s v%d depth=%d%n", "", index, depth);
			for (Cycle c : next.values())
				c.printTree(indent + 2);
		}

	}

	class DFS {
		final Vertex[] v = new Vertex[n + 1];     // all vertices
		final Stack<Vertex> st = new Stack<>();   // dfs stack of vertices

		int curNum;  // current dfs vertex number

		final PriorityQueue<Vertex> vQueue = new PriorityQueue<>();
		final PriorityQueue<Cycle> cQueue = new PriorityQueue<>();

		int diam;        // "cyclic" graph diameter
		Vertex vCenter;  // center is at the vertex
		Cycle cCenter;   // center is at the cycle
		int distCenter;  // distance to center from on of the above

		DFS() {
			for (int i = 1; i <= n; i++)
				v[i] = new Vertex(i);
		}

		// add vertex to stack
		void pushV(Vertex t) {
			t.num = ++curNum;
			st.push(t);
		}

		// visits edge from s to t in dfs
		Cycle visit(Vertex s, Vertex t) {
			if (t.num > 0) {
				if (s.num < t.num)
						return null; // already processed
				if (s.vUp == t)
						return null; // one back -- skip (cycle code will not change anything)
				// cycle
				Cycle c = new Cycle();
				Vertex j = s;
				while (j != t) {
					c.v.add(j);
					j = j.vUp;
				}
				c.v.add(t);
				Collections.reverse(c.v);
				// now from t to s
				for (int i = 1; i < c.v.size(); i++) {
					Vertex p = c.v.get(i - 1);
					Vertex q = c.v.get(i);
					Cycle oldC = p.next.remove(q);
					assert oldC.v.size() == 2 : "Cactus -- at most one loop per edge";
				}
				t.next.put(c.v.get(1), c);
				return c;
			}
			// push fresh vertex onto stack
			pushV(t);
			t.vUp = s;
			// create simple cycle (two vertices)
			Cycle c = new Cycle();
			c.v.add(s);
			c.v.add(t);
			s.next.put(t, c);
			return c;
		}

		// start dfs at a given vertex
		Vertex dfs(int s0) {
			Vertex v0 = v[s0];
			pushV(v0);
			vCenter = v0;
			dfsLoop();
			if (DEBUG) {
				System.out.printf("--- TREE diam=%d, vCenter=%s, cCenter=%s ---%n", diam, vCenter, cCenter);
				v0.printTree(2);
			}
			return v0;
		}

		// start dfs at a given cycle
		Cycle dfs(Cycle c0) {
			pushV(v[c0.v.get(0).index]);
			Cycle c = null;
			int n = c0.v.size();
			for (int i = 0; i < n; i++) {
				Cycle cc = forceVisit(c0.v.get(i).index, c0.v.get((i + 1) % n).index);
				if (cc != null)
					c = cc;
			}
			assert c != null && c.v.size() == n;
			// cleanup next references in this main loop that were created before going into dfs
			for (Vertex v : c.v)
				v.next.clear();
			// then run dfs
			dfsLoop();
			return c;
		}

		Cycle forceVisit(int index0, int index1) {
			List<Integer> adj0 = adj.get(index0);
			swap(adj0, 0, adj0.indexOf(index1)); // swap edge into position 0
			v[index0].nextAdjIdx = 1; // remember position 0 as already visited
			return visit(v[index0], v[index1]);
		}

		void dfsLoop() {
			while (!st.isEmpty()) {
				Vertex s = st.peek();
				List<Integer> sAdj = adj.get(s.index);
				int i = s.nextAdjIdx++;
				if (i >= sAdj.size()) {
					// done with it -- backtrack and compute "cyclic" depth, diameter, and center
					computeCyclicStuff(s);
					st.pop();
					continue;
				}
				Vertex t = v[sAdj.get(i)];
				visit(s, t);
			}
			assert curNum == n : "Connected";
			// finish center computation
			if (vCenter != null)
				initCenter(vCenter, distCenter);
			else
				initCenter(cCenter, distCenter);
		}

		// compute "cyclic" depth, diameter, and center
		void computeCyclicStuff(Vertex s) {
			// compute depth of the vertices and cycles
			assert s.depth == 0 : "Not initialized yet";
			// Process all cycles
			cQueue.clear();
			for (Cycle c : s.next.values()) {
				int len = c.v.size();
				vQueue.clear();
				for (int i = 1; i < len; i++)
					vQueue.add(c.v.get(i));
				// Find deepest vertex
				Vertex v0 = vQueue.remove();
				c.depth = v0.depth + 1;
				c.vDeepest = v0;
				assert c.depth % 2 == 1 : "Depth of cycles is always odd";
				cQueue.add(c);
				if (!vQueue.isEmpty()) {
					// potential diameter through this cycle c via two vertices
					Vertex v1 = vQueue.remove();
					int dd = v0.depth + v1.depth + 2;
					if (dd > diam) {
						diam = dd;
						if (v0.depth == v1.depth) {
							vCenter = null;
							cCenter = c;
							distCenter = 0;
						} else {
							assert v0.depth > v1.depth;
							vCenter = v0;
							cCenter = null;
							distCenter = (v0.depth - v1.depth - 2) / 2;
						}
					}
				}
			}
			// Find deepest cycle
			if (!cQueue.isEmpty()) {
				Cycle c0 = cQueue.remove();
				s.depth = c0.depth + 1;
				s.cDeepest = c0;
				assert s.depth % 2 == 0 : "Depth of vertices is always even";
				// potential diameters from this vertex s to deepest cycle
				int dd = c0.depth + 1;
				if (dd > diam) {
					diam = dd;
					vCenter = null;
					cCenter = c0;
					distCenter = (c0.depth - 1) / 2;
				}
				if (!cQueue.isEmpty()) {
					// potential diameter through this vertex s via two cycles
					Cycle c1 = cQueue.remove();
					dd = c0.depth + c1.depth + 2;
					if (dd > diam) {
						diam = dd;
						if (c0.depth == c1.depth) {
							vCenter = s;
							cCenter = null;
							distCenter = 0;
						} else {
							assert c0.depth > c1.depth;
							vCenter = null;
							cCenter = c0;
							distCenter = (c0.depth - c1.depth - 2) / 2;
						}
					}
				}
			}
		}

		void initCenter(Vertex v, int dist) {
			if (dist == 0) {
				vCenter = v;
				cCenter = null;
			} else
				// descend into deepest cycle
				initCenter(v.cDeepest, dist - 1);
		}

		void initCenter(Cycle c, int dist) {
			if (dist == 0) {
				vCenter = null;
				cCenter = c;
			} else
				// descend into deepest vertex
				initCenter(c.vDeepest, dist - 1);
		}
	}

	static <T> void swap(List<T> list, int i, int j) {
		T t = list.get(i);
		list.set(i, list.get(j));
		list.set(j, t);
	}

	int k;
	int[] answer;

	private void solve() {
		// PHASE 0: generate primes
		generatePrimes();

		// PHASE 1: split graph into cycles and compute its "cyclic" center (either vertex or a cycle)
		// Conceptually, we convert original cactus graph into a tree with two kinds of
		// vertices: original vertices and cycles. Then we work with this tree.

		DFS dfs = new DFS();
		dfs.dfs(new Random(n * m).nextInt(n) + 1); // prng start

		// PHASE 2: rehang at the center and compute answer

		answer = new int[nPrimes];
		if (dfs.vCenter != null) {
			Vertex v = dfs.vCenter;
			if (DEBUG)
				System.out.println("v = " + v);
			dfs = new DFS();
			v = dfs.dfs(v.index); // hang at central vertex
			v.countAutomorphisms(answer);
		} else {
			Cycle c = dfs.cCenter;
			if (DEBUG)
				System.out.println("c = " + c);
			dfs = new DFS();
			c = dfs.dfs(c); // hang at central cycle
			c.countAutomorphismsFree(answer);
		}
		for (int i = 0; i < nPrimes; i++)
			if (answer[i] != 0) {
				k++;
				if (DEBUG)
					System.out.println(primes[i] + " " + answer[i]);
			}
	}

	private void write() throws FileNotFoundException {
		PrintWriter out = new PrintWriter("cactus.out");
		out.println(k);
		for (int i = 0; i < nPrimes; i++)
			if (answer[i] != 0) {
				k++;
				out.println(primes[i] + " " + answer[i]);
			}
		out.close();
	}

//----------------- just for validation ------------------

	/**
	 * Strict scanner to verify 100% correspondence between input files and input file format specification.
	 * It is a drop-in replacement for {@link java.util.Scanner} that could be added to a solution source
	 * (cut-and-paste) without breaking its ability to work with {@link java.util.Scanner}.
	 */
	public class Scanner {
		private final BufferedReader in;
		private String line = "";
		private int pos;
		private int lineNo;

		public Scanner(File source) throws FileNotFoundException {
			in = new BufferedReader(new FileReader(source));
			nextLine();
		}

		public void close() {
			assert line == null : "Extra data at the end of file";
			try {
				in.close();
			} catch (IOException e) {
				throw new AssertionError("Failed to close with " + e);
			}
		}

		public void nextLine() {
			assert line != null : "EOF";
			assert pos == line.length() : "Extra characters on line " + lineNo;
			try {
				line = in.readLine();
			} catch (IOException e) {
				throw new AssertionError("Failed to read line with " + e);
			}
			pos = 0;
			lineNo++;
		}

		public String next() {
			assert line != null : "EOF";
			assert line.length() > 0 : "Empty line " + lineNo;
			if (pos == 0)
				assert line.charAt(0) > ' ' : "Line " + lineNo + " starts with whitespace";
			else {
				assert pos < line.length() : "Line " + lineNo + " is over";
				assert line.charAt(pos) == ' ' : "Wrong whitespace on line " + lineNo;
				pos++;
				assert pos < line.length() : "Line " + lineNo + " is over";
				assert line.charAt(pos) > ' ' : "Line " + lineNo + " has double whitespace";
			}
			StringBuilder sb = new StringBuilder();
			while (pos < line.length() && line.charAt(pos) > ' ')
				sb.append(line.charAt(pos++));
			return sb.toString();
		}

		public int nextInt() {
			String s = next();
			assert s.length() == 1 || s.charAt(0) != '0' : "Extra leading zero in number " + s + " on line " + lineNo;
			assert s.charAt(0) != '+' : "Extra leading '+' in number " + s + " on line " + lineNo;
			try {
				return Integer.parseInt(s);
			} catch (NumberFormatException e) {
				throw new AssertionError("Malformed number " + s + " on line " + lineNo);
			}
		}
	}
}
