本题参考数据来自 uva 11626
解法一:贪心
凸包问题上课讲了两种方法,其中第一种做法是按照极坐标排序+单调栈解决,这种做法比较容易理解。
单调栈判断就是看新的点在直线左边还是右边,可以用叉乘的右手法则,即判断x1y2 - x2y1的符号。
但有个问题就是当有多个点和初始点连成一条线时,这些点的极坐标顺序不好判断。
这里的解决方法是,看上一个加入集合的点,若上个点和新的两个极坐标相同的点连成一条线,就按离上个点距离从小到大来做。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
typedef tuple<int, int> P;
const int INF = 1 << 30;
double base_x, base_y;
P base;
ll isleft(P a, P start, P end)
{
ll x1, y1, x2, y2;
x1 = get<0>(end) - get<0>(start);
y1 = get<1>(end) - get<1>(start);
x2 = get<0>(a) - get<0>(start);
y2 = get<1>(a) - get<1>(start);
return x1*y2 - x2*y1;
}
int get_manhattan_dist(P a, P b)
{
return abs(get<1>(b) - get<1>(a)) + abs(get<0>(b) - get<0>(a));
}
int main()
{
freopen("in", "r", stdin);
int t, a, b, N;
scanf("%d", &N);
for(int i=0;i<N;i++)
{
vector<P> v;
scanf("%d", &t);
for (int i = 0; i < t; i++) {
scanf("%d %d", &a, &b);
v.push_back(P(a, b));
scanf("%c %c", &a, &a);
}
// 排序
sort(v.begin(), v.end(), [](P a, P b){
return get<1>(a) > get<1>(b);
});
// 令最靠下的元素为基本点
base = v.back();
v.pop_back();
// 按照距离base点的角度排序
sort(v.begin(), v.end(), [](P a, P b) {
double ang1 = atan2(get<1>(a) - get<1>(base), get<0>(a) - get<0>(base));
double ang2 = atan2(get<1>(b) - get<1>(base), get<0>(b) - get<0>(base));
if(ang1 == ang2)return get_manhattan_dist(base, a) > get_manhattan_dist(base, b);
return ang1 < ang2;
});
// 一个一个来,用单调栈做,往左拐正常,往右拐就弹出
vector<P> s{base};
for (int i = 0; i < v.size();)
{
// 如果出现共线的点,考虑是否逆转部分数组
int j = i;
while(j+1 < v.size() && isleft(base, v[j], v[j+1]) == 0)j++;
if(j>i)
{
if (isleft(s[s.size()-1], v[i], v[i+1]) == 0)
{
int d1 = get_manhattan_dist(s[s.size()-1], v[i]);
int d2 = get_manhattan_dist(s[s.size()-1], v[i+1]);
if(d1 > d2)reverse(v.begin()+i, v.begin()+j+1);
}
}
for(int k=i;k<=j;k++)
{
while (s.size() >= 2 && isleft(v[k], s[s.size() - 2], s[s.size() - 1]) < 0)
s.pop_back();
s.push_back(v[k]);
}
i = j+1;
}
// 找到最小的左边
int start = 0;
for(int i=1;i<s.size();i++)
{
if(get<0>(s[i]) == get<0>(s[start]) && get<1>(s[i]) < get<1>(s[start]))
start = i;
else if(get<0>(s[i]) < get<0>(s[start]))
start = i;
}
printf("%d\n", s.size());
for (int i = 0; i < s.size(); i++)
{
int j = (i+start) % s.size();
printf("%d %d\n", get<0>(s[j]), get<1>(s[j]));
}
}
}
解法二:分治
首先,要将图形分割到能得到逆序排列的点为止。然后形成一个左集合和两个右集合(见课件),将这三个集合合并即可。合并时,如果共线,按左右划分,左右都是划分好的有序集合,反转下即可。最终全部转为倒序(在上一个点和两个候选点都共线的情况下)。
#include <bits/stdc++.h>
typedef long long ll;
const int INF = 1 << 30;
using namespace std;
typedef tuple<int, int> P;
ll isleft(P a, P start, P end)
{
ll x1, y1, x2, y2;
x1 = get<0>(end) - get<0>(start);
y1 = get<1>(end) - get<1>(start);
x2 = get<0>(a) - get<0>(start);
y2 = get<1>(a) - get<1>(start);
return x1*y2 - x2*y1;
}
int get_manhattan_dist(P a, P b)
{
return abs(get<1>(b) - get<1>(a)) + abs(get<0>(b) - get<0>(a));
}
int decreasing(P base, vector<P>& v, int l, int r)
{
for(int t=l+1;t<=r;t++)
{
int d1 = get_manhattan_dist(base, v[t-1]);
int d2 = get_manhattan_dist(base, v[t]);
if(d1 < d2)return 0;
}
return 1;
}
void rotate(vector<P>& merged_s, P& base)
{
for (int i = 1; i < merged_s.size();)
{
// 如果出现共线的点,考虑是否逆转部分数组
int j = i;
while(j+1 < merged_s.size() && isleft(base, merged_s[j], merged_s[j+1]) == 0)j++;
// 通过旋转的方式,处理位置在i-j的元素
if(j>i)
{
// 只需处理上半部分,下半部分已经从小到大排好了
if (isleft(merged_s[i-1], merged_s[i], merged_s[i+1]) != 0)
{
// 若是递减序列,直接排除
if(decreasing(base, merged_s, i, j))return;
// 需要大的优先,整个旋转一次,之后旋转前半部分(若前半部分开始递增)
reverse(merged_s.begin()+i, merged_s.begin()+j+1);
// 如果是递减序列,不用进行下面的操作
if(decreasing(base, merged_s, i, j))return;
for (int t = j - 1; t >= i; t--)
{
int d1 = get_manhattan_dist(base, merged_s[t + 1]);
int d2 = get_manhattan_dist(base, merged_s[t]);
if (d1 < d2)
{
if(!decreasing(base, merged_s, i, t))
reverse(merged_s.begin() + i, merged_s.begin() + t + 1);
if(!decreasing(base, merged_s, t+1, j))
reverse(merged_s.begin() + t + 1, merged_s.begin() + j + 1);
break;
}
}
}
}
i = j+1;
}
}
// 输入按照x排序的点集,返回凸包上的点,逆时针排列
vector<P> divide_conquer(vector<P>& v, int l, int r)
{
if(r-l+1 <= 2)
{
// 找到逆时针排列的点的集合
vector<P> s;
for(int i=l;i<=r;i++)s.push_back(v[i]);
return s;
}
// 分开后返回按逆时针排列好的子集
int mid = (l+r)/2;
vector<P> s1=divide_conquer(v, l, mid), s2=divide_conquer(v, mid+1, r);
// 找到右边点集的上顶点和下顶点。
int right_top=0, right_bot=0, bot_min=get<1>(s2[0]), top_max=get<1>(s2[0]);
for(int i=0;i<s2.size();i++)
{
int y = get<1>(s2[i]);
if(y > top_max)top_max=y, right_top=i;
if(y < bot_min)bot_min=y, right_bot=i;
}
// 扎到右边两个集合各自长度
int len_seq, len_anti;
if(right_top >= right_bot)
{
len_anti = right_top - right_bot + 1;
len_seq = s2.size() - len_anti;
}
else
{
len_seq = right_bot - right_top - 1;
len_anti = s2.size() - len_seq;
}
// 构建右边两个集合
vector<P> r_seq, r_anti;
for(int i=1;i<=len_seq;i++)
{
int j = right_bot - i;
if(j < 0)j += s2.size();
r_seq.push_back(s2[j]);
}
for(int i=0;i<len_anti;i++)
{
int j = (i+right_bot)%s2.size();
r_anti.push_back(s2[j]);
}
// 为了保证按顺序选取所有点,应选取最左边,最靠上(保证刚出去时近的优先)的为基本点
int left_most = distance(s1.begin(), min_element(s1.begin(), s1.end()));
P base = s1[left_most];
// 循环,每次合并一个元素
vector<P> merged_s{base};
vector<int> pt{left_most+1, 0, 0};
for(int i=0;i<s1.size()+s2.size()-1;i++)
{
// mod
if(pt[0] >= s1.size())pt[0] = 0;
// 每次插入一个元素,要考虑元素连在一起的情况。
map<P, int> cand; // 将候选放入候选map中 候选点->集合序号
if(pt[0] != left_most)cand[s1[pt[0]]] = 0;
if(pt[1] < r_seq.size())cand[r_seq[pt[1]]] = 1;
if(pt[2] < r_anti.size())cand[r_anti[pt[2]]] = 2;
// 对cand中的点进行比较
P max_p = cand.begin()->first;
for(auto it=cand.begin();++it!=cand.end();)
{
P p = it->first;
// 看p和max_p哪个更符合base的要求
double ang_best = atan2(get<1>(max_p)-get<1>(base), get<0>(max_p)-get<0>(base));
double ang_new = atan2(get<1>(p)-get<1>(base), get<0>(p)-get<0>(base));
// 角度相同时,看和初始点距离
if(abs(ang_best - ang_new) < 1e-8)
{
int d_best = get_manhattan_dist(max_p, base), d_new = get_manhattan_dist(p, base);
if(d_new < d_best)max_p = p;
}
// 角度不同时,比较角度
else if(ang_new < ang_best)max_p = p;
}
merged_s.push_back(max_p);
pt[cand[max_p]]++;
}
// 对相同的点,左右分别旋转一次
rotate(merged_s, base);
// 合并后用graham-scan对合并后的集合进行扫描
vector<P> s_ret{merged_s[0], merged_s[1]};
for(int i=2;i<merged_s.size();i++)
{
while(s_ret.size() >= 2 && isleft(merged_s[i], s_ret[s_ret.size()-2], s_ret[s_ret.size()-1]) < 0)
s_ret.pop_back();
s_ret.push_back(merged_s[i]);
}
return s_ret;
}
int main()
{
freopen("in", "r", stdin);
int t, a, b, N;
scanf("%d", &N);
for(int i=0;i<N;i++)
{
vector<P> v;
scanf("%d", &t);
for (int i = 0; i < t; i++) {
scanf("%d %d", &a, &b);
v.push_back(P(a, b));
scanf("%c %c", &a, &a);
}
// 计算s数组,存放凸包的最终结果。
sort(v.begin(), v.end());
vector<P> s = divide_conquer(v, 0, v.size()-1);
// 找到最小的左边
int start = 0;
for(int i=1;i<s.size();i++)
{
if(get<0>(s[i]) == get<0>(s[start]) && get<1>(s[i]) < get<1>(s[start]))
start = i;
else if(get<0>(s[i]) < get<0>(s[start]))
start = i;
}
printf("%d\n", s.size());
for (int i = 0; i < s.size(); i++)
{
int j = (i+start) % s.size();
printf("%d %d\n", get<0>(s[j]), get<1>(s[j]));
}
}
}