import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;

public class Main {
    static class Reader
    {
        final private int BUFFER_SIZE = 1 << 16;
        private DataInputStream din;
        private byte[] buffer;
        private int bufferPointer, bytesRead;

        public Reader()
        {
            din = new DataInputStream(System.in);
            buffer = new byte[BUFFER_SIZE];
            bufferPointer = bytesRead = 0;
        }

        public Reader(String file_name) throws IOException
        {
            din = new DataInputStream(new FileInputStream(file_name));
            buffer = new byte[BUFFER_SIZE];
            bufferPointer = bytesRead = 0;
        }

        public String readLine() throws IOException
        {
            byte[] buf = new byte[1_000_010]; // line length
            int cnt = 0, c;
            while ((c = read()) != -1)
            {
                if (c == '\n')
                    break;
                buf[cnt++] = (byte) c;
            }
            return new String(buf, 0, cnt);
        }

        public int nextInt() throws IOException
        {
            int ret = 0;
            byte c = read();
            while (c <= ' ')
                c = read();
            boolean neg = (c == '-');
            if (neg)
                c = read();
            do
            {
                ret = ret * 10 + c - '0';
            }  while ((c = read()) >= '0' && c <= '9');

            if (neg)
                return -ret;
            return ret;
        }

        public long nextLong() throws IOException
        {
            long ret = 0;
            byte c = read();
            while (c <= ' ')
                c = read();
            boolean neg = (c == '-');
            if (neg)
                c = read();
            do {
                ret = ret * 10 + c - '0';
            }
            while ((c = read()) >= '0' && c <= '9');
            if (neg)
                return -ret;
            return ret;
        }

        public double nextDouble() throws IOException
        {
            double ret = 0, div = 1;
            byte c = read();
            while (c <= ' ')
                c = read();
            boolean neg = (c == '-');
            if (neg)
                c = read();

            do {
                ret = ret * 10 + c - '0';
            }
            while ((c = read()) >= '0' && c <= '9');

            if (c == '.')
            {
                while ((c = read()) >= '0' && c <= '9')
                {
                    ret += (c - '0') / (div *= 10);
                }
            }

            if (neg)
                return -ret;
            return ret;
        }

        private void fillBuffer() throws IOException
        {
            bytesRead = din.read(buffer, bufferPointer = 0, BUFFER_SIZE);
            if (bytesRead == -1)
                buffer[0] = -1;
        }

        private byte read() throws IOException
        {
            if (bufferPointer == bytesRead)
                fillBuffer();
            return buffer[bufferPointer++];
        }

        public void close() throws IOException
        {
            if (din == null)
                return;
            din.close();
        }
    }
    int[] parent;
    int[] size;
    int find(int x) {
        if (parent[x] == x) {
            return x;
        }

        int y = find(parent[x]);
        parent[x] = y;
        return y;
    }

    void merge(int x, int y) {
        int parentX = find(x);
        int parentY = find(y);
        if (parentX != parentY) {
            size[parentY] += size[parentX];
            parent[parentX] = parentY;
        }
    }
    void solve(Reader sc, PrintWriter pw) throws IOException {
        int N, M;
        N = sc.nextInt();
        M = sc.nextInt();
        parent = new int[N + 1];
        size = new int[N + 1];
        int result = 1;
        for (int i = 1; i <=N; i++) {
            parent[i] = i;
            size[i] = 1;
        }
        for (int i = 0; i < M; i++) {
            int x = sc.nextInt();
            int y = sc.nextInt();
            merge(x, y);
        }
        for (int i = 1; i <= N; i++) {
//            System.out.println(size[i]);
            result = Math.max(result, size[i]);
        }
        pw.println(result);
    }
    public static void main(String[] args) throws IOException {
        Reader sc = new Reader();
        PrintWriter pw = new PrintWriter(System.out);
//        PrintWriter pw = new PrintWriter(System.out, true);
        new Main().solve(sc, pw);
        pw.flush();
        pw.close();
        sc.close();
    }
}


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

class UnionFind{
	int parent[];
	UnionFind(int N){
		parent = new int[N];
		Arrays.fill(parent, -1);
	}

	boolean same(int x, int y){
		return root(x)==root(y);
	}
	int root(int x) {
		if(parent[x]<0)return x;
		return parent[x] = root(parent[x]);
	}
	int size(int x) {
		return -parent[root(x)];
	}
	void unite(int x, int y) {
		if(size(x)<size(y)) {
			int tmp = x;
			x = y;
			y = tmp;
		}
		int rx = root(x);
		int ry = root(y);
		if(rx==ry)return;
		parent[rx] += parent[ry];
		parent[ry] = rx;
	}
}

public class Main {
	static void solve() {
		int n = ni(), m = ni();
		UnionFind uf = new UnionFind(n);
		for(int i=0;i<m;i++) {
			int a = ni()-1, b = ni() -1;
			uf.unite(a, b);
		}

		int ans = 0;
		for(int i=0;i<n;i++) {
			ans = max(ans, uf.size(i));
		}
		out.println(ans);
	}

	//constants
	static final int inf = Integer.MAX_VALUE / 2;
	static final long linf = Long.MAX_VALUE / 3;
	static final double dinf = Double.MAX_VALUE / 3;
	static final long mod = (long) 1e9 + 7;
	static final int[] dx = { -1, 0, 1, 0 }, dy = { 0, -1, 0, 1 }, dx8 = { -1, -1, -1, 0, 0, 1, 1, 1 }, dy8 = { -1, 0, 1, -1, 1, -1, 0, 1 };
	static final double eps = 1e-10;

	//libraries
	static long[] cum(int a[]) {
		long[] cum = new long[a.length + 1];
		for(int i=0;i<a.length;i++) cum[i+1] = cum[i] + a[i];
		return cum;
	}
	static long[] cum(long a[]) {
		long[] cum = new long[a.length + 1];
		for(int i=0;i<a.length;i++) cum[i+1] = cum[i] + a[i];
		return cum;
	}
	static void reverse(int ar[]) {
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			int t = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = t;
		}
	}
	static void reverse(long ar[]) {
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			long t = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = t;
		}
	}
	static void reverse(double ar[]) {
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			double t = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = t;
		}
	}
	static void reverse(char ar[]) {
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			char t = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = t;
		}
	}
	static String getReverse(String s) {
		StringBuilder sb = new StringBuilder(s);
		return sb.reverse().toString();
	}
	static <T> void reverse(T[] ar) {
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			T t = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = t;
		}
	}
	static int[] concat(int x, int arr[]) {
		int ret[] = new int[arr.length + 1];
		System.arraycopy(arr, 0, ret, 1, ret.length - 1);
		ret[0] = x;
		return ret;
	}
	static int[] concat(int arr[], int x) {
		int ret[] = new int[arr.length + 1];
		System.arraycopy(arr, 0, ret, 0, ret.length - 1);
		ret[ret.length - 1] = x;
		return ret;
	}
	static long[] concat(long x, long arr[]) {
		long ret[] = new long[arr.length + 1];
		System.arraycopy(arr, 0, ret, 1, ret.length - 1);
		ret[0] = x;
		return ret;
	}
	static long[] concat(long arr[], long x) {
		long ret[] = new long[arr.length + 1];
		System.arraycopy(arr, 0, ret, 0, ret.length - 1);
		ret[ret.length - 1] = x;
		return ret;
	}
	static char[] concat(char x, char arr[]) {
		char ret[] = new char[arr.length + 1];
		System.arraycopy(arr, 0, ret, 0, ret.length - 1);
		ret[ret.length - 1] = x;
		return ret;
	}
	static char[] concat(char arr[], char x) {
		char ret[] = new char[arr.length + 1];
		System.arraycopy(arr, 0, ret, 0, ret.length - 1);
		ret[ret.length - 1] = x;
		return ret;
	}
	static int max(int x, int y) {
		return Math.max(x, y);
	}
	static int min(int x, int y) {
		return Math.min(x, y);
	}
	static int max(int x, int y, int z) {
		x = Math.max(x, y);
		x = Math.max(x, z);
		return x;
	}
	static int min(int x, int y, int z) {
		x = Math.min(x, y);
		x = Math.min(x, z);
		return x;
	}
	static long max(long x, long y) {
		return Math.max(x, y);
	}
	static long min(long x, long y) {
		return Math.min(x, y);
	}
	static long max(long x, long y, long z) {
		x = Math.max(x, y);
		x = Math.max(x, z);
		return x;
	}
	static long min(long x, long y, long z) {
		x = Math.min(x, y);
		x = Math.min(x, z);
		return x;
	}
	static double max(double x, double y) {
		return Math.max(x, y);
	}
	static double min(double x, double y) {
		return Math.min(x, y);
	}
	static double max(double x, double y, double z) {
		x = Math.max(x, y);
		x = Math.max(x, z);
		return x;
	}
	static double min(double x, double y, double z) {
		x = Math.min(x, y);
		x = Math.min(x, z);
		return x;
	}
	static void sort(int[] ar) {
		Arrays.sort(ar);
	}
	static void sort(long[] ar) {
		Arrays.sort(ar);
	}
	static void sort(double[] ar) {
		Arrays.sort(ar);
	}
	static void sort(char[] ar) {
		Arrays.sort(ar);
	}
	static void rsort(int[] ar) {
		Arrays.sort(ar);
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			int tmp = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = tmp;
		}
	}
	static void rsort(long[] ar) {
		Arrays.sort(ar);
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			long tmp = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = tmp;
		}
	}
	static void rsort(double[] ar) {
		Arrays.sort(ar);
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			double tmp = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = tmp;
		}
	}
	static void rsort(char[] ar) {
		Arrays.sort(ar);
		int len = ar.length;
		for (int i = 0; i < len / 2; i++) {
			char tmp = ar[i];
			ar[i] = ar[len - 1 - i];
			ar[len - 1 - i] = tmp;
		}
	}
	static void fill(int arr[], int x) {
		Arrays.fill(arr, x);
	}
	static void fill(long arr[], long x) {
		Arrays.fill(arr, x);
	}
	static void fill(boolean arr[], boolean x) {
		Arrays.fill(arr, x);
	}
	static void fill(double arr[], double x) {
		Arrays.fill(arr, x);
	}
	static void fill(int arr[][], int x) {
		for (int i = 0; i < arr.length; i++)
			Arrays.fill(arr[i], x);
	}
	static void fill(long arr[][], long x) {
		for (int i = 0; i < arr.length; i++)
			Arrays.fill(arr[i], x);
	}
	static void fill(double arr[][], double x) {
		for (int i = 0; i < arr.length; i++)
			Arrays.fill(arr[i], x);
	}
	static void fill(boolean arr[][], boolean x) {
		for (int i = 0; i < arr.length; i++)
			Arrays.fill(arr[i], x);
	}
	//MOD culc
	static long plus(long x, long y) {
		long res = (x + y) % mod;
		return res < 0 ? res + mod : res;
	}
	static long sub(long x, long y) {
		long res = (x - y) % mod;
		return res < 0 ? res + mod : res;
	}
	static long mul(long x, long y) {
		long res = (x * y) % mod;
		return res < 0 ? res + mod : res;
	}
	static long div(long x, long y) {
		long res = x * pow(y, mod - 2) % mod;
		return res < 0 ? res + mod : res;
	}
	static long pow(long x, long y) {
		if (y < 0)
			return 0;
		if (y == 0)
			return 1;
		if (y % 2 == 1)
			return (x * pow(x, y - 1)) % mod;
		long root = pow(x, y / 2);
		return root * root % mod;
	}
	public static void main(String[] args) throws Exception {
		is = INPUT.isEmpty() ? System.in : new ByteArrayInputStream(INPUT.getBytes());
		out = new PrintWriter(System.out);
		solve();
		out.flush();
	}
	//input
	static InputStream is;
	static PrintWriter out;
	static String INPUT = "";
	private static byte[] inbuf = new byte[1024];
	static int lenbuf = 0, ptrbuf = 0;
	private static int readByte() {
		if (lenbuf == -1)
			throw new InputMismatchException();
		if (ptrbuf >= lenbuf) {
			ptrbuf = 0;
			try {
				lenbuf = is.read(inbuf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (lenbuf <= 0)
				return -1;
		}
		return inbuf[ptrbuf++];
	}
	private static boolean isSpaceChar(int c) {
		return !(c >= 33 && c <= 126);
	}
	private static int skip() {
		int b;
		while ((b = readByte()) != -1 && isSpaceChar(b))
			;
		return b;
	}
	@SuppressWarnings("unused")
	private static double nd() {
		return Double.parseDouble(ns());
	}
	@SuppressWarnings("unused")
	private static char nc() {
		return (char) skip();
	}
	private static String ns() {
		int b = skip();
		StringBuilder sb = new StringBuilder();
		while (!(isSpaceChar(b))) {
			sb.appendCodePoint(b);
			b = readByte();
		}
		return sb.toString();
	}
	private static char[] ns(int n) {
		char[] buf = new char[n];
		int b = skip(), p = 0;
		while (p < n && !(isSpaceChar(b))) {
			buf[p++] = (char) b;
			b = readByte();
		}
		return n == p ? buf : Arrays.copyOf(buf, p);
	}
	@SuppressWarnings("unused")
	private static char[][] nm(int n, int m) {
		char[][] map = new char[n][];
		for (int i = 0; i < n; i++)
			map[i] = ns(m);
		return map;
	}
	@SuppressWarnings("unused")
	private static int[] na(int n) {
		int[] a = new int[n];
		for (int i = 0; i < n; i++)
			a[i] = ni();
		return a;
	}
	@SuppressWarnings("unused")
	private static long[] nla(int n) {
		long[] a = new long[n];
		for (int i = 0; i < n; i++)
			a[i] = nl();
		return a;
	}
	@SuppressWarnings("unused")
	private static int[][] na(int n, int m){
		int[][] res = new int[n][m];
		for(int i=0;i<n;i++) {
			for(int j=0;j<m;j++) {
				res[i][j] = ni();
			}
		}
		return res;
	}
	@SuppressWarnings("unused")
	private static long[][] nla(int n, int m){
		long[][] res = new long[n][m];
		for(int i=0;i<n;i++) {
			for(int j=0;j<m;j++) {
				res[i][j] = nl();
			}
		}
		return res;
	}
	private static int ni() {
		int num = 0, b;
		boolean minus = false;
		while ((b = readByte()) != -1 && !((b >= '0' && b <= '9') || b == '-'))
			;
		if (b == '-') {
			minus = true;
			b = readByte();
		}

		while (true) {
			if (b >= '0' && b <= '9') {
				num = num * 10 + (b - '0');
			} else {
				return minus ? -num : num;
			}
			b = readByte();
		}
	}
	private static long nl() {
		long num = 0;
		int b;
		boolean minus = false;
		while ((b = readByte()) != -1 && !((b >= '0' && b <= '9') || b == '-'))
			;
		if (b == '-') {
			minus = true;
			b = readByte();
		}

		while (true) {
			if (b >= '0' && b <= '9') {
				num = num * 10 + (b - '0');
			} else {
				return minus ? -num : num;
			}
			b = readByte();
		}
	}
}


import java.util.Scanner;

public class Main {

	static int MOD = 1000000007;

	public static void main(String[] args) {
		exec_1_1();

	}

	public static void exec_1_1() {

		try(var sc = new Scanner(System.in)) {

			int N = sc.nextInt();

			int M = sc.nextInt();

			int[] f = new int[N];

			for(int i=0; i<N; i++) f[i]=-1;

			for(int i=0; i<M; i++) {
				int a = sc.nextInt()-1;
				int b = sc.nextInt()-1;

				int rIdx = rootIdx(f, a);
				int rdxB = rootIdx(f, b);
				if(f[rIdx]>f[rdxB]) {
					int swap = rIdx;
					rdxB = rIdx;
					rIdx = swap;
				}
				if(rIdx==rdxB) continue ;

				f[rIdx] += f[rdxB];
				f[rdxB] = rIdx;
			}

			int min = 0;

			for(int i=0; i<N; i++) min = Math.min(min, f[i]);

			System.out.println(min*-1);
		}
	}

	public static int rootIdx(int[] f, int x) {

		if(f[x]<0)return x;

		return f[x] = rootIdx(f, f[x]);

	}
}


import java.util.Scanner;

public class Main {

	static int MOD = 1000000007;

	public static void main(String[] args) {
		exec_1_1();

	}

	public static void exec_1_1() {

		try(var sc = new Scanner(System.in)) {

			int N = sc.nextInt();

			int M = sc.nextInt();

			int[] f = new int[N];

			for(int i=0; i<N; i++) f[i]=-1;

			for(int i=0; i<M; i++) {
				int a = sc.nextInt()-1;
				int b = sc.nextInt()-1;

				if(f[a]>-1&&f[b]<0) {
					int swap = a;
					b = a;
					a = swap;
				}

				int rIdx = rootIdx(f, a);
				int rdxB = rootIdx(f, b);
				if(rIdx==rdxB) continue ;

				f[rIdx] += f[rdxB];
				f[rdxB] = rIdx;
			}

			int min = 0;

			for(int i=0; i<N; i++) min = Math.min(min, f[i]);

			System.out.println(min*-1);
		}
	}

	public static int rootIdx(int[] f, int x) {

		if(f[x]<0)return x;

		return rootIdx(f, f[x]);

	}
}


import java.util.Scanner;

public class Main {

	static int MOD = 1000000007;

	public static void main(String[] args) {
		exec_1_1();

	}

	public static void exec_1_1() {

		try(var sc = new Scanner(System.in)) {

			int N = sc.nextInt();

			int M = sc.nextInt();

			int[] f = new int[N];

			for(int i=0; i<N; i++) f[i]=-1;

			for(int i=0; i<M; i++) {
				int a = sc.nextInt()-1;
				int b = sc.nextInt()-1;

				if(f[a]>-1&&f[b]<0) {
					int swap = a;
					b = a;
					a = swap;
				}

				int rIdx = rootIdx(f, a);
				if(rIdx==rootIdx(f, b)) continue ;

				f[rIdx] += rootVal(f, b);
				f[b] = rIdx;
			}

			int min = 0;

			for(int i=0; i<N; i++) min = Math.min(min, f[i]);

			System.out.println(min*-1);
		}
	}

	public static int rootVal(int[] f, int x) {

		if(f[x]<0)return f[x];

		return rootVal(f, f[x]);

	}

	public static int rootIdx(int[] f, int x) {

		if(f[x]<0)return x;

		return rootIdx(f, f[x]);

	}
}


#pragma GCC optimize ("O2")
#pragma GCC target ("avx2")
//#include<bits/stdc++.h>
#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;
typedef long long ll;
#define rep(i, n) for(int i = 0; i < (n); i++)
#define rep1(i, n) for(int i = 1; i <= (n); i++)
#define co(x) cout << (x) << "\n"
#define cosp(x) cout << (x) << " "
#define ce(x) cerr << (x) << "\n"
#define cesp(x) cerr << (x) << " "
#define pb push_back
#define mp make_pair
#define chmin(x, y) x = min(x, y)
#define chmax(x, y) x = max(x, y)
#define Would
#define you
#define please

const int CM = 1 << 17, CL = 12;
char cn[CM + CL], * ci = cn + CM + CL, * owa = cn + CM, ct;
const ll ma0 = 1157442765409226768;
const ll ma1 = 1085102592571150095;
const ll ma2 = 71777214294589695;
const ll ma3 = 281470681808895;
const ll ma4 = 4294967295;
inline int getint() {
	if (ci - owa > 0) {
		memcpy(cn, owa, CL);
		ci -= CM;
		fread(cn + CL, 1, CM, stdin);
	}
	ll tmp = *(ll*)ci;
	int dig = 68 - __builtin_ctzll((tmp & ma0) ^ ma0);
	tmp = tmp << dig & ma1;
	tmp = tmp * 10 + (tmp >> 8) & ma2;
	tmp = tmp * 100 + (tmp >> 16) & ma3;
	tmp = tmp * 10000 + (tmp >> 32) & ma4;
	ci += 72 - dig >> 3;
	return tmp;
}
const int SZ = 200000;
class UF {
public:
	int P[SZ + 1], saidai;
	UF() : P(), saidai() {
		saidai = -1;
		rep1(i, SZ) P[i] = -1;
	}
	int find(int A) {
		if (P[A] < 0) return A;
		return P[A] = find(P[A]);
	}
	bool unite(int A, int B) {
		int a = find(A);
		int b = find(B);
		if (a == b) return false;
		if (P[a] > P[b]) swap(a, b);
		P[a] += P[b];
		if (P[a] < saidai) saidai = P[a];
		P[b] = a;
		return true;
	}
} uf;

int main() {
	//cin.tie(0);
	//ios::sync_with_stdio(false);

	int N = getint(), M = getint();

	rep(i, M) {
		uf.unite(getint(), getint());
	}

	printf("%d\n", -uf.saidai);

	Would you please return 0;
}


#include<bits/stdc++.h>
#define rep(i,n) for (ll i=0; i<(n); i++)
using namespace std;
using ll = long long;
using ld = long double;
using vl = vector<ll>;

struct UnionFind {
  vector<int> par; // par[i]:iの親の番号　(例) par[3] = 2 : 3の親が2

    UnionFind(int N) : par(N) { //最初は全てが根であるとして初期化
      rep(I,N) par[i] = i;
    }

    ll root(int x) { // データxが属する木の根を再帰で得る：root(x) = {xの木の根}
      if (par[x] == x) return x;
      return par[x] = root(par[x]);
    }

    void unite(int x, int y) { // xとyの木を併合
      ll rx = root(x); //xの根をrx
      ll ry = root(y); //yの根をry
      if (rx == ry) return; //xとyの根が同じ(=同じ木にある)時はそのまま
      par[rx] = ry; //xとyの根が同じでない(=同じ木にない)時：xの根rxをyの根ryにつける
   }

   bool same (ll x, ll y) { // 2つのデータx, yが属する木が同じならtrueを返す
     ll rx = root(x);
     ll ry = root(y);
     return rx == ry;
   }

   ll size (ll x) {
     return -r[root(x)];
   }
};

int main() {
  ll n, m;
  cin >> n >> m;

  UnionFind uf(n);

  rep(i,m) {
    ll a, b;
    cin >> a >> b;
    a--; b--;
    uf.unite(a,b);
  }
  ll ans = 0;
  rep(i,n) ans = max(uf.size(i), ans);
  cout << ans << endl;
}


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
#define P pair<ll,ll>

class UnionFind {
    vector<int> data;
    ll num;
public:
    UnionFind(int size) : data(size, -1), num(size) { }
    bool unite(int x, int y) {//xとyの集合を統合する
        x = root(x); y = root(y);
        if (x != y) {
            if (data[y] < data[x]) swap(x, y);
            data[x] += data[y]; data[y] = x;
        }
        num -= (x != y);
        return x != y;
    }
    bool findSet(int x, int y) {//xとyが同じ集合か返す
        return root(x) == root(y);
    }
    int root(int x) {//xのルートを返す
        return data[x] < 0 ? x : data[x] = root(data[x]);
    }
    ll size(int x) {//xの集合のサイズを返す
        return -data[root(x)];
    }
    ll numSet() {//集合の数を返す
        return num;
    }
};
 int main(){
ll n,m;
   cin>>n>>m;
   UnionFind uf(n);
   rep(i,m){
     ll a,b;
     cin>>a>>b;
     a--;b--;
   uf.unite(a,b));
   }
   ll ans=0;
   rep(i,n){
     ans=max(ans,uf.size(i));
   }
   cout<<ans<<endl;
}


#include <bits/stdc++.h>
using namespace std;
using ll = long long;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
#define P pair<ll,ll>

class UnionFind {
    vector<int> data;
    ll num;
public:
    UnionFind(int size) : data(size, -1), num(size) { }
    bool unite(int x, int y) {//xとyの集合を統合する
        x = root(x); y = root(y);
        if (x != y) {
            if (data[y] < data[x]) swap(x, y);
            data[x] += data[y]; data[y] = x;
        }
        num -= (x != y);
        return x != y;
    }
    bool findSet(int x, int y) {//xとyが同じ集合か返す
        return root(x) == root(y);
    }
    int root(int x) {//xのルートを返す
        return data[x] < 0 ? x : data[x] = root(data[x]);
    }
    ll size(int x) {//xの集合のサイズを返す
        return -data[root(x)];
    }
    ll numSet() {//集合の数を返す
        return num;
    }
};
 int main(){
ll n,m;
   cin>>n>>m;
   UnionFind uf(n);
   rep(i,m){
     ll a,b;
     cin>>a>>b;
     a--;b--;
   uf(.unite(a,b));
   }
   ll ans=0;
   rep(i,n){
     ans=max(ans,uf.size(i));
   }
   cout<<ans<<endl;
}


#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
inline int max(const int a, const int b){return a>b?a:b;}
//fast(?) Input
#define buf_sz 1<<24
char buf[buf_sz];
unsigned int nextint(){
	static int id = 0;
	if(id == 0)read(0, buf, buf_sz);
	unsigned int res = 0;
	while(id<buf_sz && buf[id]<'0' || '9'<buf[id])id++;
	while(id<buf_sz && '0'<=buf[id] && buf[id]<='9'){
		res *= 10;
		res += buf[id++] - '0';
	}if(id>=buf_sz)exit(1);
	return res;
}
#undef buf_sz

int d[] = {[0 ... 1<<18] -1};
int find(int x){
	if(d[x] < 0) return x;
	return d[x] = find(d[x]);
}
bool unite(int x, int y){
	x = find(x); y = find(y);
	if(x == y)return false;
	if(d[x] > d[y]){
		int tmp = x;
		x = y;
		y = tmp;
	}
	d[x] += d[y];
	d[y] = x;
	return true;
}
bool same(int x, int y){return find(x) == find(y);}
int size(int x){return -d[find(x)];}

int ans;
int main(){
	int N = nextint(), M = nextint();
	while(M--)unite(nextint(), nextint());
	for(int i=1; i<=N; i++)ans = max(ans, size(i));
	printf("%d\n", ans);
	return 0;
}


#include <ctype.h>
#include <float.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <math.h>
#ifdef __cplusplus
#include <bits/stdc++.h>
#endif
#pragma GCC target("avx2")
#pragma GCC optimize("O3")
//#pragma GCC optimize("unroll-loops")

//char rbuf[]; int rbufl;
//#define getchar() rbuf[rbufl++]
#define getchar getchar_unlocked
#define putchar putchar_unlocked
int32_t nextint(void){ char c=getchar(); while(c!='-'&&(c<'0'||'9'<c)) c=getchar(); bool s=false; if(c=='-'){s=true;c=getchar();} uint32_t x=0; while('0'<=c && c<='9'){ x=x*10+c-'0'; c=getchar(); } return s?-x:x; }
int64_t nextlong(void){ char c=getchar(); while(c!='-'&&(c<'0'||'9'<c)) c=getchar(); int s=0; if(c=='-'){s=1;c=getchar();} uint64_t x=0; while('0'<=c && c<='9'){ x=x*10+c-'0'; c=getchar(); } return s?-x:x; }
uint32_t nextstr(char *s){ char c=getchar(); while(c==' '||c=='\n') c=getchar(); uint32_t len=0; while(c!=' '&&c!='\n'){ *s++=c; len++; c=getchar(); } *s='\0'; return len; }
void printint(int32_t x){ if(x<0){x=-x; putchar('-');}else if(x==0){putchar('0');} int d=0; char s[10]; while(x){ s[d]='0'+x%10; x/=10; d++; } for (int i=d-1; i>=0; i--) putchar(s[i]); putchar('\n'); }
void rsorta_d32(void *base, const int count, const int size, const int index){
	typedef int32_t rsort_t;
	rsort_t (*a)[size] = (rsort_t(*)[size])base;
	rsort_t (*b)[size] = (rsort_t(*)[size])malloc(count*size*sizeof(rsort_t));
	int c[0x100];
	for(int k=0; k<4; k++){
		memset(c, 0, sizeof(c)); for(int i=0; i<count; i++){ c[(a[i][index]>>(k<<3))&0xFF]++; } for(int i=1; i<0x100; i++) c[i]+=c[i-1]; for(int i=count-1; i>=0; i--){ int to=--c[(a[i][index]>>(k<<3))&0xFF]; for(int j=0; j<size; j++){ b[to][j]=a[i][j]; } }{rsort_t(*t)[size]=a; a=b; b=t;}
	}
	free(b);
}

typedef struct union_find{
	int *prev, *rank, *size_a;
	int (*root)(struct union_find *, int);
	void (*join)(struct union_find *, int, int);
	int (*size)(struct union_find *, int);
} union_find;

void union_find_init(struct union_find *p, int n){
	int union_find_root(struct union_find *p, int x){ return p->prev[x]==x?x:(p->prev[x]=union_find_root(p, p->prev[x])); }
	void union_find_join(struct union_find *p, int x, int y){
		x=union_find_root(p, x), y=union_find_root(p, y); if(x==y) return;
		if(p->rank[x]<p->rank[y]){
			p->prev[x]=y; p->size_a[y]+=p->size_a[x];
		}else if(p->rank[x]>p->rank[y]){
			p->prev[y]=x; p->size_a[x]+=p->size_a[y];
		}else{
			p->prev[x]=y; p->rank[y]++; p->size_a[y]+=p->size_a[x];
		}
	}
	int union_find_size(struct union_find *p, int x){
		return p->size_a[union_find_root(p, x)];
	}
	p->prev = malloc(n*sizeof(int));
	p->rank = malloc(n*sizeof(int));
	p->size_a = malloc(n*sizeof(int));
	p->root = union_find_root;
	p->join = union_find_join;
	p->size = union_find_size;
	for(int i=0; i<n; i++){ p->prev[i]=i; p->rank[i]=1; p->size_a[i]=1; }
}

union_find f;

int main(void){
	int n=nextint();
	int m=nextint();
	union_find_init(&f, n+1);
	for(int i=0; i<m; i++){
		int a=nextint();
		int b=nextint();
		f.join(&f, a, b);
	}
	int ans=0;
	for(int i=1; i<=n; i++){
		int now = f.size(&f, i);
		if(now>ans) ans=now;
	}
	printf("%d\n", ans);
}


#include <ctype.h>
#include <float.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <math.h>
#ifdef __cplusplus
#include <bits/stdc++.h>
#endif
#pragma GCC target("avx2")
#pragma GCC optimize("O3")
//#pragma GCC optimize("unroll-loops")

//char rbuf[]; int rbufl;
//#define getchar() rbuf[rbufl++]
#define getchar getchar_unlocked
#define putchar putchar_unlocked
int32_t nextint(void){ char c=getchar(); while(c!='-'&&(c<'0'||'9'<c)) c=getchar(); bool s=false; if(c=='-'){s=true;c=getchar();} uint32_t x=0; while('0'<=c && c<='9'){ x=x*10+c-'0'; c=getchar(); } return s?-x:x; }
int64_t nextlong(void){ char c=getchar(); while(c!='-'&&(c<'0'||'9'<c)) c=getchar(); int s=0; if(c=='-'){s=1;c=getchar();} uint64_t x=0; while('0'<=c && c<='9'){ x=x*10+c-'0'; c=getchar(); } return s?-x:x; }
uint32_t nextstr(char *s){ char c=getchar(); while(c==' '||c=='\n') c=getchar(); uint32_t len=0; while(c!=' '&&c!='\n'){ *s++=c; len++; c=getchar(); } *s='\0'; return len; }
void printint(int32_t x){ if(x<0){x=-x; putchar('-');}else if(x==0){putchar('0');} int d=0; char s[10]; while(x){ s[d]='0'+x%10; x/=10; d++; } for (int i=d-1; i>=0; i--) putchar(s[i]); putchar('\n'); }
void rsorta_d32(void *base, const int count, const int size, const int index){
	typedef int32_t rsort_t;
	rsort_t (*a)[size] = (rsort_t(*)[size])base;
	rsort_t (*b)[size] = (rsort_t(*)[size])malloc(count*size*sizeof(rsort_t));
	int c[0x100];
	for(int k=0; k<4; k++){
		memset(c, 0, sizeof(c)); for(int i=0; i<count; i++){ c[(a[i][index]>>(k<<3))&0xFF]++; } for(int i=1; i<0x100; i++) c[i]+=c[i-1]; for(int i=count-1; i>=0; i--){ int to=--c[(a[i][index]>>(k<<3))&0xFF]; for(int j=0; j<size; j++){ b[to][j]=a[i][j]; } }{rsort_t(*t)[size]=a; a=b; b=t;}
	}
	free(b);
}

int prev[200001];
int rank[200001];
int size[200001];

void init(int n){
	for(int i=1; i<=n; i++){ prev[i]=i; rank[i]=1; size[i]=1; }
}
int root(int x){
	return prev[x]==x?x:(prev[x]=root(prev[x]));
}
void join(int x, int y){
	x=root(x), y=root(y);
	if(x==y) return;
	if(rank[x]<rank[y]){
		prev[x]=y;
		size[y]+=size[x];
	}else if(rank[x]>rank[y]){
		prev[y]=x;
		size[x]+=size[y];
	}else{
		prev[x]=y;
		rank[y]++;
		size[y]+=size[x];
	}
}

int main(void){
	int n=nextint();
	int m=nextint();
	init(n);
	for(int i=0; i<m; i++){
		int a=nextint();
		int b=nextint();
		join(a, b);
	}
	int ans=0;
	for(int i=1; i<=n; i++){
		int now = size[root(i)];
		if(now>ans) ans=now;
	}
	printf("%d\n", ans);
}


#include<stdio.h>
#include<stdlib.h>
int *a, *MAXGROUP;
int findRoot(int x){
  if (a[x]<0) return x;
  else return findRoot(a[x]);
}
void unite( int c, int b, int *p ){
  int q=findRoot(c);
  int w=findRoot(b);
  if (w!=q){
    a[q]+=a[w];
    a[w]=q;
    if (*p>a[q]) *p=a[q];
  }
}
int main(){
  int m,n,k1,k2,max=-1;
  MAXGROUP=&max;
  scanf("%d%d",&n,&m);
  int i;
  a=(int*)malloc(n*sizeof(int));
  for (i=0;i<n;i++) a[i]=-1;
  for (i=0;i<m;i++){
    scanf("%d%d",&k1,&k2);
    k1--;k2--;
    unite(k1,k2,MAXGROUP);
  }
  free(a);
  printf("%d\n",-max);
}


#include<stdio.h>
#include<stdlib.h>
int *a, *MAXGROUP;
int findRoot(int x){
  if (a[x]<0) return x;
  else return findRoot(a[x]);
}
void unite( int c, int b, int *p ){
  int q=findRoot(c);
  int w=findRoot(b);
  if (w!=q){
    a[q]+=a[w];
    a[w]=q;
    if (*p>a[q]) *p=a[q];
  }
}
int main(){
  int m,n,k1,k2,max=-1;
  MAXGROUP=&max;
  scanf("%d%d",&n,&m);
  int i;
  a=(int*)malloc(n*sizeof(int));
  for (i=0;i<n;i++) a[i]=-1;
  for (i=0;i<m;i++){
    scanf("%d%d",&k1,&k2);
    k1--;k2--;
    unite(k1,k2,MAXGROUP);
  }
  free(a);
  printf("%d\n",max);
}


#include<stdio.h>
#include<stdlib.h>
int *a, *MAXGROUP;
int findRoot(int x){
  if (a[x]<0) return x;
  else return findRoot(a[x]);
}
void unite( int c, int b, int *p ){
  int q=findRoot(c);
  int w=findRoot(b);
  if (w!=q){
    a[q]+=a[w];
    a[w]=q;
    if (*p>a[q]) *p=a[q];
  }
}
int main(){
  int m,n,k1,k2,max=-1;
  MAXGROUP=&max;
  scanf(%d%d,&n,&m);
  int i;
  a=(int*)malloc(n*sizeof(int));
  for (i=0;i<n;i++) a[i]=-1;
  for (i=0;i<m;i++){
    scanf("%d%d",&k1,&k2);
    k1--;k2--;
    unite(k1,k2,MAXGROUP);
  }
  free(a);
  printf("%d\n",max);
}


let input = require('fs').readFileSync('/dev/stdin', 'utf8')
// let input = require('fs').readFileSync('./sample_d.txt', 'utf8')
.split(/\n| /)
.slice(0 ,-1)
.map(item => parseInt(item))

const [N, M, ..._pair] = input

let r = new Array(N).fill(-1)

const root = x => {
    if (r[x] < 0) return x
    return r[x] = root(r[x])
}

const unite = (x, y) => {
    x = root(x)
    y = root(y)
    if (x == y) return false
    if (r[x] > r[y]) [x, y] = [y, x]
    r[x] += r[y]
    r[y] = x
    return true
}

const size = x => {
    return -r[root(x)]
}

for (let i=0; i<_pair.length; i+=2){
    unite(_pair[i]-1, _pair[i+1]-1)
}

//最大の友達集合を求める
let ans = 0
for ( let i=0; i<N; i++){
    ans = Math.max(ans, size(i));
}

console.log(ans)

// const [N, M, ..._pair] = input
// const pair = []
// for(let i=0; i<N; i++){
//     pair.push(new Array(N-i).fill(true))
// }

// for(let i=0; i<_pair.length; i+=2){
//     const a = [_pair[i], _pair[i+1]].sort()
//     pair[a[0]-1][a[1]-a[0]] = false
// }

// const member = [ new Set() ]

// pair[0].forEach( (item, index) => {
//     if(item) member[0].add(index+1)
//     else{
//         member.push(new Set())
//         member[member.length-1].add(index+1)
//     }
// } )

// for( let i=1; i<N; i++){
//     for( let j=1; j<pair[i].length; j++){
//         if(!pair[i][j]){
//             member.forEach( (sample, index) => {
//                 if( sample.has(i+1) && sample.has(i+j+1) ){
//                     sample.delete(i+j)
//                     if(member.length - 1 <= index) member.push(new Set())
//                     member[index+1].add(i+j)
//                 }
//             })
//         }
//     }
// }

// console.log(member.length)


class UnionFind {
    constructor(N){
        this.par = new Array(N).fill(-1)
    }

    root(x) {
        if (this.par[x] < 0) return x;
        return this.par[x] = this.root(this.par[x]);
    }

    unite(x, y) {
        let rx = this.root(x);
        let ry = this.root(y);
        if (rx == ry) return;
        if(this.par[rx] > this.par[ry]) [rx, ry] = [ry, rx]
        this.par[rx] += this.par[ry]
        this.par[ry] = rx
    }

    isSame(x, y) {
        let rx = this.root(x);
        let ry = this.root(y);
        return rx == ry;
    }

    size(x) {
        return -this.par[this.root(x)];
    }
};

function main(arg) {

    arg = arg.split("\n")
    let [N,M] = arg[0].split(" ").map(Number)
    let A = arg.slice(1).map(x=>Number(x.split(" ")[0]))
    let B = arg.slice(1).map(x=>Number(x.split(" ")[1]))

    let tree = new UnionFind(N)
    for(let i=0; i<M; i++) {
        tree.unite(A[i]-1,B[i]-1)
    }

    let ans=0
    for(let i=0; i<N; i++) {
        ans = Math.max(ans, tree.size(i))
    }
    console.log(ans)
}

main(require('fs').readFileSync('/dev/stdin', 'utf8'))


class UnionFind {
  constructor(n) {
    this.p = new Array(n).fill(null).map((_, i) => i)
    this.height = new Array(n).fill(0)
    this.size = new Array(n).fill(1)
  }

  unite(a, b) {
    this.link(this.findSet(a), this.findSet(b))
  }

  link(a, b) {
    if (a === b) return null

    if (this.height[a] > this.height[b]) {
      this.p[b] = a
      this.size[a] += this.size[b]
    } else {
      this.p[a] = b
      this.size[b] += this.size[a]

      if (this.height[a] === this.height[b]) {
        this.height[b]++
      }
    }
  }

  findSet(x) {
    if (this.p[x] === x) return x
    return this.findSet(this.p[x])
  }
}

const main = (input) => {
  input = input.trim().split('\n')
  const [n,] = input.shift().split(' ').map(Number)
  input = Array.from(new Set(input))

  const uf = new UnionFind(n+1)

  for (let i=0; i<input.length; i++) uf.unite(...input[i].split(' ').map(Number))

  return console.log(uf.size.reduce((a, b) => Math.max(a, (b || 0)), 0))
}

process.env.MYTEST
  ? (process.env.MYTEST === 'test' ? test() : main(require('fs').readFileSync('dev/stdin', 'utf8')))
  : main(require('fs').readFileSync('/dev/stdin', 'utf8'))


(async () => {
    let input = '';
    for await (const chunk of process.stdin) input += chunk;
    let lines = input.split(/\n/);
    const [n, m] = lines.shift().split(/\s+/);

    lines = lines.slice(0, m);

    let friends = [];
    lines.forEach(line => {
        const [a, b] = line.split(/\s+/).map(v => v - 1);
        const groups = friends.filter(f => f.has(a) || f.has(b));
        if(groups.length == 0){
            groups[0] = new Set();
            friends.push(groups[0]);
        }
        groups[0].add(a);
        groups[0].add(b);
        if(groups.length > 1){
            console.log(groups);
            let group = groups[0]
            for (let i = 1; i < groups.length; i++) {
                friends.splice(friends.indexOf(groups[i], 1));
                groups[i].forEach(f => {
                    group.add(f);
                });
            }
        }
    });

    console.log(friends.reduce((count, group) => Math.max(count, group.size), 0));

})();


(async () => {
    let input = '';
    for await (const chunk of process.stdin) input += chunk;
    let lines = input.split(/\n/);
    const [n, m] = lines.shift().split(/\s+/);

    lines = lines.slice(0, m);

    let friends = [];
    lines.forEach(line => {
        const [a, b] = line.split(/\s+/).map(v => v - 1);
        const groups = friends.filter(f => f.includes(a) || f.includes(b));
        if(groups.length == 0){
            friends.push([a, b]);
        }else{
            if(!groups[0].includes(a)){
                groups[0].push(a);
            }
            if(!groups[0].includes(b)){
                groups[0].push(b);
            }
        }
        if(groups.length > 1){
            console.log(groups);
            let index = friends.indexOf(groups[0]);
            let group = groups[0]
            for (let i = 1; i < groups.length; i++) {
                friends.splice(friends.indexOf(groups[i], 1));
                group = group.join(groups[i]);
            }
            friends[index] = group;
        }
    });

    let groupCount = 0;
    friends.forEach(group => {
        const friend = new Set();
        for(const e of group){
            friend.add(e);
        }
        groupCount = Math.max(groupCount, friend.size);
    })

    console.log(groupCount);

})();


(async () => {
    let input = '';
    for await (const chunk of process.stdin) input += chunk;
    let lines = input.split(/\n/);
    const [n, m] = lines.shift().split(/\s+/);

    lines = lines.slice(0, m);

    let friends = [];
    lines.forEach(line => {
        const [a, b] = line.split(/\s+/).map(v => v - 1);
        const groups = friends.filter(f => f.includes(a) || f.includes(b));
        if(groups.length == 0){
            friends.push([a, b]);
        }else{
            if(!groups[0].includes(a)){
                groups[0].push(a);
            }
            if(!groups[0].includes(b)){
                groups[0].push(b);
            }
        }
        if(groups.length > 1){
            console.log(groups);
            let index = friends.indexOf(groups[0]);
            let group = groups[0]
            for (let i = 1; i < groups.length; i++) {
                friends.splice(friends.indexOf(groups[i], 1));
                group = group.join(groups[i]);
            }
            friends[index] = group;
        }
    });

    console.log(friends.reduce((count, friend) => Math.max(count, friend.length), 0));

})();


package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
)

func main() {
	n, m := readI(), readI()
	uf := newUF(n)
	for i := 0; i < m; i++ {
		uf.unite(readI()-1, readI()-1)
	}
	fmt.Println(-min(uf.d))
}

func min(a []int) int {
	l := len(a)
	m := a[0]
	for i := 1; i < l; i++ {
		if a[i] < m {
			m = a[i]
		}
	}
	return m
}

//Union-Find
type unionFind struct {
	d []int
}

func newUF(n int) *unionFind {
	d := make([]int, n)
	for i := range d {
		d[i] = -1
	}
	u := unionFind{d}
	return &u
}

func (u *unionFind) root(a int) int {
	if u.d[a] < 0 {
		return a
	}
	u.d[a] = u.root(u.d[a])
	return u.d[a]
}

func (u *unionFind) unite(a, b int) {
	ar, br := u.root(a), u.root(b)
	if ar == br {
		return
	}
	if -u.d[ar] < -u.d[br] {
		ar, br = br, ar
	}
	u.d[ar] += u.d[br]
	u.d[br] = ar
}

func (u *unionFind) isSame(a, b int) bool {
	return u.root(a) == u.root(b)
}

func (u *unionFind) size(a int) int {
	return -u.d[u.root(a)]
}

/*-----------Input utils-----------*/

var sc = bufio.NewScanner(os.Stdin)

func init() {
	if len(os.Args) >= 2 {
		if os.Args[1] == "debug" {
			debug()
		}
	}
	const buf = 200100
	sc.Split(bufio.ScanWords)
	sc.Buffer(make([]byte, buf), buf)
}

func debug() {
	testFile, err := os.Open("./test/sample-1.in")
	if err != nil {
		fmt.Fprintln(os.Stderr, "There is no testfile.")
		os.Exit(1)
	}
	sc = bufio.NewScanner(testFile)
}

func readS() string {
	sc.Scan()
	return sc.Text()
}

func readR() []rune {
	return []rune(readS())
}

func readI() int {
	i, _ := strconv.Atoi(readS())
	return i
}

func readF() float64 {
	f, _ := strconv.ParseFloat(readS(), 64)
	return f
}


package main

import (
	"bufio"
	"container/heap"
	"fmt"
	"os"
	"sort"
	"strconv"
)

const (
	initialBufSize = 1e4
	maxBufSize     = 1e6 + 7
)

var buf []byte = make([]byte, maxBufSize)
var sc = bufio.NewScanner(os.Stdin)

func init() {
	sc.Split(bufio.ScanWords)
	sc.Buffer(buf, maxBufSize)
}

var d = []Position{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
var d8 = []Position{{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}

func main() {
	N, M := nextInt(), nextInt()
	uf := newUnionFind(N)
	for i := 0; i < M; i++ {
		A, B := nextInt()-1, nextInt()-1
		uf.unite(A, B)
	}
	var ans int
	for i := 0; i < N; i++ {
		ans = MaxOf(ans, uf.size(i))
	}
	fmt.Println(ans)
}

// Position 迷路問題での現在地を表す構造体
type Position struct {
	H int
	W int
}

func nextLine() string {
	sc.Scan()
	return sc.Text()
}

func nextInt() int {
	sc.Scan()
	i, e := strconv.Atoi(sc.Text())
	if e != nil {
		panic(e)
	}
	return i
}

func abs(a int) int {
	if a > 0 {
		return a
	} else {
		return -a
	}
}

func pow(p, q int) int {
	res := p
	if q == 0 {
		return 1
	}
	for i := 0; i < q-1; i++ {
		res *= p
	}
	return res
}

func ceil(a, b int) int {
	return (a + (b - 1)) / b
}

func MinOf(vars ...int) int {
	min := vars[0]
	for _, i := range vars {
		if min > i {
			min = i
		}
	}
	return min
}

func MaxOf(vars ...int) int {
	max := vars[0]
	for _, i := range vars {
		if max < i {
			max = i
		}
	}
	return max
}

func gcdof2numbers(a int, b int) int {
	if b == 0 {
		return a
	}
	return gcdof2numbers(b, a%b)
}

func lcmof2numbers(a int, b int) int {
	return a / gcdof2numbers(a, b) * b
}

// マイナスの場合は考慮していない
func fuctorial(a int) int {
	if a == 1 || a == 0 {
		return 1
	} else {
		return fuctorial(a-1) * a
	}
}

func NextPermutation(x sort.Interface) bool {
	n := x.Len() - 1
	if n < 1 {
		return false
	}
	j := n - 1
	for ; !x.Less(j, j+1); j-- {
		if j == 0 {
			return false
		}
	}
	l := n
	for !x.Less(j, l) {
		l--
	}
	x.Swap(j, l)
	for k, l := j+1, n; k < l; {
		x.Swap(k, l)
		k++
		l--
	}
	return true
}

// 素因数分解したmapを返す
func primeFuctorize(n int) map[int]int {
	pf := map[int]int{}
	for i := 2; i*i <= n; i++ {
		for n%i == 0 {
			pf[i]++
			n /= i
		}
	}
	if n != 1 {
		pf[n]++
	}
	return pf
}

// golangの公式サンプルより
// https://xn--go-hh0g6u.com/pkg/container/heap/#example__intHeap
// IntHeap は，整数の最小ヒープです。
type IH []int

func (h IH) Len() int           { return len(h) }
func (h IH) Less(i, j int) bool { return h[i] < h[j] }
func (h IH) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IH) Push(x interface{}) {
	// Push と Pop はポインタレシーバを使っています。
	// なぜなら，スライスの中身だけでなく，スライスの長さも変更するからです。
	*h = append(*h, x.(int))
}

func (h *IH) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

// golangの公式サンプルより
// https://xn--go-hh0g6u.com/pkg/container/heap/#example__priorityQueue

// Item は，優先キューで管理する項目です。
type Item struct {
	value    string // 値。任意です。
	priority int    // キューにおける優先度
	// index は heap.Interface メソッドで更新されます。
	index int // ヒープにおけるインデックス
}

// PriorityQueue は heap.Interface を実装し，Item のリストを保持します。
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
	// Pop が最小ではなく最大の優先度を持つ項目を返して欲しいので，ここでは > を使っています。
	return pq[i].priority > pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
	pq[i], pq[j] = pq[j], pq[i]
	pq[i].index = i
	pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
	n := len(*pq)
	item := x.(*Item)
	item.index = n
	*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
	old := *pq
	n := len(old)
	item := old[n-1]
	old[n-1] = nil  // メモリリークを避ける
	item.index = -1 // 安全のため
	*pq = old[0 : n-1]
	return item
}

// update はキューの Item の優先度と値を更新します。
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
	item.value = value
	item.priority = priority
	heap.Fix(pq, item.index)
}

// UnionFind の定義
// 下記を参考
// https://youtu.be/TdR816rqc3s?t=6822
// https://github.com/atcoder/live_library/blob/master/uf.cpp
// https://qiita.com/haru1843/items/2295d0ec1f5002bd5c33#%E5%AE%9F%E8%A3%85
type UnionFind struct {
	parent []int
}

// UnionFind のスライス初期化
func newUnionFind(N int) *UnionFind {
	u := new(UnionFind)
	u.parent = make([]int, N)
	for i := range u.parent {
		u.parent[i] = -1
	}
	return u
}

// xの根を見つける
func (u UnionFind) find(x int) int {
	if u.parent[x] < 0 {
		return x
	}
	u.parent[x] = u.find(u.parent[x])
	return u.parent[x]
}

// xとyのグループを結合する
func (u UnionFind) unite(x, y int) {
	xf := u.find(x)
	yf := u.find(y)
	if xf == yf {
		return
	}
	// 常に大きい方に小さい木をくっつける
	if xf > yf {
		xf, yf = yf, xf
	}
	u.parent[xf] += u.parent[yf]
	u.parent[yf] = xf
}

// xとyが同じグループに所属するかどうかを返す
func (u UnionFind) same(x, y int) bool {
	if u.find(x) == u.find(y) {
		return true
	}
	return false
}

// xの所属するグループの木の大きさを返す

func (u UnionFind) size(x int) int {
	return -u.parent[u.find(x)]
}


package main

import (
	"bufio"
	"container/heap"
	"fmt"
	"os"
	"sort"
	"strconv"
)

const (
	initialBufSize = 1e4
	maxBufSize     = 1e6 + 7
)

var buf []byte = make([]byte, maxBufSize)
var sc = bufio.NewScanner(os.Stdin)

func init() {
	sc.Split(bufio.ScanWords)
	sc.Buffer(buf, maxBufSize)
}

var d = []Position{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
var d8 = []Position{{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}}

func main() {
	N, M := nextInt(), nextInt()
	uf := newUnionFind(N)
	for i := 0; i < M; i++ {
		A, B := nextInt()-1, nextInt()-1
		uf.unite(A, B)
	}
	fmt.Println(uf.maxlen)
}

// Position 迷路問題での現在地を表す構造体
type Position struct {
	H int
	W int
}

func nextLine() string {
	sc.Scan()
	return sc.Text()
}

func nextInt() int {
	sc.Scan()
	i, e := strconv.Atoi(sc.Text())
	if e != nil {
		panic(e)
	}
	return i
}

func abs(a int) int {
	if a > 0 {
		return a
	} else {
		return -a
	}
}

func pow(p, q int) int {
	res := p
	if q == 0 {
		return 1
	}
	for i := 0; i < q-1; i++ {
		res *= p
	}
	return res
}

func ceil(a, b int) int {
	return (a + (b - 1)) / b
}

func MinOf(vars ...int) int {
	min := vars[0]
	for _, i := range vars {
		if min > i {
			min = i
		}
	}
	return min
}

func MaxOf(vars ...int) int {
	max := vars[0]
	for _, i := range vars {
		if max < i {
			max = i
		}
	}
	return max
}

func gcdof2numbers(a int, b int) int {
	if b == 0 {
		return a
	}
	return gcdof2numbers(b, a%b)
}

func lcmof2numbers(a int, b int) int {
	return a / gcdof2numbers(a, b) * b
}

// マイナスの場合は考慮していない
func fuctorial(a int) int {
	if a == 1 || a == 0 {
		return 1
	} else {
		return fuctorial(a-1) * a
	}
}

func NextPermutation(x sort.Interface) bool {
	n := x.Len() - 1
	if n < 1 {
		return false
	}
	j := n - 1
	for ; !x.Less(j, j+1); j-- {
		if j == 0 {
			return false
		}
	}
	l := n
	for !x.Less(j, l) {
		l--
	}
	x.Swap(j, l)
	for k, l := j+1, n; k < l; {
		x.Swap(k, l)
		k++
		l--
	}
	return true
}

// 素因数分解したmapを返す
func primeFuctorize(n int) map[int]int {
	pf := map[int]int{}
	for i := 2; i*i <= n; i++ {
		for n%i == 0 {
			pf[i]++
			n /= i
		}
	}
	if n != 1 {
		pf[n]++
	}
	return pf
}

// golangの公式サンプルより
// https://xn--go-hh0g6u.com/pkg/container/heap/#example__intHeap
// IntHeap は，整数の最小ヒープです。
type IH []int

func (h IH) Len() int           { return len(h) }
func (h IH) Less(i, j int) bool { return h[i] < h[j] }
func (h IH) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IH) Push(x interface{}) {
	// Push と Pop はポインタレシーバを使っています。
	// なぜなら，スライスの中身だけでなく，スライスの長さも変更するからです。
	*h = append(*h, x.(int))
}

func (h *IH) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

// golangの公式サンプルより
// https://xn--go-hh0g6u.com/pkg/container/heap/#example__priorityQueue

// Item は，優先キューで管理する項目です。
type Item struct {
	value    string // 値。任意です。
	priority int    // キューにおける優先度
	// index は heap.Interface メソッドで更新されます。
	index int // ヒープにおけるインデックス
}

// PriorityQueue は heap.Interface を実装し，Item のリストを保持します。
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
	// Pop が最小ではなく最大の優先度を持つ項目を返して欲しいので，ここでは > を使っています。
	return pq[i].priority > pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
	pq[i], pq[j] = pq[j], pq[i]
	pq[i].index = i
	pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
	n := len(*pq)
	item := x.(*Item)
	item.index = n
	*pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
	old := *pq
	n := len(old)
	item := old[n-1]
	old[n-1] = nil  // メモリリークを避ける
	item.index = -1 // 安全のため
	*pq = old[0 : n-1]
	return item
}

// update はキューの Item の優先度と値を更新します。
func (pq *PriorityQueue) update(item *Item, value string, priority int) {
	item.value = value
	item.priority = priority
	heap.Fix(pq, item.index)
}

// UnionFind の定義
// 下記を参考
// https://youtu.be/TdR816rqc3s?t=6822
// https://github.com/atcoder/live_library/blob/master/uf.cpp
// https://qiita.com/haru1843/items/2295d0ec1f5002bd5c33#%E5%AE%9F%E8%A3%85
type UnionFind struct {
	parent []int
	maxlen int
}

// UnionFind のスライス初期化
func newUnionFind(N int) *UnionFind {
	u := new(UnionFind)
	u.parent = make([]int, N)
	u.maxlen = 1
	for i := range u.parent {
		u.parent[i] = -1
	}
	return u
}

// xの根を見つける
func (u *UnionFind) find(x int) int {
	if u.parent[x] < 0 {
		return x
	}
	u.parent[x] = u.find(u.parent[x])
	return u.parent[x]
}

// xとyのグループを結合する
func (u *UnionFind) unite(x, y int) {
	xf := u.find(x)
	yf := u.find(y)
	if xf == yf {
		return
	}
	// 常に大きい方に小さい木をくっつける
	if xf > yf {
		xf, yf = yf, xf
	}
	u.parent[xf] += u.parent[yf]
	u.parent[yf] = xf
	if u.parent[xf] < 0 {
		u.maxlen = MaxOf(u.maxlen, abs(u.parent[xf]))
	}
}

// xとyが同じグループに所属するかどうかを返す
func (u *UnionFind) same(x, y int) bool {
	if u.find(x) == u.find(y) {
		return true
	}
	return false
}

// xの所属するグループの木の大きさを返す
func (u *UnionFind) size(x int) int {
	return -u.parent[u.find(x)]
}

// 根の数を返す
func (u UnionFind) rootcnt() int {
	var cnt int
	for _, v := range u.parent {
		if v < 0 {
			cnt++
		}
	}
	return cnt
}


package main

import (
	"bufio"
	"fmt"
	"os"
	"sort"
	"strconv"
)

func main() {
	sc := NewScanner()

	n := sc.ReadInt()
	membersOfPerson := make([]*IntList, n+1)
	maxMemberCount := 1

	m := sc.ReadInt()
	for i := 0; i < m; i++ {
		ai := sc.ReadInt()
		bi := sc.ReadInt()

		if membersOfPerson[ai] == nil && membersOfPerson[bi] == nil {
			list := NewIntList()
			list.Add(ai)
			list.Add(bi)

			membersOfPerson[ai] = list
			membersOfPerson[bi] = list
			if 2 > maxMemberCount {
				maxMemberCount = 2
			}
		} else if membersOfPerson[ai] != nil && membersOfPerson[bi] == nil {
			list := membersOfPerson[ai]
			list.Add(bi)
			membersOfPerson[bi] = list

			if list.Len() > maxMemberCount {
				maxMemberCount = list.Len()
			}
		} else if membersOfPerson[ai] == nil && membersOfPerson[bi] != nil {
			list := membersOfPerson[bi]
			list.Add(ai)
			membersOfPerson[ai] = list

			if list.Len() > maxMemberCount {
				maxMemberCount = list.Len()
			}
		} else if membersOfPerson[ai] != membersOfPerson[bi] {
			list := membersOfPerson[ai]
			list.Concat(membersOfPerson[bi])
			updateTargets := membersOfPerson[bi]
			updateTargets.Each(func(person int) {
				membersOfPerson[person] = list
			})

			if list.Len() > maxMemberCount {
				maxMemberCount = list.Len()
			}
		}
	}

	fmt.Println(maxMemberCount)
}

type IntList struct {
	first *intListNode
	last  *intListNode
	len   int
}

type intListNode struct {
	child *intListNode
	value int
}

func NewIntList() *IntList {
	return &IntList{nil, nil, 0}
}

func NewIntListFromArray(a []int) *IntList {
	list := NewIntList()
	for _, elem := range a {
		list.Add(elem)
	}
	return list
}

func (list *IntList) Len() int {
	return list.len
}

func (list *IntList) Add(elem int) {
	node := intListNode{nil, elem}
	if list.first == nil {
		list.first = &node
		list.last = &node
	} else {
		list.last.child = &node
		list.last = &node
	}
	list.len++
}

func (list *IntList) Concat(other *IntList) {
	if list.first == nil {
		*list = *other
	} else if other.first == nil {
		// Do nothing
	} else {
		list.last.child = other.first
		list.last = other.last
		list.len += other.len
	}
}

func (list *IntList) Each(f func(elem int)) {
	cur := list.first
	for cur != nil {
		f(cur.value)
		cur = cur.child
	}
}

func (list *IntList) ToA() []int {
	a := make([]int, list.Len())
	{
		index := 0
		list.Each(func(elem int) {
			a[index] = elem
			index++
		})
	}
	return a
}

// from [my library](https://github.com/ikngtty/go-contestlib)
// -   math/simple

func Abs(x int) int {
	if x >= 0 {
		return x
	}
	return -x
}

func Min(values ...int) int {
	if len(values) == 0 {
		panic("no values")
	}

	min := values[0]
	for _, v := range values {
		if v < min {
			min = v
		}
	}
	return min
}

func Max(values ...int) int {
	if len(values) == 0 {
		panic("no values")
	}

	max := values[0]
	for _, v := range values {
		if v > max {
			max = v
		}
	}
	return max
}

func Pow(base, exponent int) int {
	if exponent < 0 {
		panic(fmt.Sprintf("exponent (%d) should not be a minus", exponent))
	}

	answer := 1
	for i := 0; i < exponent; i++ {
		answer *= base
	}
	return answer
}

func Ceil(divident, dividor int) int {
	if dividor == 0 {
		panic("dividor should not be 0")
	}

	quo := divident / dividor
	rem := divident % dividor

	if rem != 0 {
		if (divident > 0 && dividor > 0) ||
			(divident < 0 && dividor < 0) {
			return quo + 1
		}
	}
	return quo
}

// -   sortutil

func ReverseInts(a []int) {
	sort.Sort(sort.Reverse(sort.IntSlice(a)))
}

func ReverseStrings(a []string) {
	sort.Sort(sort.Reverse(sort.StringSlice(a)))
}

// -   io

type Scanner struct {
	bufScanner *bufio.Scanner
}

func NewScanner() *Scanner {
	bufSc := bufio.NewScanner(os.Stdin)
	bufSc.Split(bufio.ScanWords)
	bufSc.Buffer(nil, 100000000)
	return &Scanner{bufSc}
}

func (sc *Scanner) ReadString() string {
	bufSc := sc.bufScanner
	bufSc.Scan()
	return bufSc.Text()
}

func (sc *Scanner) ReadInt() int {
	bufSc := sc.bufScanner
	bufSc.Scan()
	text := bufSc.Text()

	num, err := strconv.Atoi(text)
	if err != nil {
		panic(err)
	}
	return num
}

func (sc *Scanner) ReadFloat() float64 {
	bufSc := sc.bufScanner
	bufSc.Scan()
	text := bufSc.Text()

	num, err := strconv.ParseFloat(text, 64)
	if err != nil {
		panic(err)
	}
	return num
}


// refs: https://github.com/maitaken/atcoder-template
package main

import (
	"bufio"
	"fmt"
	"math"
	"os"
	"strconv"
	"strings"
)

const (
	BUFSIZE = 10000000
	MOD     = 1000000007
	INT_INF = math.MaxInt32
)

var rdr *bufio.Reader

func main() {
	rdr = bufio.NewReaderSize(os.Stdin, BUFSIZE)
	solve()
}

func solve() {
	N, M := readint2()
	par := union_find(N + 1)
	for i := 0; i < M; i++ {
		A, B := readint2()
		par = unite(par, A, B)
	}
	result := count(par, N+1)
	fmt.Println(max_list(result))
}

func union_find(N int) []int {
	par := make([]int, N)
	for i := 0; i < N; i++ {
		par[i] = i
	}
	return par
}

func root(par []int, x int) int {
	// データxが木の根の場合は,xを返す
	// データxが木の根でない場合はparで親を再帰的にたぐり、根を返す
	if par[x] == x {
		return x
	} else {
		return root(par, par[x])
	}
}

func unite(par []int, x, y int) []int {
	// xの根のrxとyの根のryが同じでないならばxの根のrxを根ryにつける
	rx := root(par, x)
	ry := root(par, y)
	if rx != ry {
		par[rx] = ry
	}
	return par
}

func count(par []int, N int) []int {
	cnt := make([]int, N)
	for i := 0; i < N; i++ {
		cnt[i] = 0
	}
	for i := 0; i < N; i++ {
		cnt[root(par, i)] += 1
	}
	return cnt
}

func cumsum(base []int) []int {
	dst := make([]int, len(base))
	tmp := 0
	for i := 0; i < len(base); i++ {
		tmp += base[i]
		dst[i] = mod(tmp, MOD)
	}
	return dst
}

func readline() string {
	buf := make([]byte, 0, 16)
	for {
		l, p, e := rdr.ReadLine()
		if e != nil {
			fmt.Println(e.Error())
			panic(e)
		}
		buf = append(buf, l...)
		if !p {
			break
		}
	}
	return string(buf)
}

func readIntSlice() []int {
	slice := make([]int, 0)
	lines := strings.Split(readline(), " ")
	for _, v := range lines {
		slice = append(slice, s2i(v))
	}
	return slice
}

func readint() int {
	return s2i(readline())
}

func readint2() (int, int) {
	lines := strings.Split(readline(), " ")
	return s2i(lines[0]), s2i(lines[1])
}

func readint3() (int, int, int) {
	lines := strings.Split(readline(), " ")
	return s2i(lines[0]), s2i(lines[1]), s2i(lines[2])
}

func readint4() (int, int, int, int) {
	lines := strings.Split(readline(), " ")
	return s2i(lines[0]), s2i(lines[1]), s2i(lines[2]), s2i(lines[3])
}

// For int
func b2i(b bool) int {
	if b {
		return 1
	}
	return 0
}

func abs(v int) int {
	if v < 0 {
		return -v
	}
	return v
}

func pow(x, y int) int {
	return int(math.Pow(float64(x), float64(y)))
}

func mod(x, y int) int {
	m := x % y
	if m < 0 {
		return m + y
	}
	return m
}

func modpow(x, y int) int {
	ret := 1
	for ; y != 0; y >>= 1 {
		if y&1 == 1 {
			ret = mod((ret * x), MOD)
		}
		x = mod(x*x, MOD)
	}
	return ret
}

func min(values ...int) int {
	ret := INT_INF
	for _, v := range values {
		if ret > v {
			ret = v
		}
	}
	return ret
}

func max(values ...int) int {
	ret := -INT_INF
	for _, v := range values {
		if ret < v {
			ret = v
		}
	}
	return ret
}

func max_list(values []int) int {
	ret := -INT_INF
	for _, v := range values {
		if ret < v {
			ret = v
		}
	}
	return ret
}

func s2i(s string) int {
	v, ok := strconv.Atoi(s)
	if ok != nil {
		panic("Faild : " + s + " can't convert to int")
	}
	return v
}

func i2s(i int) string {
	return strconv.Itoa(i)
}

func gcd(v1, v2 int) int {
	if v1 > v2 {
		v1, v2 = v2, v1
	}
	for v1 != 0 {
		v1, v2 = v2%v1, v1
	}
	return v2
}

func lcm(v1, v2 int) int {
	return v1 * v2 / gcd(v1, v2)
}

func extgcd(a, b, c int) (int, int, int) {
	if a < b {
		a, b = b, a
	}
	if b == 0 {
		return a, 1, 0
	}
	d, x, y := extgcd(b, mod(a, b), c)
	return d, y, x - int(a/b)*y
}

func bit(size int) [][]bool {
	bSize := int(math.Pow(2, float64(size)))
	b := make([][]bool, bSize)
	for i := 0; i < bSize; i++ {
		b[i] = make([]bool, size)
		for j := 0; j < size; j++ {
			if i>>j&1 == 1 {
				b[i][j] = true
			} else {
				b[i][j] = false
			}
		}
	}
	return b
}

/* ------------------------------------------------ */
/* Data stracture                                   */
/* ------------------------------------------------ */
type IntHeap []int

func (h *IntHeap) Push(x interface{}) {
	*h = append(*h, x.(int))
}

func (h *IntHeap) Pop() interface{} {
	old := *h
	n := len(old)
	x := old[n-1]
	*h = old[0 : n-1]
	return x
}

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

// BIT Tree
type FenwickTree []int

func NewFenwickTree(size int) *FenwickTree {
	var t FenwickTree
	t = make([]int, size+1)
	return &t
}

func (t FenwickTree) add(index, v int) {
	for i := index + 1; i < len(t); i += (i & -i) {
		t[i] += v
	}
}

func (t FenwickTree) sum(index int) int {
	total := 0
	for i := index + 1; i != 0; i -= (i & -i) {
		total += t[i]
	}
	return total
}

type Comb struct {
	length int
	fac    []int
	inv    []int
	finv   []int
}

func NewComb() *Comb {
	return &Comb{
		length: 2,
		fac:    []int{1, 1},
		inv:    []int{1, 1},
		finv:   []int{1, 1},
	}
}

func (c Comb) calc(n, k int) int {
	if n < k {
		return 0
	}
	if n < 0 || k < 0 {
		return 0
	}

	if c.length <= n {
		for i := c.length; i <= n; i++ {
			c.fac = append(c.fac, mod(c.fac[i-1]*i, MOD))
			c.inv = append(c.inv, MOD-mod(c.inv[mod(MOD, i)]*(MOD/i), MOD))
			c.finv = append(c.finv, mod(c.finv[i-1]*c.inv[i], MOD))
		}
	}
	return mod(c.fac[n]*mod(c.finv[k]*c.finv[n-k], MOD), MOD)
}


package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
)

var scanner = bufio.NewScanner(os.Stdin)

func NextStr() string {
	scanner.Scan()
	return scanner.Text()
}

func NextInt() int {
	i, _ := strconv.Atoi(NextStr())
	return i
}

func max(x, y int) int {
	if x > y {
		return x
	}
	return y
}

func sort(x, y int) (int, int) {
	if x > y {
		return y, x
	}
	return x, y
}

func main() {
	scanner.Split(bufio.ScanWords)
	n, m := NextInt(), NextInt()
	groups := make([]int, n)
	belongings := make([]int, n)
	members := make([][]int, n)
	// 初期は各メンバーは一人グループ
	for i := 0; i < n; i++ {
		groups[i] = 1
		belongings[i] = i
		members[i] = []int{i}
	}

	var ans int

	for i := 0; i < m; i++ {
		a, b := NextInt()-1, NextInt()-1
		a, b = sort(a, b)
		belong := belongings[a]
		target := belongings[b]
		if target != belong {
			// aの所属グループにbのメンバーを所属させる
			groups[belong] += groups[b]
			oldmembers := members[target]
			members[belong] = append(members[belong], members[target]...)
			groups[target] = 0
			members[target] = []int{}
			// 旧メンバーの所属先をaの所属先に更新
			for _, member := range oldmembers {
				belongings[member] = belong
			}
			ans = max(ans, groups[belong])
		}
	}
	fmt.Println(ans)
}


class UnionFind
  def initialize(n)
    @parents = Array.new(n, -1)
  end
  def find(x)
    @parents[x] < 0 ? x : @parents[x] = find(@parents[x])
  end
  def parents
    @parents
  end
  def union(x, y)
    x = find(x)
    y = find(y)
    return if x == y
    if @parents[x] > @parents[y]
      x, y = y, x
    end
    @parents[x] += @parents[y]
    @parents[y] = x
  end
end

n, m = gets.split.map(&:to_i)
u = UnionFind.new(n)
m.times do
  a, b = gets.split.map(&:to_i)
  u.union(a - 1, b - 1)
end
puts -u.parents.min


def find(parent, i)
  t = parent[i]
  return i if t < 0
  t = find(parent, t)
  parent[i] = t
end

def unite(parent, i, j)
  i = find(parent, i)
  j = find(parent, j)
  return if i == j
  parent[j] += parent[i]
  parent[i] = j
end

n, m = gets.split.map(&:to_i)
parent = [-1] * n
m.times do
  a, b = gets.split.map{ |e| e.to_i - 1 }
  unite(parent, a, b)
end
puts -parent.min


class UnionFind
  def initialize(n)
    @n = n
    @par = Array.new(@n, -1)
  end
  def find(x)
    @par[x] < 0 ? x : @par[x] = find(@par[x])
  end
  def union(a, b)
    pa, pb = find(a), find(b)
    return if pa == pb
    if pa < pb then
      @par[pa] += @par[pb]
      @par[pb] = pa
    else
      @par[pb] += @par[pa]
      @par[pa] = pb
    end
  end
  def max_size
    -@par.min
  end
  def show
    p @par
  end
end

N, M = gets.chomp.split(" ").map(&:to_i)
uf = UnionFind.new(N)
(0...M).map{
  a, b = gets.chomp.split(" ").map(&:to_i)
  uf.union(a-1, b-1)
}

puts uf.max_size


require 'set'
n,m = gets.split.map(&:to_i)
check = Array.new(n+1){Set.new}
max = 1
m.times do
  a,b = gets.split.map(&:to_i)
  check[b].add?(a)
  check[a].add?(b)
end
ans = 0
check.each do |c|
  ans = [ans,c.length+1].max
end
p ans


require 'set'
n,m = gets.split.map(&:to_i)
groups = {}
h = {}
m.times do
  a, b = gets.split.map(&:to_i)
  a, b = [a, b].sort

  if g = h[a] || h[b]
    g << a
    g << b
    h[a] = g
    h[b] = g
  else
    s = Set.new
    s << a
    s << b
    h[a] = s
    h[b] = s
  end
end

sets = h.keys.map do |i|
  h[i]
end.uniq

sets.sort_by! do |s|
  s.size
end

puts sets.last.size


n,m = gets.split.map(&:to_i)
groups = {}
h = {}
m.times do
  a, b = gets.split.map(&:to_i)
  a, b = [a, b].sort
  group = h[a] || h[b]
  if group
    h[a] = group
    h[b] = group
    groups[group] << a
    groups[group] << b
  else
    h[a] = a
    h[b] = a
    groups[a] = [a, b]
  end
end

groups.keys.each do |k|
  groups[k].uniq!
end

ans = -Float::INFINITY

groups.keys.each do |k|
  ans = [ans, groups[k].size].max
end

puts ans


pub trait Zero: PartialEq + Sized {
    fn zero() -> Self;
    #[inline]
    fn is_zero(&self) -> bool {
        self == &Self::zero()
    }
}
pub trait One: PartialEq + Sized {
    fn one() -> Self;
    #[inline]
    fn is_one(&self) -> bool {
        self == &Self::one()
    }
}
macro_rules !zero_one_impls {($({$Trait :ident $method :ident $($t :ty ) *,$e :expr } ) *) =>{$($(impl $Trait for $t {#[inline ] fn $method () ->Self {$e } } ) *) *} ;}
zero_one_impls !({Zero zero u8 u16 u32 u64 usize i8 i16 i32 i64 isize u128 i128 ,0 } {Zero zero f32 f64 ,0. } {One one u8 u16 u32 u64 usize i8 i16 i32 i64 isize u128 i128 ,1 } {One one f32 f64 ,1. } );
pub trait IterScan: Sized {
    type Output;
    fn scan<'a, I: Iterator<Item = &'a str>>(iter: &mut I) -> Option<Self::Output>;
}
pub trait MarkedIterScan: Sized {
    type Output;
    fn mscan<'a, I: Iterator<Item = &'a str>>(self, iter: &mut I) -> Option<Self::Output>;
}
#[derive(Clone, Debug)]
pub struct Scanner<'a> {
    iter: std::str::SplitAsciiWhitespace<'a>,
}
mod scanner_impls {
    use super::*;
    impl<'a> Scanner<'a> {
        #[inline]
        pub fn new(s: &'a str) -> Self {
            let iter = s.split_ascii_whitespace();
            Self { iter }
        }
        #[inline]
        pub fn scan<T: IterScan>(&mut self) -> <T as IterScan>::Output {
            <T as IterScan>::scan(&mut self.iter).expect("scan error")
        }
        #[inline]
        pub fn mscan<T: MarkedIterScan>(&mut self, marker: T) -> <T as MarkedIterScan>::Output {
            marker.mscan(&mut self.iter).expect("scan error")
        }
        #[inline]
        pub fn scan_vec<T: IterScan>(&mut self, size: usize) -> Vec<<T as IterScan>::Output> {
            (0..size)
                .map(|_| <T as IterScan>::scan(&mut self.iter).expect("scan error"))
                .collect()
        }
        #[inline]
        pub fn iter<'b, T: IterScan>(&'b mut self) -> ScannerIter<'a, 'b, T> {
            ScannerIter {
                inner: self,
                _marker: std::marker::PhantomData,
            }
        }
    }
    macro_rules !iter_scan_impls {($($t :ty ) *) =>{$(impl IterScan for $t {type Output =Self ;#[inline ] fn scan <'a ,I :Iterator <Item =&'a str >>(iter :&mut I ) ->Option <Self >{iter .next () ?.parse ::<$t >() .ok () } } ) *} ;}
    iter_scan_impls !(char u8 u16 u32 u64 usize i8 i16 i32 i64 isize f32 f64 u128 i128 String );
    macro_rules !iter_scan_tuple_impl {($($T :ident ) *) =>{impl <$($T :IterScan ) ,*>IterScan for ($($T ,) *) {type Output =($(<$T as IterScan >::Output ,) *) ;#[inline ] fn scan <'a ,It :Iterator <Item =&'a str >>(_iter :&mut It ) ->Option <Self ::Output >{Some (($(<$T as IterScan >::scan (_iter ) ?,) *) ) } } } ;}
    iter_scan_tuple_impl!();
    iter_scan_tuple_impl!(A);
    iter_scan_tuple_impl !(A B );
    iter_scan_tuple_impl !(A B C );
    iter_scan_tuple_impl !(A B C D );
    iter_scan_tuple_impl !(A B C D E );
    iter_scan_tuple_impl !(A B C D E F );
    iter_scan_tuple_impl !(A B C D E F G );
    iter_scan_tuple_impl !(A B C D E F G H );
    iter_scan_tuple_impl !(A B C D E F G H I );
    iter_scan_tuple_impl !(A B C D E F G H I J );
    iter_scan_tuple_impl !(A B C D E F G H I J K );
    pub struct ScannerIter<'a, 'b, T> {
        inner: &'b mut Scanner<'a>,
        _marker: std::marker::PhantomData<fn() -> T>,
    }
    impl<'a, 'b, T: IterScan> Iterator for ScannerIter<'a, 'b, T> {
        type Item = <T as IterScan>::Output;
        #[inline]
        fn next(&mut self) -> Option<Self::Item> {
            <T as IterScan>::scan(&mut self.inner.iter)
        }
    }
}
pub mod marker {
    use super::*;
    use std::{iter::FromIterator, marker::PhantomData};
    #[derive(Debug, Copy, Clone)]
    pub struct Usize1;
    impl IterScan for Usize1 {
        type Output = usize;
        #[inline]
        fn scan<'a, I: Iterator<Item = &'a str>>(iter: &mut I) -> Option<Self::Output> {
            Some(<usize as IterScan>::scan(iter)?.checked_sub(1)?)
        }
    }
    #[derive(Debug, Copy, Clone)]
    pub struct Chars;
    impl IterScan for Chars {
        type Output = Vec<char>;
        #[inline]
        fn scan<'a, I: Iterator<Item = &'a str>>(iter: &mut I) -> Option<Self::Output> {
            Some(iter.next()?.chars().collect())
        }
    }
    #[derive(Debug, Copy, Clone)]
    pub struct CharsWithBase(pub char);
    impl MarkedIterScan for CharsWithBase {
        type Output = Vec<usize>;
        #[inline]
        fn mscan<'a, I: Iterator<Item = &'a str>>(self, iter: &mut I) -> Option<Self::Output> {
            Some(
                iter.next()?
                    .chars()
                    .map(|c| (c as u8 - self.0 as u8) as usize)
                    .collect(),
            )
        }
    }
    #[derive(Debug, Copy, Clone)]
    pub struct Collect<T: IterScan, B: FromIterator<<T as IterScan>::Output>> {
        size: usize,
        _marker: PhantomData<fn() -> (T, B)>,
    }
    impl<T: IterScan, B: FromIterator<<T as IterScan>::Output>> Collect<T, B> {
        pub fn new(size: usize) -> Self {
            Self {
                size,
                _marker: PhantomData,
            }
        }
    }
    impl<T: IterScan, B: FromIterator<<T as IterScan>::Output>> MarkedIterScan for Collect<T, B> {
        type Output = B;
        #[inline]
        fn mscan<'a, I: Iterator<Item = &'a str>>(self, iter: &mut I) -> Option<Self::Output> {
            Some(
                (0..self.size)
                    .map(|_| <T as IterScan>::scan(iter).expect("scan error"))
                    .collect::<B>(),
            )
        }
    }
}
#[macro_export]
macro_rules !min {($e :expr ) =>{$e } ;($e :expr ,$($es :expr ) ,+) =>{std ::cmp ::min ($e ,min !($($es ) ,+) ) } ;}
#[macro_export]
macro_rules !chmin {($dst :expr ,$($src :expr ) ,+) =>{{let x =std ::cmp ::min ($dst ,min !($($src ) ,+) ) ;$dst =x ;} } ;}
#[macro_export]
macro_rules !max {($e :expr ) =>{$e } ;($e :expr ,$($es :expr ) ,+) =>{std ::cmp ::max ($e ,max !($($es ) ,+) ) } ;}
#[macro_export]
macro_rules !chmax {($dst :expr ,$($src :expr ) ,+) =>{{let x =std ::cmp ::max ($dst ,max !($($src ) ,+) ) ;$dst =x ;} } ;}
#[derive(Clone, Debug)]
pub struct UnionFind {
    parents: Vec<isize>,
}
impl UnionFind {
    pub fn new(n: usize) -> Self {
        let parents = vec![-1; n];
        Self { parents }
    }
    pub fn find(&mut self, x: usize) -> usize {
        if self.parents[x] < 0 {
            x
        } else {
            let xx = self.parents[x] as usize;
            let y = self.find(xx);
            self.parents[x] = y as isize;
            y
        }
    }
    pub fn unite(&mut self, x: usize, y: usize) -> bool {
        use std::mem::swap;
        let mut x = self.find(x);
        let mut y = self.find(y);
        if x == y {
            return false;
        }
        if self.parents[x] > self.parents[y] {
            swap(&mut x, &mut y);
        }
        self.parents[x] += self.parents[y];
        self.parents[y] = x as isize;
        true
    }
    pub fn size(&mut self, x: usize) -> usize {
        let x = self.find(x);
        (-self.parents[x]) as usize
    }
    pub fn same(&mut self, x: usize, y: usize) -> bool {
        self.find(x) == self.find(y)
    }
    pub fn members(&mut self, x: usize) -> Vec<usize> {
        let root = self.find(x);
        (0..self.parents.len())
            .filter(|i| self.find(*i) == root)
            .collect::<Vec<usize>>()
    }
    pub fn roots(&mut self) -> Vec<usize> {
        (0..self.parents.len())
            .filter(|i| self.parents[*i] < 0)
            .collect::<Vec<usize>>()
    }
    pub fn all_group_members(&mut self) -> std::collections::HashMap<usize, Vec<usize>> {
        let mut groups_map = std::collections::HashMap::new();
        for x in 0..self.parents.len() {
            let r = self.find(x);
            groups_map.entry(r).or_insert(vec![]).push(x);
        }
        groups_map
    }
}
fn main() {
    #![allow(unused_imports, unused_macros)]
    use std::io::{stdin, stdout, BufWriter, Read as _, Write as _};
    let mut _in_buf = Vec::new();
    stdin().read_to_end(&mut _in_buf).expect("io error");
    let _in_buf = unsafe { String::from_utf8_unchecked(_in_buf) };
    let mut scanner = Scanner::new(&_in_buf);
    macro_rules !scan {() =>{scan !(usize ) } ;(($($t :tt ) ,*) ) =>{($(scan !($t ) ) ,*) } ;([$t :tt ;$len :expr ] ) =>{(0 ..$len ) .map (|_ |scan !($t ) ) .collect ::<Vec <_ >>() } ;([$t :ty ;$len :expr ] ) =>{scanner .scan_vec ::<$t >($len ) } ;([$t :ty ] ) =>{scanner .iter ::<$t >() } ;({$e :expr } ) =>{scanner .mscan ($e ) } ;($t :ty ) =>{scanner .scan ::<$t >() } ;}
    let _out = stdout();
    let mut _out = BufWriter::new(_out.lock());
    macro_rules !print {($($arg :tt ) *) =>(::std ::write !(_out ,$($arg ) *) .expect ("io error" ) ) }
    macro_rules !println {($($arg :tt ) *) =>(::std ::writeln !(_out ,$($arg ) *) .expect ("io error" ) ) }
    macro_rules! echo {
        ($iter :expr ) => {
            echo!($iter, '\n')
        };
        ($iter :expr ,$sep :expr ) => {
            let mut iter = $iter.into_iter();
            if let Some(item) = iter.next() {
                print!("{}", item);
            }
            for item in iter {
                print!("{}{}", $sep, item);
            }
            println!();
        };
    }
    let n = scan!();
    let m = scan!();
    let ab = scan!([(marker::Usize1, marker::Usize1)]);
    let mut uf = UnionFind::new(n);
    for (x, y) in ab.take(m) {
        uf.unite(x, y);
    }
    println!("{}", (0..n).map(|x| uf.size(x)).max().unwrap());
}


#![allow(dead_code)]
#![allow(unused)]
use proconio::input;

mod union_find {
    pub struct UnionFind { pub(self) list: Vec<i32> }
    impl UnionFind {
        pub fn new(size: usize) -> UnionFind { UnionFind{ list: vec![-1; size] } }
        pub fn root(&self, n: usize) -> usize { if self.list[n] < 0 { n } else { self.root(self.list[n] as usize) } }
        pub fn size_at(&self, n: usize) -> i32 { std::cmp::max(0, -self.list[n]) }
        pub fn unite(&mut self, x: usize, y: usize) {
             let (mut x_root, mut y_root) = (self.root(x), self.root(y));
              if(x_root == y_root) { return; }
              if(self.list[x_root] > self.list[y_root]) { std::mem::swap(&mut x_root, &mut y_root); }
              self.list[x_root] += self.list[y_root];
              self.list[y_root] = x_root as i32; }
        pub fn are_in_same_tree(self, x: usize, y: usize) -> bool { self.root(x) == self.root(y) }
    }
}

fn main() {
    input!{n: usize, m: usize}
    let mut uf = union_find::UnionFind::new(n);
    for _ in 0..m {
        input!{a: usize, b: usize}
        uf.unite(a - 1, b - 1);
    }
    let mut max = 0;
    for i in 0..n {
        max = std::cmp::max(max, uf.size_at(i));
    }
    println!("{}", max);
}


use proconio::marker::Usize1;
fn main() {
    proconio::input! {
        n: usize,
        m: usize,
        ab: [(Usize1,Usize1);m],
    }
    let mut uf=vec![-1;n];
    for (a,b) in ab {
        unite(&mut uf, a, b);
    }
    println!("{}",(0..n).map(|i|count(&mut uf,i)).max().unwrap());
}
fn find(uf: &mut Vec<i32>, i: usize) -> usize {
    if uf[i]<0 {
        i
    }
    else {
        uf[i]=find(uf, uf[i]as usize)as i32;
        uf[i]as usize
    }
}
fn unite(uf: &mut Vec<i32>, i: usize, j: usize) {
    let mut i=find(uf, i);
    let mut j=find(uf, j);
    if i==j {
        return;
    }
    if uf[i]>uf[j] {
        std::mem::swap(&mut i, &mut j);
    }
    uf[i]+=uf[j];
    uf[j]=i as i32;
}
fn count(uf: &mut Vec<i32>, i: usize) -> usize {
    let i=find(uf, i);
    (-uf[i])as usize
}


struct UnionFind {
    root: Vec<usize>,
    rank: Vec<usize>,
    sizes: Vec<usize>,
}

impl UnionFind {
    fn new(n: usize) -> UnionFind {
        let mut vec = vec![0;n];
        for i in 0..n {
            vec[i] = i;
        }
        UnionFind {
            root: vec,
            rank: vec![0;n],
            sizes: vec![1;n],
        }
    }

    fn find(&mut self, x: usize) -> usize {
        if self.root[x] == x { return x }
        self.root[x] = self.find(self.root[x]);
        self.root[x]
    }

    fn unite(&mut self, x: usize, y: usize) -> () {
        let x = self.find(x);
        let y = self.find(y);
        if x == y { return }
        if self.rank[x] < self.rank[y] {
            self.root[x] = y;
            self.sizes[y] += self.sizes[x];
        } else {
            self.root[y] = x;
            self.sizes[x] += self.sizes[y];
            if self.rank[x] == self.rank[y] {
                self.rank[x] += 1;
            }
        }
    }

    fn same(&mut self, x: usize, y: usize) -> bool {
        self.find(x) == self.find(y)
    }

    fn size(&mut self, x: usize) -> usize{
        let f = self.find(x);
        self.sizes[f]
    }

    fn roots(&mut self, n: usize) -> Vec<usize> {
        let mut vec = vec![];
        for i in 0..n {
            if self.root[i] == i {
                vec.push(self.root[i]);
            }
        }
        vec
    }
}


use proconio::{input, fastout};
use proconio::marker::Usize1;
use std::collections::HashSet;

pub struct GraphL {
    g: Vec<HashSet<usize>>,
    n: usize,
}
impl GraphL {
    pub fn dfs(&self, s: usize) -> usize {
        let mut stack = Vec::new();
        let mut visit = vec![false; self.n];
        stack.push(s);
        let mut res = 0;
        let mut depth = 1;
        while let Some(&u) = stack.last() {
            visit[u] = true;
            let v = self.g[u].iter().find(|&i| !visit[*i]);
            if let Some(v) = v {
                stack.push(*v);
                depth += 1;
            } else {
                stack.pop();
                res = std::cmp::max(res, depth);
                depth = 0;
            }
        }
        res
    }
}

#[fastout]
fn main() {
    input! {
        n: usize,
        m: usize,
        v: [(Usize1,Usize1);m]
    }
    let mut g = vec![HashSet::new();n];
    for (a,b) in v {
        g[a].insert(b);
        g[b].insert(a);
    }
    let g = GraphL {
        g, n,
    };
    let mut res = 0;
    for i in 0..n {
        res = std::cmp::max(res, g.dfs(i))
    }
    println!("{}", res);
}


use proconio::input;

struct UnionFind {
    parent: Vec<usize>,
    rank: Vec<usize>,
}

impl UnionFind {
    fn new(n: usize) -> Self {
        let mut parent = vec![0; n];
        (0..n).for_each(|i| parent[i] = i);
        let rank = vec![0; n];
        UnionFind { parent, rank }
    }

    fn root(&mut self, x: usize) -> usize {
        if x == self.parent[x] {
            x
        } else {
            self.parent[x] = self.root(self.parent[x]);
            self.parent[x]
        }
    }

    fn same(&mut self, x: usize, y: usize) -> bool {
        self.root(x) == self.root(y)
    }

    fn merge(&mut self, x: usize, y: usize) {
        let x = self.parent[x];
        let y = self.parent[y];

        if x == y {
            return;
        }

        if self.rank[x] < self.rank[y] {
            self.parent[x] = y;
        } else {
            self.parent[y] = x;
            if self.rank[x] == self.rank[y] {
                self.rank[x] += 1;
            }
        }
    }

    fn each_size(&self) -> Vec<usize> {
        use std::collections::HashMap;
        let mut map: HashMap<usize, usize> = HashMap::new();
        for i in self.parent.iter() {
            let count = map.entry(*i).or_insert(0);
            *count += 1;
        }
        let v = map.iter().map(|(_, &b)| b).collect();
        v
    }
}

fn main() {
    input! {
        n: usize,
        m: usize
    }

    let mut union = UnionFind::new(n);
    for _ in 0..m {
        input! {
            x: usize,
            y: usize
        }
        union.merge(x - 1, y - 1);
    }
    println!("{}", union.each_size().iter().max().unwrap());
}


//=Main Part==================================================================//
func main(args: [String]) -> Int32 {
  let (n, m): (Int, Int) = *input()

  var tree = UnionFind(size: n)
  for _ in 0..<m {
    let (a, b): (Int, Int) = *input()
    tree.join(a - 1, b - 1)
  }

  var ans = 0
  for i in 0..<n {
    ans = max(ans, tree.size(of: i))
  }

  print(ans)

  return 0
}

//=Utilities for Programming Contests ========================================//
import Foundation
// input: read a line to ...
@inlinable @inline(__always) func input() -> String { readLine()! }
@inlinable @inline(__always) func input() -> Int    { Int(readLine()!)! }
@inlinable @inline(__always) func input() -> Double { Double(readLine()!)! }
@inlinable @inline(__always) func input(separator c: Character = " ") -> [String] { input().split(separator: c).map(String.init) }
@inlinable @inline(__always) func input(separator c: Character = " ") -> [Int]    { input().split(separator: c).map{ Int(String($0))! } }
@inlinable @inline(__always) func input(separator c: Character = " ") -> [Double] { input().split(separator: c).map{ Double(String($0))! } }
// structured bindings: splat - let (a, b) = *arrayOfInt
prefix operator *
@inlinable @inline(__always) prefix func * <E> (a: [E]) -> (E, E)          { (a[0], a[1]) }
@inlinable @inline(__always) prefix func * <E> (a: [E]) -> (E, E, E)       { (a[0], a[1], a[2]) }
@inlinable @inline(__always) prefix func * <E> (a: [E]) -> (E, E, E, E)    { (a[0], a[1], a[2], a[3]) }
@inlinable @inline(__always) prefix func * <E> (a: [E]) -> (E, E, E, E, E) { (a[0], a[1], a[2], a[3], a[4]) }
// ruby like assigment for nullable
infix operator ||=
@inlinable @inline(__always) func ||= <T> (lhs: inout T?, rhs: @autoclosure () -> T) { lhs = lhs ?? rhs() }
// can be used as both stack and queue
public struct Skewer<T> {
  private class Node { let value: T; var next, prev: Node?; init(_ value: T) { self.value = value } }
  private var __head, __tail: Node?
  public var isEmpty: Bool { self.__head == nil && self.__tail == nil }
  public var head: T? { self.__head?.value }
  public var tail: T? { self.__tail?.value }
  public init() { /* nothing to do */ }
  public init(from a: [T], reverse: Bool = false) { if reverse { for v in a { self.pushHead(v) } } else { for v in a { self.pushTail(v) } } }
  public mutating func pushHead(_ v: T) -> Void { let n = Node(v); self.__head?.prev = n; n.next = self.__head; self.__head = n; self.__tail ||= n }
  public mutating func pushTail(_ v: T) -> Void { let n = Node(v); self.__tail?.next = n; n.prev = self.__tail; self.__tail = n; self.__head ||= n }
  @discardableResult public mutating func popHead() -> T? { guard let n = self.__head else { return nil }; n.next?.prev = nil; self.__head = n.next; return n.value }
  @discardableResult public mutating func popTail() -> T? { guard let n = self.__tail else { return nil }; n.prev?.next = nil; self.__tail = n.prev; return n.value }
}
// heap: for priority queue
public struct Fork<T> {
  private var data: [T]
  private var compare: (T, T) -> Bool
  public var isEmpty: Bool { self.data.isEmpty }
  public init(from a: [T] = [], compare: @escaping (T, T) -> Bool) { self.data = a; self.compare = compare; self.heapify() }
  public func peek() -> T? { self.data.first }
  public mutating func insert(_ v: T) -> Void { self.data.append(v); self.shiftUp(self.data.count - 1) }
  @discardableResult
  public mutating func remove() -> T? {
    if self.data.count <= 1 { return self.data.popLast() }
    let value = self.data[0]
    self.data[0] = self.data.removeLast()
    self.shiftDown(0)
    return value
  }
  @inlinable @inline(__always) internal func parentIndex(_ index: Int) -> Int { (index - 1) / 2 }
  @inlinable @inline(__always) internal func lChildIndex(_ index: Int) -> Int { 2 * index + 1 }
  @inlinable @inline(__always) internal func rChildIndex(_ index: Int) -> Int { 2 * index + 2 }
  private mutating func heapify() -> Void {
    if self.data.count < 2 { return }
    for i in stride(from: (self.data.count / 2 - 1), through: 0, by: -1) { shiftDown(i) }
  }
  internal mutating func shiftUp(_ index: Int) -> Void {
    let put = self.data[index]
    var (insertIndex, parentIndex) = (index, self.parentIndex(index))
    while insertIndex > 0 && self.compare(put, self.data[parentIndex]) {
      self.data[insertIndex]     = self.data[parentIndex]
      (insertIndex, parentIndex) = (parentIndex, self.parentIndex(parentIndex))
    }
    self.data[insertIndex] = put
  }
  internal mutating func shiftDown(from: Int, until end: Int) -> Void {
    var here = from
    let l = self.lChildIndex(from)
    let r = self.rChildIndex(from)
    if l < end && self.compare(self.data[l], self.data[here]) { here = l }
    if r < end && self.compare(self.data[r], self.data[here]) { here = r }
    if here == from { return }
    self.data.swapAt(from, here)
    self.shiftDown(from: here, until: end)
  }
  internal mutating func shiftDown(_ index: Int) -> Void { self.shiftDown(from: index, until: self.data.count) }
}
// union find tree
public struct UnionFind {
  private var data: [Int]
  public init(size: Int) { self.data = [Int](repeating: -1, count: size) }
  public mutating func size(of group: Int) -> Int { -self.data[self.root(of: group)] }
  public mutating func root(of id: Int) -> Int { if (self.data[id] < 0) { return id }; self.data[id] = root(of: self.data[id]); return self.data[id] }
  public mutating func join(_ x: Int, _ y: Int) -> Void {
    let (px, py) = (self.root(of: x), self.root(of: y))
    if px == py { return }
    if (self.data[px] > self.data[py]) { self.data.swapAt(px, py) }
    (self.data[px], self.data[py]) = (self.data[px] &+ self.data[py], px)
  }
}

//=Execution==================================================================//
exit(main(args: CommandLine.arguments))


import Foundation

let main: () = {
    let (N, M) = *listOfInt()
    var U = UnionFind(N)
    repeate(M) {
        let (A, B) = *listOfInt(-1)
        if A > B {
            U.unite(B, A)
        } else {
            U.unite(A, B)
        }
    }
    var S = Set<Int>()
    for n in 0 ..< N {
        S.insert(U.root(n))
    }
    var ans = 0
    for s in S {
        let size = U.size(s)
        //print(U.size(s))
        if ans < size { ans = size }
    }
    print(ans)
}()

//----------------------------------

let dx = [1, 0, -1, 0] //[1, 0, -1, 0, -1,  1, -1, 1]
let dy = [0, 1, 0, -1] //[0, 1, 0, -1, -1, -1,  1, 1]

@inlinable func gcd(_ a: Int, _ b: Int) -> Int { (b > 0) ? gcd(b, a % b) : a }
@inlinable func lcm(_ a: Int, _ b: Int) -> Int { a / gcd(a, b) * b }

//----------------------------------

infix operator  =~ : ComparisonPrecedence
infix operator !=~ : ComparisonPrecedence
func =~ <T: FixedWidthInteger>(left: T, right: Range<T>) -> Bool { right ~= left }
func !=~ <T: FixedWidthInteger>(left: T, right: Range<T>) -> Bool { !(right ~= left) }
func =~ <T: FixedWidthInteger>(left: T, right: ClosedRange<T>) -> Bool { right ~= left }
func !=~ <T: FixedWidthInteger>(left: T, right: ClosedRange<T>) -> Bool { !(right ~= left) }

func =~ <T: Collection > (left: T.Element, right: T) -> Bool where T.Element : Equatable { right.contains(left) }
func !=~ <T: Collection > (left: T.Element, right: T) -> Bool where T.Element : Equatable { !right.contains(left) }

infix operator /^ : MultiplicationPrecedence
infix operator /^= : AssignmentPrecedence
func /^ <T: FixedWidthInteger>(left: T, right: T) -> T { (left + (right - 1)) / right }
func /^= <T: FixedWidthInteger>(left: inout T, right: T) { left = (left + (right - 1)) / right }

@inlinable func chmax<T: Comparable>(_ m: inout T, _ q: T) { if (m < q) { m = q } }
@inlinable func chmin<T: Comparable>(_ m: inout T, _ q: T) { if (m > q) { m = q } }
@inlinable func chRmax<T: Comparable>(_ m: inout T, _ q: T) -> Bool { if (m < q) { m = q; return true } else { return false } }
@inlinable func chRmin<T: Comparable>(_ m: inout T, _ q: T) -> Bool { if (m > q) { m = q; return true } else { return false } }

prefix operator *
prefix func * <T> (a: [T]) -> (T, T) { (a[0], a[1]) }
prefix func * <T> (a: [T]) -> (T, T, T) { (a[0], a[1], a[2]) }
prefix func * <T> (a: [T]) -> (T, T, T, T) { (a[0], a[1], a[2], a[3]) }
prefix func * <T> (a: [T]) -> (T, T, T, T, T) { (a[0], a[1], a[2], a[3], a[4]) }
prefix func * <T> (a: [T]) -> (T, T, T, T, T, T) { (a[0], a[1], a[2], a[3], a[4], a[5]) }

func printErr(_ str: String, terminator: String = "\n") {
    let s = str + terminator
    FileHandle.standardError.write(s.data(using: .utf8)!)
    FileHandle.standardError.synchronizeFile()
}

@inlinable func next() -> String { readLine()! }
@inlinable func nextInt() -> Int { Int(next())! }
@inlinable func nextDouble() -> Double { Double(next())! }
@inlinable func listOfString() -> [String] { next().split(separator: " ").map(String.init) }
@inlinable func listOfInt() -> [Int] { listOfString().map { Int($0)! } }
@inlinable func listOfInt(_ offset: Int = 0) -> [Int] { listOfString().map { Int($0)! + offset } }
@inlinable func listOfInt(_ offsets: Int ...) -> [Int] { listOfString().enumerated().map { Int($0.element)! + offsets[$0.offset % offsets.count] } }
@inlinable func listOfDouble(_ offset: Double = 0.0) -> [Double] { listOfString().map { Double($0)! + offset } }
@inlinable func listOfDouble(_ offsets: Double ...) -> [Double] { listOfString().enumerated().map { Double($0.element)! + offsets[$0.offset % offsets.count] } }

extension Int {
    init(s: String) { self.init(s)! }
    var isEven: Bool { self.isMultiple(of: 2) }
    var isOdd: Bool { !self.isEven }
    var isTrue: Bool { self != 0 }
    var isFalse: Bool { !self.isTrue }
    var toString: String { String(self) }
    var toChar: Character { Character(UnicodeScalar(UInt8(self))) }
}
func + (left: Int, right: Bool) -> Int { left + (right ? 1 : 0) }
extension Character {
    var asciiCode: Int { Int(self.asciiValue!) }
    var string: String { String(self) }
}
extension String {
    @inlinable subscript (position: Int) -> Character { self[self.index(self.startIndex, offsetBy: position)] }
    @inlinable func repeate(_ times: Int) -> String { String(repeating: self, count: times) }
    //  @inlinable subscript (n: Int) -> String { String(self[self.index(self.startIndex, offsetBy: n)]) }
    //  @inlinable subscript (n: Int) -> Int { Int(self[self.index(self.startIndex, offsetBy: n)].asciiValue!) }
    var toInt: Int { Int(self)! }
}
extension String { // VB like
    @inlinable func _index(_ position: Int) -> String.Index! {
        if !(0 ..< self.count ~= position) { return nil }
        return self.index(self.startIndex, offsetBy: position)
    }
    @inlinable func mid(from index: Int, length: Int) -> String! {
        if let startPosition = self._index(index) {
            if let endPosition = self._index(index + length - 1) {
                return String(self[startPosition...endPosition])
            } else {
                let endPosition = self.index(before: self.endIndex)
                return String(self[startPosition...endPosition])
            }
        }
        return ""
    }
    @inlinable func left(_ length: Int) -> String { String(self.prefix(length)) }
    @inlinable func right(_ length: Int) -> String { String(self.suffix(length)) }
}

extension Dictionary {
    @inlinable mutating func getOrSet(_ key: Key, _ defaultValue: () -> Value) -> Value {
        if let stored = self[key] { return stored }
        else {
            let newValue = defaultValue()
            self[key] = newValue
            return newValue
        }
    }
    @inlinable mutating func getOrElse(_ key: Key, _ defaultValue: () -> Value) -> Value {
        if let stored = self[key] { return stored } else { return defaultValue() }
    }
    @inlinable func containsKey(_ key: Key) -> Bool { self.keys.contains(key) }
}

extension Set {
    static func += (left: inout Set<Element>, right: Element) { left.insert(right) }
    static func -= (left: inout Set<Element>, right: Element) { left.remove(right) }
}

extension Array {
    static func += (left: inout Array<Element>, right: Element) { left.append(right) }
    init(_ size: Int, _ initValue: ((Int) -> Element)) {
        self = [Element]()
        for index in 0 ..< size {
            let value = initValue(index)
            self.append(value)
        }
    }
    init(_ size: Int, _ initValue: (() -> Element)) {
        self = [Element]()
        for _ in 0 ..< size {
            let value = initValue()
            self.append(value)
        }
    }
}
typealias IntArray = Array<Int>
typealias DoubleArray = Array<Double>
typealias BoolArray = Array<Bool>
typealias StringArray = Array<String>

extension Array where Element == Int {
    init(_ size: Int) {
        self = [Element](repeating: 0, count: size)
    }
}
extension Array where Element == Double {
    init(_ size: Int) {
        self = [Element](repeating: 0.0, count: size)
    }
}
extension Array where Element == Bool {
    init(_ size: Int) {
        self = [Element](repeating: false, count: size)
    }
}
extension Array where Element == String {
    init(_ size: Int) {
        self = [Element](repeating: "", count: size)
    }
}

extension Array {
    @inlinable func count(_ predicate: ((Element) -> Bool)) -> Int {
        var cnt = 0; for e in self { if (predicate(e)) { cnt += 1 } }
        return cnt
    }
    @inlinable func groupBy<K>(_ keySelector: (Element) -> K) -> [K : [Element]] {
        var destination = [K : [Element]]()
        for element in self {
            let key = keySelector(element)
            var list = destination.getOrSet(key) { [Element]() }
            list.append(element)
            destination[key] = list
        }
        return destination
    }
    @inlinable func groupByIndexed<K>(_ keySelector: (Element) -> K) -> [K : [Int]] {
        var destination = [K : [Int]]()
        for element in self.enumerated() {
            let key = keySelector(element.element)
            var list = destination.getOrSet(key) { [Int]() }
            list.append(element.offset)
            destination[key] = list
        }
        return destination
    }
}

extension Array {
    func binarySearch(_ element: Element, by compare: (Element, Element) -> Int, fromIndex: Int = 0, toIndex: Int = -1) -> Int {
        var low = fromIndex
        var high = (toIndex == -1) ? self.count - 1 : toIndex - 1
        while (low <= high) {
            let mid = (low + high) >> 1 // safe from overflows
            let midVal = self[mid]
            let cmp = compare(midVal, element)
            if (cmp < 0) { low = mid + 1 }          //-1
            else if (0 < cmp) { high = mid - 1 }    // 1
            else { return mid } // key found        // 0
        }
        return -(low + 1)  // key not found
    }
}
extension Array where Element : Comparable {
    func binarySearch(_ element: Element, fromIndex: Int = 0, toIndex: Int = -1) -> Int {
        var low = fromIndex
        var high = (toIndex == -1) ? self.count - 1 : toIndex - 1
        while (low <= high) {
            let mid = (low + high) >> 1 // safe from overflows
            let midVal = self[mid]
            if (midVal < element) { low = mid + 1 }
            else if (midVal > element) { high = mid - 1 }
            else { return mid } // key found
        }
        return -(low + 1)  // key not found
    }
}

extension Array where Element == Int {
    @inlinable func distinct() -> [Element] {
        var set = Set<Element>(), ary = [Element]()
        for n in self.indices { if !set.contains(self[n]) { ary.append(self[n]); set.insert(self[n]) } }
        return ary
    }
    @inlinable func any(_ predicate: (Element) -> Bool) -> Bool {
        var any = false; for element in self { if predicate(element) { any = true } }
        return any
    }
    @inlinable func all(_ predicate: (Element) -> Bool) -> Bool {
        self.allSatisfy(predicate)
    }
    @inlinable func sum() -> Element { self.reduce(0, +) }
    @inlinable func sum(by predicate: (Element) -> Element) -> Element {
        var sum = 0; for e in self { sum += predicate(e) }
        return sum
    }
    @inlinable func joinToString(_ sepatator: String = " ") -> String { self.map { String($0) }.joined(separator: sepatator) }
}

extension Array where Element == Double {
    @inlinable func sum() -> Element { self.reduce(0.0, +) }
    @inlinable func sum(by predicate: (Element) -> Element) -> Element {
        var sum = 0.0; for e in self { sum += predicate(e) }
        return sum
    }
    @inlinable func joinToString(_ sepatator: String = " ") -> String { self.map { String($0) }.joined(separator: sepatator) }
}
extension Array where Element == String {
    @inlinable func toIntDouble() -> (Int, Double) { (Int(self[0])!, Double(self[1])!) }
    @inlinable func toIntString() -> (Int, String) { (Int(self[0])!, self[1]) }
    @inlinable func toIntIntString() -> (Int, Int, String) { (Int(self[0])!, Int(self[1])!, self[2]) }
}

@inlinable func block( _ action: () -> Void) {
    action()
}
@inlinable func repeate(_ times1: Int, _ action: () -> Void) {
    for _ in 0 ..< times1 { action() }
}
@inlinable func repeate(_ times1: Int, _ action: (Int) -> Void) {
    for index1 in 0 ..< times1 {
        action(index1)
    }
}
@inlinable func repeate(_ times1: Int, _ times2: Int, _ action: (Int, Int) -> Void) {
    for index1 in 0 ..< times1 {
        for index2 in 0 ..< times2 {
            action(index1, index2)
        }
    }
}
@inlinable func repeate(_ times1: Int, _ times2: Int, _ times3: Int, _ action: (Int, Int, Int) -> Void) {
    for index1 in 0 ..< times1 {
        for index2 in 0 ..< times2 {
            for index3 in 0 ..< times3 {
                action(index1, index2, index3)
            }
        }
    }
}
@inlinable func repeate(_ times1: Int, _ times2: Int, _ times3: Int, _ times4: Int, _ action: (Int, Int, Int, Int) -> Void) {
    for index1 in 0 ..< times1 {
        for index2 in 0 ..< times2 {
            for index3 in 0 ..< times3 {
                for index4 in 0 ..< times4 {
                    action(index1, index2, index3, index4)
                }
            }
        }
    }
}

func measureTimeSeconds(block: (() -> Void)) -> Double {
    let startTime = Date()
    block()
    let duration = -startTime.timeIntervalSinceNow
    return duration
}

struct UnionFind {
    var rank: [Int]
    var par:  [Int]
    var size: [Int]
    init(_ size: Int) {
        self.rank = IntArray(size) { 0 }
        self.par  = IntArray(size) { $0 }
        self.size = IntArray(size) { 1 }
    }
    @discardableResult
    mutating func unite(_ x: Int, _ y: Int) -> Bool {
        var rx = self.root(x)
        var ry = self.root(y)
        if (rx == ry) { return false }
        if (self.rank[rx] < self.rank[ry]) {
            let w = rx
            rx = ry
            ry = w
        } else if (self.rank[rx] == self.rank[ry]) {
            self.rank[rx] += 1
        }
        self.par[ry] = rx
        self.size[rx] += self.size[ry]
        return true
    }
    mutating func find(_ x: Int) -> Int {
        if (self.par[x] == x) { return x }
        else {
            self.par[x] = self.find(self.par[x])
            return self.par[x]
        }
    }
    mutating func isSame(_ x: Int, _ y: Int) -> Bool { self.root(x) == self.root(y) }
    mutating func size(_ x: Int) -> Int { self.size[self.find(x)] }
    mutating func root(_ x: Int) -> Int { self.find(x) }
}