#include <set>
#include <utility>
#include <iostream>
#include <cstdio>
#include <vector>
#include <tuple>
#include <cassert>
using namespace std;

struct DisjointSet {
    vector<int> node;
    
    DisjointSet(int N) : node(N, -1) {}
    // 経路圧縮はたぶんだめ
    int find(int x) {
        return node[x] < 0 ? x : find(node[x]);
    }
    int size(int x) {
        return -node[find(x)];
    }
    bool unite(int x, int y) {
        x = find(x), y = find(y);
        if(x == y) return false;
        if(size(x) < size(y)) swap(x, y);

        node[x] += node[y];
        node[y] = x;
        return true;
    }

    // DisjointSet の部分的な初期化
    void initialize(vector< pair<int, int> > info) {
        for(auto x : info) {
            int vertex_id, node_val;
            tie(vertex_id, node_val) = x;
            node[ vertex_id ] = node_val;
        }
    }
};

const int BUCKET_SIZE = 200;
int main() {
    int N, Q; cin >> N >> Q;

    // 前のバケットまで処理した際に現存する辺の集合
    set< pair<int, int> > whole_edge_set;

    // while に入る回数は sqrt(Q) 回にしかならない
    while(Q > 0) {
        // UF (たぶん 1 個で良い？)
        DisjointSet uf(N);
        vector<int> query(BUCKET_SIZE), u(BUCKET_SIZE), v(BUCKET_SIZE);
        
        // クエリ処理時に使用する local な情報だけ保持した edge set
        // 削除される辺の情報だけ覚えた del_edge_set
        // クエリで呼び出された頂点を管理する used_vtx
        set< pair<int, int> > edge_set, del_edge_set;
        set<int> used_vtx;
        
        for(int i=0; i<BUCKET_SIZE and Q > 0; i++, Q--) {
            cin >> query[i] >> u[i] >> v[i];
            if(u[i] > v[i]) swap(u[i], v[i]);

            // 辺削除クエリが来たら、辺を覚える
            if(query[i] == 2) {
                del_edge_set.insert(make_pair(u[i], v[i]));
            }

            // クエリで呼ばれたので覚えておく
            used_vtx.insert(u[i]);
            used_vtx.insert(v[i]);
        }

        // 削除される辺以外は UF に反映させて良い
        for(auto e : whole_edge_set) {
            int a, b; tie(a, b) = e;
            if(del_edge_set.count(e)) {
                // 削除される辺は UF には反映させてはならないが
                // edge_set には追加しなければならない
                // (削除するまでは辺が存在するため)
                edge_set.insert(e);
            }
            else {
                uf.unite(a, b);
            }
        }

        // クエリに関係した場所の UF の情報は
        // かんたんに戻せるようにしておく
        vector< pair<int, int> > initial_info;
        for(auto v : used_vtx) {
            int par = uf.find(v);
            initial_info.push_back(make_pair(par, uf.node[par]));
        }

        for(int i=0; i<BUCKET_SIZE; i++) {
            if(query[i] == 1) {
                edge_set.insert(make_pair(u[i], v[i]));
                whole_edge_set.insert(make_pair(u[i], v[i]));
            }
            if(query[i] == 2) {
                assert(edge_set.count(make_pair(u[i], v[i])));
                assert(whole_edge_set.count(make_pair(u[i], v[i])));
                edge_set.erase(make_pair(u[i], v[i]));
                whole_edge_set.erase(make_pair(u[i], v[i]));
            }
            if(query[i] == 3) {
                // UF を初期化し、edge_set の情報も追加
                uf.initialize(initial_info);
                for(auto e : edge_set) {
                    int a, b; tie(a, b) = e;
                    uf.unite(a, b);
                }

                if(uf.find(u[i]) == uf.find(v[i])) {
                    cout << "YES" << endl;
                }
                else {
                    cout << "NO" << endl;
                }
            }
        }
    }
    return 0;
}


Measured memory_bytes: 4816