这个题我是按照discussion里面的说法,先求凸包,然后枚举过的。因为开始先把求凸包算法里面的用到了数组名搞混了,无故wa了好多次。后面求凸包换了种算法过了。结果发现bug是搞混了数组名,然后把前面wa掉的代码下载下来,改好之后也都过了。
这个题主要是凸包算法需要处理有重复点,有多点共线之类的情况。那个按极角排序后,再求凸包的算法,对共点共线处理的不是很好,不过那个算法也过了这个题。有个直接按坐标排序后,再求上凸包和下凸包的算法,可以处理共点共线的情况。这个算法比较优美啊,既不需要找y坐标最小的点,也不需要按极角排序,直接按坐标排序下,然后求凸包即可。
这个算法的一点解释:http://www.algorithmist.com/index.php/Monotone_Chain_Convex_Hull
另外,演算法笔记:http://www.csie.ntnu.edu.tw/~u91029/ConvexHull.html#a3上也有提到这个算法,我也是从这上面看到的。
这个算法可以假设是Graham排序基准点在无限远处,于是夹角大小的比较可以直接按水平坐标比较。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <algorithm>
#include <math.h>
using namespace std;
struct Point
{
int x, y;
bool operator<(const Point p) const
{
return x < p.x || x == p.x y < p.y;
}
};
Point pts[50100];
Point pcs[50100];
int nN;
int nM;
inline int SDis(const Point a, const Point b)
{
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}

void Convex()
{
sort(pts, pts + nN);

nM = 0;
for (int i = 0; i < nN; ++i)
{
while(nM >= 2 Cross(pcs[nM - 2], pcs[nM - 1], pts[i]) <= 0)
{
nM--;
}
pcs[nM++] = pts[i];
}
for (int i= nN - 2, t = nM + 1; i >= 0; --i)
{
while (nM >= t Cross(pcs[nM - 2], pcs[nM - 1], pts[i]) <= 0)
{
nM--;
}
pcs[nM++] = pts[i];
}
nM--;//起点会被重复包含
}

int main()
{
while (scanf("%d", nN) == 1)
{
for (int i = 0; i < nN; ++i)
{
scanf("%d%d", pts[i].x, pts[i].y);
}
Convex();
int nMax = -1;
for (int i = 0; i < nM; ++i)
{
for (int j = i + 1; j < nM; ++j)
{
nMax = max(nMax, SDis(pcs[i], pcs[j]));
}
}
printf("%d\n", nMax);
}

return 0;
}

也可以用旋转卡壳算法来求最远点对,此题的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <vector>
#include <algorithm>
using namespace std;

struct Point
{
int x, y;
bool operator < (const Point p)const
{
return x < p.x || x == p.x y < p.y;
}
};

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}

//输入点集合,输出凸包
void Convex(vector<Point> in, vector<Point> out)
{
int nN = in.size();
int nM = 0;

sort(in.begin(), in.end());
out.resize(nN);

for (int i = 0; i < nN; ++i)
{
while (nM >= 2 Cross(out[nM - 2], out[nM - 1], in[i]) <= 0)
{
nM--;
}
out[nM++] = in[i];
}

for (int i = nN - 2, t = nM + 1; i >= 0; --i)
{
while (nM >= t Cross(out[nM - 2], out[nM - 1], in[i]) <= 0)
{
nM--;
}
out[nM++] = in[i];
}
out.resize(nM);
out.pop_back();//起始点重复
}

int SDis(Point a,Point b)
{
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
}

int RC(vector<Point> vp)
{
int nP = 1;
int nN = vp.size();
vp.push_back(vp[0]);
int nAns = 0;
for (int i = 0; i < nN; ++i)
{
while (Cross(vp[i], vp[i + 1], vp[nP + 1]) > Cross(vp[i], vp[i + 1], vp[nP]))
{
nP = (nP + 1) % nN;
}
nAns = max(nAns, max(SDis(vp[i], vp[nP]), SDis(vp[i + 1], vp[nP + 1])));
}
vp.pop_back();
return nAns;
}

int main()
{
int nN;
vector<Point> in, out;
Point p;
while (scanf("%d", &nN) == 1)
{
in.clear(), out.clear();
while (nN--)
{
scanf("%d%d", p.x, p.y);
in.push_back(p);
}
Convex(in, out);

printf("%d\n", RC(out));
}

return 0;
}

关于旋转卡壳的算法描述,网上有很多资料,比如,http://www.cppblog.com/staryjy/archive/2010/09/25/101412.html尤其关于这个求最远点对的。

这个题的题意是给定一个凸多边形表示的海岛,求海岛离大海最远的距离。可以转化为一个凸多边形内部最多能够放入一个多大的圆。
显然可以对圆的半径进行二分,但是怎么确定圆心了。确定是否存在圆心,可以把原来的凸多边形往内部移动r(圆的半径)的距离之后,再对新的多边形求半平面交,如果半平面交存在(是个点即可),那么当前大小的圆能够放入。
求半平面交的算法可以用上一篇中的 N * N 复杂度的基本算法。
本题还涉及到一个知识,就是如何把一条直线往逆时针方向或者顺时针方向移动R的距离。其实,可以根据单位圆那种思路计算。因为相当于以原来直线上的一点为圆心,以r为半径做圆,而且与原来的直线成90的夹角,那么后来点的坐标是((x0 + cos(PI / 2 +θ )),(y0 + sin(PI / 2 + θ))),转化一下就是(x0 - sinθ,y0 + cosθ)。那么直接可以求出dx = (vp[i].y - vp[(i + 1) % nN].y) * fR / fDis,dy = (vp[(i + 1) % nN].x - vp[i].x) * fR / fDis,fDis是线段的长度。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#include <vector>
using namespace std;

const double fPre = 1e-8;

struct Point
{
double x,y;
Point(){}
Point(const Point p){x = p.x, y = p.y;}
Point(double fX, double fY):x(fX), y(fY){}
Point operator+(const Point p)
{
x += p.x;
y += p.y;
return *this;
}
Point operator+=(const Point p)
{
return *this = *this + p;
}

Point operator-(const Point p)
{
x -= p.x;
y -= p.y;
return *this;
}
Point operator*(double fD)
{
x *= fD;
y *= fD;
return *this;
}
};
typedef vector<Point> Polygon;
int DblCmp(double fD)
{
return fabs(fD) < fPre ? 0 : (fD > 0 ? 1 : -1);
}

double Cross(Point a, Point b)
{
return a.x * b.y - a.y * b.x;
}

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}

Point Intersection(Point a1, Point a2, Point b1, Point b2)
{
Point a = a2 - a1;
Point b = b2 - b1;
Point s = b1 - a1;
return a1 + a * (Cross(b, s) / Cross(b, a));
}

Polygon Cut(Polygon pg, Point a, Point b)
{
Polygon pgRet;
int nN = pg.size();

for (int i = 0; i < nN; ++i)
{
double fC = Cross(a, b, pg[i]);
double fD = Cross(a, b, pg[(i + 1) % nN]);

if (DblCmp(fC) >= 0)
{
pgRet.push_back(pg[i]);
}
if (DblCmp(fC * fD) < 0)
{
pgRet.push_back(Intersection(a, b, pg[i], pg[(i + 1) % nN]));
}
}
//printf("pgRet number:%d\n", pgRet.size());
return pgRet;
}

double Dis(Point a, Point b)
{
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
//返回半平面的顶点个数
int HalfPlane(Polygon vp, double fR)
{
Polygon pg;
pg.push_back(Point(-1e9, -1e9));
pg.push_back(Point(1e9, -1e9));
pg.push_back(Point(1e9, 1e9));
pg.push_back(Point(-1e9, 1e9));
int nN = vp.size();
for (int i = 0; i < nN; ++i)
{
double fDis = Dis(vp[i], vp[(i + 1) % nN]);
double dx = (vp[i].y - vp[(i + 1) % nN].y) * fR / fDis;
double dy = (vp[(i + 1) % nN].x - vp[i].x) * fR / fDis;
Point a = vp[i], b = vp[(i + 1) % nN], c(dx, dy);
a += c;
b += c;
//printf("%f %f %f %f\n", a.x, a.y, b.x, b.y);
pg = Cut(pg, a, b);
if (pg.size() == 0)
{
return 0;
}
}
return pg.size();
}

int main()
{
int nN;
vector<Point> vp;

while (scanf("%d", &nN), nN)
{
vp.clear();
Point p;
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &p.x, &p.y);
vp.push_back(p);
}
double fMin = 0.0, fMax = 10000.0;
while (DblCmp(fMin - fMax))
{
double fMid = (fMin + fMax) / 2;
int nRet = HalfPlane(vp, fMid);
//printf("fMid:%f, nRet:%d\n", fMid, nRet);
if (nRet == 0)
{
fMax = fMid;
}
else
{
fMin = fMid;
}
}
printf("%.6f\n", fMax);
}

return 0;
}

半平面交的一个题,也是求多边形的核心。求出这个好像也可以用于解决一些线性规划问题。我用的是N * N的基本算法,每加入一条直线,就对原来求出的半平面交进行处理,产生新的核心。
代码参照台湾的一个网站演算法笔记上的内容和代码。表示这个网站巨不错,求凸包的算法也参照了这个网站上的内容和代码。半平面交的地址:http://www.csie.ntnu.edu.tw/~u91029/Half-planeIntersection.html#a4

代码思路主要是:先读入所有的多边形顶点,放入一个vector(vp)里面,然后对多边形的每条边求一个半平面。刚开始的时候,用一个vector(Polygon)保存代表上下左右四个无限远角的四个点,表示原始的半平面。然后,用读入的多边形的每条边去切割原来的半平面。
切割的过程是,如果原来(Polygon)中的点在当前直线的指定一侧,那么原来的点还是有效的。如果原来的点和它相邻的下一个点与当前直线相交,那么还需要把交点加入Polygon集合。
还有求交点的方法比较奇葩,类似于黑书上面的那种根据面积等分的方法。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <vector>
#include <algorithm>
using namespace std;

double fPre = 1e-8;
struct Point
{
double x;
double y;
Point() {}
Point(double fX, double fY)
{
x = fX, y = fY;
}
};
typedef vector<Point> Polygon;
typedef pair<Point, Point> Line;
Point operator+(const Point a, const Point b)
{
Point t;
t.x = a.x + b.x;
t.y = a.y + b.y;
return t;
}

Point operator-(const Point a, const Point b)
{
Point t;
t.x = a.x - b.x;
t.y = a.y - b.y;
return t;
}

Point operator*(Point a, double fD)
{
Point t;
t.x = a.x * fD;
t.y = a.y * fD;
return t;
}

int DblCmp(double fD)
{
return fabs(fD) < fPre ? 0 : (fD > 0 ? 1 : -1);
}

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}
//3点叉积
double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}
//向量叉积
double Cross(Point a, Point b)
{
return a.x * b.y - a.y * b.x;
}

//求直线交点的一种简便方法
//平行四边形面积的比例等于高的比例
Point Intersection(Point a1, Point a2, Point b1, Point b2)
{
Point a = a2 - a1;
Point b = b2 - b1;
Point s = b1 - a1;

return a1 + a * (Cross(b, s) / Cross(b, a));
}

Polygon HalfPlane(Polygon pg, Point a, Point b)
{
Polygon pgTmp;
int nN = pg.size();
for (int i = 0; i < nN; ++i)
{
double fC = Cross(a, b, pg[i]);
double fD = Cross(a, b, pg[(i + 1) % nN]);
if (DblCmp(fC) >= 0)
{
pgTmp.push_back(pg[i]);
}
if (fC * fD < 0)
{
pgTmp.push_back(Intersection(a, b, pg[i], pg[(i + 1) % nN]));
}
}
return pgTmp;
}

int main()
{
int nN;
Point p;
vector<Point> vp;
Polygon pg;

while (scanf("%d", &nN), nN)
{
vp.clear();
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &p.x, &p.y);
vp.push_back(p);
}
pg.clear();
pg.push_back(Point(-1e9, 1e9));
pg.push_back(Point(-1e9, -1e9));
pg.push_back(Point(1e9, -1e9));
pg.push_back(Point(1e9, 1e9));
for (int i = 0; i < nN; ++i)
{
pg = HalfPlane(pg, vp[i], vp[(i + 1) % nN]);
if (pg.size() == 0)
{
printf("0\n");
break;
}
}
if (pg.size())
{
printf("1\n");
}
}

return 0;
}

这个题需要多个计算几何算法。第一个是判断一系列点是否能够构成凸多边形,第二个是判断一个点是否在一个简单多边形内部,第三个是求一个点到一条线段(或者说直线)的距离,第四个是判断一个圆是否则一个凸多边形内部。
其实,我是要判断一个圆是否则一个凸多边形内部而用到算法二和三。其实,有不需要判断圆心是否则多边形内部的算法。算法一的思想,求所有边的偏转方向,必须都是逆时针或者顺时针偏转。算法二则是我前面发的那篇改进弧长法判断点和多边形的关系,算法三尤其简单,直线上面取2点,用叉积求出这三点构成的三角形面积的2倍,再除以底边。算法四则是先判断圆心在多边形内部,然后判断圆心到所有边的距离要大于圆的半径。
贴出代码,纯粹为了以后作为模版使用等,防止遗忘,方便查找,其实现在也能手敲出来了。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#include <vector>
using namespace std;

const double fPre = 1e-8;
int DblCmp(double fD)
{
if (fabs(fD) < fPre)
{
return 0;
}
else
{
return fD > 0 ? 1 : -1;
}
}

struct Point
{
double x, y;
bool operator == (const Point p)
{
return DblCmp(x - p.x) == 0 DblCmp(y - p.y) == 0;
}
};

Point operator-(const Point a, const Point b)
{
Point p;
p.x = a.x - b.x;
p.y = a.y - b.y;
return p;
}

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}

bool IsConvexPolygon(vector<Point> vp)
{
int nN = vp.size();
int nDirection = 0;
bool bLine = true;//避免所有点共线
for (int i = 0; i < nN; ++i)
{
int nTemp = DblCmp(Cross(vp[i], vp[(i + 1) % nN], vp[(i + 2) % nN]));
if (nTemp)
{
bLine = false;
}
//这次的方向和上次的方向必须是相同的或者是3点和3点以上共线的情况
if (nDirection * nTemp < 0)
{
return false;
}
nDirection = nTemp;
}
return bLine == false;
}

int GetQuadrant(Point p)
{
return p.x >= 0 ? (p.y >= 0 ? 0 : 3) : (p.y >= 0 ? 1 : 2);
}

bool IsPtInPolygon(vector<Point> vp, Point p)
{
int nN = vp.size();
int nA1, nA2, nSum = 0;
int i;

nA1 = GetQuadrant(vp[0] - p);
for (i = 0; i < nN; ++i)
{
int j = (i + 1) % nN;
if (vp[i] == p)
{
break;
}
int nC = DblCmp(Cross(p, vp[i], vp[j]));
int nT1 = DblCmp((vp[i].x - p.x) * (vp[j].x - p.x));
int nT2 = DblCmp((vp[i].y - p.y) * (vp[j].y - p.y));
if (!nC nT1 <= 0 nT2 <= 0)
{
break;
}
nA2 = GetQuadrant(vp[j] - p);
switch ((nA2 - nA1 + 4) % 4)
{
case 1:
nSum++;
break;
case 2:
if (nC > 0)
{
nSum += 2;
}
else
{
nSum -= 2;
}
break;
case 3:
nSum--;
break;
}
nA1 = nA2;
}

if (i < nN || nSum)
{
return true;
}
return false;
}

double PtDis(Point a, Point b)
{
return sqrt((a.x - b.x) * (a.x - b.x) + (b.y - a.y) * (b.y - a.y));
}
//点p到直线ab的距离
//h = (2 * Spab) / |ab|
double GetDis(Point a, Point b, Point p)
{
return fabs(Cross(a, b, p)) / PtDis(a, b);
}

bool IsCircleInPolygon(vector<Point> vp, Point p, double fR)
{
if (!IsPtInPolygon(vp, p))
{
return false;
}

int nN = vp.size();
for (int i = 0; i < nN; ++i)
{
if (GetDis(vp[i], vp[(i + 1) % nN], p) < fR)
{
return false;
}
}
return true;
}

int main()
{
int nN;
double fR, fPx, fPy;
vector<Point> vp;
Point p;

while (scanf("%d%lf%lf%lf", &nN, &fR, &fPx, &fPy), nN >= 3)
{
vp.clear();
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &p.x, &p.y);
vp.push_back(p);
}

if (IsConvexPolygon(vp))
{
p.x = fPx;
p.y = fPy;
if (IsCircleInPolygon(vp, p, fR))
{
printf("PEG WILL FIT\n");
}
else
{
printf("PEG WILL NOT FIT\n");
}
}
else
{
printf("HOLE IS ILL-FORMED\n");
}
}

return 0;
}

此题用到了几个知识,一个是求多边形面积的公式。然后是,根据顶点都在整点上求多边形边界上的顶点数目的方法。最后一个是pick定理。根据前面2个信息和pick定理算出在多边形内部的整点的个数。
求多边形面积的方法还是叉积代表有向面积的原理,把原点看做另外的一个点去分割原来的多边形为N个三角形,然后把它们的有向面积加起来。
判断边界上点的个数是根据Gcd(dx,dy)代表当前边上整数点的个数的结论。这个结论的证明其实也比较简单,假设dx = a,dy = b。初始点是x0,y0,假设d = Gcd(a,b)。那么边上的点可以被表示为(x0 + k (a / d),y0 + k (b / d))。为了使点是整数点,k必须是整数,而且0 求多边形内部点的个数用的是pick定理。面积 = 内部点 + 边界点 / 2 - 1。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <math.h>
using namespace std;
#define MAX (100 + 10)

struct Point
{
double x, y;
};
Point pts[MAX];

int nN;
const int IN = 1;
const int EAGE = 2;
const int OUT = 3;
const double fPre = 1e-8;

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}

double GetArea()
{
double fArea = 0.0;
Point ori = {0.0, 0.0};

for (int i = 0; i < nN; ++i)
{
fArea += Cross(ori, pts[i], pts[(i + 1) % nN]);
}
return fabs(fArea) * 0.5;
}

int gcd(int nX, int nY)
{
if (nX < 0)
{
nX = -nX;
}
if (nY < 0)
{
nY = -nY;
}
if (nX < nY)
{
swap(nX, nY);
}
while (nY)
{
int nT = nY;
nY = nX % nY;
nX = nT;
}
return nX;
}

int main()
{
int nT;
int nI, nE;
double fArea;

scanf("%d", &nT);
int dx ,dy;

for (int i = 1; i <= nT; ++i)
{
scanf("%d", &nN);
nI = nE = 0;
pts[0].x = pts[0].y = 0;
for (int j = 1; j <= nN; ++j)
{
scanf("%d%d", &dx, &dy);
pts[j].x = pts[j - 1].x + dx;
pts[j].y = pts[j - 1].y + dy;
nE += gcd(dx, dy);
}
fArea = GetArea();
nI = (fArea + 1) - nE / 2;
printf("Scenario #%d:\n%d %d %.1f\n\n", i, nI, nE, fArea);
}

return 0;
}

转角法判断点和多边形的关系大家都知道,原理比较简单,在多边形内扫过的转角一定是360度,在边界上和外面则不一定。
实现起来也比较麻烦,浮点误差比较大,而且还要考虑些特殊情况。
在网上找到一种叫做改进弧长法的算法,原理和转角法类似,但是做了很多重要的改进。比如,计算转角改成了计算叉积,根据叉积决定旋转方向,还要根据计算下一个点的象限决定偏转多少,每次偏转的都是90度的倍数。该算法可以方便判断出点在多边形内,还是边界上,还是在多边形外面。

摘自别人对该算法的描述如下:
首先从该书中摘抄一段弧长法的介绍:“弧长法要求多边形是有向多边形,一般规定沿多边形的正向,边的左侧为多边形的内侧域。以被测点为圆心作单位圆,将全部有向边向单位圆作径向投影,并计算其中单位圆上弧长的代数和。若代数和为0,则点在多边形外部;若代数和为2π则点在多边形内部;若代数和为π,则点在多边形上。”
按书上的这个介绍,其实弧长法就是转角法。但它的改进方法比较厉害:将坐标原点平移到被测点P,这个新坐标系将平面划分为4个象限,对每个多边形顶点P ,只考虑其所在的象限,然后按邻接顺序访问多边形的各个顶点P,分析P和P[i+1],有下列三种情况:
(1)P[i+1]在P的下一象限。此时弧长和加π/2;
(2)P[i+1]在P的上一象限。此时弧长和减π/2;
(3)P[i+1]在Pi的相对象限。首先计算f=y[i+1]x-x[i+1]y(叉积),若f=0,则点在多边形上;若f<0,弧长和减π;若f>0,弧长和加π。
最后对算出的代数和和上述的情况一样判断即可。实现的时候还有两点要注意,第一个是若P的某个坐标为0时,一律当正号处理;第二点是若被测点和多边形的顶点重合时要特殊处理。

以上就是书上讲解的内容,其实还存在一个问题。那就是当多边形的某条边在坐标轴上而且两个顶点分别在原点的两侧时会出错。如边(3,0)-(-3,0),按以上的处理,象限分别是第一和第二,这样会使代数和加π/2,有可能导致最后结果是被测点在多边形外。而实际上被测点是在多边形上(该边穿过该点)。对于这点,我的处理办法是:每次算P和P[i+1]时,就计算叉积和点积,判断该点是否在该边上,是则判断结束,否则继续上述过程。这样牺牲了时间,但保证了正确性。
具体实现的时候,由于只需知道当前点和上一点的象限位置,所以附加空间只需O(1)。实现的时候可以把上述的“π/2”改成1,“π”改成2,这样便可以完全使用整数进行计算。不必考虑顶点的顺序,逆时针和顺时针都可以处理,只是最后的代数和符号不同而已。整个算法编写起来非常容易。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include <stdio.h>
#include <math.h>

#define MAX (100 + 10)
struct Point
{
double x,y;
};

Point pts[MAX];
const int OUT = 0;
const int IN = 1;
const int EDGE = 2;
const double fPre = 1e-8;

int DblCmp(double fD)
{
if (fabs(fD) < fPre)
{
return 0;
}
else
{
return fD > 0 ? 1 : -1;
}
}

int GetQuadrant(Point p)
{
return DblCmp(p.x) >= 0 ? (DblCmp(p.y) >= 0 ? 0 : 3) :
(DblCmp(p.y) >= 0 ? 1 : 2);
}

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

int PtInPolygon(Point* pts, int nN, Point p)
{
int i, j, k;
for (j = 0; j < nN; ++j)
{
pts[j].x -= p.x;
pts[j].y -= p.y;
}
int nA1, nA2;
int nSum = 0;
nA1 = GetQuadrant(pts[0]);
for (i = 0; i < nN; ++i)
{
k = (i + 1) % nN;
if (DblCmp(pts[k].x) == 0 DblCmp(pts[k].y) == 0)
{
break;//与顶点重合
}
int nC = DblCmp(Det(pts[i].x, pts[i].y,
pts[k].x, pts[k].y));
if (!nC DblCmp(pts[i].x * pts[k].x) <= 0
DblCmp(pts[i].y * pts[k].y) <= 0)
{
break;//边上
}
nA2 = GetQuadrant(pts[k]);
if ((nA1 + 1) % 4 == nA2)
{
nSum += 1;
}
else if ((nA1 + 2) % 4 == nA2)
{
if (nC > 0)
{
nSum += 2;
}
else
{
nSum -= 2;
}
}
else if ((nA1 + 3) % 4 == nA2)
{
nSum -= 1;
}
nA1 = nA2;
}

for (j = 0; j < nN; ++j)
{
pts[j].x += p.x;
pts[j].y += p.y;
}

if (i < nN)
{
return EDGE;
}
else if (nSum)//逆时针nSum == 4, 顺时针nSum == -4
{
return IN;
}
else
{
return OUT;
}
}

int main()
{
int nN, nM;
int nCase = 1;

while (scanf("%d%d", &nN, &nM), nN)
{
if (nCase > 1)
{
printf("\n");
}

for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &pts[i].x, &pts[i].y);
}
printf("Problem %d:\n", nCase++);
for (int i = 0; i < nM; ++i)
{
Point p;
scanf("%lf%lf", p.x, p.y);
if (PtInPolygon(pts, nN, p))
{
printf("Within\n");
}
else
{
printf("Outside\n");
}
}
}

return 0;
}

这个题用到2个计算几何算法。求解凸包和简单多边形面积。凸包算法详细解释见算法导论。求解多边形面积的思想是将多边形分解为三角形,一般是假设按顺序取多边形上面连续的2点与原点组合成一个三角形,依次下去用叉积求有向面积之和,最后取绝对值即可。注意,这些点必须是在多边形上逆时针或者顺时针给出的,而求出凸包刚好给了这样的一系列点。
凸包代码,其实先找出一个y坐标最小的点,再对剩下的所有点按极角排序。然后对排序后的点进行一个二维循环即可。二维循环的解释是当加入新的点进入凸包集合时候,如果与以前加入的点形成的偏转方向不一致,那么前面那些点都需要弹出集合。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <math.h>
using namespace std;
#define MAX_N (10000 + 10)

struct Point
{
double x, y;
bool operator <(const Point p) const
{
return y < p.y || y == p.y x < p.x;
}
};

Point pts[MAX_N];
int nN;
Point ans[MAX_N];
int nM;

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.x - a.x, b.y - a.y, c.x - a.x, c.y - a.y);
}

bool CrossCmp(const Point a, const Point b)
{
double cs = Cross(pts[0], a, b);
return cs > 0 || cs == 0 a.x < b.x;
}

void Scan()
{
nM = 0;
sort(pts + 1, pts + nN, CrossCmp);//对所有点按极角排序,逆时针偏转

//第0-2个点,其实不会进入第二重循环的
//从第3个点开始,就依次检查与凸包中前面点形成的边的偏转方向
//如果不是逆时针偏转,则弹出该点
for (int i = 0; i < nN; ++i)
{
while (nM >= 2 Cross(ans[nM - 2], ans[nM - 1], pts[i]) <= 0)
{
nM--;
}
ans[nM++] = pts[i];
}
}

double GetArea()
{
double fAns = 0.0;
Point ori = {0.0, 0.0};
for (int i = 0; i < nM; ++i)
{
fAns += Cross(ori, ans[i], ans[(i + 1) % nM]);
}
return fabs(fAns) * 0.5;
}

int main()
{
while (scanf("%d", &nN) == 1)
{
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &pts[i].x, &pts[i].y);
if (pts[i] < pts[0])
{
swap(pts[i], pts[0]);//pts[0]是y坐标最小的点
}
}

Scan();//扫描出凸包
double fArea = GetArea();
printf("%d\n", (int)(fArea / 50));
}

return 0;
}

典型的最近点对算法的应用,不过这个题多了个限制条件,就是点分为2类,最短距离必须在不同的类之间。为了满足这个限制,只需要把同类别点直接的距离都当作INF处理即可。
最近点对的算法,算导上面说是nlogn的。但是这个复杂度实现起来略微麻烦点,有一种实现方法是nlognlogn的,其只对x坐标进行了排序。nlogn的算法需要对x和y分量分别进行排序,还需要用到其它的辅助数组。
第一个题我用了n
logn算法实现了,第二个题则用了nlognlogn算法实现了。但是时间上相差不大,因为第一个算法每次进行分治时候消耗的O(n)时间也有几次。第二个算法分治时候,需要再次排序的时间也不一定很多,因为可能数据量不够大。
算法的本质就是二分按照X排序好的点数组,nlognlogn变成n*logn的关键是预先对y也排序好一个点数组,因为按y排序好的点数组,在分治后的合并中要用到。算法更详细的解释请参照算法导论。
poj 3714 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
using namespace std;
#define MAX_N (100000 * 2 + 10)
const double FINF = 1LL << 60;
struct Point
{
double x, y;
int nKind;
};
Point pts[MAX_N], ptsY[MAX_N], ptsTemp[MAX_N];
Point ptsSave[MAX_N];
int nNum;
bool CmpX(const Point a, const Point b)
{
return a.x < b.x;
}

bool CmpY(const Point a, const Point b)
{
return a.y < b.y;
}

double Dis(Point a, Point b)
{
if (a.nKind == b.nKind)
{
return FINF;
}
else
{
return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
}

double FindMinDis(Point pts[], Point ptsY[], Point ptsTemp[], int nBeg, int nEnd)
{
if (nBeg == nEnd)
{
return FINF;
}
else if (nBeg + 1 == nEnd)
{
return Dis(pts[nBeg], pts[nEnd]);
}
else if (nBeg + 2 == nEnd)
{
return min(min(Dis(pts[nBeg], pts[nBeg + 1]), Dis(pts[nBeg], pts[nEnd])),
Dis(pts[nBeg + 1], pts[nEnd]));
}
else
{
memcpy(ptsSave + nBeg, ptsY + nBeg, sizeof(Point) * (nEnd - nBeg + 1));//保存当前的Y坐标顺序
int nMid = (nBeg + nEnd) / 2;
int nL = nBeg;
int nR = nMid + 1;
for (int i = nBeg; i <= nEnd; ++i)
{
if (ptsY[i].x - pts[nMid].x <= 0)
{
ptsTemp[nL++] = ptsY[i];
}
else
{
ptsTemp[nR++] = ptsY[i];
}
}

double fAns = min(FindMinDis(pts, ptsTemp, ptsY, nBeg, nMid),
FindMinDis(pts, ptsTemp, ptsY, nMid + 1, nEnd));
int nK = 1;

for (int i = nBeg; i <= nEnd; ++i)
{
if (fabs(ptsSave[i].x - pts[nMid].x) <= fAns)
{
ptsTemp[nK++] = ptsSave[i];
}
}
for (int i = 1; i < nK; ++i)
{
for (int j = i + 1; j < nK; ++j)
{
if (ptsTemp[j].y - ptsTemp[i].y > fAns)
{
break;
}
fAns = min(fAns, Dis(ptsTemp[i], ptsTemp[j]));
}
}
return fAns;
}
}

int main()
{
int nT;
int nN;

//printf("%f", FINF);
scanf("%d", &nT);
while (nT--)
{
scanf("%d", &nN);
nNum = nN * 2;
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &pts[i].x, &pts[i].y);
pts[i].nKind = 1;
}
for (int i = nN; i < nNum; ++i)
{
scanf("%lf%lf", &pts[i].x, &pts[i].y);
pts[i].nKind = 2;
}
memcpy(ptsY, pts, sizeof(Point) * nNum);
sort(pts, pts + nNum, CmpX);
sort(ptsY, ptsY + nNum, CmpY);
printf("%.3f\n", FindMinDis(pts, ptsY, ptsTemp, 0, nNum - 1));
}

return 0;
}

hdu 1007 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <math.h>
#include <algorithm>
using namespace std;
#define MAX (100000 + 10)
struct Point
{
double x, y;
};
Point pts[MAX];
Point ptsTemp[MAX];
const double FINF = 1LL << 60;
bool CmpX(const Point a, const Point b)
{
return a.x < b.x;
}

bool CmpY(const Point a, const Point b)
{
return a.y < b.y;
}

double Dis(Point a, Point b)
{
return sqrt((a.x - b.x) * (a.x - b.x) + (b.y - a.y) * (b.y - a.y));
}

double Find(int nL, int nH)
{
if (nL == nH)
{
return FINF;
}
else if (nL + 1 == nH)
{
return Dis(pts[nL], pts[nH]);
}
else if (nL + 2 == nH)
{
return min(Dis(pts[nL], pts[nL + 1]),
min(Dis(pts[nL], pts[nH]), Dis(pts[nH], pts[nL + 1])));
}
else
{
int nMid = (nL + nH) / 2;
double fAns = min(Find(nL, nMid), Find(nMid + 1, nH));
int nK = 0;
for (int i = nL; i <= nH; ++i)
{
if (fabs(pts[i].x - pts[nMid].x) <= fAns)
{
ptsTemp[nK++] = pts[i];
}
}
sort(ptsTemp, ptsTemp + nK, CmpY);
for (int i = 0; i < nK; ++i)
{
for (int j = i + 1; j < nK; ++j)
{
if (ptsTemp[j].y - ptsTemp[i].y >= fAns)
{
break;
}
fAns = min(fAns, Dis(ptsTemp[j], ptsTemp[i]));
}
}

return fAns;
}
}

int main()
{
int nN;

while (scanf("%d", &nN), nN)
{
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf", &pts[i].x, &pts[i].y);
}
sort(pts, pts + nN, CmpX);
printf("%.2f\n", Find(0, nN -1) * 0.5);
}

return 0;
}

题目意思是给出2条直线,然后判断其是否相交,平行,还是重合。刚开始以为是判断2条线段的关系,用了黑书的模板写了,发现连样例都过不了。后面改了很多才过了。先判断2条直线所在的向量是否平行,这个可以判断这2个向量的叉积是否为0,然后再判断线段是否重合,可以选3点判断叉积是否为0。如果向量不平行的话,直接求交点。求交点的公式是用了黑书里面的方法,先求出2个叉积代表2个三角形的有向面积,然后根据定比分点的关系(面积的比例等于交点分其中一条线段的比例)可以推出计算公式。
有叉积和点积这2个工具确实能方便的解决很多问题。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <stdio.h>
#include <string.h>
#include <math.h>
struct Point
{
double fX;
double fY;
};
Point beg[2], end[2];
int nN;
const double fPrecision = 1e-8;

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

double Cross(Point a, Point b, Point c)
{
return Det(b.fX - a.fX, b.fY - a.fY, c.fX - a.fX, c.fY - a.fY);
}

int DblCmp(double fD)
{
if (fabs(fD) < fPrecision)
{
return 0;
}
else
{
return (fD > 0 ? 1 : -1);
}
}

double DotDet(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fX2 + fY1 * fY2;
}

double Dot(Point a, Point b, Point c)
{
return DotDet(b.fX - a.fX, b.fY - a.fY, c.fX - a.fX, c.fY - a.fY);
}

int BetweenCmp(Point a, Point b, Point c)
{
return DblCmp(Dot(a, b, c));
}

int SegCross(Point a, Point b, Point c, Point d, Point p)
{
double s1, s2, s3, s4;
int d1, d2, d3, d4;
d1 = DblCmp(s1 = Cross(a, b, c));
d2 = DblCmp(s2 = Cross(a, b, d));
d3 = DblCmp(s3 = Cross(c, d, a));
d4 = DblCmp(s4 = Cross(c, d, b));

Point e, f;
e.fX = a.fX - b.fX;
e.fY = a.fY - b.fY;
f.fX = c.fX - d.fX;
f.fY = c.fY - d.fY;
if (DblCmp(Det(e.fX, e.fY, f.fX, f.fY)) == 0)//2个向量共线
{
if (d1 * d2 > 0 d3 * d4 > 0)//不在同一条直线上
{
return 0;
}
else
{
return 2;
}
}

//2条直线相交
p.fX = (c.fX * s2 - d.fX * s1) / (s2 - s1);
p.fY = (c.fY * s2 - d.fY * s1) / (s2 - s1);
return 1;
}

int main()
{
//freopen("out.txt", "w", stdout);
while (scanf("%d", &nN) == 1)
{
printf("INTERSECTING LINES OUTPUT\n");
Point p;
for (int i = 0; i < nN; ++i)
{
scanf("%lf%lf%lf%lf", &beg[0].fX, &beg[0].fY, &end[0].fX, &end[0].fY);
scanf("%lf%lf%lf%lf", &beg[1].fX, &beg[1].fY, &end[1].fX, &end[1].fY);
int nRet = SegCross(beg[0], end[0], beg[1], end[1], p);
if (nRet == 0)
{
printf("NONE\n");
}
else if (nRet == 1)
{
printf("POINT %.2f %.2f\n", p.fX, p.fY);
}
else
{
printf("LINE\n");
}
}
printf("END OF OUTPUT\n");
}

return 0;
}

这是一个计算几何的题目。题意是,按顺序给一系列的线段,问最终哪些线段处在顶端。只需要穷举判断,当前的线段会与哪些线段有交点即可。也就是暴力求解,但是线段数目N有10的5次方,平方算法是不能过的。这个题能过的原因是题目描述里面说了,top的stick不会超过1000个。那么修改下暴力的方式题目就能过了。
从小到大枚举每个棍子,判断它是否与后面的棍子相交,如果相交直接把当前棍子的top属性置为false,然后break内层循环。这样就不会超时了,暴力也是需要技巧的,这句话说的很对啊。
判断2条线段是否相交的算法直接按照黑书上的模板代码写了,那个模板代码还不错吧。。。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <stdio.h>
#include <string.h>
#include <math.h>
#define MAX_N (100000 + 10)
struct POS
{
double fX;
double fY;
};

POS begs[MAX_N], ends[MAX_N];
bool bAns[MAX_N];
int nN;
const double fPrecision = 1e-8;

double Det(double fX1, double fY1, double fX2, double fY2)
{
return fX1 * fY2 - fX2 * fY1;
}

//以a作为公共点,计算叉积
double Cross(POS a, POS b, POS c)
{
return Det(b.fX - a.fX, b.fY - a.fY, c.fX - a.fX, c.fY - a.fY);
}

int DblCmp(double fD)
{
if (fabs(fD) < fPrecision)
{
return 0;
}
else
{
return fD > 0 ? 1 : -1;
}
}
//
bool IsSegCross(int nI, int nJ)
{
return (DblCmp(Cross(begs[nI], ends[nI], begs[nJ]))
^ DblCmp(Cross(begs[nI], ends[nI], ends[nJ]))) == -2
(DblCmp(Cross(begs[nJ], ends[nJ], begs[nI]))
^ DblCmp(Cross(begs[nJ], ends[nJ], ends[nI]))) == -2;
}

int main()
{
while (scanf("%d", &nN), nN)
{
for (int i = 1; i <= nN; ++i)
{
scanf("%lf%lf%lf%lf", &begs[i].fX, &begs[i].fY,
&ends[i].fX, &ends[i].fY);
}

memset(bAns, true, sizeof(bAns));

//暴力也是需要技巧的
for (int i = 1; i < nN; ++i)
{
for (int j = i + 1; j <= nN; ++j)
{
if (IsSegCross(i, j))
{
bAns[i] = false;
break;
}
}
}

printf("Top sticks:");
bool bPre = false;
for (int i = 1; i <= nN; ++i)
{
if (bAns[i])
{
if (bPre)
{
printf(",");
}
bPre = true;
printf(" %d", i);
}
}
printf(".\n");
}

return 0;
}