#include <bits/stdc++.h>

#define set unordered_set
#define map unordered_map

using namespace std;

class UnionFind {
private:
	int size;
	int parent[40000];
	int rank[40000];

public:
	UnionFind() {}
	UnionFind(int sz) {
		init(sz);
	}
	void init(int sz) {
		size = sz;
		fill(rank, rank+sz, 0);
		for (int i = 0; i < sz; ++i) {
			parent[i] = i;
		}
	}
	int root(int n) {
		if (parent[n] == n) return n;
		return parent[n] = root(parent[n]);
	}
	void unite(int n, int m) {
		int x = root(n);
		int y = root(m);
		if (x == y) return;

		if (rank[x] < rank[y]) {
			parent[x] = y;
		}
		else if (rank[x] > rank[y]) {
			parent[y] = x;
		}
		else {
			parent[x] = y;
			++rank[y];
		}
	}
	bool isUnited(int n, int m) {
		return root(n) == root(m);
	}
};

struct Query {
	int type, u, v;
};

typedef vector< map<int, int> > Graph;

const int bucket = 50;

map<int, int> make_table(set<int>& use) {
	map<int, int> res;
	int n = 0;
	for (set<int>::iterator it = use.begin(); it != use.end(); ++it, ++n) {
		res[*it] = n;
	}
	return res;
}
Graph trim(Graph& G, set<int>& use, map<int, int>& tbl) {
	tbl = make_table(use);

	int i = 0;
	Graph res(use.size());
	UnionFind uf(G.size());
	for (int i = 0; i < G.size(); ++i) {
		for (map<int, int>::iterator it = G[i].begin(); it != G[i].end(); ++it) {
			if (it->second) {
				int v = it->first;
				if ( !use.count(i) || !use.count(v) ) {
					uf.unite(i, v);
				}
			}
		}
	}
	for (set<int>::iterator it1 = use.begin(); it1 != use.end(); ++it1) {
		for (set<int>::iterator it2 = use.begin(); it2 != use.end(); ++it2) {
			if ( uf.isUnited(*it1, *it2) ) {
				tbl[*it1] = tbl[*it2];
			}
		}
	}
	for (set<int>::iterator it1 = use.begin(); it1 != use.end(); ++it1) {
		for (set<int>::iterator it2 = use.begin(); it2 != use.end(); ++it2) {
			if (G[*it1].find(*it2) != G[*it1].end() && G[*it1].find(*it2)->second) {
				res[tbl[*it1]][tbl[*it2]]++;
			}
		}
	}
	return res;
}
void unite(Graph& G, int u, int v) {
	G[u][v]++;
	G[v][u]++;
}
void remove(Graph& G, int u, int v) {
//	--G[u][v];
//	--G[v][u];
	if ( --G[u][v] == 0 ) G[u].erase(v);
	if ( --G[v][u] == 0 ) G[v].erase(u);
}
bool judge(Graph& G, int v, int t, vector<bool>& reached) {
	if (v == t) return true;
	reached[v] = true;
	for (map<int, int>::iterator it = G[v].begin(); it != G[v].end(); ++it) {
		int to = it->first;
		if ( !reached[to] ) {
			if ( judge(G, to, t, reached) ) return true;
		}
	}
	return false;
}
bool judge(Graph& G, int s, int t) {
	vector<bool> reached(G.size(), false);
	return judge(G, s, t, reached);
}
/*
bool judge(Graph& G, int s, int t) {
	assert(G.size() <= bucket*2);
	bool reached[bucket*2] = {0};
	queue<int> Q; Q.push(s); reached[s] = true;
	while ( !Q.empty() ) {
		int v = Q.front(); Q.pop();
		for (map<int, int>::iterator it = G[v].begin(); it != G[v].end(); ++it) {
			int to = it->first;
			if ( !reached[to] ) {
				reached[to] = true;
				Q.push(to);
			}
		}
	}
	return reached[t];
}
*/

int main() {
	int N, M; cin >> N >> M;
	Query Q[40000];
	for (int i = 0; i < M; ++i) {
		int t, u, v; scanf("%d%d%d", &t, &u, &v);
		Q[i] = (Query){t, u, v};
	}
	set<int> use[40000/bucket+1];
	for (int i = 0; i < M; ++i) {
		int type = Q[i].type, u = Q[i].u, v = Q[i].v;
		use[i/bucket].insert(u);
		use[i/bucket].insert(v);
	}
	Graph G(N);
	Graph g;
	map<int, int> tbl;
	for (int i = 0; i < M; ++i) {
		int type = Q[i].type, u = Q[i].u, v = Q[i].v;
		if (i % bucket == 0) {
			g = trim(G, use[i/bucket], tbl);
		}
		switch (type) {
		case 1: // unite
			unite(G, u, v);
			unite(g, tbl[u], tbl[v]);
			break;
		case 2: // remove
			remove(G, u, v);
			remove(g, tbl[u], tbl[v]);
			break;
		case 3: // judge
			if ( judge(g, tbl[u], tbl[v]) ) {
				cout << "YES" << endl;
			}
			else {
				cout << "NO" << endl;
			}
			break;
		}
	}
}

Measured memory_bytes: 12368