Problem: In a magical land there are n cities conveniently numbered 1, 2, ..., n. Some pairs of these cities are connected by magical colored roads. Magic is unstable, so at any time, new roads may appear between two cities.

Vicky the witch has been tasked with performing deliveries between some pairs of cities. However, Vicky is a beginner, so she can only complete a delivery if she can move from her starting city to her destination city through a double rainbow. A double rainbow is a sequence of cities c_1, c_2, ..., c_k satisfying the following properties:

  * For each i with 1 ≤ i ≤ k - 1, the cities c_i and c_{i + 1} are connected by a road. 
  * For each i with 1 ≤ i ≤ (k - 1)/(2), the roads connecting c_{2i} with c_{2i - 1} and c_{2i + 1} have the same color. 



For example if k = 5, the road between c_1 and c_2 must be the same color as the road between c_2 and c_3, and the road between c_3 and c_4 must be the same color as the road between c_4 and c_5.

Vicky has a list of events in chronological order, where each event is either a delivery she must perform, or appearance of a new road. Help her determine which of her deliveries she will be able to complete.

Input

The first line contains four integers n, m, c, and q (2 ≤ n ≤ 10^5, 1 ≤ m, c, q ≤ 10^5), denoting respectively the number of cities, the number of roads initially present, the number of different colors the roads can take, and the number of events.

Each of the following m lines contains three integers x, y, and z (1 ≤ x, y ≤ n, 1 ≤ z ≤ c), describing that there initially exists a bidirectional road with color z between cities x and y.

Then q lines follow, describing the events. Each event is one of the following two types: 

  1. + x y z (1 ≤ x, y ≤ n, 1 ≤ z ≤ c), meaning a road with color z appears between cities x and y; 
  2. ? x y (1 ≤ x, y ≤ n), meaning you should determine whether Vicky can make a delivery starting at city x and ending at city y. It is guaranteed that x ≠ y. 



It is guaranteed that at any moment, there is at most one road connecting any pair of cities, and that no road connects a city to itself. It is guaranteed that the input contains at least one event of the second type.

Output

For each event of the second type, print a single line containing "Yes" (without quotes) if the delivery can be made, or a single line containing "No" (without quotes) otherwise.

Example

Input


4 3 2 4
1 2 1
2 3 1
3 4 2
? 1 4
? 4 1
+ 3 1 2
? 4 1


Output


Yes
No
Yes

Note

The following picture corresponds to the sample.

<image>

For her first delivery, Vicky can use the sequence 1, 2, 3, 4 which is a double rainbow. However, she cannot complete the second delivery, as she can only reach city 3. After adding the road between cities 1 and 3, she can now complete a delivery from city 4 to city 1 by using the double rainbow 4, 3, 1.

Solution (in CPP):

Solution:
#include <bits/stdc++.h>
using namespace std;
using ipair = pair<int, int>;
using int64 = int64_t;
using uint64 = uint64_t;
template <typename T>
void remin(T& a, const T& b) {
  if (b < a) {
    a = b;
  }
}
template <typename T>
void remax(T& a, const T& b) {
  if (b > a) {
    a = b;
  }
}
template <class T1, class T2>
inline istream& operator>>(istream& s, pair<T1, T2>& p) {
  return s >> p.first >> p.second;
}
template <class T1, class T2>
inline ostream& operator<<(ostream& s, const pair<T1, T2>& p) {
  return s << "(" << p.first << " " << p.second << ")";
}
template <class T1>
inline ostream& operator<<(ostream& s, const set<T1>& container) {
  for (const auto& x : container) {
    s << x << ' ';
  }
  return s;
}
template <class T1>
inline ostream& operator<<(ostream& s, const vector<T1>& container) {
  for (const auto& x : container) {
    s << x << ' ';
  }
  return s;
}
template <class T1>
inline istream& operator>>(istream& s, vector<T1>& container) {
  for (auto& x : container) {
    s >> x;
  }
  return s;
}
template <class T>
inline T euclide(T a, T b, T& x, T& y) {
  if (a < 0) {
    T d = euclide(-a, b, x, y);
    x = -x;
    return d;
  }
  if (b < 0) {
    T d = euclide(a, -b, x, y);
    y = -y;
    return d;
  }
  if (b == 0) {
    x = 1;
    y = 0;
    return a;
  } else {
    T d = euclide(b, a % b, x, y);
    T t = x;
    x = y;
    y = t - (a / b) * y;
    return d;
  }
}
template <class T>
inline T gcd(T a, T b) {
  while (b > 0) {
    T tmp = a;
    a = a % b;
    b = tmp;
  }
  return a;
}
template <class T>
inline vector<pair<T, int>> factorize(T n) {
  vector<pair<T, int>> R;
  for (T i = 2; n > 1;) {
    if (n % i == 0) {
      int C = 0;
      for (; n % i == 0; C++, n /= i)
        ;
      R.push_back(make_pair(i, C));
    }
    i++;
    if (i > n / i) i = n;
  }
  if (n > 1) R.push_back(make_pair(n, 1));
  return R;
}
template <class T>
inline bool isPrimeNumber(T n) {
  if (n <= 1) return false;
  for (T i = 2; i * i <= n; i++)
    if (n % i == 0) return false;
  return true;
}
vector<bool> gen_primes(int limit) {
  vector<bool> is_prime(limit + 1, true);
  is_prime[0] = false;
  is_prime[1] = false;
  for (int i = 2; i * i <= limit; i += 1) {
    if (is_prime[i]) {
      for (int j = i + i; j <= limit; j += i) {
        is_prime[j] = false;
      }
    }
  }
  return is_prime;
}
template <int64_t P>
struct TZp {
  static_assert(P > 1, "P should be > 1");
  static_assert(numeric_limits<int64_t>::max() / P >= P,
                "P * P should be less then 2^63");
  int64_t Value = 0;
  TZp(int64_t val = 0) : Value(val) {}
  TZp& operator+=(const TZp& other) {
    Value = (Value + other.Value) % P;
    return *this;
  }
  TZp& operator-=(const TZp& other) {
    Value = (Value - other.Value + P) % P;
    return *this;
  }
  TZp& operator*=(const TZp& other) {
    Value = (Value * other.Value) % P;
    return *this;
  }
  TZp Inverse() const {
    int64_t x, y;
    euclide(Value, P, x, y);
    return x;
  }
  TZp& operator/=(const TZp& other) {
    Value = (Value * other.Inverse().Value) % P;
    return *this;
  }
  TZp operator-() const { return TZp((P - Value) % P); }
  TZp operator+(const TZp& other) const {
    TZp result(Value);
    result += other;
    return result;
  }
  TZp operator-(const TZp& other) const {
    TZp result(Value);
    result -= other;
    return result;
  }
  TZp operator*(const TZp& other) const {
    TZp result(Value);
    result *= other;
    return result;
  }
  TZp operator/(const TZp& other) const {
    TZp result(Value);
    result /= other;
    return result;
  }
  int64_t ToInt() const { return Value; }
};
template <class T>
struct TPlus {
  using Type = T;
  const static T Neutral;
  T operator()(const T& x, const T& y) const { return x + y; }
  T WithAdd(const T& original, const T& add, size_t n) const {
    return original + add * n;
  }
};
template <class T>
const T TPlus<T>::Neutral = 0;
template <class T>
struct TMax {
  using Type = T;
  static const T Neutral;
  T operator()(const T& x, const T& y) const { return max(x, y); }
  T WithAdd(const T& original, const T& add, size_t) const {
    return original + add;
  }
};
template <class T>
T const TMax<T>::Neutral = numeric_limits<T>::min();
template <class T>
struct TMin {
  using Type = T;
  static const T Neutral;
  T operator()(const T& x, const T& y) const { return min(x, y); }
  T WithAdd(const T& original, const T& add, size_t) const {
    return original + add;
  }
};
template <class T>
T const TMin<T>::Neutral = numeric_limits<T>::max();
template <class TF>
struct TSegmentTree {
  using T = typename TF::Type;
  size_t N = 0;
  TF Func;
  vector<T> Data;
  TSegmentTree(size_t n, const T& x = T(0), const TF& f = TF())
      : N(n), Func(f), Data(N * 2, x) {}
  void Set(size_t i, const T& x) {
    i += N;
    Data[i] = x;
    for (i /= 2; i > 0; i /= 2) {
      Data[i] = Func(Data[i * 2], Data[i * 2 + 1]);
    }
  }
  T Get(size_t left, size_t right) const {
    if (left >= right) {
      return TF::Neutral;
    }
    auto result = TF::Neutral;
    for (left += N, right += N; left < right; left /= 2, right /= 2) {
      if (left % 2) {
        result = Func(result, Data[left]);
        ++left;
      }
      if (right % 2) {
        --right;
        result = Func(result, Data[right]);
      }
    }
    return result;
  }
  const T& operator[](size_t i) const { return Data[i + N]; }
};
template <class TF>
struct TSegmentTreeIntervalAdd : public TSegmentTree<TF> {
  using TParent = TSegmentTree<TF>;
  using T = typename TParent::T;
  using TParent::Data;
  using TParent::Func;
  using TParent::N;
  vector<T> Add_;
  vector<size_t> ChildrenCount;
  void PropagateDownOne(size_t n) {
    if (n * 2 < Add_.size()) {
      Add_[n * 2] += Add_[n];
      if (n * 2 + 1 < Add_.size()) {
        Add_[n * 2 + 1] += Add_[n];
      }
    }
    Data[n] = Func.WithAdd(Data[n], Add_[n], ChildrenCount[n]);
    Add_[n] = 0;
  }
  void PropagateDown(size_t n) {
    if (n == 0) {
      return;
    }
    PropagateDown(n / 2);
    PropagateDownOne(n);
  }
  void PropagateUp(size_t n, const T& x) {
    if (n == 0) {
      return;
    }
    auto v = Func.WithAdd(Data[n], x, ChildrenCount[n]);
    Data[n / 2] = Func(Data[n ^ 1], v);
    for (size_t i = n / 2; i > 1; i /= 2) {
      Data[i / 2] = Func(Data[i], Data[i ^ 1]);
    }
  }
  TSegmentTreeIntervalAdd(size_t n, const T& x = T(0), const TF& f = TF())
      : TParent(n, x, f), Add_(2 * n, T(0)), ChildrenCount(2 * n, 0) {
    std::fill(ChildrenCount.begin() + N, ChildrenCount.end(), 1);
    for (size_t i = 2 * N - 1; i > 0; --i) {
      ChildrenCount[i / 2] += ChildrenCount[i];
    }
  }
  T Get(size_t left, size_t right) {
    if (left >= right) {
      return TF::Neutral;
    }
    PropagateDown((left + N) / 2);
    PropagateDown((right + N - 1) / 2);
    auto result = TF::Neutral;
    for (left += N, right += N; left < right; left /= 2, right /= 2) {
      if (left % 2) {
        PropagateDownOne(left);
        result = Func(result, Data[left]);
        ++left;
      }
      if (right % 2) {
        --right;
        PropagateDownOne(right);
        result = Func(result, Data[right]);
      }
    }
    return result;
  }
  void Add(size_t left, size_t right, const T& x) {
    for (left += N, right += N; left < right; left /= 2, right /= 2) {
      if (left % 2) {
        Add_[left] += x;
        PropagateUp(left, x);
        ++left;
      }
      if (right % 2) {
        --right;
        Add_[right] += x;
        PropagateUp(right, x);
      }
    }
  }
};
template <class T>
struct TDisjointIntervalSet : public set<pair<T, T>> {
  using TParent = set<pair<T, T>>;
  void Insert(T left, T right) {
    if (right <= left) {
      return;
    }
    auto it = TParent::upper_bound(make_pair(right, numeric_limits<T>::max()));
    auto it1 = it;
    while (it != TParent::begin()) {
      --it;
      if (it->second >= left) {
        remin(left, it->first);
        remax(right, it->second);
      } else {
        ++it;
        break;
      }
    }
    TParent::erase(it, it1);
    TParent::emplace(left, right);
  }
  void Remove(T left, T right) {
    if (right <= left) {
      return;
    }
    auto it = TParent::upper_bound(make_pair(right, numeric_limits<T>::max()));
    auto it1 = it;
    T l = left;
    T r = right;
    while (it != TParent::begin()) {
      --it;
      if (it->second >= left) {
        remin(l, it->first);
        remax(r, it->second);
      } else {
        ++it;
        break;
      }
    }
    TParent::erase(it, it1);
    if (l < left) {
      TParent::emplace(l, left);
    }
    if (r > right) {
      TParent::emplace(right, r);
    }
  }
};
struct TDSU {
  vector<size_t> Parents;
  vector<unordered_set<int>> SingleHops;
  TDSU(size_t n) : Parents(n), SingleHops(n) {
    iota(Parents.begin(), Parents.end(), 0);
  }
  size_t Get(size_t x) {
    if (Parents[x] != x) {
      Parents[x] = Get(Parents[x]);
    }
    return Parents[x];
  }
  void Unite(size_t x, size_t y) {
    x = Get(x);
    y = Get(y);
    if (x == y) {
      return;
    }
    if (SingleHops[x].size() > SingleHops[y].size()) {
      std::swap(x, y);
    }
    Parents[x] = y;
    SingleHops[y].insert(SingleHops[x].begin(), SingleHops[x].end());
    SingleHops[x].clear();
  }
};
int main(void) {
  std::ios::sync_with_stdio(false);
  int n, m, c, q;
  cin >> n >> m >> c >> q;
  vector<unordered_map<int, vector<int>>> roads(n);
  for (int i = 0; i < m; ++i) {
    int x, y, z;
    cin >> x >> y >> z;
    --x;
    --y;
    --z;
    roads[x][z].push_back(y);
    roads[y][z].push_back(x);
  }
  TDSU dsu(n);
  for (int i = 0; i < n; ++i) {
    for (auto& rs : roads[i]) {
      for (auto x : rs.second) {
        dsu.SingleHops[dsu.Get(i)].insert(x);
      }
      for (size_t j = 1; j < rs.second.size(); ++j) {
        dsu.Unite(rs.second[0], rs.second[j]);
      }
    }
  }
  for (int i = 0; i < q; ++i) {
    char s;
    cin >> s;
    if (s == '+') {
      int x, y, z;
      cin >> x >> y >> z;
      --x;
      --y;
      --z;
      dsu.SingleHops[dsu.Get(x)].insert(y);
      dsu.SingleHops[dsu.Get(y)].insert(x);
      {
        auto it = roads[x].find(z);
        if (it != roads[x].end()) {
          dsu.Unite(it->second[0], y);
        }
      }
      {
        auto it = roads[y].find(z);
        if (it != roads[y].end()) {
          dsu.Unite(it->second[0], x);
        }
      }
      roads[x][z].push_back(y);
      roads[y][z].push_back(x);
    } else {
      int x, y;
      cin >> x >> y;
      --x;
      --y;
      if (dsu.Get(x) == dsu.Get(y) || dsu.SingleHops[dsu.Get(x)].count(y)) {
        cout << "Yes" << endl;
      } else {
        cout << "No" << endl;
      }
    }
  }
  return 0;
}