POJ3681[Finding the Rectangle]【枚举+限界】(轉)

来源:互联网 发布:地牢探测器JS手机版 编辑:程序博客网 时间:2024/05/19 22:46

POJ3681[Finding the Rectangle]【枚举+限界】(轉)

【原题链接】
    http://acm.pku.edu.cn/JudgeOnline/problem?id=3681
【题意描述】
    给出N、M和N个点的坐标,要求找出面积最小的一个矩形,使其中至少包含M个点(恰处在边上的点不算包含在内)。
【数据范围】
    1 ≤ M ≤ N ≤ 200
    1 ≤ Xi, Yi ≤ 10000 ,(Xi,Yi)为第i个点坐标
【解题思路】
    该题直观上看似乎有特殊办法对解进行构造,但仔细一想,也没有什么好的策略(至少我没有想出来),包括贪心、动态规划都不大适用。观察数据量,点的数量最多为200,则最多不同的X坐标和Y坐标分别为200,若将坐标离散化,则最多有200x200个坐标,则可能的矩形数为 O(N^4)≤1600000000。这样看来数据量也不算太大,如果进行适当的剪枝和限界则可行解的数量更会大大减少。于是决定使用最普通的枚举方法,加上适当的剪枝和限界。
这道题首先要做对数据做一些处理,直接在原坐标系上进行枚举肯定是不合适的,因为一共有10000^2个坐标点,而实际给出的点只有200个。因此需要将坐标离散化,即只留下那些上面有点出现的坐标线。这里先要考虑一个问题,题目中说,矩形边上的点不算包含在矩形内,这样,矩形的顶点不就有可能出现在没有实际点的坐标线上了吗,但是仔细想想,并不用这样考虑,因为矩形有边上都不包含点,因此每个矩形的最外面一层都是无用的(因为不包含点),故在选择矩形的时候假设矩形的四个顶点都向内收缩一个单位,而在计算其面积的时候将其连长各加上二之后在相乘,也就是说,我们在枚举矩形的时候,枚举的是其边上的点计算在内的矩形顶点,而在计算矩形面积的时候再将其还原。有了这个等价的条件,我们可以有允足的理由将坐标系离散化了,因为要寻找的是最小的矩形,为使每个矩形包含尽可能多的点,我们枚举的矩形要在其边上尽可能地包含点(因为若边上不包含点,则包含同样这些点的矩形可以再缩小,注意,我们这里说的矩形是指顶点经过收缩的矩形,以下均这么说)。因此,矩形的顶点只可能出现在其X、Y坐标线上有点的坐标上,而其X、Y坐标线上没有点的坐标不予考虑,所以我们选择将坐标系离散化。离散化结果是,将坐标系中所有可能的X轴坐标仅为在所给点中出现的X坐标,Y轴坐标亦然。具体用下图来说明,
 
若原有坐标系中有三个红点,则依据这三个点进行离散化后的坐标系中的坐标线只剩下图中加粗的线。离散化的具体做法是,先将点依据X坐标排序,然后将所有不同的X轴坐标做为一条X轴坐标线保存,并记录其在原坐标系上的值,同时,依据该坐标线在新坐标系中的排序给点分配新的X坐标。对Y轴的离散化做法与之类似。
    第二个处理是,在新坐标系上记录各坐标上的点的数目。将坐标系离散化后,将用一个二维数组MAP表示新的坐标系,其大小为200x200。因为在对坐标进行离散化的时候已经将点的坐标转换化新坐标系中的坐标,因此只要直接对MAP[point.x][point.y]进行累加就可以了。
    第三个处理是,在新坐标系中计算从坐标系的(0,0)-(x,y)的矩形内的点的数目。因为之后的枚举中需要用到计算(x0,y0)-(x1,y1)的矩形内的点数,因些在先打表计算出所有(0,0)-(x,y)矩形的点数可以加快之后的计算。具体的算法是逐行逐列的累加,时间复杂度为O(N^2)。使用一个数组Cache存储已计算过的行中每一列的点数,由于计算时先沿列走再沿行走,故在同一行中,将要计算的坐标(x,y)的点数为刚计算完的(x,y-1)的点数,加上前x-1行y列累加的点数,再加上坐标点(x,y)上的点数。即运用下式
num(i,j)=num(i,j-1)+cache(j)+map(i,j)
之后再累加Cache,即
        cache(j)+=map(i,j)
则把每个坐标点遍历一遍即可算出每个以(0,0)-(x,y)的矩形内的点的数目。
完成这些处理工作之后可以开始进行枚举了。枚举的方法很简单,四重循环,枚举出两个顶点,先计算其面积是否比当前的最小面积小,若是,则计算矩形内的点数是否足够M,若还是,则修改最小面积。只是如果直接枚举的话速度还是太慢,好在这里还有许多可以优化的地方。枚举中的四重循环有四个变量i,j,k,h,我们对每个变量都可以进行限界,以排除明显不可能得到解的情况。首先说明这四个量的含义,他们构成对角两个顶点(i,j)-(k,h)组成的矩形,此处 k≥i,h≥j,另有两个量cx,cy表示x,y坐标的最大范围。对这四个量的具体限界如下:
对i来说,若从该行的第一列起,到图形的右下角,即(i,1)-(cx,cy)构成的矩形中的点数都不足M的话,则继续枚举任何一个点都不再可能有解,即故跳出循环;
    对j来说,若在某一个点(i,j)-(cx,cy)构成的矩形中的点数不足M的话,则对任意i’ ≥i,j’>j,(i’,j’)-(k,h)构成的矩形中的点数都不能达到M,因为k≤cx,h≤cy,该矩形被包括在矩形 (i,j)-(cx,cy)内,因此进入下一行循环的时候,循环变量j也不必达到或超过当前的j。故此时的j成为循环变量j的新界,我们用lj保存j的上界,即此时,lj=j-1。
对k来说,首先计算矩形(i,j)-(k,cy)内的点数是否足够M,若不足则明显无法在该行找到合适的h,k进入下一行。另外,当i进入下一行的同一列后,k的起始循环量也不会小于上一行中M点足够的第一个k。故用一个数组lk存储k的下界,lk[j’]为j=j’时k的起始循环量。
    对h来说,当某个矩形(i,j)-(k,h)的原面积大于当前最小面积的时候,则对于任何h’>h,k’>k,(i,j)-(k’,h’)矩形的面积也必定大于最小面积,故不必枚举,此时可对h限上界,别外,当矩形(i,j)-(k,h)内包含的点足够M之后,也不必枚举矩形 (i,j)-(k’,h’)的情况,此时也应限界,对h 的限界方法与对j的限界方法类似。
利用上述四个限界方法,可使枚举的情况大大减少。

【具体代码】
复制内容到剪贴板
代码:
#include <iostream>
using namespace std;

struct point_elem{int x,y;} point[200]; //用于存放点坐标
int coordx[201];    //用于存放离散化后的X坐标
int coordy[201];    //用于存放离散化后的Y坐标
int cx;             //离散X坐标的数目
int cy;             //离散Y坐标的数目
int map[201][201];  //用于存放离散化后的坐标上的点的数目
int num[201][201];  //用于统计从左上角到该点坐标的点的数目
int n,m;            //存储题目中读入的N个点,及要包含在矩形内的M个点

//比较函数,比较X坐标的大小,用于qsort排序
int cmp_by_x(const void *cp1,const void *cp2)
{
    return ((point_elem*)cp1)->x-((point_elem*)cp2)->x;
}

//比较函数,比较Y坐标的大小,用于qsort排序
int cmp_by_y(const void *cp1,const void *cp2)
{
    return ((point_elem*)cp1)->y-((point_elem*)cp2)->y;
}

//创建离散化的X坐标
int create_coordx()
{
    coordx[1]=point[0].x;
    int k=1;
    for (int i=0;i<n;i++)
    {
        //为每个不同的X坐标创建一条X坐标
        if (point.x!=coordx[k])
        {
            coordx[++k]=point.x;
        }
        point.x=k;
    }
    return k;
}

//创建离散化的Y坐标
int create_coordy()
{
    coordy[1]=point[0].y;
    int k=1;
    for (int i=0;i<n;i++)
    {
        //为每个不同的Y坐标创建一条Y坐标
        if (point.y!=coordy[k])
        {
            coordy[++k]=point.y;
        }
        point.y=k;
    }
    return k;
}


//将原有点标在离散化后的坐标系上
void create_map()
{
    memset(map,0,sizeof(map));
    for (int i=0;i<n;i++)
    {
        map[point.x][point.y]++;
    }
}

//统计坐标系上,每个坐标的左上方的所有输入点的数目
void create_num()
{
    memset(num,0,sizeof(num));
    int cache[201]={0};
    for (int i=1;i<=cx;i++)
    {
        for (int j=1;j<=cy;j++)
        {
            num[j]=num[j-1]+cache[j]+map[j];
            cache[j]+=map[j];
        }
    }
}

//计算(x0,y0),(x1,y1)之间所有输入点的数目
inline int calpoint(int x0,int y0,int x1,int y1)
{
    return num[x1][y1]-num[x1][y0-1]-num[x0-1][y1]+num[x0-1][y0-1];
}

//枚举矩形的左上角和右下角,计算其中的点数,找出符合条件的最小矩形
int find()
{
    int min=2000000000;
    int lj=cy;  //对j进行限界
    int lk[201];//对k进行限界
    for (int i=1;i<=cy;i++) lk=1;
    for (int i=1;i<=cx;i++)
    {
        if (calpoint(i,1,cx,cy)<m) break;
        for (int j=1;j<=lj;j++)
        {
            if (calpoint(i,j,cx,cy)<m)
            {
                lj=j-1;
                break;
            }
            int lh=cy;//对h进行限界
            if (lk[j]<i) lk[j]=i;
            for (int k=lk[j];k<=cx;k++)
            {
                if (calpoint(i,j,k,lh)<m)
                {
                    if (lh==cy) lk[j]++;
                    continue;
                }
                for (int h=j;h<=lh;h++)
                {
                    if ((coordx[k]-coordx+2)*(coordy[h]-coordy[j]+2)<min)
                    {
                        if (calpoint(i,j,k,h)>=m)
                        {
                            min=(coordx[k]-coordx+2)*(coordy[h]-coordy[j]+2);
                            lh=h-1;
                        }
                    }
                    else
                    {
                        lh=h-1;
                    }
                }
            }
        }
    }
    return min;
}


int main()
{
    int ca;
    cin>>ca;
    for (int no=0;no<ca;no++)
    {
        //输入数据
        cin>>n>>m;
        for (int i=0;i<n;i++)
        {
            scanf("%d %d",&point.x,&point.y);
        }
        //依据点的X坐标进行排序后创建离散的X坐标
        qsort(point,n,sizeof(point_elem),cmp_by_x);
        cx=create_coordx();
        //依据点的Y坐标进行排序后创建离散的Y坐标
        qsort(point,n,sizeof(point_elem),cmp_by_y);
        cy=create_coordy();
        //在离散后的坐标上标点
        create_map();
        //计算从左上角到每个坐标的点数并打表
        create_num();
        //查找最小的矩形
        cout<<find()<<endl;
    }
    return 0;
}
【AC感想】
这道题是POJ七月份月赛中最简单的一道题,到目前为止通过的人数也最多。那天比赛的时候也尝试了这道题的枚举方法,只是没加上那么多的限界,时间上无法通过,于是认为此题有更神奇的方法,便没再考虑。后来再稍加限界后发现效果明显才继续研究。
在做过了这道题之后,我体会到一点,有时候巧妙地使用最暴力的方法也能做出一些题目。这道题枚举的方法也可以看作是搜索,深度为4,每层最多扩展200个结点,其限界的方法与剪枝的思想是相同,因此这道题更加让我体会了剪枝的重要性,巧妙的剪枝可以大大减少枚举的情况。
另外此题中对坐标的离散化处理也是一种重要的手段,而打表事先计算(0,0)-(x,y)的点数也是加快运算的好办法,值得回味。
0 0