题目描述
【题意】
给一个N*M的数矩阵
现在求一个子矩阵 要求子矩阵中最大值与最小值的差<=C。而且子矩阵的宽度(横)不超过100(长(竖)没有限制)。 求子矩阵的最大面积。
【输入格式】
第一行两个整数 M(左右方向),N(上下方向)和 C (N,M<=500 0<=C<= 10 )
接下来 N行 每行M个数 每个数(-30000~30000)
【输出格式】
子矩阵的最大面积
【样例输入】
10 15 4
41 40 41 38 39 39 40 42 40 40
39 40 43 40 36 37 35 39 42 42
44 41 39 40 38 40 41 38 35 37
38 38 33 39 36 37 32 36 38 40
39 40 39 39 39 40 40 41 43 41
39 40 41 38 39 38 39 39 39 42
36 39 39 39 39 40 39 41 40 41
31 37 36 41 41 40 39 41 40 40
40 40 40 42 41 40 39 39 39 39
42 40 44 40 38 40 39 39 37 41
41 41 40 39 39 40 41 40 39 40
47 45 49 43 43 41 41 40 39 42
42 41 41 39 40 39 42 40 42 42
41 44 49 43 46 41 42 41 42 42
45 40 42 42 46 42 44 40 42 41
【样例输出】
35
这是一个好题啊啊啊!感觉我不能直接做出来都是好题。。
题目叫做单调队列,因为这是scy给我们单调队列的作业啦
一开始我还是很务正业的,写了一发单调队列
但是由于是暴力枚举答案+判断,判断可以类似bzoj的理想正方形
然后复杂度有点爆炸
于是就T到不知道哪里去了。。但居然有80分。。
(下面代码由于数据范围改了,所以有一点点冲突。。但问题不大)
#include<cstdio>
#include<cstring>
const int MAX=1<<30;
const int N=705;
int m,n,c;
int a[N][N];
int ans=0;
int mymax (int x,int y){return x>y?x:y;}
int mymin (int x,int y){return x<y?x:y;}
int f[N][N];//第i行以j结尾的前n个数的最大(横着)
int f1[N][N];//第i行以j结尾的前n个数的最小(横着)
int f2[N][N];//第i行以j结尾的前n个数的最大(竖着)
int f3[N][N];//第i行以j结尾的前n个数的最小(竖着)
int q[N],q1[N];
void heng (int x,int y)
{
for (int u=1;u<=n;u++)
{
f[u][1]=a[u][1];q[1]=a[u][1];q1[1]=1;
int st=1,ed=1;
for (int i=2;i<=m;i++)
{
while (st<=ed&&q1[st]<=i-y) st++;
while (st<=ed&&q[ed]<=a[u][i]) ed--;
q[++ed]=a[u][i];q1[ed]=i;f[u][i]=q[st];
}
}
for (int u=1;u<=n;u++)
{
f1[u][1]=a[u][1];q[1]=a[u][1];q1[1]=1;
int st=1,ed=1;
for (int i=2;i<=m;i++)
{
while (st<=ed&&q1[st]<=i-y) st++;
while (st<=ed&&q[ed]>=a[u][i]) ed--;
q[++ed]=a[u][i];q1[ed]=i;f1[u][i]=q[st];
}
}
}
int shu (int x,int y)
{
for (int u=1;u<=m;u++)
{
f2[1][u]=f[1][u];q[1]=f[1][u];q1[1]=1;
int st=1,ed=1;
for (int i=2;i<=n;i++)
{
while (st<=ed&&q1[st]<=i-x) st++;
while (st<=ed&&q[ed]<=f[i][u]) ed--;
q[++ed]=f[i][u];q1[ed]=i;f2[i][u]=q[st];
}
}
for (int u=1;u<=m;u++)
{
f3[1][u]=f1[1][u];q[1]=f1[1][u];q1[1]=1;
int st=1,ed=1;
for (int i=2;i<=n;i++)
{
while (st<=ed&&q1[st]<=i-x) st++;
while (st<=ed&&q[ed]>=f1[i][u]) ed--;
q[++ed]=f1[i][u];q1[ed]=i;f3[i][u]=q[st];
}
}
}
bool check (int x,int y)//问题转化为在一个n*m的图中是否存在一个x*y的矩阵使得那啥
{
heng(x,y);
shu(x,y);
for (int u=x;u<=n;u++)
for (int i=y;i<=m;i++)
if (f2[u][i]-f3[u][i]<=c)
return true;
return false;
}
int main()
{
scanf("%d%d%d",&m,&n,&c);
for (int u=1;u<=n;u++)
for (int i=1;i<=m;i++)
scanf("%d",&a[u][i]);
for (int u=mymin(100,n);u>=1;u--)
{
int l=1,r=mymin(100,m);
while (l<=r)
{
if (u*r<=ans) break;
int mid=(l+r)>>1;
if (check(u,mid)==true) {ans=mymax(ans,u*mid);l=mid+1;}
else r=mid-1;
}
}
printf("%d\n",ans);
return 0;
}
于是就有了更好的单调队列做法,然而我不会TAT
那我还是用我较熟悉的吧——线段树
大概思路就是先枚举左右的范围,这里是
O(m∗100)
的
然后枚举下面边界,很明显,上面边界是可以二分的
于是就转化为一个矩阵是否可以
那么怎么求最大最小值呢?
我们可以弄一个可持久化线段树,于是就可以以二分的姿势来弄出来最大最小值了
这个的复杂度是
O(m∗100∗n∗∗log(n)∗log(T))
T为数据范围
然而这么复杂度只有60分QAQ但后来我加了些玄学优化就80分了
#include<cstdio>
#include<cstdlib>
#include<cstring>
const int N=505;
const int M=30001*2;
int m,n,c;
struct qq
{
int s1,s2;
int c;
}s[N*N*50];
int root[N][N],num=0;
int a[N][N];
void change (int &now,int l,int r,int x)
{
if (now==0) now=++num;
if (l==r)
{
s[now].c=1;
return ;
}
int mid=(l+r)>>1;
if (x<=mid) change(s[now].s1,l,mid,x);
else change(s[now].s2,mid+1,r,x);
s[now].c=s[s[now].s1].c+s[s[now].s2].c;
}
int Merge (int x,int y)//将x和y合并
{
if (!x||!y) return x+y;
int z=++num;
s[z].c=s[x].c+s[y].c;
s[z].s1=Merge(s[x].s1,s[y].s1);
s[z].s2=Merge(s[x].s2,s[y].s2);
return z;
}
int mymin (int x,int y){return x<y?x:y;}
int mymax (int x,int y){return x>y?x:y;}
int get_min (int rt1,int rt2,int rt3,int rt4,int l,int r)
{
// printf("%d %d\n",l,r);
if (l==r) return l;
int s1=s[rt1].s1;
int s2=s[rt2].s1;
int s3=s[rt3].s1;
int s4=s[rt4].s1;
int mid=(l+r)>>1;
if (s[s1].c-s[s2].c-s[s3].c+s[s4].c>0) return get_min(s1,s2,s3,s4,l,mid);
else return get_min(s[rt1].s2,s[rt2].s2,s[rt3].s2,s[rt4].s2,mid+1,r);
}
int get_max (int rt1,int rt2,int rt3,int rt4,int l,int r)
{
/* printf("%d %d\n",l,r);
system("pause");*/
if (l==r) return l;
int s1=s[rt1].s2;
int s2=s[rt2].s2;
int s3=s[rt3].s2;
int s4=s[rt4].s2;
int mid=(l+r)>>1;
if (s[s1].c-s[s2].c-s[s3].c+s[s4].c>0) return get_max(s1,s2,s3,s4,mid+1,r);
else return get_max(s[rt1].s1,s[rt2].s1,s[rt3].s1,s[rt4].s1,l,mid);
}
bool check (int y1,int y2,int x1,int x2)//x1<=x2 y1<=y2
{
// printf("%d %d %d %d\n",x1,y1,x2,y2);
int a=get_min(root[x2][y2],root[x1-1][y2],root[x2][y1-1],root[x1-1][y1-1],1,M);
int b=get_max(root[x2][y2],root[x1-1][y2],root[x2][y1-1],root[x1-1][y1-1],1,M);
//printf("%d %d\n",a,b);
if (b-a<=c) return true;
return false;
}
void dfs (int now,int l,int r)
{
if (now==0) return ;
//printf("%d %d %d\n",l,r,s[now].c);
int mid=(l+r)>>1;
dfs(s[now].s1,l,mid);
dfs(s[now].s2,mid+1,r);
}
void change1 (int rt1,int rt2,int l,int r)
{
if (rt2==0) return ;
s[rt1].c-=s[rt2].c;
if (l==r) return ;
int mid=(l+r)>>1;
change1(s[rt1].s1,s[rt2].s1,l,mid);
change1(s[rt1].s2,s[rt2].s2,mid+1,r);
}
int main()
{
num=0;
scanf("%d%d%d",&m,&n,&c);
for (int u=1;u<=n;u++)
for (int i=1;i<=m;i++)
{
scanf("%d",&a[u][i]);
a[u][i]+=30001;
}
for (int u=1;u<=n;u++) {change(root[u][1],1,M,a[u][1]);root[u][1]=Merge(root[u][1],root[u-1][1]);}
// dfs(root[10][1],1,M);
for (int u=1;u<=m;u++) {change(root[1][u],1,M,a[1][u]);root[1][u]=Merge(root[1][u],root[1][u-1]);}
for (int u=2;u<=n;u++)
for (int i=2;i<=m;i++)
{
change(root[u][i],1,M,a[u][i]);
root[u][i]=Merge(root[u][i],root[u-1][i]);
root[u][i]=Merge(root[u][i],root[u][i-1]);
change1(root[u][i],root[u-1][i-1],1,M);
}
int ans=0;
for (int u=1;u<=m;u++)
{
for (int j=u;j<=mymin(u+99,m);j++)
{
for (int k=1;k<=n;k++)
{
int l=1,r=k;//上端点在哪里
int ooo=-1;
while (l<=r)
{
int mid=(l+r)>>1;
if (check(u,j,mid,k)==true) {ooo=mid;r=mid-1;}
else l=mid+1;
}
if (ooo!=-1)
{
//if ((j-u+1)*(k-ooo+1)==150) printf("%d %d %d %d\n",u,j,ooo,k);
ans=mymax(ans,(j-u+1)*(k-ooo+1));
}
}
}
}
//check(1,10,1,15);
printf("%d\n",ans);
return 0;
}
于是经过我冥思苦想。。我们发现,二分上界是没有必要的,我们可以线性扫过去,因为上面不可以,下面肯定也不可以!于是就少了一个log
然后我就想,要是可以两个线段树一起跑,肯定可以快一些,因为中途不满足就false了!
同时,我又想了一个十分强力的优化,要是宽度是递增的,那么对于同一个下界,他的上界肯定是递减的,于是就可以开一个数组来记录。。
那么这样的话理论复杂度就是
O(n∗m∗log(T))
了
但是似乎常数有一点点大,于是跑得比正解单调队列的
O(n∗m∗100)
慢,自带大常数,唉。。
但还是蛮快的啦,哦,空间也有一点点大。。
然而poj有点卡空间。。
这题做了我很久啊!
同时,因为我们的数字有负数,我们要把它转正啊,要不不是很好搞
#include<cstdio>
#include<cstdlib>
#include<cstring>
const int N=505;
const int MAX=1<<30;
int M=0;
int MM=MAX;
int m,n,c;
struct qq
{
int s1,s2;
int c;
}s[N*N*100];
int root[N][N],num=0;
int a[N][N];
int cnt=0,cntt=0;
void change (int &now,int l,int r,int x)
{
if (now==0) now=++num;
if (l==r)
{
s[now].c++;
return ;
}
int mid=(l+r)>>1;
if (x<=mid) change(s[now].s1,l,mid,x);
else change(s[now].s2,mid+1,r,x);
s[now].c=s[s[now].s1].c+s[s[now].s2].c;
}
int Merge (int x,int y)//将x和y合并
{
if (!x||!y) return x+y;
int z=++num;
s[z].c=s[x].c+s[y].c;
s[z].s1=Merge(s[x].s1,s[y].s1);
s[z].s2=Merge(s[x].s2,s[y].s2);
return z;
}
int mymin (int x,int y){return x<y?x:y;}
int mymax (int x,int y){return x>y?x:y;}
bool check (int y1,int y2,int x1,int x2)//x1<=x2 y1<=y2
{
int rt1=root[x2][y2],rt2=root[x1-1][y2],rt3=root[x2][y1-1],rt4=root[x1-1][y1-1];
int Rt1=root[x2][y2],Rt2=root[x1-1][y2],Rt3=root[x2][y1-1],Rt4=root[x1-1][y1-1];
int l=MM,r=M,L=MM,R=M;
while (l!=r||L!=R)
{
if (L-r>c) return false;
if (l!=r)
{
int s1=s[rt1].s1,s2=s[rt2].s1,s3=s[rt3].s1,s4=s[rt4].s1;
int mid=(l+r)>>1;
if (s[s1].c-s[s2].c-s[s3].c+s[s4].c>0) {rt1=s1;rt2=s2;rt3=s3;rt4=s4;r=mid;}
else rt1=s[rt1].s2,rt2=s[rt2].s2,rt3=s[rt3].s2,rt4=s[rt4].s2,l=mid+1;
}
if (L!=R)
{
int s1=s[Rt1].s2,s2=s[Rt2].s2,s3=s[Rt3].s2,s4=s[Rt4].s2;
int mid=(L+R)>>1;
if (s[s1].c-s[s2].c-s[s3].c+s[s4].c>0) {Rt1=s1;Rt2=s2;Rt3=s3;Rt4=s4;L=mid+1;}
else Rt1=s[Rt1].s1,Rt2=s[Rt2].s1,Rt3=s[Rt3].s1,Rt4=s[Rt4].s1,R=mid;
}
}
return L-l<=c;
}
void dfs (int now,int l,int r)
{
if (now==0) return ;
int mid=(l+r)>>1;
dfs(s[now].s1,l,mid);
dfs(s[now].s2,mid+1,r);
}
void change1 (int &rt1,int rt2,int l,int r)
{
if (rt2==0) return ;
if (rt1==0) rt1=++num;
s[rt1].c+=s[rt2].c;
if (l==r) return ;
int mid=(l+r)>>1;
change1(s[rt1].s1,s[rt2].s1,l,mid);
change1(s[rt1].s2,s[rt2].s2,mid+1,r);
}
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int ok[N];
int shen;
int main()
{
// freopen("airport9.in","r",stdin);
num=0;s[0].s1=s[0].s2=0;
m=read();n=read();c=read();
for (int u=1;u<=n;u++)
for (int i=1;i<=m;i++)
{
a[u][i]=read();
a[u][i]+=30001;
M=mymax(M,a[u][i]);
MM=mymin(MM,a[u][i]);
}
for (int u=1;u<=n;u++) {change(root[u][1],MM,M,a[u][1]);root[u][1]=Merge(root[u][1],root[u-1][1]);}
for (int u=1;u<=m;u++) {change(root[1][u],MM,M,a[1][u]);root[1][u]=Merge(root[1][u],root[1][u-1]);}
for (int u=2;u<=n;u++)
{
shen=0;change(shen,MM,M,a[u][1]);
for (int i=2;i<=m;i++)
{
change(shen,MM,M,a[u][i]);
change1(root[u][i],shen,MM,M);
root[u][i]=Merge(root[u][i],root[u-1][i]);
}
}
int ans=0;
for (int u=1;u<=m;u++)
{
for (int i=1;i<=n;i++) ok[i]=1;
for (int j=u;j<=mymin(u+99,m);j++)
{
int L=(j-u+1);
int l=1;
for (int k=1;k<=n;k++)
{
l=mymax(l,ok[k]);
for (;l<=k;l++)
{
if (check(u,j,l,k)==true)
break;
}
ok[k]=l;
if (l<=k) ans=mymax(ans,L*(k-l+1));
}
}
}
printf("%d\n",ans);
return 0;
}
在博客的最后,大概提一下单调队列的做法吧。。是tyb大佬告诉我的
我的方法是先预处理出mx[i][j][k]和mn[i][j][k],分别表示第i行第j列的数数起k个数中的最大最小值,然后枚举列的起点j与跨度k,然后用一个单调队列维护最多能延伸多少行,时间复杂度为O(100∗n∗m)。
但tyb被卡了一个点,哈哈哈