#include <bits/stdc++.h>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/tree_policy.hpp>
#include <ext/rope>
#define ll long long
#define pb push_back
#define sz(x) (int)(x).size()
#define mp make_pair
#define f first
#define s second
#define all(x) x.begin(), x.end()
#define D(x) cerr << #x << " is " << (x) << "\n";
using namespace std;
using namespace __gnu_pbds;
using namespace __gnu_cxx;
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
template<class T> using ordered_set = tree<T, null_type, less<T>, rb_tree_tag,tree_order_statistics_node_update>; ///find_by_order(),order_of_key()
template<class T1, class T2> ostream& operator<<(ostream& os, const pair<T1,T2>& a) { os << '{' << a.f << ", " << a.s << '}'; return os; }
template<class T> ostream& operator<<(ostream& os, const vector<T>& a){os << '{';for(int i=0;i<sz(a);i++){if(i>0&&i<sz(a))os << ", ";os << a[i];}os<<'}';return os;}
template<class T> ostream& operator<<(ostream& os, const set<T>& a) {os << '{';int i=0;for(auto p:a){if(i>0&&i<sz(a))os << ", ";os << p;i++;}os << '}';return os;}
template<class T> ostream& operator<<(ostream& os, const multiset<T>& a) {os << '{';int i=0;for(auto p:a){if(i>0&&i<sz(a))os << ", ";os << p;i++;}os << '}';return os;}
template<class T1,class T2> ostream& operator<<(ostream& os, const map<T1,T2>& a) {os << '{';int i=0;for(auto p:a){if(i>0&&i<sz(a))os << ", ";os << p;i++;}os << '}';return os;}
const int N=300*300+1;
struct node{
int l,r,le,ri,val;
};
vector<node> tre;
int copyNode(int i)
{
node ne{tre[i].l,tre[i].r,tre[i].le,tre[i].ri,tre[i].val};
tre.pb(ne);
return tre.size()-1;
}
int build(int l=0,int r=N-1)
{
node ne{l,r,-1,-1,-1};
if(l==r)
{
ne.val=l;
tre.pb(ne);
return tre.size()-1;
}
int m=(l+r)>>1;
ne.le=build(l,m);
ne.ri=build(m+1,r);
tre.pb(ne);
return tre.size()-1;
}
int root;
int sett(int pos,int val,int tr=root)
{
if(pos<tre[tr].l||pos>tre[tr].r)
return tr;
int ne=copyNode(tr);
if(tre[ne].l==pos&&tre[ne].r==pos)
{
tre[ne].val=val;
return ne;
}
tre[ne].le=sett(pos,val,tre[ne].le);
tre[ne].ri=sett(pos,val,tre[ne].ri);
return ne;
}
int get(int pos,int tr=root)
{
if(pos<tre[tr].l||pos>tre[tr].r)
return 0;
if(tre[tr].l==pos&&tre[tr].r==pos)
return tre[tr].val;
return get(pos,tre[tr].le)+get(pos,tre[tr].ri);
}
int find(int a,int tr,bool change=false)
{
int p=get(a,tr);
if(p==a)
return a;
int b=find(p,tr,change);
if(change)
root=sett(a,b);
return b;
}
void merge(int a,int b)
{
a=find(a,root,true);
b=find(b,root,true);
if(a==b)
return;
root=sett(a,b);
}
int n;
int convert(int a,int b)
{
return a*n+b;
}
bool inside(int x,int y)
{
return x>=0&&x<n&&y>=0&&y<n;
}
vector<int> dx={1,-1,0,0},dy={0,0,1,-1};
int main()
{
FILE *in=fopen("matrice2.in","r"),*out=fopen("matrice2.out","w");
int q;
fscanf(in,"%i %i",&n,&q);
vector<int> vals;
vector<pair<int,pair<int,int> > > mat;
vector<int> order;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
{
int m;
fscanf(in,"%i",&m);
mat.pb({m,{i,j}});
vals.pb(m);
order.pb(m);
}
root=build();
sort(all(vals));
sort(all(mat));
vals.erase(unique(all(vals)),vals.end());
reverse(all(vals));
reverse(all(mat));
int m=vals.size();
vector<int> kojiroot(m);
int tr=0;
for(int i=0;i<m;i++)
{
while(tr<(int)mat.size()&&mat[tr].f==vals[i])
{
int x=mat[tr].s.f,y=mat[tr].s.s;
for(int k=0;k<4;k++)
{
int xx=x+dx[k],yy=y+dy[k];
if(inside(xx,yy)&&order[convert(xx,yy)]>=mat[tr].f)
merge(convert(xx,yy),convert(x,y));
}
tr++;
}
kojiroot[i]=root;
}
for(int i=0;i<q;i++)
{
int x1,y1,x2,y2;
fscanf(in,"%i %i %i %i",&x1,&y1,&x2,&y2);
x1--;y1--;x2--;y2--;
int a=convert(x1,y1),b=convert(x2,y2);
int l=0,r=m-1;
while(l<r)
{
int mm=(l+r)>>1;
if(find(a,kojiroot[mm])==find(b,kojiroot[mm]))
r=mm;
else
l=mm+1;
}
fprintf(out,"%i\n",vals[l]);
}
return 0;
}