#include <fstream>
#define MOD 666013
using namespace std;
ifstream cin ("arbint.in");
ofstream cout ("arbint.out");
/// arbori de intervale
/// se face un vector unde copii ai indicele 2i si 2i+1, i fiind indicele parintelui
/// cand se cauta prop unui interval anume consideram numai intervalele incluse in intervalul dat
/// daca un interval contine info de valoare atunci coboram pe el
/// un nod este caracterizat in cazul nostru printr-o pozitie in vectorul v[] si un interval st-dr, deci la fiecare functie recursiva sunt date aceste valori
/// pentru a descrie nodul curent, si se pleaca din nodul 'cap' care este poz pe v[1]
int v[500009] ;
/// primul pas este updateul in arbore
/// se cauta practic binar locul unde va va fi inserat elemenul curent
/// se face aceasta pentru a retine capabilitatea de a schimba un element de ex daca vrem sa zicem ca v[15] = 500 dar precedent v[15] = 100
/// astfel se compara la fiecare pas cele doua intervale in care este splituit intervalul de pe nodul curent cu indicele elementului care trebuie modificat
/// se alege intervalul in care este indicele elementului si se continua pe acea ramura pana cand intervalul curent consista dintr-un singur element,
/// chiar indicele pe care trebuie sa il schimbam
/// pentru a pastra proprietatiile arborelui, dupa apelul recursiv se updateaza fiecare nod din arbore afectat de schimbarea acelui indice prin acel v[poz] = max(...) ;
void update(int st, int dr, int f, int poz, int a)
{
/// st si dreapta este rangeul in care updatam
/// a este valoarea vare o bagam iar f este indicele acestei valori
/// poz este pozitia in v[] a nodului curent (cand dam update mereu incepem din cap, nodul initial)
int mij = (st + dr) / 2 ;
///cout << st << " " << dr << endl ;
if(st == dr)
{
v[poz] = a ;
return ;
}
if(f > mij)
{
update(mij + 1, dr, f, poz * 2 + 1, a) ;
v[poz] = max(v[poz * 2], v[poz * 2 + 1]) ;
}
else
{
update(st, mij, f, poz * 2, a) ;
v[poz] = max(v[poz * 2], v[poz * 2 + 1]) ;
}
}
/// pentru un query parcurgem arborele si consideram fiecare nod
/// daca nodul curent este complet in afara intervalului care ne intereseaza pe noi atunci returnam o valoare nesemnificativa, 0 in cazul nostru
/// daca nodul curent este complet inauntrul intervalului care ne intereaseaza returnam valoarea sa (de aici vine eficienta algoritmului)
/// altfel returnam ceea ce ne intereseaza dintre copii nodului curent, in cazul nostru maximul dintre cei doi
int query(int st, int dr, int poz, int ST, int DR)
{
/// daca intervalul curent este complet inauntrul celui cautat returnam val lui
/// alftel returnam valoarea din el
/// daca intervalul e off rau ii dam return 0 ;
if(DR < st || ST > dr)return 0 ;
if(ST >= st && DR <= dr)return v[poz] ;
return max(query(st, dr, poz * 2, ST, (ST + DR) / 2), query(st, dr, poz * 2 + 1, (ST + DR) / 2 + 1, DR)) ;
}
int main()
{
int n, q ;
cin >> n >> q ;
for(int f = 1, a ; f <= n ; f ++)
{
cin >> a ;
update(1, n, f, 1, a) ;
}
while(q --)
{
int cer, a, b ;
cin >> cer >> a >> b ;
if(!cer)cout << query(a, b, 1, 1, n) << '\n' ;
else update(1, n, a, 1, b) ;
}
return 0;
}
/// 3 5 6 2