对于“什么是离散化”,搜索帖子你会发现有各种说法,比如“排序后处理”、“对坐标的近似处理”等等。哪个是对的呢?哪个都对。关键在于,这需要一些例子和不少的讲解才能完全解释清楚离散化是程序设计中一个非常常用的技巧,它可以有效的降低时间复杂度。其基本思想就是在众多可能的情况中“只考虑我需要用的值”。下面我将用三个例子说明,如何运用离散化改进一个低效的,甚至根本不可能实现的算法《算法艺术与信息学竞赛》中的计算几何部分,黄亮举了一个经典的例子,我认为很适合用来介绍离散化思想。这个问题是UVA10173,题目意思很简单,给定平面上n个点的坐标,求能够覆盖所有这些点的最小矩形面积。这个问题难就难在,这个矩形可以倾斜放置(边不必平行于坐标轴)。
这里的倾斜放置很不好处理,因为我们不知道这个矩形最终会倾斜多少度。假设我们知道这个矩形的倾角是α,那么答案就很简单了:矩形面积最小时四条边一定都挨着某个点。也就是说,四条边的斜率已经都知道了的话,只需要让这些边从外面不断逼近这个点集直到碰到了某个点。你不必知道这个具体应该怎么实现,只需要理解这可以通过某种方法计算出来,毕竟我们的重点在下面的过程。
我们的算法很显然了:枚举矩形的倾角,对于每一个倾角,我们都能计算出最小的矩形面积,最后取一个最小值。
这个算法是否是正确的呢?我们不能说它是否正确,因为它根本不可能实现。矩形的倾角是一个实数,它有无数种可能,你永远不可能枚举每一种情况。我们说,矩形的倾角是一个“连续的”变量,它是我们无法枚举这个倾角的根本原因。我们需要一种方法,把这个“连续的”变量变成一个一个的值,变成一个“离散的”变量。这个过程也就是所谓的离散化。
我们可以证明,最小面积的矩形不但要求四条边上都有一个点,而且还要求至少一条边上有两个或两个以上的点。试想,如果每条边上都只有一个点,则我们总可以把这个矩形旋转一点使得这个矩形变“松”,从而有余地得到更小的矩形。于是我们发现,矩形的某条边的斜率必然与某两点的连线相同。如果我们计算出了所有过两点的直线的倾角,那么α的取值只有可能是这些倾角或它减去90度后的角(直线按“\”方向倾斜时)这么C(n,2)种。我们说,这个“倾角”已经被我们 “离散化”了。虽然这个算法仍然有优化的余地,但此时我们已经达到了本文开头所说的目的。
对于某些坐标虽然已经是整数(已经是离散的了)但范围极大的问题,我们也可以用离散化的思想缩小这个规模。
上图为二维基础离散化
最后简单讲一下计算几何以外的一个运用实例(实质仍然是坐标的离散)。才考的VOJ1238中,标程开了一个与时间范围一样大的数组来储存时间段的位置。这种方法在空间上来看十分危险。一旦时间取值范围再大一点,盲目的空间开销将导致Memory Limit Exceeded。我们完全可以采用离散化避免这种情况。我们对所有给出的时间坐标进行一次排序,然后同样用时间段的开始点和结束点来计算每个时刻的游戏数,只是一次性加的经验值数将乘以排序后这两个相邻时间点的实际差。这样,一个1..n的数组就足够了。
离散化的应用相当广泛,以后你会看到还有很多其它的用途。
校门外的树(加强版)
题目描述
某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,……,L,都种有一棵树。
由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。
输入输出格式
输入格式:
输入文件tree.in的第一行有两个整数L(1 <= L <= 10^9)和 M(1 <= M <= 100),L代表马路的长度,M代表区域的数目,L和M之间用一个空格隔开。接下来的M行每行包含两个不同的整数,用一个空格隔开,表示一个区域的起始点和终止点的坐标。
输出格式:
输出文件tree.out包括一行,这一行只包含一个整数,表示马路上剩余的树的数目。
输入输出样例
输入样例#1:
500 3
150 300
100 200
470 471
输出样例#1:
298
这是一道一维离散化的版题,非常简单。不会就多看看上面的讲解
#include<bits/stdc++.h>
using namespace std;
const int nn=105;
struct aty{
int x,s;
}a[nn*2];
int newx[nn],newy[nn],l,m,v[nn*2],ans;
bool cmp(aty a,aty b)
{
return a.x<b.x;
}
int main(){
cin>>l>>m;
for(int i=1;i<=m;i++){
cin>>a[i].x>>a[m+i].x;
a[i].s=a[m+i].s=i; //记录坐标
}
a[m*2+1].x=l; //右边界扩充
sort(a+1,a+2*m+1,cmp); //坐标排序
for(int i=1;i<=2*m;i++)
if(newx[a[i].s])
newy[a[i].s]=i; //重置新坐标
else newx[a[i].s]=i;
for(int i=1;i<=m;i++) //在新坐标上模拟
for(int j=newx[i];j<newy[i];j++)
v[j]=1;
for(int i=1;i<=2*m;i++){
if(v[i]) ans+=a[i+1].x-a[i].x; //区间范围 ( x , y ]
if(v[i]==0&&v[i+1]==1) ans++; //记得连续段的左边界+1
}
cout<<l-ans;
return 0;
}