来源与剑指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)
来检测输入流文件的结尾,
![哭](http://static.blog.csdn.net/xheditor/xheditor_emot/default/cry.gif)
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 |
![尴尬](http://static.blog.csdn.net/xheditor/xheditor_emot/default/awkward.gif)
但是无论如何,可以明显地看出最优的算法应该是四,每一次稳定剪枝掉一行或者一列的算法。
五、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()中,具体情况有待后期测试。