HAOI2007-理想的正方形 BZOJ1047

题目描述

有一个a*b的整数组成的矩阵,现请你从中找出一个n*n的正方形区域,使得该区域所有数中的最大值和最小值
的差最小。

 


输入格式

第一行为3个整数,分别表示a,b,n的值第二行至第a+1行每行为b个非负整数,表示矩阵中相应位置上的数。每
行相邻两数之间用一空格分隔。
100%的数据2<=a,b<=1000,n<=a,n<=b,n<=1000

 


输出格式

仅一个整数,为a*b矩阵中所有“n*n正方形区域中的最大整数和最小整数的差值”的最小值。

 


样例输入

5 4 2
1 2 5 6
0 17 16 0
16 17 2 1
2 10 2 1
1 2 2 2

样例输出

1

 

思考

首先考虑最暴力的做法,遍历每个n*n的方阵,求出每个方阵的最大值和最小值,时间复杂度为O(a*b*n^2),通过数据范围可以知道,暴力的算法肯定是超时的。

不要灰心,大部分优化算法是在暴力的基础上找到优化的方法的,观察上述暴力算法的时间复杂度,首先每个方阵的最大值和最小值肯定是要求出来的,a*b没有优化的空间,可以从n^2方面入手。

假设n=3,通过下图1我们可以发现,从方块(i,j)位置转移到方块(i,j+1)位置时,图中红色方框范围的最值重复计算了,当n越大时,需要重复计算的区域就越大。

HAOI2007-理想的正方形 BZOJ1047_第1张图片(图1)

 

那么如何让这部分已经计算最值的区域不再计算呢?根据以往做题的经验,可以使用堆(优先队列)来维护区间的最值,方块转移到下一个位置的时候,把下一个位置的数据插入堆,当需要获取最值时,只需要判断一下堆的首元素的下标是否在当前区间,如果不在,则删除队首元素,直到获取到一个合法区间的首元素,perfect!

对于上述的思路,当需要计算的方块只有一行时,是正确的,但是当我们计算第二行的方块时,就发现原先在计算过程中抛弃的方块在第二行是需要用到的。

如图2所示,当我们要计算X(黄色区域)所代表的方阵区间最值,必然要抛弃Y区域(红色方框范围)的值,但是计算Z(紫色区域)方阵时可能要用到Y区域的值。只用一个堆变量的话,只能维护某一行方阵的最值。

HAOI2007-理想的正方形 BZOJ1047_第2张图片(图2)

 

平常我们使用的优先队列只能维护一个一维区间的最值,既然是二维的区间,那么我们何不使用多个堆来维护二维区间的的最值呢?

维护二维区间每一列的最值。

设:

Qmax[j]为维护第j列最大值的优先队列

Qmin[j]为维护第j列最小值的优先队列

每次第j列添加一个新元素时,就判断一下Qmax[j]的首元素不在当前的区间时,不在的话则删除首元素,Qmin[j]的操作也同理。

 

通过二维的优先队列,在求n*n方阵的区间最值时,可以求出当前方阵每一列的最值。

现在仅仅是只知道,当前方阵的每一列的最值,如何计算整个方阵的最值?

只需要再定义两个优先队列保存每一列的最值即可!!

设tempMax为保存当前方阵每一列的最大值的优先队列。

设tempMin为保存当前方阵第一列的最小值的优先队列。

通过取tempMax和tempMin首元素,就可以得到方阵的最值。需要注意的是取队首元素的时候照样要判断是否在合法区间内。

总而言之,就是我们创建通过二维优先队列来保存每一列的最值,再定义两个优先队列保存每一列的最值。

用优先队列优化之后,原先的n^2优化到log2(n^2),时间复杂度为:O( a*b*log2(n^2))

【参考代码】

//二维优先队列维护二维区间最值 
#include 
#include 
#include 
using namespace std;
const int MAXa=1002;

struct dataMin{
	int i,j,val;
	bool operator<(const dataMin &a)const{
	return a.valval;
	}
	dataMax(int _i,int _j,int _val):i(_i),j(_j),val(_val){}
	dataMax(){
		i=0,j=0,val=0;
	}
};
priority_queue qMin[MAXa];
priority_queue qMax[MAXa];


int a,b,n;
int arr[MAXa][MAXa];
//int ans=666666; //初始化时没仔细看数值范围,导致只过了两个点,写程序还是要保持严谨的态度啊! 
int ans=0x7fffffff;

bool checkLoc(int a,int b,int x,int y){
  if(a>=x-n+1&&a<=x && b>=y-n+1&&b<=y) return true;
  return false;	
}

int main(){
	scanf("%d%d%d",&a,&b,&n);
	for(int i=1;i<=a;i++)
		for(int j=1;j<=b;j++)
		 scanf("%d",&arr[i][j]);
		 
    for(int i=1;i<=n-1;i++)
		for(int j=1;j<=b;j++)
        {
         qMin[j].push(dataMin(i,j,arr[i][j]));
		 qMax[j].push(dataMax(i,j,arr[i][j]));	
        } 
        
	for(int i=n;i<=a;i++)
	{
		priority_queue tempMin;
		priority_queue tempMax;
		
		for(int j=1;j<=b;j++)
		{
		 qMin[j].push(dataMin(i,j,arr[i][j]));
		 qMax[j].push(dataMax(i,j,arr[i][j]));
		 
		 //get col min val.
		 while(!checkLoc(qMin[j].top().i,qMin[j].top().j,i,j)) qMin[j].pop();
		 
		 //get col max val.
		 while(!checkLoc(qMax[j].top().i,qMax[j].top().j,i,j)) qMax[j].pop();
		 
		 tempMax.push(qMax[j].top());
		 tempMin.push(qMin[j].top());
		
		 if(j>=n) {
		 	//get local min val.
		 	 while( !checkLoc(tempMin.top().i,tempMin.top().j,i,j)&&tempMin.size()>1) tempMin.pop();
		 	
		 	//get local max val. 
		     while( !checkLoc(tempMax.top().i,tempMax.top().j,i,j)&& tempMax.size()>1) tempMax.pop();
		    
		    //get answer.
		     ans=min(ans,tempMax.top().val-tempMin.top().val);
		}
	}
		
	}
	printf("%d",ans);
	return 0;
} 

 

单队队列优化

虽然上面的代码可以AC,这道题我们还可以继续优化,本题求区间最值并满足决策单调,我们可以使用二维的单调队列来求解。

使用数组来模拟二维单调队列,写起来较为麻烦,操作头指针和尾指针部分很容易出错。为了重复利用上述里面的代码,我是直接使用双端队列(deque)来操作,双端队列满足从队首队尾删除、或添加元素,使用双端队列需要注意的地方就是从队首插入元素会覆盖队列中旧有的数据。

 

因为每个元素最多只会入队一次,出队一次,时间复杂度为:O(a*b)

双端队列的相关操作:

定义:deque q;

队首插入元素:q.push_front(val);

队尾插入元素:q.push_back(val);

删除队首元素:q.pop_front();

删除队尾元素:q.pop_back();

其他函数的调用与FIFO队列一致,这里不再赘述。

 

【参考代码】

//双端队列 
#include 
#include 
#include 
using namespace std;
const int MAXa=1002;

struct data{
	int i,j,val;
	data(int _i,int _j,int _val):i(_i),j(_j),val(_val){}
	data(){
		i=0,j=0,val=0;
	}
};

deque qMin[MAXa];
deque qMax[MAXa];

int a,b,n;
int arr[MAXa][MAXa];
int ans=0x7fffffff; 

bool checkLoc(int a,int b,int x,int y){
  if(a>=x-n+1&&a<=x && b>=y-n+1&&b<=y) return true;
  return false;	
}

int main(){
	scanf("%d%d%d",&a,&b,&n);
	for(int i=1;i<=a;i++)
		for(int j=1;j<=b;j++)
		 scanf("%d",&arr[i][j]);
		 
    for(int i=1;i<=n-1;i++)
		for(int j=1;j<=b;j++)
        {
        
         while(qMin[j].size()>=1&&arr[i][j]<=qMin[j].back().val) qMin[j].pop_back();
         while(qMax[j].size()>=1&&arr[i][j]>=qMax[j].back().val) qMax[j].pop_back();
         
         qMin[j].push_back(data(i,j,arr[i][j]));
		 qMax[j].push_back(data(i,j,arr[i][j]));	
        } 
        
	for(int i=n;i<=a;i++)
	{
		deque tempMin;
		deque tempMax;
		
		for(int j=1;j<=b;j++)
		{
		 while( qMin[j].size()>=1 && arr[i][j]<=qMin[j].back().val ) qMin[j].pop_back();
         while( qMax[j].size()>=1 && arr[i][j]>=qMax[j].back().val ) qMax[j].pop_back();
         
		 qMin[j].push_back(data(i,j,arr[i][j]));
		 qMax[j].push_back(data(i,j,arr[i][j]));
		 
		 //get col min val.
		 while(!checkLoc(qMin[j].front().i,qMin[j].front().j,i,j)) qMin[j].pop_front();
		 
		 //get col max val.
		 while(!checkLoc(qMax[j].front().i,qMax[j].front().j,i,j)) qMax[j].pop_front();
		 
		 
		 while( tempMin.size()>=1 && qMin[j].front().val<=tempMin.back().val ) tempMin.pop_back();
         while( tempMax.size()>=1 && qMax[j].front().val>=tempMax.back().val  ) tempMax.pop_back();
		 tempMax.push_back(qMax[j].front());
		 tempMin.push_back(qMin[j].front());

		if(j>=n) {
		 	//get local min val.
		 	 while( tempMin.size()>1 && !checkLoc(tempMin.front().i,tempMin.front().j,i,j)) tempMin.pop_front() ;
		 	//get local max val. 
		    while( tempMax.size()>1 && !checkLoc(tempMax.front().i,tempMax.front().j,i,j) ) tempMax.pop_front();
		    
		    //get answer.
		    ans=min(ans,tempMax.front().val-tempMin.front().val);
		}
	}
		
	}
	printf("%d",ans);
	return 0;
} 

p.s.提一下代码里面的细节,当我们对队列进行操作时,判断的语句应该先判断队列是否为空,再判断队首列元素是否为空,否则会出错。比如在对数组进行访问时,也是判断下标是否合法,再取数组的值。在我的第一个参考代码里面使用的是错误的方法,没有报错的原因也是因为优先队列中肯定至少有一个合法值。

while( tempMin.size()>1 && !checkLoc(tempMin.front().i,tempMin.front().j,i,j))

 

运行速度比较

图3所示的是从上到下分别使用双端队列模拟单调队、二维数组模拟单调队列和优先队列求解的结果。因为这道题的数据范围并不是太大,这几种方法都能够AC.从运行速度来看使用二维数组模拟单调队列是最快的,从占用空间来看双端队列更占优势。

HAOI2007-理想的正方形 BZOJ1047_第3张图片(图3)

 

总结

看了这篇blog的同学们,建议在分析问题时从最简单的方面(暴力)入手,再从暴力算法中思考优化的方向,一般来说如果涉及到了大量的重复计算,基本就找对了优化的方向。

如果开始没有找到最优的算法,尝试先写出自己不那么完美的代码,把可以先得到的分数得到再说。写完以后再去查找题解,仔细思考自己为什么没有找到更优的算法。这样做有助于锻炼你做题的思维。

你可能感兴趣的:(题解)