P2216 [HAOI2007]理想的正方形 方法記錄

  • 2022 年 10 月 11 日
  • 筆記

[HAOI2007]理想的正方形

題目描述

有一個 \(a \times b\) 的整數組成的矩陣,現請你從中找出一個 \(n \times n\) 的正方形區域,使得該區域所有數中的最大值和最小值的差最小。

輸入格式

第一行為 \(3\) 個整數,分別表示 \(a,b,n\) 的值。

第二行至第 \(a+1\) 行每行為 \(b\) 個非負整數,表示矩陣中相應位置上的數。每行相鄰兩數之間用一空格分隔。

輸出格式

僅一個整數,為 \(a \times b\) 矩陣中所有「 \(n \times n\) 正方形區域中的最大整數和最小整數的差值」的最小值。

樣例 #1

樣例輸入 #1

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

樣例輸出 #1

1

提示

問題規模。

矩陣中的所有數都不超過 \(1,000,000,000\)

\(20\%\) 的數據 \(2 \le a,b \le 100,n \le a,n \le b,n \le 10\)

\(100\%\) 的數據 \(2 \le a,b \le 1000,n \le a,n \le b,n \le 100\)

題解

前置知識

試想一下,如果我們把\(a*b\)的矩陣改為長度為\(a\)的序列,要找的東西變成長度為\(n\)的子段,那不就變成單調隊列之滑動窗口了嘛。

可以先看看我寫的這篇關於滑動窗口的部落格。

有了這些對單調隊列的基本認知,我們不難想出一種本題的做法。

方法分析

首先可以將\(a*b\)的矩陣理解為\(b\)行長度為\(a\)的序列,然後對每一行的序列使用「滑動窗口」,這樣就可以處理出每一行中長度為\(n\)的子段中所有數的最大值和最小值

我們用\(board[i][j]\)來存原來的矩陣,用\(x1[i][j]\)\(x2[i][j]\)分別記錄第\(i\)行第\(j\)個窗口的最小值和最大值。那麼我們每一行就能產生\(b-n+1\)個窗口。處理出來的\(x1\)\(x2\)規模就是\(a*(b-n+1)\).

for(int i=1;i<=a;i++)
	{
		int h=0,t=0;
		memset(q1,0,sizeof(q1));
		memset(q2,0,sizeof(q2));
		for(int j=1;j<=b;j++)
		{
			while(h<=t&&j-q1[h]>=n) h++;
			while(h<=t&&board[i][j]<board[i][q1[t]]) t--;
			q1[++t]=j;
			if(j>=n) x1[i][j-n+1]=board[i][q1[h]];
		}
		for(int j=1;j<=b;j++)
		{
			while(h<=t&&j-q2[h]>=n) h++;
			while(h<=t&&board[i][j]>board[i][q2[t]]) t--;
			q2[++t]=j;
			if(j>=n) x2[i][j-n+1]=board[i][q2[h]];
		}
	}

然後我們在處理好的\(x1\)\(x2\)基礎上,對每一列使用「滑動窗口」。如果說每一行的滑動窗口是從左往右滑動的,那麼每一列的滑動窗口就是從上往下滑動的。

我們用用\(y1[i][j]\)\(y2[i][j]\)分別記錄第\(i\)列第\(j\)個窗口的最小值和最大值。那麼我們每一列就能產生\(a-n+1\)個窗口。處理出來的\(y1\)\(y2\)規模就是\((a-n+1)*(b-n+1)\).

for(int i=1;i<=b-n+1;i++)
	{
		int h=0,t=0;
		memset(q1,0,sizeof(q1));
		memset(q2,0,sizeof(q2));
		for(int j=1;j<=a;j++)
		{
			while(h<=t&&j-q1[h]>=n) h++;
			while(h<=t&&x1[j][i]<x1[q1[t]][i]) t--;
			q1[++t]=j;
			if(j>=n) y1[j-n+1][i]=x1[q1[h]][i];
		}
		for(int j=1;j<=a;j++)
		{
			while(h<=t&&j-q2[h]>=n) h++;
			while(h<=t&&x2[j][i]>x2[q2[t]][i]) t--;
			q2[++t]=j;
			if(j>=n) y2[j-n+1][i]=x2[q2[h]][i];
		}
	}

回想一下,\(x\)數組處理出的是每一行長度為\(n\)的子段中最小/最大值,\(y\)數組處理出的是\(x\)的基礎上每一列長度為\(n\)的子段中最小/最大值,那麼這樣一來\(y\)數組中就是整個矩陣中\(n*n\)的正方形區域中的最小/最大值。

然後只需要遍歷一遍,求出最小的\(y2[i][j]-y1[i][j]\)即可。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1005;
int a,b,n;
int board[N][N];
int x1[N][N],x2[N][N],y1[N][N],y2[N][N];
int q1[N],q2[N];
int cntx1,cntx2,cnty1,cnty2;
int minn(int a,int b)
{
	return a<b?a:b;
}
int ans=2147483647;
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",&board[i][j]);
	for(int i=1;i<=a;i++)
	{
		int h=0,t=0;
		memset(q1,0,sizeof(q1));
		memset(q2,0,sizeof(q2));
		for(int j=1;j<=b;j++)
		{
			while(h<=t&&j-q1[h]>=n) h++;
			while(h<=t&&board[i][j]<board[i][q1[t]]) t--;
			q1[++t]=j;
			if(j>=n) x1[i][j-n+1]=board[i][q1[h]];
		}
		for(int j=1;j<=b;j++)
		{
			while(h<=t&&j-q2[h]>=n) h++;
			while(h<=t&&board[i][j]>board[i][q2[t]]) t--;
			q2[++t]=j;
			if(j>=n) x2[i][j-n+1]=board[i][q2[h]];
		}
	}
	memset(q1,0,sizeof(q1));
	memset(q2,0,sizeof(q2));
	for(int i=1;i<=b-n+1;i++)
	{
		int h=0,t=0;
		memset(q1,0,sizeof(q1));
		memset(q2,0,sizeof(q2));
		for(int j=1;j<=a;j++)
		{
			while(h<=t&&j-q1[h]>=n) h++;
			while(h<=t&&x1[j][i]<x1[q1[t]][i]) t--;
			q1[++t]=j;
			if(j>=n) y1[j-n+1][i]=x1[q1[h]][i];
		}
		for(int j=1;j<=a;j++)
		{
			while(h<=t&&j-q2[h]>=n) h++;
			while(h<=t&&x2[j][i]>x2[q2[t]][i]) t--;
			q2[++t]=j;
			if(j>=n) y2[j-n+1][i]=x2[q2[h]][i];
		}
	}
	for(int i=1;i<=a-n+1;i++)
		for(int j=1;j<=b-n+1;j++)
			ans=minn(ans,y2[i][j]-y1[i][j]);
	printf("%d\n",ans);
	return 0;
}