#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> ii;
const int INF = 1e9;
const ll LINF = 1e18;
const int N = 1e5 + 5;
int n, m, q;
vector<int> adj[N];
int tin[N]; // tin[u] = thời gian dfs đi vào đỉnh u
int tout[N]; // tout[u] = thời gian dfs đi ra khỏi đỉnh u
int low[N], timer;
vector<int> child[N]; // child[u]: danh sách các đỉnh là con của u trên cây dfs, được sort tăng dần theo giá trị tin[]
bool is_cutpoint[N]; // is_cutpoint[u] = true/false: u có phải là đỉnh khớp hay không
void dfs(int u, int p) {
tin[u] = low[u] = ++timer;
for (int v : adj[u]) {
if (v == p) continue;
if (!tin[v]) {
child[u].push_back(v);
dfs(v, u);
low[u] = min(low[u], low[v]);
if (low[v] >= tin[u] && p != -1) is_cutpoint[u] = true;
}
else {
low[u] = min(low[u], tin[v]);
}
}
if (p == -1) {
is_cutpoint[u] = (child[u].size() >= 2);
}
tout[u] = ++timer;
}
// Kiểm tra u có phải là tổ tiên của v hay không
bool isAncestor(int u, int v) {
return (tin[u] <= tin[v] && tin[v] <= tout[u]);
}
// Trả lời truy vấn loại 1
bool solve1(int a, int b, int g1, int g2) {
// Không mất tính tổng quát, giả sử g1 là cha của g2
if (tin[g1] > tin[g2]) swap(g1, g2);
// Nếu cạnh (g1, g2) không phải là cầu thì kết quả auto là yes
if (low[g2] != tin[g2]) return true;
// Nếu cạnh (g1, g2) là cầu thì a, b phải cùng thuộc một thành phần liên thông sau khi xoá cạnh (g1, g2)
// tức a, b phải đều thuộc cây con gốc g2 hoặc đều nằm ngoài cây con gốc g2
// để kiểm tra cây con gốc y có chứa đỉnh x hay không thì ta kiểm tra y có phải tổ tiên của x hay không
if ((isAncestor(g2, a) && isAncestor(g2, b)) ||
(!isAncestor(g2, a) && !isAncestor(g2, b)))
return true;
return false;
}
// Tìm đỉnh v sao cho v là con của c và v là tổ tiên của u
// Ta có nhận xét:
// Một đỉnh v là tổ tiên của u nếu đoạn [tin[v], tout[v]] chứa tin[u]
// Khi xét các đỉnh con v của c với thứ tự tăng dần theo tin
// thì các đoạn [tin[v], tout[v]] sẽ xếp tăng dần và liên tiếp nhau (chứng minh thông qua quá trình dfs)
// Nên ta có thể tìm nhanh đỉnh v thoả mãn bằng tìm kiếm nhị phân
int findP(int c, int u) {
// Trường hợp u nằm ngoài cây con gốc c thì hiển nhiên không tồn tại đỉnh v cần tìm
if (!isAncestor(c, u)) return -1;
int l = 0, r = child[c].size() - 1, ans = -1;
while (l <= r) {
int mid = (l + r) >> 1;
int cur = child[c][mid];
if (tin[cur] <= tin[u]) {
ans = mid;
l = mid + 1;
}
else {
r = mid - 1;
}
}
return child[c][ans];
}
// Trả lời truy vấn loại 2
bool solve2(int a, int b, int c) {
// Trường hợp c không phải là khớp thì kết quả auto là yes
if (!is_cutpoint[c]) return true;
// pa là con của c mà là tổ tiên của a
// pb là con của c mà là tổ tiên của b
// Sau đó ta chỉ cần xét qua một số trường hợp
int pa = findP(c, a), pb = findP(c, b);
// a và b đều nằm ngoài cây con gốc c
if (pa == -1 && pb == -1) {
return true;
}
// a nằm ngoài cây con gốc c, b nằm trong cây con gốc c
// khi đó khi xoá c thì b phải có đường đi lên tổ tiên nào đấy của c
if (pa == -1 && pb != -1) {
if (low[pb] < tin[c]) return true;
return false;
}
// a nằm trong cây con gốc c, b nằm ngoài cây con gốc c
// tương tự trường hợp trên
if (pa != -1 && pb == -1) {
if (low[pa] < tin[c]) return true;
return false;
}
// a và b cùng nằm trong cây con gốc c
if (pa == pb) return true; // a, b cùng thuộc chung một nhánh
if (low[pa] < tin[c] && low[pb] < tin[c]) return true; // ngược lại khi xoá c
// thì a, b đều phải có đường đi lên tổ tiên nào đấy của c
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
timer = 0;
dfs(1, -1);
cin >> q;
while (q--) {
int type, a, b;
cin >> type >> a >> b;
if (type == 1) {
int g1, g2;
cin >> g1 >> g2;
cout << (solve1(a, b, g1, g2) ? "yes" : "no") << '\n';
}
else {
int c;
cin >> c;
cout << (solve2(a, b, c) ? "yes" : "no") << '\n';
}
}
}