#include <bits/stdc++.h>
using namespace std;
ifstream in("matrice2.in");
ofstream out("matrice2.out");
const int qmax = 2e4 + 5;
const int nmax = 3e2 + 5;
struct query
{
int ind;
int16_t x1, y1, x2, y2;
query(int ind = 0, int16_t x1 = 0, int16_t y1 = 0, int16_t x2 = 0, int16_t y2 = 0) : ind(ind), x1(x1), y1(y1), x2(x2), y2(y2) {}
};
struct queryNode
{
int st, dr;
int16_t depth;
vector<query *> queries;
queryNode(vector<query *> &queries, int st, int dr, int16_t depth = 0) : queries(queries), st(st), dr(dr), depth(depth) {}
};
struct dijNode
{
dijNode *p;
int h;
dijNode(int h = 0, dijNode *p = nullptr) : h(h), p(p) {}
dijNode *getParent()
{
if (p == nullptr)
{
return this;
}
else
{
p = p->getParent();
return p;
}
}
bool sameTree(dijNode *other)
{
return (getParent() == other->getParent());
}
void unite(dijNode *other)
{
dijNode *pa = getParent();
dijNode *pb = other->getParent();
if (pa == pb)
return;
if (pa->h < pb->h)
{
pa->p = pb;
}
else if (pa->h > pb->h)
{
pb->p = pa;
}
else
{
pb->p = pa;
pa->h++;
}
}
void reset()
{
p = nullptr;
h = 0;
}
};
int v[nmax][nmax]; // harta
unordered_map<int, vector<pair<int16_t, int16_t>>> coords; // coordonatele unei valori
vector<int> vals; // valori unice
int res[qmax]; // resultatul queriurilor
dijNode *nodes[nmax][nmax]; // matrice de pointere catre noduri de multimi djuncte
int lastInd;
int diffx[] = {0, 1, 0, -1};
int diffy[] = {1, 0, -1, 0};
int n, q;
void uniteTill(int poz)
{
for (int i = lastInd - 1; i >= poz; i--)
{
int val = vals[i]; // valoarea cautata
for (auto j : coords[val])
{
auto tmpNode = nodes[j.first][j.second]; // nodul cu val
for (int k = 0; k < 4; k++)
{
int x = j.first + diffx[k];
int y = j.second + diffy[k];
if (x >= 1 && x <= n && y >= 1 && y <= n)
{
if (v[x][y] >= val)
{
tmpNode->unite(nodes[x][y]);
}
}
}
}
}
}
bool eval(query *q)
{
return nodes[q->x1][q->y1]->sameTree(nodes[q->x2][q->y2]);
}
int main()
{
in >> n >> q;
set<int> s;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
in >> v[i][j];
nodes[i][j] = new dijNode();
coords[v[i][j]].push_back({i, j});
s.insert(v[i][j]);
}
}
for (auto i : s)
vals.push_back(i);
vector<query *> queries;
for (int i = 0; i < q; i++)
{
int x1, y1, x2, y2;
in >> x1 >> y1 >> x2 >> y2;
queries.push_back(new query(i, x1, y1, x2, y2));
}
lastInd = vals.size();
queue<queryNode *> qq;
qq.push(new queryNode(queries, 0, vals.size() - 1));
int lastDepth = vals.size();
while (!qq.empty())
{
queryNode *nod = qq.front();
qq.pop();
if (nod->st == nod->dr)
{
for (auto i : nod->queries)
{
res[i->ind] = vals[nod->st];
}
}
else
{
int mid = (nod->st + nod->dr + 1) / 2;
if (lastDepth != nod->depth)
{
lastInd = vals.size();
lastDepth = nod->depth;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
nodes[i][j]->reset();
}
}
}
uniteTill(mid);
lastInd = mid;
vector<query *> st, dr;
for (auto i : nod->queries)
{
if (eval(i))
{
dr.push_back(i);
}
else
{
st.push_back(i);
}
}
qq.push(new queryNode(dr, mid, nod->dr, nod->depth + 1));
qq.push(new queryNode(st, nod->st, mid - 1, nod->depth + 1));
delete nod;
}
}
for (int i = 0; i < q; i++)
{
out << res[i] << '\n';
}
}