题目:安装栅栏
在一个二维的花园中,有一些用 (x, y) 坐标表示的树。由于安装费用十分昂贵,你的任务是先用最短的绳子围起所有的树。只有当所有的树都被绳子包围时,花园才能围好栅栏。你需要找到正好位于栅栏边界上的树的坐标。
示例 1:
输入: [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]]
输出: [[1,1],[2,0],[4,2],[3,3],[2,4]]
解释:
示例 2:
输入: [[1,2],[2,2],[4,2]]
输出: [[1,2],[2,2],[4,2]]
解释:
即使树都在一条直线上,你也需要先用绳子包围它们。
注意:
所有的树应当被围在一起。你不能剪断绳子来包围树或者把树分成一组以上。
输入的整数在 0 到 100 之间。
花园至少有一棵树。
所有树的坐标都是不同的。
输入的点没有顺序。输出顺序也没有要求。
个人解题
方法一(伪):个人瞎想
(想直接看正确解析的朋友可以跳过这个不看)
自己的核心思路是:起初我们先找到一个一定为凸集的一个点,根据这个点遍历其他所有的点,判断其是否为下一个凸集的点。
1.如何找到一个一定为凸集的一个点:
可以肯定的是,二维平面内最左边的点, 哪怕是其中之一,就一定是凸集的一个点,在这里我们取的是最左下方的一个点
2.如何判断其他的点为所求凸集中一个点:
我的思考思路是,我们所求凸集,最后形成一个集合,集合里的点,从集合开始的点到集合最后的点,其按顺序连接可以形成一个凸多边形包括住所有的点。我们就拿第一个点A打比方,我们会考虑所有其他的点,比如选取一个点B,在此基础上再考虑所有其他的点,比如点C,假若存在有一个AB,对于任意的AC,都有AC均在AB的同一侧,我们可以说这个AB是凸多边形的一边,也就是B是凸集中的一个点(是不是有种高数极限的味了hh),接下来就是以B为起点(相当于前面的A)进行下一次迭代遍历。
3.如何判断AC在AB的哪一侧:
可以使用叉积判断,在此判断方向就ok,我们就只用关心叉积的正负号。叉积是啥?M=Fl,F=qvB,(这里黑体表示向量),是不是看这公式非常熟悉?一个是算力矩的,一个是算洛伦兹力的,这就是叉乘,虽说我们物理中用这算一般也是用标量算,但是判断方向使用右手定则判断的,我们在这里也用右手定则判断方向(直觉上,实际上没用仅仅是为了直观理解,做题就判断个正负号就完事了)
叉积的计算公式也就是个二阶行列式的计算方法(画个蝴蝶结再一减hh)
最后我们还得考虑一种情况,就是共线问题
这是我根据测试数据
[[3,0],[4,0],[5,0],[6,1],[7,2],[7,3],[7,4],[6,5],[5,5],[4,5],[3,5],[2,5],[1,4],[1,3],[1,2],[2,1],[4,2],[0,3]]
用matlab画了个图,可以看到共线的那块,比如(3,5)(4,5)(5,5)都没办法解决,(4,0)不存在这个问题是因为开始我们对集合进行了次排序,算法会先考虑到(4,0)再是(5,0)
因此为了解决这个问题我最后加了个判断是否两点连接的线段是否存在中间点的循环(叉积真好用,叉积为0就是共线了)
结果最后么…还是失败了,原因是起初以为这种思路算法复杂度是O(n2),结果写完才知道是O(n3),然后就超时了,但能写出来自己还是挺高兴的~
下面附上这种思路的代码。
class Solution {
public:
static bool cmp(const vector<int>&a,const vector<int>&b)
//&是引用传递,防止值传递的复制数据,防止传值时拷贝构造函数的调用开销
//const是为了保护数据不被改动
//但是写这种面向对象的时候这个自定义判断大小函数前面还得要加个static,具体原因可参见java中的static
{
if(a[0]<b[0])
return true;
else if(a[0]==b[0])
if(a[1]<b[1])
return true;
return false;
}
int cross_product(vector<int>v11,vector<int>v12,vector<int>v21,vector<int>v22) //定义叉积
{
int p11,p12,p21,p22;
p11=v12[0]-v11[0];
p12=v12[1]-v11[1];
p21=v22[0]-v21[0];
p22=v22[1]-v21[1];
return p11*p22-p12*p21;
}
vector<vector<int>> outerTrees(vector<vector<int>>& trees) {
vector<int>vis(trees.size(),0);
vector<vector<int>> re;
sort(trees.begin(),trees.end(),cmp);//对数组排序,找到最左的点,该点一定为凸集的一点
re.push_back(trees[0]);
vis[0]=1;
int cnt=0;
while(cnt++<=trees.size())
{
int i;
vector<int>begin=re[re.size()-1];
for(i=0;i<trees.size();i++)
{
if(vis[i])
continue;
int flag=1;
int symbol=1;//在这里,symbol取为1最后就是顺序就是逆时针,取为-1就是顺时针
for(int j=0;j<trees.size();j++)
{
if(symbol*cross_product(begin,trees[i],begin,trees[j])<0)
{
flag=0;
break;
}
}
if(flag)
{
re.push_back(trees[i]);
vis[i]=1;
break;
}
}
if(i==trees.size())
break;
}
//判断三点共线的情况
int siz=re.size();
for(int i=0;i<siz;i++)
{
int j=(i+1)%siz;
for(int k=0;k<trees.size();k++)
{
if(vis[k])
continue;
if(cross_product(re[i],re[j],re[j],trees[k])==0)
{
re.push_back(trees[k]);
vis[k]=1;
}
}
}
return re;
}
};
方法二:Graham扫描法
关于这种凸包问题在算法导论中计算几何那有,就瞄了瞄,书上讲了两种,一种是Graham扫描法一种是Jarvis步进法,其中前者时间复杂度是O(nlogn),后者是O(nh),其中n是所给点数,h是所构成凸集点数,很明显就时间复杂度而言是前者好。
省心一点,我们来看看Graham扫描法,这是从书上截取下来的流程
下面我们进行简单分析下
1.选取一个坐标最小点p0,这个好弄
2.对所有点根据在1中选取的p0进行极角排序…emm这个看上去有点难,毕竟极角怎么算也不知道
3-6.对栈进行初始化
7-10.毕竟graham扫描法思路就是生成凸边,用栈维护,对于新增判断的点,每次与栈中前两个点一起组成三个点检测,因为三个点构成两条边,有两条边我们才能判断是叉积是正还是负,或者说是左拐还是右拐,很显然本题中我们需要一直左拐,同时我们还得考虑多次栈弹出的情况,所以用的是while而不是if(回想一下在OS关于死锁的讨论中是不是也有需要用while而最好别用if的情况hh)
我自己脑中一个简单的图景就是,从一个点开始,开始寻找线段,然后首尾相连形成折线,最后形成一个闭合的轮廓。
对于步骤2的排序自己没头脑,以为我不知道极角怎么算,我所仅能知道的是给我两个点我能判断它们的极角谁大谁小,然后看了眼参考答案,emm知道我所知道的就够了…照样还是自定义个cmp用sort进行排序。
以上就是大体思路,接下来来说一说小细节方面
1.还是老生常谈的共线问题,如果是要求求出周长这个问题倒可以不用考虑,但是题目要求我们求出所有在轮廓上的点,我们还是用这个例子的图来看
但如果我们对排序不做处理的话最后结果是没有(1,4)这个点的,可以参考下下面的图理理思路猜猜是为什么
原因是在排序过程中,(1,4)这个点是排在(2,5)前面的,所以程序会先处理(1,4)再处理(2,5),此时出现一个右拐!按照逻辑我们得把它舍弃。
这是个大问题,我们还得猜想一下在除了最后一条边外有没有可能也会发生这种情况。
出现这种情况的目前已知的原因是什么?极角相同
我们是不是需要使在生成的折线返回阶段的当极角相同时的排序需要与之前相反呢?我们又该如何判断生成的折线是处于返回阶段呢?
我们再稍加思索不难想到,在极角相同的多个点均在所求的凸集上只有两种情况,分别是以基点p0为起点的线段上,和以基点p0为终点的线段上。所以我们只需要对生成的排序最后的那些极角相同的点做特殊处理就好了!
最终代码如下
class Solution {
public:
static vector<int> base;
static int distance(const vector<int>&a,const vector<int>&b)
{
return ((b[0]-a[0])*(b[0]-a[0])+(b[1]-a[1])*(b[1]-a[1]));
//return 0;
}
static int cross_product(vector<int>v11,vector<int>v12,vector<int>v21,vector<int>v22) //定义叉积
{
int p11,p12,p21,p22;
p11=v12[0]-v11[0];
p12=v12[1]-v11[1];
p21=v22[0]-v21[0];
p22=v22[1]-v21[1];
return p11*p22-p12*p21;
}
static bool cmp1(const vector<int>&a,const vector<int>&b)
//&是引用传递,防止值传递的复制数据,防止传值时拷贝构造函数的调用开销
//const是为了保护数据不被改动
//但是写这种面向对象的时候这个自定义判断大小函数前面还得要加个static,具体原因可参见java中的static
//这个cmp1是用来找出最左下的点的
{
if(a[0]<b[0])
return true;
else if(a[0]==b[0])
if(a[1]<b[1])
return true;
return false;
}
static bool cmp2(const vector<int>&a,const vector<int>&b)
//这个cmp2是用来根据极角进行排序的
{
int v=cross_product(base,a,base,b);
if(v>0)
return true;
else if(v==0)
{
if(distance(base,a)<distance(base,b))
return true;
return false;
}
else
return false;
}
vector<vector<int>> outerTrees(vector<vector<int>>& trees) {
if(trees.size()<4)
return trees;
vector<vector<int>> re;
sort(trees.begin(),trees.end(),cmp1);
base=trees[0];
sort(trees.begin()+1,trees.end(),cmp2);
int reverse;
for(reverse=trees.size()-1;reverse>0;reverse--) //最后一条边需要特殊处理一下,比如出现多点共线情况,距离需要交换,远的应该排在前面
if(cross_product(base,trees[reverse-1],base,trees[reverse])!=0)
break;
for(int i=0;reverse+i<trees.size()-1-i;i++)
{
vector<int>tem=trees[trees.size()-1-i];
trees[trees.size()-1-i]=trees[reverse+i];
trees[reverse+i]=tem;
}
stack<vector<int>>s;
s.push(trees[0]);
s.push(trees[1]);
for(int i=2;i<trees.size();i++)
{
int flag=0;
if(trees[i][0]==1&&trees[i][1]==4)
cout<<"exe"<<endl;
while(1)
{
vector<int>v1=s.top();
s.pop();
vector<int>v2=s.top();
if(cross_product(v2,v1,v1,trees[i])<0)
{
;
}
else
{
s.push(v1);
s.push(trees[i]);
break;
}
}
}
while(!s.empty())
{
re.push_back(s.top());
s.pop();
}
return re;
}
};
std::vector<int> Solution::base;//这个得声明下,因为类不会开辟存储空间,但是静态变量在声明时就需要开辟空间
我们看看代码的最后一行:std::vector Solution::base;
加这行的原因是,类不会开辟存储空间,但是静态变量在声明时就需要开辟空间。
这感觉也是LeetCode上编程一个小坑叭