OJ-NOI 1.11 03:矩形分割

题目

http://noi.openjudge.cn/ch0111/03/

描述

平面上有一个大矩形,其左下角坐标(0,0),右上角坐标(R,R)。大矩形内部包含一些小矩形,小矩形都平行于坐标轴且互不重叠。所有矩形的顶点都是整点。要求画一根平行于y轴的直线x=k(k是整数) ,使得这些小矩形落在直线左边的面积必须大于等于落在右边的面积,且两边面积之差最小。并且,要使得大矩形在直线左边的的面积尽可能大。注意:若直线穿过一个小矩形,将会把它切成两个部分,分属左右两侧。

输入

第一行是整数R,表示大矩形的右上角坐标是(R,R) (1 <= R <= 1,000,000)。
接下来的一行是整数N,表示一共有N个小矩形(0 < N <= 10000)。
再接下来有N 行。每行有4个整数,L,T, W 和 H, 表示有一个小矩形的左上角坐标是(L,T),宽度是W,高度是H (0<=L,T <= R, 0 < W,H <= R). 小矩形不会有位于大矩形之外的部分。

输出

输出整数n,表示答案应该是直线 x=n。 如果必要的话,x=R也可以是答案。

样例输入

1000
2
1 1 2 1
5 1 2 1

样例输出

5

思路

  1. 二分查找,找到满足“左侧面积>=右侧面积”的可能值
  2. 调整可能值
    对于非特殊情况的:最终值 mid=min
    对于左侧面积 = = 右侧面积 的:找到满足“左侧面积 = =右侧面积”的最大整数值
    对于右侧面积为0的:最终值 = 大矩形的边界 r

几种特殊情况

  • 情况1
    由于“只能取整数”且“左侧要比右侧大”,所以划分直线应该为x=2
    然而又考虑到“大矩形左侧尽可能大”,所以划分直线应该为x=r(r为大矩形边界)
    在这里插入图片描述
  • 情况2
    两侧面积相等,中间有一段空间区域,如果只满足“左侧要比右侧大或相等”,则直线可以在这段空间内移动
    但同时受条件“大矩形左侧尽可能大”就有了一个边界点
    在这里插入图片描述

注意点

  • 使用 long long,而不是int(总面积很有可能超出2^32)
  • 两个限制条件以及只能取整数点带来的一些问题,比如特殊情况x=r

几个问题

二分查找的边界收敛问题

可以使用 mid=min
参考 https://blog.csdn.net/OCEANtroye/article/details/82811432

使用函数,而不是把面积计算放入代码里:

  • 实参、形参传递
    注意“传地址时”函数定义内使用,需要为 *left 格式,而不是直接left
    注意实参形参传递时,函数 定义 和 声明 内均需要标注 &或 *,如果有二者有其一为标注 ,则会在编译时提示“严重错误:无法解析的外部符号”
    方法一 :传地址
//定义
void area(long long * left1,long long * right1, Rec re[],long long mid,int n)
{
	int i;
	*right1 = 0, *left1 = 0;
	for (i = 0; i < n; i++)
	{
		if (re[i].l >= mid)
		{
			*right1 += re[i].w * re[i].h;
		}
		else
		{
			if (re[i].l + re[i].w <= mid)
			{
				*left1 += re[i].w  * re[i].h;
			}
			else
			{
				*left1 += (mid - re[i].l) * re[i].h;
				*right1 += (re[i].l + re[i].w - mid) * re[i].h;
			}
		}
	}
}

//使用
area(&left1,&right1, re, mid, n);

**方法二:**传引用

//定义
void area(long long & left1,long long & right1, Rec re[],long long mid,int n)
{
	int i;
	right1 = 0, left1 = 0;
	for (i = 0; i < n; i++)
	{
		if (re[i].l >= mid)
		{
			right1 += re[i].w * re[i].h;
		}
		else
		{
			if (re[i].l + re[i].w <= mid)
			{
				left1 += re[i].w  * re[i].h;
			}
			else
			{
				left1 += (mid - re[i].l) * re[i].h;
				right1 += (re[i].l + re[i].w - mid) * re[i].h;
			}
		}
	}
}

//使用
area(left1,right1, re, mid, n);
  • 全局变量
    问题1
    left、right是不是不可以作为全局变量的名字?使用后报错(定义不明确)?
    解决:
    可能与using namespace std有关
    https://www.cnblogs.com/dj0325/p/8491649.html(定义不明确问题)
    问题2:
    “全局变量使用无效”问题,即将左右面积left、right定义为全局变量,但面积计算函数中left、right的计算传递不到 main函数中
    后来发现我对全局变量的理解有问题,我把这两个定义为全局变量了,就不必再用形参传递了
    参考https://blog.csdn.net/xiaohu1996/article/details/83957446
    例如:
    以下面积计算函数是正确的,但如果是
    void area(long long right1,long long left1, Rec re[],long long mid,int n)
    就是错误的
//错误
void area(long long right1,long long left1, Rec re[],long long mid,int n)
//正确
void area( Rec re[],long long mid,int n)
//正确
void area( Rec re[],long long mid,int n)
{
	int i;
	right1 = 0, left1 = 0;
	for (i = 0; i < n; i++)
	{
		if (re[i].l >= mid)
		{
			right1 += re[i].w * re[i].h;
		}
		else
		{
			if (re[i].l + re[i].w <= mid)
			{
				left1 += re[i].w  * re[i].h;
			}
			else
			{
				left1 += (mid - re[i].l) * re[i].h;
				right1 += (re[i].l + re[i].w - mid) * re[i].h;
			}
		}
	}
}

测试数据

1000 1
0 1 4 1
答案:2

1000 1
0 1 5 1
答案:3

1000 2
0 1 4 1
8 1 4 1
答案:8

1000 2
0 1 2 1
1 2 1 1
答案:1000

1000 1
0 1 1 1
答案:1000

我的代码

#include<iostream>
#include<iomanip>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;

struct Rec
{
	long long l, t, w, h;//注意不能用int,用long (4分错误)
};

//在某一处的最左边,如果左边没有,就全部直接最边界(对于题中某个话的解释
//这个题的另一个关键,就是只能是整数

int main()
{
	//***********输入部分
	int r;
	cin >> r;
	int n;
	cin >> n;
	Rec re[10000];
	int i = 0;
	for (i = 0; i < n; i++)
	{
		cin >> re[i].l >> re[i].t >> re[i].w >> re[i].h;
	}
	long long min = 0, max = r;//二分查找范围
	long long mid = (min + max) / 2;
	long long right = 2, left = 1;//左右两侧面积
	
	//******二分查找部分,如果遇到两侧面积相等,则提前结束
	while (max>=min)
	{
		/*****计算面积******/
		right = 0, left = 0;
		for (i = 0; i < n; i++)
		{
			if (re[i].l >= mid)
			{
				right += re[i].w * re[i].h ;
			}
			else
			{
				if (re[i].l + re[i].w <= mid)
				{
					left += re[i].w  * re[i].h;
				}
				else
				{
					left += (mid - re[i].l) * re[i].h;
					right += (re[i].l + re[i].w - mid) * re[i].h;
				}
			}
		}
		/*****计算面积结束******/

		if (left > right)
		{
			//max = mid - 1;
			max = mid-1;
		}
		else if (left < right)
			min = mid+1 ;
		else
			break;

		mid = (max + min) / 2;
	}

	//如果二分查找是自然结束的,就令mid=min,并计算新情况下的左右两侧面积;
	//否则(即是两侧面积相等的情况),则跳过
	if (right != left)
	{
		mid = min;
		/*****计算面积******/
		right = 0, left = 0;
		for (i = 0; i < n; i++)
		{
			if (re[i].l >= mid)
			{
				right += re[i].w * re[i].h;
			}
			else
			{
				if (re[i].l + re[i].w <= mid)
				{
					left += re[i].w * re[i].h;
				}
				else
				{
					left += (mid - re[i].l) * re[i].h;
					right += (re[i].l + re[i].w - mid) * re[i].h;
				}
			}
		}
		/*****计算面积结束******/
	}

	//****如果右侧矩形面积为0,则可以取极限情况,划分直线 为大矩形的边界
	//如果左右侧相等,则说明二分查找是提前结束,尝试是否可以取更大的mid值(有可能矩形中是有一块空的,两侧面积相等
	//除此之外即为非特殊情况,直接输出即可
	if (right == 0)
	{
		cout << r;
	}
	else if (right == left)
	{
		do
		{
			++mid;
			/*****计算面积******/
			right = 0, left = 0;
			for (int i = 0; i < n; i++)
			{
				if (re[i].l >= mid)
				{
					right += re[i].w * re[i].h;
				}
				else
				{
					if (re[i].l + re[i].w <= mid)
					{
						left += re[i].w * re[i].h;
					}
					else
					{
						left += (mid - re[i].l);
						right += (re[i].l + re[i].w - mid) * re[i].h;
					}
				}
			}
			/*****计算面积结束******/

		} while (right == left);
		cout << --mid;
	}
	else
		cout << mid;
	return 0;
}

参考代码

参考代码中对于矩形面积计算的处理方法很有趣,即他将每一列的面积都存储在一个数组里,这在之后的使用时只需要进行加法计算即可。
这种方法对于小矩形数量较多,且较大的情况比较适用
但对于矩形数量少的地方,可能会有大量的+0操作
可能这就是参考代码用时大约是我的5倍的原因吧
思路:
因为小矩形都在大矩形范围之类,并且互不重叠,坐标又全部是整数型,所以我用一个数组来保存每一列中小矩形所占的面积,然后再使用二分找到k
需要注意的是移动k的条件!具体逻辑见代码

#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

int area[1000005]; //每一列的小矩形的面积,如area[0]表示横坐标为0到1之间的小矩形的面积 R最大为1000000
int R; //题目中的R
int N; //题目中的N

int main()
{
    scanf("%d%d",&R,&N);
    for(int i=0; i<N; i++)
    {
        //通过输入的四个数据计算出我们需要的数据
        int l,t,w,h;
        scanf("%d%d%d%d",&l,&t,&w,&h);
        for(int i=l; i<l+w; i++)
        {
            area[i]+=h;
        }
    }

    int l,r,mid;
    l=0;
    r=R;
    long long left=0,right=0; //注意总面积很有可能超出2^31
    while(r>l)
    {
        left=0;
        right=0;
        mid=(l+r)/2;

        for(int i=0; i<mid; i++)
        {
            left+=area[i];
        }
        for(int i=mid; i<R; i++)
        {
            right+=area[i];
        }

        if(r-l==1) //经过二分后的最后判断
        {
            if(left>=right) //如果当前左边大于右边
            {
                if(left+area[l]>=right-area[l] && //如果把k再往右边移动以后仍然满足左边大于右边(可省略)
                        (left+area[l]-(right-area[l])<=left-right)) //如果把k再往右边移动以后面积差不增加
                {
                    l++; //那就移动吧
                }
                else
                {
                    break; //不移
                }
            }
            else //如果当前左边已经比右边小了
            {
                l++; //肯定要移动了
            }
        }
        else //二分中
        {
            if(left<=right) //为了尽量让k靠右,需要用小于等于
            {
                l=mid;
            }
            if(left>right) //为了尽量减少面积差
            {
                r=mid;
            }
        }
    }
    while(l+1<=R&&area[l+1]==0) //考虑到可能会有右边没有矩形的情况,对k进行最后的判断
    {
        l++;
    }
    printf("%d",l);
    return 0;
}
  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值