题目
题目描述
J
O
I
O
I
JOIOI
JOIOI 王国是一个
h
h
h 行
w
w
w 列的长方形网格,每个 的子网格都是一个正方形的小区块。为了提高管理效率,我们决定把整个国家划分成两个省
J
O
I
JOI
JOI 和
I
O
I
IOI
IOI 。这个名字蠢透了。
我们定义,两个同省的区块互相连接,意为从一个区块出发,不用穿过任何一个不同省的区块,就可以移动到另一个区块。有公共边的区块间可以任意移动。
我们不希望划分得过于复杂,因此划分方案需满足以下条件:
- 能否将一个区块被分割为两半,一半属 J O I JOI JOI 省,一半属 I O I IOI IOI 省?不能。
- 每个省必须包含至少一个区块,每个区块也必须属于且只属于其中一个省。
- 同省的任意两个小区块互相连接。
- 对于每一行(列),如果我们将这一行(列)单独取出,这一行(列)里同省的任意两个区块互相连接。这一行(列)内的所有区块可以全部属于一个省。
现给出所有区块的海拔,第 i i i 行第 j j j 列的区块的海拔为 a i j a_{ij} aij 。设 J O I JOI JOI 省内各区块海拔的极差(最大值减去最小值)为 R J O I R_{_{JOI}} RJOI ,而 I O I IOI IOI 省内各区块海拔的极差为 R I O I R_{_{IOI}} RIOI 。在划分后,省内的交流有望更加活跃。但如果两个区块的海拔差太大,两地间的交通会很不方便。 因此,理想的划分方案是 D = max ( R J O I , R I O I ) D=\max(R_{_{JOI}},R_{_{IOI}}) D=max(RJOI,RIOI) 尽可能小。
你的任务是求出 D D D 至少为多大。
输入格式
第一行,两个整数
h
,
w
h,w
h,w ,用空格分隔。
在接下来的 h h h 行中,第 i i i 行有 w w w 个整数 a i 1 , a i 2 , a i 3 , … , a i w a_{i1},a_{i2},a_{i3},\dots,a_{iw} ai1,ai2,ai3,…,aiw ,用空格分隔。
输入的所有数的含义见题目描述。
输出格式
一行,一个整数,表示
D
D
D 可能的最小值。
思路
直接将某个角落作为一个省,至少有 max a − min a \max_a-\min_a maxa−mina 的答案。
不妨设 max a ∈ J O I , min a ∈ I O I \max_a\in JOI,\min_a\in IOI maxa∈JOI,mina∈IOI (否则一定不优)。
此时我们就要 min a ∈ J O I \min_a\in JOI mina∈JOI 最大。最小值最大?二分答案 min J O I \min JOI minJOI 。
写成奇怪格式:(就是值域啦)
J O I : [ x , max a ] I O I : [ min a , min a + ( max a − x ) ] JOI:[x,\max_a]\\ IOI:[\min_a,\min_a+(\max_a-x)] JOI:[x,amax]IOI:[amin,amin+(amax−x)]
I O I IOI IOI 的最大值之所以是那个玩意儿,是因为这样的取值不会影响答案 D = max a − x D=\max_a-x D=maxa−x 。
如何判断是否可行? d p \tt{dp} dp 扫描一遍就是了!
根据题意,发现可能的方案其实很少,应该是如图所示的样子——
没错,每一行都有清晰的分界线,并且分界线一定是不断右移(或左移)的。每一行的分界线左边隶属于同一个州。读者自证不难。
所以用 f ( x , y , 0 / 1 / 2 ) f(x,y,0/1/2) f(x,y,0/1/2) 表示前 x x x 行,第 x x x 行的分界线是 y y y (即,左边的 y y y 个属于同一个州),分界线曾经有这样的案底: 1 1 1 左移、 2 2 2 右移、 0 0 0 没移。
转移也很简单。记得用上二分的值的判断!
代码
代码实现有个技巧——将列翻转,再跑一遍 d p \tt{dp} dp。
这样就枚举了分界线左边是 J O I JOI JOI 和右边是 J O I JOI JOI 的情况。
一个州是否有区块不需要判断。有总比没有好。
#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
inline int readint(){
int a = 0, f = 1; char c = getchar();
for(; c<'0' or c>'9'; c=getchar())
if(c == '-') f = -1;
for(; '0'<=c and c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(long long x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar(x%10+'0');
}
const int MaxN = 2005;
int a[MaxN][MaxN], h, w, max_a, min_a = (1<<30)-1;
void input(){
h = readint(), w = readint();
for(int i=1; i<=h; ++i)
for(int j=1; j<=w; ++j){
a[i][j] = readint();
if(max_a < a[i][j])
max_a = a[i][j];
if(a[i][j] < min_a)
min_a = a[i][j];
}
}
bool dp[MaxN][MaxN][3];
// joi的最右边一个区块在哪里
// 0:没推 1:左推 2:右推
bool check(int x,int y){
for(int i=1; i<=h; ++i){
for(int j=0; j<=w; ++j)
dp[i][j][0] = dp[i-1][j][0];
for(int j=0; j<=w; ++j){
dp[i][j][2] = dp[i-1][j][2] or dp[i-1][j][0];
if(j) dp[i][j][2] = dp[i][j][2] or dp[i][j-1][2];
}
for(int j=w; ~j; --j){
dp[i][j][1] = dp[i-1][j][1] or dp[i-1][j][0];
if(j != w) dp[i][j][1] = dp[i][j][1] or dp[i][j+1][1];
}
bool f = true; // 检查[x,max_a]
for(int j=1; j<=w; ++j){
f = f and a[i][j] >= x;
dp[i][j][0] = f and dp[i][j][0];
dp[i][j][1] = f and dp[i][j][1];
dp[i][j][2] = f and dp[i][j][2];
}
f = true; // 检查[min_a,y]
for(int j=w-1; ~j; --j){
f = f and a[i][j+1] <= y;
dp[i][j][0] = f and dp[i][j][0];
dp[i][j][1] = f and dp[i][j][1];
dp[i][j][2] = f and dp[i][j][2];
}
// 这里本来可以剪枝,但是优化效果可能不大,就没打
// 如果本行dp数组已经全部为false,就return false
}
for(int j=0; j<=w; ++j)
if(dp[h][j][0] or dp[h][j][1] or dp[h][j][2])
return true;
return false;
}
void solve(){
for(int j=0; j<=w; ++j)
dp[0][j][0] = true; // 初始化
int ans = max_a-min_a; // 至多是这么大
int L = min_a, R = max_a;
while(L != R){
int mid = (L+R+1)>>1;
if(check(mid,min_a+max_a-mid)) L = mid;
else R = mid-1;
}
ans = min(ans,max_a-L);
for(int i=1; i<=h; ++i) // 列翻转!
for(int j=1; j<w+1-j; ++j)
swap(a[i][j],a[i][w+1-j]);
L = min_a, R = max_a; // do it again ~
while(L != R){
int mid = (L+R+1)>>1;
if(check(mid,min_a+max_a-mid)) L = mid;
else R = mid-1;
}
ans = min(ans,max_a-L);
writeint(ans), putchar('\n');
}
int main(){
input(), solve();
return 0;
}