二维数组中的查找

来源与剑指OFFER中的一道题目,收录在九度OJ上,地址:http://ac.jobdu.com/problem.php?pid=1384


设矩形左上角坐标为(sx,sy),右上角坐标为(ex,ey)


一、分治

很简单的题意,自然想到的是分治,我最先尝试的算法是。

1.如果矩形大小为0,退出子查询

2.查看矩形最小最大点,检测目标值是否在其区间内,若不在则退出

3.取屏幕最中心的点;

4.如果中心点比目标值大,则去掉中心点右下方矩形,剩下左上,左下,右上三块矩形;

5.如果中心点比目标点小,则去掉中心点左上方矩形,剩下右下,左下,右上三块举行;

6.对剩下矩形,进入子查询


代码如下:

#include<stdio.h>
int m, n, t;
bool flag;
int num[1001][1001];
int findx, findy;
void find(int sx, int sy, int ex, int ey){
	//printf("%d %d %d %d", sx, sy, ex, ey);
	if (sx > ex || sy > ey)	return;
	if (num[sx][sy] > t)	return;
	else if (num[sx][sy] == t){
		flag = 1;
		return;
	}
	if (num[ex][ey] < t)	return;
	else if (num[ex][ey] == t){
		flag = 1;
		return;
	}
	int mx = sx + (ex - sx) / 2;
	int my = sy + (ey - sy) / 2;
	if (num[mx][my] < t){
		find(mx, sy, ex, my - 1);
		find(sx, my, mx - 1, ey);
		find(mx + 1, my + 1, ex, ey);
	}
	else if (num[mx][my]>t){
		find(mx + 1, sy, ex, my);
		find(sx, my + 1, mx, ey);
		find(mx - 1, my - 1, ex, ey);
	}
	else{
		flag = true;
		return;
	}
}
int main(){
	while (true){
		scanf("%d%d", &m, &n);
		if (m < 1 || n < 1) break;
		flag = false;
		scanf("%d", &t);
		for (int i = 0; i < m; i++)
		for (int j = 0; j < n; j++){
			scanf("%d", &num[i][j]);
		}
		find(0, 0, m - 1, n - 1);
		if (flag)	printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

遗憾的是,TlE了。个人事后推倒了一下,认为这样分治的时间复杂度应该是O(根3^(log(mn))),并不是一个好的算法。


二、二分查询

每次剔除1/4显然不能满足我们,于是我想使用二分查询的思想来解决:

1.取当前矩阵最中间一行,二分查询找到最大的比目标值小的点

2.这个点(mx,my),将当前矩阵分为两块,左下方和右上方

3.在左下方和右上方的矩阵中继续步骤1


代码如下:

#include<stdio.h>
int m, n, t;
bool flag;
int num[1001][1001];
int binfind(int mx, int sy, int ey){
	int findy;
	if (num[mx][sy] == t){
		flag = true;
		return -1;
	}
	else if (num[mx][sy] > t){
		return sy;
	}
	if (num[mx][ey] == t){
		flag = true;
		return -1;
	}
	else if (num[mx][ey] < t){
		return ey;
	}
	while (sy <= ey){
		findy = sy + (ey - sy) / 2;
		if (num[mx][findy] > t)	 ey = findy - 1;
		else if (num[mx][findy] < t)	sy = findy + 1;
		else{
			flag = true;
			return -1;
		}
	}
	return sy;
}
void find(int sx, int ex, int sy, int ey){
	if (flag == 1) return;
	if (sx > ex || sy > ey) return;
	int mx = sx + (ex - sx) / 2;
	int my = binfind(mx, sy, ey);
	if (my == -1)
		return;
	else if (num[mx][my] > t){
		find(sx, mx - 1, my, ey);
		find(mx + 1, ex, sy, my - 1);
	}
	else if (num[mx][my] < t){
		find(sx, mx - 1, my + 1, ey);
		find(mx + 1, ex, sy, my);
	}
}

int main(){
	while (true){
		scanf("%d%d", &m, &n);
		if (m < 1 || n < 1) break;
		flag = false;
		scanf("%d", &t);
		for (int i = 0; i < m; i++)
		for (int j = 0; j < n; j++){
			scanf("%d", &num[i][j]);
		}

		find(0, m - 1, 0, n - 1);
		if (flag)	printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}


时间复杂度是近似于O(logmlogn)的,但考虑到矩阵破碎之后,每一行的数据变短,行二分查询效率可能接近O(n),所以其查询速度应是O(mlogn),但肯定会优于一行行二分查找的算法。


但是仍然TLE了。


三、尝试剪枝

于是我尝试了另外一个想法,对于矩阵的上下左右边,每一条未经过查询的边使用二分查询找到其最大的比目标值小的点,都能够剪枝掉一部分矩阵。所以流程如下:

1.对上下左右依次使用二分查询,找到其最大的比目标值小的点;

2.对于上下左右边,依次利用步骤1中找到的点进行剪枝,去掉无用的矩阵;

3.在新的矩阵中重复步骤1,直到找到目标值


代码如下:

#include<stdio.h>
#include<iostream>
using namespace std;
int m, n, t;
bool flag;
int num[1001][1001];
int findx, findy;
void find(int sx, int sy, int ex, int ey){
	//printf("%d %d %d %d", sx, sy, ex, ey);
	if (sx > ex || sy > ey)	return;
	if (num[sx][sy] > t)	return;
	else if (num[sx][sy] == t){
		flag = 1;
		return;
	}
	if (num[ex][ey] < t)	return;
	else if (num[ex][ey] == t){
		flag = 1;
		return;
	}
	int mx = sx + (ex - sx) / 2;
	int my = sy + (ey - sy) / 2;
	if (num[mx][my] < t){
		find(mx, sy, ex, my - 1);
		find(sx, my, mx - 1, ey);
		find(mx + 1, my + 1, ex, ey);
	}
	else if (num[mx][my]>t){
		find(mx + 1, sy, ex, my);
		find(sx, my + 1, mx, ey);
		find(mx - 1, my - 1, ex, ey);
	}
	else{
		flag = true;
		return;
	}
}
int main(){
	while (true){
		scanf("%d%d", &m, &n);
		if (m < 1 || n < 1) break;
		flag = false;
		scanf("%d", &t);
		for (int i = 0; i < m; i++)
		for (int j = 0; j < n; j++){
			scanf("%d", &num[i][j]);
		}
		find(0, 0, m - 1, n - 1);
		if (flag)	printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

遗憾的是,很容易制造出一个数据,让上述算法效率很低,每次只能去掉最外面的一圈,跟剥洋葱一样最终才能拨到中心点。最终仍然TLE


四、可耻百度

OK,我承认这题弄得我很是崩溃然后进行了百度,算法非常优雅:

1.对于算法左上方的点x,y(抑或是右下方,都可以)

2.若num[x][y]小于目标值,删掉所在行

3.若num[x][y]大于目标值,删掉所在列

4.对新的矩形,继续步骤1,知道找到目标值或矩阵为空


代码如下:

// acm.cpp : 定义控制台应用程序的入口点。
//
#include<stdio.h>
int m, n, t;
bool flag;
int num[1001][1001];
int sx, sy, ex, ey;
int main(){
	while (true){
		scanf("%d %d", &m, &n);
		flag = false;
		scanf("%d", &t);
		for (int i = 0; i < m; i++)
		for (int j = 0; j < n; j++){
			scanf("%d", &num[i][j]);
		}
		sx = 0;
		sy = 0;
		ex = m - 1;
		ey = n - 1;
		if (num[sx][sy] > t)	flag = 0;
		else if (num[ex][ey] < t)	flag = 0;
		else if (num[sx][sy] == t)	flag = 1;
		else if (num[ex][ey] == t)	flag = 1;
		else{
			while (sx <= ex && sy <= ey){
				if (num[sx][ey] > t)	 ey--;
				else if (num[sx][ey] < t)	sx++;
				else{
					flag = true;
					break;
				}
			}
		}
		if (flag)	printf("Yes\n");
		else printf("No\n");
	}
	return 0;
}

代码简单美观,时间复杂度O(m+n)我实在想不到这样的算法再一次TLE,但现实往往如此无情。上述所有的尝试全部TLE

918607 gongchenic 1384 Time Limit Exceed 4936KB 872B >1000MS C++ / 代码 / 编辑 00:51:42
918606 gongchenic 1384 Time Limit Exceed 4936KB 683B >1000MS C++ / 代码 / 编辑 00:49:07
918603 gongchenic 1384 Time Limit Exceed 5432KB 1500B >1000MS C++ / 代码 / 编辑 00:34:25
917592 gongchenic 1384 Time Limit Exceed 5432KB 2643B >1000MS C++ / 代码 / 编辑 17:58:32
917538 gongchenic 1384 Time Limit Exceed 5432KB 1918B >1000MS C++ / 代码 / 编辑 17:08:49
917464 gongchenic 1384 Time Limit Exceed 5432KB 2236B >1000MS C++ / 代码 / 编辑 16:30:11
917390 gongchenic 1384 Time Limit Exceed 5432KB 1382B >1000MS C++ / 代码 / 编辑 15:34:44


五、TRUE END

无奈之下点开了下面的讨论版,里面有人提示了一个Tips,需要在循环中加入

while (scanf("%d %d", &m, &n) != EOF)
来检测输入流文件的结尾, 哭,然后瞬间我彻悟了(因为这种问题在POJ上一般是RUNTIME ERROR,所以一时没想到。。)。修改了之前的四份代码然后依次提交:

918616 gongchenic 1384 Time Limit Exceed 5432KB 1171B >1000MS C++ / 代码 / 编辑 01:02:52
918615 gongchenic 1384 Accepted 5432KB 1863B 680MS C++ / 代码 / 编辑 01:01:44
918614 gongchenic 1384 Accepted 5432KB 1461B 670MS C++ / 代码 / 编辑 01:00:09
918613 gongchenic 1384 Accepted 4936KB 873B 670MS C++ / 代码 / 编辑 00:57:50
除了三那份明显可以构造出TLE的数据的代码,其他全部AC 尴尬,膝盖跪烂


但是无论如何,可以明显地看出最优的算法应该是四,每一次稳定剪枝掉一行或者一列的算法。


五、MY TIPS

1.c++中如何检测输入流的结尾?

答:实际上这一题我之前完全是用c++的cin、cout来进行输入输出的,写法是while(cin>>m>>n)。

但是频频导致TLE,查阅了一些资料之后看到scanf以及printf效率会优于cin和cout,于是全部换成了c风格的输入输出。

while(cin>>m>>n)是否可以检测流文件的结尾有待测试。


2.一个异常的中断。

最后一段代码我使用了全局变量,但事实上只有一个main()函数,完全没有使用全局变量的意义。其实一开始我是没有把他们声明为全局变量的,但是放入main()函数中会产生莫名其妙的异常中断:

0x00901A37 处的第一机会异常(在 acm.exe 中):  0xC00000FD: Stack overflow (参数:  0x00000000, 0x00462000)。

这个异常中断是编译期间造成的,困惑了我很久找不到问题所在,只知道是num[1001][1001]不能声明在main()中,具体情况有待后期测试。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值