0%

小羊-2025-12-04-M. Ahmad's Dish(圆和正多边形的这个 minkowski 和)

题目大意

艾哈迈德对物理充满热情,最近他学到了一些新知识。

他知道,为了防止一个物体从桌子上掉下来,该物体的质心必须放在桌子上,即使该物体的其他部分在桌子外面。

艾哈迈德得知这个新概念后非常兴奋,于是他去找伊斯梅尔分享这个想法。

不幸的是,伊斯梅尔喜欢几何,而且学得很快,很快就理解了艾哈迈德的新想法,并给他出了一道好题。

这道题涉及一张半径为 RR 的圆桌和一个边长为 NN 、每边长为 LL 的正多边形盘子。

盘子的数量不受限制;盘子可以放在桌子上的任何地方,只要它们不从桌子上掉下来,并且至少有一边与 x 轴平行。

我们的任务是找出这些盘子所能覆盖的最大面积。

image

在上例 R=4,N=4,L=2R = 4, N = 4, L = 2 中。紫色的形状就是盘子所能覆盖的最大面积。

作为阿默德的好朋友,你想帮助他解决这个问题。

思路讲解

把正方形在圆上移动,看成圆在正方形上移动,因为

正多边形面积公式,nn 是这个边数,ll 是边长。

A=nl24tan(πn)A = \frac{n \cdot l^2}{4 \cdot \tan(\frac{\pi}{n})}

1
2
3
4
5
6
7
8
9
10
void Solve() {
ll R,N,L;
cin >> R >> N >> L;
// 加上平移的得到的长方形
DB ans=N*R*L;
// 加上一个圆
ans+=pi*R*R;
ans+=N*L*L/(4*tan(pi/N));
cout<<fsp(10)<<ans<<"\n";
}

AC代码

AC
https://codeforces.com/gym/104493/submission/361481631

心路历程(WA,TLE,MLE……)

给定一个含有 nn 个顶点和 mm 条边的森林。假设该森林有 kk 个连通块,其大小分别为 s1,s2,,sks_1, s_2, \dots, s_k。已知将该森林添加 k1k-1 条边使其成为一棵树的方案总数为:

如果还有二分图,则可以进一步变为这个 disudisv=1|dis_u-dis_v|= 1,原因如下

二分图的作用就在这里:二分图等价于“所有环长度为偶数”,也等价于“从固定根出发的最短路距离奇偶性给出一个合法二分划分”。因此对任意边 (u,v)(u,v),必有 disudis_udisvdis_v 奇偶性相反,也就是 disudisvdis_u\ne dis_v

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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
/*2026-2-3 增加编译器识别 Real 类型的功能,以使用不同类型的这个实现
*
*/
using Real = ll;

int sgn(auto x) {
if (-eps < x && x < eps) return 0;
return x < 0 ? -1 : 1;
}

struct Point {
Real x, y;

explicit Point(Real x_ = 0, Real y_ = 0) : x(x_), y(y_) {
}

// 只对左值生效的复合运算符
Point &operator+=(Point p) & {
x += p.x, y += p.y;
return *this;
}

Point &operator-=(Point p) & {
x -= p.x, y -= p.y;
return *this;
}

Point &operator*=(Real t) & {
x *= t, y *= t;
return *this;
}

Point &operator/=(Real t) & {
x /= t, y /= t;
return *this;
}

Point operator-() const { return Point(-x, -y); }

friend Point operator+(Point a, Point b) { return a += b; }
friend Point operator-(Point a, Point b) { return a -= b; }
friend Point operator*(Point a, Real b) { return a *= b; }
friend Point operator*(Real a, Point b) { return b *= a; }
friend Point operator/(Point a, Real b) { return a /= b; }

friend bool operator<(Point a, Point b) {
int sx = sgn(a.x - b.x);
if (sx != 0) return a.x < b.x;
return a.y < b.y;
}

friend bool operator>(Point a, Point b) { return b < a; }

friend bool operator==(Point a, Point b) {
return sgn(a.x - b.x) == 0 && sgn(a.y - b.y) == 0;
}

friend bool operator!=(Point a, Point b) { return !(a == b); }

friend istream &operator>>(istream &is, Point &p) { return is >> p.x >> p.y; }
// friend ostream &operator<<(ostream &os, Point p) { return os << "(" << p.x << ", " << p.y << ")"; }

auto norm() const {
if constexpr (is_integral_v<Real>) {
return (__int128_t) x * x + (__int128_t) y * y;
} else {
return x * x + y * y;
}
}

DB abs() const { return sqrt((long double) norm()); }
};

using Vector = Point;

auto cross(Point a, Point b) {
// 在 Real 为整数的时候,使用 __int128_t 整型进行计算
if constexpr (is_integral_v<Real>) {
return (__int128_t) a.x * b.y - (__int128_t) a.y * b.x;
} else {
return a.x * b.y - a.y * b.x;
}
}

// Real cross(Point p1, Point p2, Point p0) { // 叉乘 (p1 - p0) x (p2 - p0);
// return cross(p1 - p0, p2 - p0);
// }
auto dot(Point a, Point b) {
// 在 Real 为整数的时候,使用 __int128_t 整型进行计算
if constexpr (is_integral_v<Real>) {
return (__int128_t) a.x * b.x + (__int128_t) a.y * b.y;
} else {
return a.x * b.x + a.y * b.y;
}
}

// 采用两点式
struct Line {
Point p1, p2;

Line() {
}

Line(Point a, Point b) : p1(a), p2(b) {
}

friend bool operator<(Line a, Line b) {
if (a.p1 != b.p1) return a.p1 < b.p1;
return a.p2 < b.p2;
}

friend bool operator==(Line a, Line b) {
if (a.p1 != b.p1) return false;
if (a.p2 != b.p2) return false;
return true;
}

friend istream &operator>>(istream &is, Line &e) {
Real a, b, c, d;
is >> a >> b >> c >> d;
e = Line(Point(a, b), Point(c, d));
return is;
}

// friend ostream&operator<<(ostream &os, Line l) {
// return os << "<" << l.p << ", " << l.q << ">";
// }
};

// 使用这个东西,Point 必须是 double 等小数类型
Point project(Point p, Line l) {
Point vec = l.p2 - l.p1;
// 这个距离反正早除晚除都要除掉
DB r = dot(vec, p - l.p1) / ((DB) vec.x * vec.x + vec.y * vec.y);
return l.p1 + vec * r;
}

Point reflect(Point p, Line l) {
Point proj = project(p, l);
return proj + proj - p;
}

mt19937 rng(233);

#define ON_SEGMENT 0
#define COUNTER_CLOCKWISE 1
#define CLOCKWISE (-1)
#define ONLINE_BACK 2
#define ONLINE_FRONT (-2)

int CCW(Point p, Line l) {
Vector a = l.p2 - l.p1, b = p - l.p1;
if (sgn(cross(a, b)) > 0) {
return COUNTER_CLOCKWISE;
}
if (sgn(cross(a, b)) < 0) {
return CLOCKWISE;
}
if (sgn(dot(a, b)) < 0) {
return ONLINE_BACK;
}
if (a.norm() < b.norm()) return ONLINE_FRONT;
return ON_SEGMENT;
}

bool isOrthogonal(Line a, Line b) {
Vector c = a.p2 - a.p1, d = b.p2 - b.p1;
return !sgn(dot(c, d));
}

bool isParallel(Line a, Line b) {
Vector c = a.p2 - a.p1, d = b.p2 - b.p1;
return !sgn(cross(c, d));
}

bool isIntersect(Line a, Line b) {
// 判断线段相交
return CCW(b.p1, a) * CCW(b.p2, a) <= 0 && CCW(a.p1, b) * CCW(a.p2, b) <= 0;
}

// bool isIntersectStrict(Line a,Line b){ // 判断线段相交
// return CCW(b.p1,a)*CCW(b.p2,a)<0 && CCW(a.p1,b)*CCW(a.p2,b)<0;
// }
// p+tv=q+sw
// (p-q) x w+tv x w = 0 (利用叉乘乘以自身为0,消掉sw)
// t=-(p-q)x w/(vxw)
// 传入的是方向式 拿到的是两直线交点
Point getintersect(const Line &a, const Line &b) {
Point p = a.p1, v = a.p2 - a.p1, q = b.p1, w = b.p2 - b.p1;
Point u = p - q;
DB t = cross(w, u) / cross(v, w);
return p + v * t;
}

// 也可以用 project 来算
DB distancePL(Point p, Line l) {
auto val = cross(l.p2 - l.p1, p - l.p1);
return sgn(val) * val / (l.p2 - l.p1).abs();
}

// AC https://qoj.ac/submission/2001668
DB distancePS(Point p, Line l) {
// 避免 distanceSS 调用的时候发生这个退化(即线段退化为了点)
if (l.p1 == l.p2) {
return (p - l.p1).abs();
}
Vector t = l.p2 - l.p1;
if (sgn(dot(t, p - l.p1)) < 0) {
return (p - l.p1).abs();
}
if (sgn(dot(t, p - l.p2)) > 0) {
return (p - l.p2).abs();
}
return distancePL(p, l);
}

// AC https://qoj.ac/submission/2001588
DB distanceSS(Line a, Line b) {
if (isIntersect(a, b)) return 0;
return min({distancePS(a.p1, b), distancePS(a.p2, b), distancePS(b.p1, a), distancePS(b.p2, a)});
}

DB polygonArea(const vector<Point> &poly) {
DB res = 0;
size_t n = poly.size();
for (int i = 0; i < n; ++i) {
res += cross(poly[i], poly[(i + 1) % n]);
}
res = abs(res) / 2.0;
return res;
}

DB polygon_perimeter(const vector<Point> &poly) {
DB res = 0;
size_t n = poly.size();
for (int i = 0; i < n; ++i) {
res += (poly[i] - poly[(i + 1) % n]).abs();
}
return res;
}

void reorder_polygon(vector<Point> &poly) {
ll n = poly.size();
Point p1 = poly[1] - poly[0], p2 = poly[2] - poly[0];
if (sgn(cross(p1, p2)) <= 0) {
reverse(poly.begin(), poly.end());
}
// 找到字典序最小的点
ll pos_mn = 0;
for (int i = 1; i < n; ++i) {
if (poly[i] < poly[pos_mn]) {
pos_mn = i;
}
}
// Performs a left rotation on a range of elements
// 向左循环移动,把 pos_mn 移动到第一个 [0]
rotate(poly.begin(), poly.begin() + pos_mn, poly.end());
}

// 不允许自交的多边形,顺序都可以,会把 poly 变为 CCW 顺序(逆时针)
// AC https://qoj.ac/submission/2002121
bool isConvex(vector<Point> &poly) {
ll n = poly.size();
reorder_polygon(poly);
// 在一个标准的凸多边形中,如果我们站在最左下角的点(poly[0])往外看
// 其他所有顶点 poly[1], poly[2], ..., poly[n-1] 应该按照逆时针方向整齐排列。
// 从而排除星形(自交)多边形和乱序点集。
for (int i = 1; i < n - 1; ++i) {
if (sgn(cross(poly[i] - poly[0], poly[i + 1] - poly[0])) <= 0) {
return false;
}
}
// 判断单个内角是否 ≥ 180。
for (int i = 0; i < n; ++i) {
ll i1 = (i + 1) % n, i2 = (i + 2) % n;
Vector a = poly[i1] - poly[i], b = poly[i2] - poly[i1];
// 加上大于等于,就相当于排除所有的退化情况,不允许内角为 180 度
if (sgn(cross(a, b)) <= 0) {
return false;
}
}
return true;
}

// 2 是包含,1是在线上,0是出界
int isInPolygon(Point p, const vector<Point> &poly) {
ll res = 0;
Line l(p, p + Point(2e5, rng()));
for (int i = 0; i < poly.size(); ++i) {
int i1 = (i + 1) % SZ(poly);
Line seg = Line(poly[i], poly[i1]);
int ccw = CCW(p, seg);
if (ccw == ON_SEGMENT) {
return 1;
}
if (isIntersect(l, seg)) {
++res;
}
}
// 奇偶法则,多边形内点的射线一定只会经过奇数条边。
if (res & 1) return 2;
return 0;
}

// 定义获取区域的辅助函数,辅助
int get_part_of_point(const Point &p) {
if (p.y < 0) return 0; // 下半平面 (-pi, 0)
if (p.y == 0 && p.x >= 0) return 1; // 角度为 0 的部分 (含原点)
return 2; // 上半平面 (0, pi]
}

// 你需要将所有点按照 atan2(yi,xi)进行排序,也即从 x 轴负半轴(不含)开始进行逆时针排序。
// AC https://qoj.ac/submission/2001485
void polar_angle_sort(vector<Point> &poly) {
sort(poly.begin(), poly.end(), [](const Point &a, const Point &b)-> bool {
int part1 = get_part_of_point(a), part2 = get_part_of_point(b);
if (part1 != part2) {
return part1 < part2;
}
return sgn(cross(a, b)) == 1;
});
}

// 两个小凸包,合成一个大凸包,这个大凸包,即闵可夫斯基和
// 参考了 https://github.com/hh2048/XCPC 的 mincowski
// minkowski 的减法就是加上关于原点的中心对称图形
// AC https://qoj.ac/submission/2002545
vector<Point> minkowski(vector<Point> &P1, vector<Point> &P2) {
reorder_polygon(P1);
reorder_polygon(P2);
int n = P1.size(), m = P2.size();
vector<Point> V1(n), V2(m);
// 预处理:计算差分向量(边向量)
for (int i = 0; i < n; i++) {
V1[i] = P1[(i + 1) % n] - P1[i];
}
for (int i = 0; i < m; i++) {
V2[i] = P2[(i + 1) % m] - P2[i];
}
vector<Point> ans = {P1.front() + P2.front()};
int t = 0, i = 0, j = 0;
while (i < n && j < m) {
// cross(V1,V2)> 0,就说明 V1 小于这个 V2(在 CCW 下)
// 所以先选择这个小的 V1,反之同理
Point val = sgn(cross(V1[i], V2[j])) > 0 ? V1[i++] : V2[j++];
ans.push_back(ans.back() + val);
}
// 处理剩余的边
while (i < n) ans.push_back(ans.back() + V1[i++]);
while (j < m) ans.push_back(ans.back() + V2[j++]);
return ans;
}

vector<Point> make_convex_hull(vector<Point> A) {
size_t n = A.size();
if (n <= 2) {
// 特判
return A;
}
vector<Point> ans(n * 2);
// Andrew 算法的核心:先按 X 坐标排序,X 相同按 Y 坐标排序
// 这样我们就可以保证处理点的顺序是从左到右
sort(A.begin(), A.end());
int now = -1;
for (int i = 0; i < n; i++) {
// 维护下凸包
// 这个是严格的,不保存共线点的实现
// 只有严格大于 0 的时候才是这个 CCW 的
while (now > 0 && cross(ans[now] - A[i], ans[now - 1] - A[i]) <= 0) {
now--; // 弹出栈顶,因为它会导致凹陷或共线
}
ans[++now] = A[i];
}
int pre = now;
// 从右向左扫描所有点(注意是从 n-2 开始,因为 n-1 已经在栈顶了)
for (int i = n - 2; i >= 0; i--) {
// 维护上凸包
while (now > pre && cross(ans[now] - A[i], ans[now - 1] - A[i]) <= 0) {
now--;
}
ans[++now] = A[i];
}
ans.resize(now);
return ans;
}