#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <climits>
#include <limits>
#include <functional>

namespace structure {
  template<typename T, size_t dcap = 16ULL>
  class Vector {
    size_t sz;
    size_t cap;
    T* a;
    void extend() {
      size_t ncap = cap << 1;
      T* b = new T[ncap];
      for (int i = 0; i < cap; i++) b[i] = a[i];
      delete[] a;
      a = b;
      cap = ncap;
    }
  public:
    Vector() : sz(0), cap(dcap) {
      a = new T[cap];
    }
    Vector(size_t n) : sz(n), cap(dcap) {
      while (cap < sz) cap <<= 1;
      a = new T[cap];
    }
    Vector(size_t n, const T& v) : sz(n), cap(dcap) {
      while (cap < sz) cap <<= 1;
      a = new T[cap];
      for (size_t i = 0; i < sz; i++) {
        a[i] = v;
      }
    }
    Vector(const Vector<T>& v) {
      sz = v.sz;
      cap = v.cap;
      a = new T[cap];
      std::memcpy(a, v.a, sizeof(T) * sz);
    }
    Vector<T>& operator=(const Vector<T>& v) {
      sz = v.sz;
      cap = v.cap;
      delete[] a;
      a = new T[cap];
      std::memcpy(a, v.a, sizeof(T) * sz);
      return *this;
    }
    T& operator[](size_t i) {
      return a[i];
    }
    T front() const {
      return a[0];
    }
    T back() const {
      return a[sz - 1];
    }
    void push_back(const T& x) {
      if (sz == cap) extend();
      a[sz++] = x;
    }
    void pop_back() {
      sz--;
    }
    size_t size() const {
      return sz;
    }
    bool empty() const {
      return !sz;
    }
    void clear() {
      sz = 0;
    }
    void output(std::ostream& out, char delim = ' ') const {
      for (size_t i = 0; i < sz; i++) {
        if (i) out << delim;
        out << a[i];
      }
    }
    friend std::ostream& operator<<(std::ostream& out, const Vector& v) {
      v.output(out);
      return out;
    }
    void input(std::istream& in) {
      for (size_t i = 0; i < sz; i++) {
        in >> a[i];
      }
    }
    friend std::istream& operator>>(std::istream& in, Vector& v) {
      v.input(in);
      return in;
    }
    ~Vector() {
      delete[] a;
    }
  };
  template<typename T, size_t dcap = 16ULL>
  class Deque {
    size_t begin, end;
    size_t sz;
    size_t cap;
    T* a;
    void extend() {
      size_t ncap = cap << 1;
      T* b = new T[ncap];
      for (size_t i = begin; i < begin + cap; i++) b[i] = a[i & (cap - 1)];
      end += cap;
      delete[] a;
      a = b;
      cap = ncap;
    }
  public:
    Deque() : begin(0), end(0), sz(0), cap(dcap) {
      a = new T[cap];
    }
    Deque(size_t n) : begin(0), end(n), sz(n), cap(dcap) {
      while (cap < sz) cap <<= 1;
      a = new T[cap];
    }
    Deque(size_t n, const T& v) : begin(0), end(n), sz(n), cap(dcap) {
      while (cap < sz) cap <<= 1;
      a = new T[cap];
      for (size_t i = 0; i < sz; i++) {
        a[i] = v;
      }
    }
    T& front() {
      return a[begin];
    }
    T& back() {
      return a[(end - 1) & (cap - 1)];
    }
    void push_back(const T& x) {
      if (sz == cap) extend();
      a[end++] = x;
      sz++;
      end &= cap - 1;
    }
    void push_front(const T& x) {
      if (sz == cap) extend();
      begin = (begin - 1) & (cap - 1);
      a[begin] = x;
      sz++;
    }
    void pop_front() {
      begin = (begin + 1) & (cap - 1);
      sz--;
    }
    void pop_back() {
      end = (end - 1) & (cap - 1);
      sz--;
    }
    T& operator[](size_t i) {
      return a[(begin + i) & (cap - 1)];
    }
    size_t size() const {
      return sz;
    }
    bool empty() const {
      return !sz;
    }
    void output(std::ostream& out, char delim = ' ') const {
      if (sz == cap) {
        for (size_t i = begin; i != (cap + end - 1) & (cap - 1); i = (i + 1) & (cap - 1)) {
          if (i != begin) out << delim;
          out << a[i];
        }
        out << delim << a[(cap + end - 1) & (cap - 1)];
      }
      else {
        for (size_t i = begin; i != end; i = (i + 1) & (cap - 1)) {
          if (i != begin) out << delim;
          out << a[i];
        }
      }
    }
    friend std::ostream& operator<<(std::ostream& out, const Deque& v) {
      v.output(out);
      return out;
    }
    ~Deque() {
      delete[] a;
    }
  };
  template<typename S, typename T>
  struct Pair {
    S first;
    T second;
    Pair() {}
    Pair(S first, T second) : first(first), second(second) {}
    void output(std::ostream& out, char delim = ' ') const {
      out << first << delim << second;
    }
    friend std::ostream& operator<<(std::ostream& out, const Pair& p) {
      p.output(out);
      return out;
    }
    void input(std::istream& in) {
      in >> first >> second;
    }
    friend std::istream& operator>>(std::istream& in, Pair& p) {
      p.input(in);
      return in;
    }
  };
  template<typename T>
  class List {
    struct Node {
      T val;
      Node* prev;
      Node* next;
      Node() : prev(nullptr), next(nullptr) {}
      Node(Node* prev, Node* next) : prev(prev), next(next) {}
      Node(const T& val, Node* prev, Node* next) : val(val), prev(prev), next(next) {}
    };
    Node* head;
    size_t sz;
  public:
    List() {
      head = new Node;
      head->next = head->prev = head;
    }
    Node* begin() const {
      return head->next;
    }
    Node* end() const {
      return head;
    }
    Node* insert(Node* itr, const T& x) {
      Node* n = new Node(x, itr->prev, itr);
      itr->prev->next = n;
      itr->prev = n;
      sz++;
      return n;
    }
    Node* push_front(const T& val) {
      return insert(head->next);
    }
    Node* push_back(const T& val) {
      return insert(head);
    }
    Node* distance(Node* itr, int d) const {
      if (d < 0) {
        d = -d;
        for (int i = 0; i < d; i++) itr = itr->prev;
      }
      else if (d > 0) {
        for (int i = 0; i < d; i++) itr = itr->next;
      }
      return itr;
    }
    Node* erase(Node* n) {
      Node* ret = n->next;
      n->prev->next = n->next;
      n->next->prev = n->prev;
      delete n;
      sz--;
      return ret;
    }
    Node* pop_front() {
      return erase(head->next);
    }
    Node* pop_back() {
      return erase(head->prev);
    }
    Node* erase(const T& val) {
      for (Node* n = head->next; n != head; n = n->next) {
        if (n->val == val) {
          return erase(n);
        }
      }
      return nullptr;
    }
    size_t size() const {
      return sz;
    }
    void output(std::ostream& out, char delim = ' ') const {
      for (Node* n = head->next; n != head; n = n->next) {
        if (n != head->next) out << delim;
        out << n->val;
      }
    }
    friend std::ostream& operator<<(std::ostream& out, const List& l) {
      l.output(out);
      return out;
    }
  };
  template<typename T>
  class Treap {
    uint64_t _r = 88172645463325252LL;
    unsigned rnd() {
      _r = _r ^ (_r << 7);
      return _r = _r ^ (_r >> 9);
    }
    struct Node {
      T key;
      unsigned priority;
      Node* parent;
      Node* left;
      Node* right;
      Node() {}
      Node(const T& key) : key(key), priority(rnd()), parent(nullptr), left(nullptr), right(nullptr) {}
      Node(const T& key, unsigned priority) : key(key), priority(priority), parent(nullptr), left(nullptr), right(nullptr) {}
    };
    Node* minimum(Node* x) {
      while (x->left) x = x->left;
      return x;
    }
    Node* successor(Node* x) {
      if (x->right) return minimum(x->right);
      Node* y = x->parent;
      while (y && x == y->right) {
        x = y;
        y = y->parent;
      }
      return y;
    }
    Node* right_rotate(Node* t) {
      Node* s = t->left;
      t->left = s->right;
      s->right = t;
      return s;
    }
    Node* left_rotate(Node* t) {
      Node* s = t->right;
      t->right = s->left;
      s->left = t;
      return s;
    }
    Node* insert(Node* t, const T& key, unsigned priority) {
      if (!t) return new Node(key, priority);
      if (key == t->key) return t;
      if (key < t->key) {
        t->left = insert(t->left, key, priority);
        if (t->priority < t->left->priority) t = right_rotate(t);
      }
      else {
        t->right = insert(t->right, key, priority);
        if (t->priority < t->right->priority) t = left_rotate(t);
      }
      return t;
    }
    Node* erase(Node* t, const T& key) {
      if (!t) return t;
      if (key < t->key) t->left = erase(t->left, key);
      else if (key > t->key) t->right = erase(t->right, key);
      else return _erase(t, key);
      return t;
    }
    Node* _erase(Node* t, const T& key) {
      if (!t->left && !t->right) {
        delete t;
        return nullptr;
      }
      else if (!t->left) t = left_rotate(t);
      else if (!t->right) t = right_rotate(t);
      else t = t->left->priority > t->right->priority ? right_rotate(t) : left_rotate(t);
      return erase(t, key);
    }
    void free(Node* x) {
      if (!x) return;
      if (x->left) free(x->left);
      if (x->right) free(x->right);
      delete x;
    }
    Node* root;
  public:
    Treap() : root(nullptr) {}
    void insert(const T& key) {
      root = insert(root, key, rnd());
    }
    void insert(const T& key, unsigned priority) {
      root = insert(root, key, priority);
    }
    Node* find(const T& key) const {
      Node* x = root;
      while (x && key != x->key) x = key < x->key ? x->left : x->right;
      return x;
    }
    void erase(const T& key) {
      root = erase(root, key);
    }
    void print_preorder(const Node* n) const {
      if (!n) return;
      std::cout << ' ' << n->key;
      print_preorder(n->left);
      print_preorder(n->right);
    }
    void print_preorder() const {
      print_preorder(root);
    }
    void print_inorder(const Node* n) const {
      if (!n) return;
      print_inorder(n->left);
      std::cout << ' ' << n->key;
      print_inorder(n->right);
    }
    void print_inorder() const {
      print_inorder(root);
    }
    void print_postorder(const Node* n) const {
      if (!n) return;
      print_postorder(n->left);
      print_postorder(n->right);
      std::cout << ' ' << n->key;
    }
    void print_postorder() const {
      print_postorder(root);
    }
    ~Treap() {
      free(root);
    }
  };
  template<typename T>
  class Heap {
    using F = std::function<bool(T, T)>;
    F cmp;
    size_t sz;
    Vector<T> a;
    void ascend(int i) {
      if (!i) return;
      int p = (i - 1) >> 1;
      if (cmp(a[i], a[p])) {
        T tmp = a[p];
        a[p] = a[i];
        a[i] = tmp;
        ascend(p);
      }
    }
    void descend(int i) {
      int l = i * 2 + 1, r = i * 2 + 2, largest;
      if (l < sz && cmp(a[l], a[i])) largest = l;
      else largest = i;
      if (r < sz && cmp(a[r], a[largest])) largest = r;
      if (largest != i) {
        T tmp = a[i];
        a[i] = a[largest];
        a[largest] = tmp;
        descend(largest);
      }
    }
    void build() {
      for (int i = sz / 2; i >= 0; i--) {
        descend(i);
      }
    }
  public:
    Heap() : sz(0), cmp([](const T& a, const T& b) { return a > b; }) {}
    Heap(const F cmp) : sz(0), cmp(cmp) {}
    Heap(const Vector<T>& data, const F cmp) : sz(data.size()), cmp(cmp), a(data) {
      build();
    }
    Heap(const Heap<T>& h) {
      cmp = h.cmp;
      sz = h.sz;
      a = h.a;
    }
    Heap& operator=(const Heap<T>& h) {
      cmp = h.cmp;
      sz = h.sz;
      a = h.a;
      return *this;
    }
    void push(const T& key) {
      a.push_back(key);
      ascend((int)a.size() - 1);
      sz++;
    }
    T top() {
      return a[0];
    }
    void pop() {
      T ret = a[0];
      a[0] = a.back();
      a.pop_back();
      sz--;
      descend(0);
    }
    size_t size() const {
      return sz;
    }
    bool empty() const {
      return !sz;
    }
    Vector<T> data() const {
      return a;
    }
  };
  class DisjointSet {
  private:
    Vector<int> par;
    Vector<int> rank;
    Vector<int> sz;	// 要素の個数
    int set_sz;			// 集合の個数
  public:
    DisjointSet() : set_sz(0) {}
    DisjointSet(int n) : par(n), rank(n, 0), sz(n, 1), set_sz(n) {
      for (int i = 0; i < n; i++) par[i] = i;
    }
    int set_size() { return set_sz; }
    int size(int v) { return sz[find(v)]; }
    void makeset() {
      par.push_back((int)par.size());
      rank.push_back(0);
      sz.push_back(1);
      set_sz++;
    }
    void makeset(int n) {
      for (int i = 0; i < n; i++) makeset();
    }
    int find(int x) {
      return par[x] == x ? x : par[x] = find(par[x]);
    }
    void unite(int x, int y) {
      x = find(x); y = find(y);
      if (x == y) return;
      if (rank[x] < rank[y]) par[x] = y;
      else {
        par[y] = x;
        if (rank[x] == rank[y]) rank[x]++;
      }
      sz[x] += sz[y];
      sz[y] = sz[x];
      set_sz--;
    }
    bool same(int x, int y) {
      return find(x) == find(y);
    }
    friend std::ostream& operator<<(std::ostream& out, const DisjointSet& ds) {
      out
        << "\nparent : " << ds.par
        << "\nrank : " << ds.rank
        << "\nsize : " << ds.sz
        << "\nsize of set : " << ds.set_sz
        << std::endl;
      return out;
    }
  };
  // T have +-*/ and 0
  template<class T> class BIT {
  public:
    Vector<T> dat;
    int N;
    BIT() {}
    BIT(int N) : N(N), dat(N, 0) {}
    // sum [0,c)
    T sum(int i) {
      T ret = 0;
      for (--i; i >= 0; i = (i & (i + 1)) - 1) ret += dat[i];
      return ret;
    }
    // sum [c,j1)
    T sum(int i1, int i2) { return sum(i2) - sum(i1); }
    T at(int i) { return sum(i, i + 1); }
    // add x to c
    void add(int i, T x) { for (; i < N; i |= i + 1) dat[i] += x; }
  };
}

namespace algorithm {
  template<typename T>
  void insertion_sort(structure::Vector<T>& v, bool verbose = false) {
    for (int i = 0, n = v.size(); i < n; i++) {
      int x = v[i], j;
      for (j = i - 1; j >= 0 && v[j] > x; j--) {
        v[j + 1] = v[j];
      }
      v[j + 1] = x;
      if (verbose) std::cout << v << std::endl;
    }
  }
  template<typename T>
  size_t bubble_sort(structure::Vector<T>& v) {
    size_t cnt = 0;
    int n = v.size();
    for (int i = 0; i < n - 1; i++) {
      for (int j = n - 1; j > i; j--) {
        if (v[j] < v[j - 1]) {
          T t = v[j - 1];
          v[j - 1] = v[j];
          v[j] = t;
          cnt++;
        }
      }
    }
    return cnt;
  }
  template<typename T>
  size_t selection_sort(structure::Vector<T>& v) {
    size_t cnt = 0;
    int n = v.size();
    for (int i = 0; i < n; i++) {
      int minj = i;
      for (int j = i; j < n; j++) {
        if (v[j] < v[minj]) minj = j;
      }
      T t = v[i];
      v[i] = v[minj];
      v[minj] = t;
      cnt += (v[i] != v[minj]);
    }
    return cnt;
  }
  template<typename T>
  size_t upper_bound(structure::Vector<T>& v, const T& x) {
    // x より大きい最初の要素の添字を返す
    if (x < v[0]) return 0;
    if (v.back() <= x) return v.size();
    int ng = 0, ok = v.size() - 1;
    while (abs(ok - ng) > 1) {
      int mid = (ng + ok) / 2;
      (v[mid] > x ? ok : ng) = mid;
    }
    return ok;
  }
  template<typename T>
  size_t count(structure::Vector<T>& v, const T& x) {
    return upper_bound(v, x) - upper_bound(v, x - 1);
  }
  namespace impl {}
  namespace {
    namespace impl {
      template<typename T>
      void merge(structure::Vector<T>& v, int l, int m, int r) {
        T inf = std::numeric_limits<T>::max();
        int i = l, j = 0, k = 0, n1 = m - l, n2 = r - m;
        int* L = new int[n1 + 1];
        int* R = new int[n2 + 1];
        std::memcpy(L, &v[l], sizeof(T) * n1);
        std::memcpy(R, &v[m], sizeof(T) * n2);
        L[n1] = R[n2] = inf;
        while (i < r) {
          L[j] <= R[k] ? v[i] = L[j++] : v[i] = R[k++];
          i++;
        }
        delete[] L;
        delete[] R;
      }
      template<typename T>
      void align(structure::Vector<T>& v, int l, int r) {
        if (l + 1 < r) {
          int m = (l + r) / 2;
          align(v, l, m);
          align(v, m, r);
          merge(v, l, m, r);
        }
      }
    }
    template<typename T>
    void merge_sort(structure::Vector<T>& v) {
      impl::align(v, 0, v.size());
    }
  }
  template<typename T>
  struct MergeSorter {
    using F = std::function<bool(T, T)>;
    const F cmp;
    const T inf;
    structure::Vector<T> v;
    MergeSorter(const structure::Vector<T>& v, const T& inf, const F cmp) : v(v), inf(inf), cmp(cmp) {}
    void merge(int l, int m, int r) {
      int i = l, j = 0, k = 0, n1 = m - l, n2 = r - m;
      T* L = new T[n1 + 1];
      T* R = new T[n2 + 1];
      std::memcpy(L, &v[l], sizeof(T) * n1);
      std::memcpy(R, &v[m], sizeof(T) * n2);
      L[n1] = R[n2] = inf;
      while (i < r) {
        cmp(L[j], R[k]) ? v[i] = L[j++] : v[i] = R[k++];
        i++;
      }
      delete[] L;
      delete[] R;
    }
    void align(int l, int r) {
      if (l + 1 < r) {
        int m = (l + r) / 2;
        align(l, m);
        align(m, r);
        merge(l, m, r);
      }
    }
    void sort() {
      align(0, v.size());
    }
  };
  template<typename T>
  struct QuickSorter {
    using F = std::function<bool(T, T)>;
    const F cmp;
    structure::Vector<T> v;
    QuickSorter(const structure::Vector<T>& v, const F cmp) : v(v), cmp(cmp) {}
    int partition(int p, int r) {
      T x = v[r], tmp;
      int i = p - 1;
      for (int j = p; j < r; j++) {
        if (cmp(v[j], x)) {
          i++; tmp = v[i]; v[i] = v[j]; v[j] = tmp;
        }
      }
      i++; tmp = v[i]; v[i] = v[r]; v[r] = tmp;
      return i;
    }
    void align(int p, int r) {
      if (p < r) {
        int q = partition(p, r);
        align(p, q - 1);
        align(q + 1, r);
      }
    }
    void sort() {
      align(0, v.size() - 1);
    }
  };
}



int main() {
  using namespace structure;
  using namespace algorithm;
  using std::cin;
  using std::cout;
  using std::cerr;
  using std::endl;
  using std::string;

  std::ios::sync_with_stdio(false);
  cin.tie(0);

  int n, q;
  cin >> n >> q;

  Vector<Heap<int>> vs(n);

  for (int i = 0; i < q; i++) {
    int op, t, x;
    cin >> op;
    switch (op) {
    case 0:
      cin >> t >> x;
      vs[t].push(x);
      break;
    case 1:
      cin >> t;
      if(!vs[t].empty()) cout << vs[t].top() << endl;
      break;
    default:
      cin >> t;
      if(!vs[t].empty()) vs[t].pop();
      break;
    }
  }  

  return 0;
}


Measured memory_bytes: 4140