UVa 241 Sail Race
题目描述
大西洋海岸水手帆船俱乐部需要开发一个比赛时间估计工具。给定一系列标位(最多 101010 个)、恒定的风速和风向、以及帆船的航行性能参数,要求计算帆船完成比赛的最短时间。
关键规则
- 坐标系:正北为 yyy 轴正方向,正东为 xxx 轴正方向,单位为海里(nm\texttt{nm}nm)。
- 风向:用 000.0∘∼359.9∘000.0^\circ \sim 359.9^\circ000.0∘∼359.9∘ 表示,000.0∘000.0^\circ000.0∘ 为正北,顺时针增加。
- 航行限制:帆船不能驶入与风向夹角小于“顶风角”(point angle\texttt{point angle}point angle)的区域。
- 如果目标方向与风向的夹角 α≥\alpha \geα≥ 顶风角,可以直接航行。
- 否则,必须通过换舷(tack\texttt{tack}tack)来回航行:先沿顶风角方向行驶一段,再换到另一侧顶风角方向行驶,从而间接到达目标点。
- 速度计算:速度 = 风速 × 速度比,速度比取决于“与风夹角”(angle off wind\texttt{angle off wind}angle off wind)所在的区间:
- 顶风区(point angle\texttt{point angle}point angle ≤α<\le \alpha <≤α< reach angle\texttt{reach angle}reach angle):使用 point speed ratio\texttt{point speed ratio}point speed ratio
- 横风区(reach angle\texttt{reach angle}reach angle ≤α<\le \alpha <≤α< downwind angle\texttt{downwind angle}downwind angle):使用 reach speed ratio\texttt{reach speed ratio}reach speed ratio
- 顺风区(α≥\alpha \geα≥ downwind angle\texttt{downwind angle}downwind angle):使用 downwind speed ratio\texttt{downwind speed ratio}downwind speed ratio
- 罚时:每次换舷或经过标位(起点除外)都会产生一次罚时,罚时时间固定(输入给出)。
- 最优化要求:每段赛程(两个标位之间)必须以 最少换舷次数 和 最短航行距离 完成。
题目分析与解题思路
核心难点
本题的核心在于“最少换舷次数”与“最短距离”的同时满足。这实际上是一个几何问题:
- 可行航行方向只有两个(对称于风向):ϕ1=windDir+pointAngle\phi_1 = windDir + pointAngleϕ1=windDir+pointAngle 和 ϕ2=windDir−pointAngle\phi_2 = windDir- pointAngleϕ2=windDir−pointAngle。
- 任意航向必须是这两个方向的线性组合(因为不能直接驶入禁止区)。
- 由起点到终点的向量 D⃗\vec{D}D 可以表示为:
D⃗=a⋅u⃗1+b⋅u⃗2 \vec{D} = a \cdot \vec{u}_1 + b \cdot \vec{u}_2 D=a⋅u1+b⋅u2
其中 u⃗1,u⃗2\vec{u}_1, \vec{u}_2u1,u2 分别是 ϕ1,ϕ2\phi_1, \phi_2ϕ1,ϕ2 方向的单位向量。 - 解出 a,ba, ba,b 后,如果 a,b≥0a, b \ge 0a,b≥0,则最少换舷次数为 111(两段航行),否则不可达(但题目保证可达)。
注意:此时的分段数(tack legs\texttt{tack legs}tack legs)为 222,换舷事件次数为 111。 - 如果目标方向 α≥pointAngle\alpha \ge pointAngleα≥pointAngle,则可以直接航行(a=∣D⃗∣,b=0a = |\vec{D}|, b = 0a=∣D∣,b=0),分段数为 111,换舷事件次数为 000。
因此,我们只需对每一段赛程判断是否直接航行,否则解线性方程组即可。
关键计算步骤
1. 方向角与夹角
给定起点 (x1,y1)(x_1, y_1)(x1,y1) 和终点 (x2,y2)(x_2, y_2)(x2,y2):
-
位移向量:Δx=x2−x1, Δy=y2−y1\Delta x = x_2 - x_1,\ \Delta y = y_2 - y_1Δx=x2−x1, Δy=y2−y1。
-
距离:d=Δx2+Δy2d = \sqrt{\Delta x^2 + \Delta y^2}d=Δx2+Δy2。
-
目标方向(单位:度,从正北顺时针):
targetDir=atan2(Δx,Δy) targetDir = \text{atan2}(\Delta x, \Delta y) targetDir=atan2(Δx,Δy)
返回值范围 (−π,π](-\pi, \pi](−π,π],需要转换到 [0,360)[0, 360)[0,360)。 -
与风向夹角:
α=min(∣targetDir−windDir∣, 360−∣targetDir−windDir∣) \alpha = \min(|targetDir - windDir|,\ 360 - |targetDir - windDir|) α=min(∣targetDir−windDir∣, 360−∣targetDir−windDir∣)
2. 速度计算
设风速为 wswsws,与风夹角为 α\alphaα,速度比由 α\alphaα 所在区间决定:
ratio={prif α∈[pa,ra)rrif α∈[ra,da)drif α≥da \text{ratio} = \begin{cases} p_r & \text{if } \alpha \in [p_a, r_a) \\ r_r & \text{if } \alpha \in [r_a, d_a) \\ d_r & \text{if } \alpha \ge d_a \end{cases} ratio=⎩
⎨
⎧prrrdrif α∈[pa,ra)if α∈[ra,da)if α≥da
其中 pa,ra,dap_a, r_a, d_apa,ra,da 分别表示 point angle\texttt{point angle}point angle、reach angle\texttt{reach angle}reach angle、downwind angle\texttt{downwind angle}downwind angle,pr,rr,drp_r, r_r, d_rpr,rr,dr 是对应的速度比。
则船速 v=ratio×wsv = ratio \times wsv=ratio×ws。
3. 分段航行(必须换舷时)
设两个可行方向:
ϕ1=windDir+pointAngle,ϕ2=windDir−pointAngle \phi_1 = windDir + pointAngle,\quad \phi_2 = windDir - pointAngle ϕ1=windDir+pointAngle,ϕ2=windDir−pointAngle
将角度转换为弧度,计算单位向量:
u⃗1=(cosϕ1,sinϕ1),u⃗2=(cosϕ2,sinϕ2) \vec{u}_1 = (\cos\phi_1, \sin\phi_1),\quad \vec{u}_2 = (\cos\phi_2, \sin\phi_2) u1=(cosϕ1,sinϕ1),u2=(cosϕ2,sinϕ2)
解线性方程组:
{acosϕ1+bcosϕ2=Δxasinϕ1+bsinϕ2=Δy \begin{cases} a \cos\phi_1 + b \cos\phi_2 = \Delta x \\ a \sin\phi_1 + b \sin\phi_2 = \Delta y \end{cases} {acosϕ1+bcosϕ2=Δxasinϕ1+bsinϕ2=Δy
使用克莱姆法则:
Δ=sinϕ1cosϕ2−sinϕ2cosϕ1a=Δxcosϕ2−Δysinϕ2Δ,b=Δysinϕ1−Δxcosϕ1Δ \Delta = \sin\phi_1 \cos\phi_2 - \sin\phi_2 \cos\phi_1 \\ a = \frac{\Delta x \cos\phi_2 - \Delta y \sin\phi_2}{\Delta},\quad b = \frac{\Delta y \sin\phi_1 - \Delta x \cos\phi_1}{\Delta} Δ=sinϕ1cosϕ2−sinϕ2cosϕ1a=ΔΔxcosϕ2−Δysinϕ2,b=ΔΔysinϕ1−Δxcosϕ1
理论上 a,b≥0a, b \ge 0a,b≥0,若出现微小负数可视为 000。
4. 总段数与罚时
- 直接航行:该赛程贡献 111 个航段(tack leg\texttt{tack leg}tack leg)。
- 两段航行:贡献 222 个航段。
- 总航段数 S=∑S = \sumS=∑ 每个赛程的航段数。
- 罚时次数 = S−1S - 1S−1(因为第一次航行前无罚时,此后每新开始一段航程罚一次)。
- 总罚时时间 = (S−1)×tackPenalty(S - 1) \times tackPenalty(S−1)×tackPenalty。
5. 总时间
总航行时间 = ∑各航段距离各航段速度\sum \frac{\text{各航段距离}}{\text{各航段速度}}∑各航段速度各航段距离
总比赛时间 = 总航行时间 + 总罚时时间。
输入输出处理
- 多组测试数据,以
0 0 0 0结束。 - 输出格式需严格按照题目要求:先输出
Race信息,再输出layout长度,然后逐Leg输出,最后输出汇总。 - 每组数据之间用空行隔开。
代码实现
// Sail Race
// UVa ID: 241
// Verdict: Accepted
// Submission Date: 2026-05-16
// UVa Run Time: 0.000s
//
// 版权所有(C)2026,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
// 角度转弧度
double toRadians(double deg) { return deg * M_PI / 180.0; }
// 弧度转角度
double toDegrees(double rad) { return rad * 180.0 / M_PI; }
// 将角度归一化到 [0, 360)
double normalizeAngle(double a) {
a = fmod(a, 360.0);
if (a < 0) a += 360.0;
return a;
}
// 计算两个角度之间的最小夹角 (0~180)
double angleDiff(double a, double b) {
double d = fabs(a - b);
if (d > 180) d = 360 - d;
return d;
}
// 根据与风夹角计算船速
double boatSpeed(double ws, double off, double pa, double pr,
double ra, double rr, double da, double dr) {
if (off < pa - 1e-9) return 0; // 不可航行(理论上不会发生)
if (off < ra) return pr * ws;
if (off < da) return rr * ws;
return dr * ws;
}
int main() {
int raceNum = 0;
double windDir, windSpeed, tackPenalty;
int n;
while (cin >> windDir >> windSpeed >> tackPenalty >> n) {
if (windDir == 0 && windSpeed == 0 && tackPenalty == 0 && n == 0) break;
raceNum++;
// 读取第二行:6 个航行性能参数
double pointAngle, pointRatio, reachAngle, reachRatio, downwindAngle, downwindRatio;
cin >> pointAngle >> pointRatio >> reachAngle >> reachRatio >> downwindAngle >> downwindRatio;
// 读取 n 个标位信息
vector<tuple<string, double, double>> marks(n);
for (int i = 0; i < n; i++) {
cin >> get<0>(marks[i]) >> get<1>(marks[i]) >> get<2>(marks[i]);
}
// 预先计算 layout 总长度(各段直线距离之和)
double layoutLen = 0;
for (int i = 0; i < n - 1; i++) {
auto [name1, x1, y1] = marks[i];
auto [name2, x2, y2] = marks[i + 1];
double dx = x2 - x1, dy = y2 - y1;
layoutLen += sqrt(dx * dx + dy * dy);
}
// 输出 race 标题和 layout 长度
cout << "Race " << raceNum << " has " << n - 1 << " legs\n";
cout << "The race layout is " << fixed << setprecision(2) << layoutLen << " nm long\n\n";
double totalDist = 0, totalTime = 0;
int totalSegments = 0, tackSeq = 0;
// 处理每个 Leg(两个相邻标位之间)
for (int i = 0; i < n - 1; i++) {
auto [name1, x1, y1] = marks[i];
auto [name2, x2, y2] = marks[i + 1];
double dx = x2 - x1, dy = y2 - y1;
double directDist = sqrt(dx * dx + dy * dy);
double targetDir = normalizeAngle(toDegrees(atan2(dx, dy)));
cout << "Leg " << i + 1 << " from Mark " << name1 << " to " << name2
<< ": direction = " << fixed << setprecision(1) << targetDir
<< ", distance = " << fixed << setprecision(2) << directDist << " nm\n";
double alpha = angleDiff(targetDir, windDir);
// 情况1:可以直接航行
if (alpha >= pointAngle) {
double speed = boatSpeed(windSpeed, alpha, pointAngle, pointRatio,
reachAngle, reachRatio, downwindAngle, downwindRatio);
totalDist += directDist;
totalTime += directDist / speed;
totalSegments++;
cout << "Tack " << ++tackSeq << ": speed = " << fixed << setprecision(1) << speed
<< ", direction = " << fixed << setprecision(1) << targetDir
<< ", distance = " << fixed << setprecision(2) << directDist << " nm\n";
}
// 情况2:必须换舷,两段航行
else {
double phi1 = normalizeAngle(windDir + pointAngle);
double phi2 = normalizeAngle(windDir - pointAngle);
double rad1 = toRadians(phi1), rad2 = toRadians(phi2);
double sin1 = sin(rad1), cos1 = cos(rad1);
double sin2 = sin(rad2), cos2 = cos(rad2);
// 解线性方程组求两段距离 a, b
double det = sin1 * cos2 - sin2 * cos1;
double a = (dx * cos2 - dy * sin2) / det;
double b = (dy * sin1 - dx * cos1) / det;
// 两段速度相同(因为与风夹角 = pointAngle)
double speed = boatSpeed(windSpeed, pointAngle, pointAngle, pointRatio,
reachAngle, reachRatio, downwindAngle, downwindRatio);
totalDist += a + b;
totalTime += a / speed + b / speed;
totalSegments += 2;
cout << "Tack " << ++tackSeq << ": speed = " << fixed << setprecision(1) << speed
<< ", direction = " << fixed << setprecision(1) << phi1
<< ", distance = " << fixed << setprecision(2) << a << " nm\n";
cout << "Tack " << ++tackSeq << ": speed = " << fixed << setprecision(1) << speed
<< ", direction = " << fixed << setprecision(1) << phi2
<< ", distance = " << fixed << setprecision(2) << b << " nm\n";
}
cout << '\n';
}
// 罚时计算:总航段数 - 1
int penaltyCount = totalSegments - 1;
double totalPenalty = penaltyCount * tackPenalty;
cout << "Race " << raceNum << " was " << fixed << setprecision(2) << totalDist
<< " nm long with " << totalSegments << " tack legs\n";
cout << "Estimated Race Duration is " << fixed << setprecision(2) << totalTime + totalPenalty
<< " hours with " << fixed << setprecision(2) << totalPenalty << " hours of Tack Penalty\n\n";
}
return 0;
}
总结
本题的关键在于将帆船航行约束转化为向量分解问题,通过解线性方程组得到最少换舷次数下的最短路径。在实现时需要注意:
- 角度归一化与夹角的正确计算。
- 速度比的分段查找。
- 罚时次数与航段数的关系:罚时次数 = 总航段数 −−− 111。
- 多组输入的处理与严格的输出格式。
- 特别注意:题目描述中的输出格式和在线测试数据的输出格式并不一致,需要适当更改(以下为正确的输出格式)。
Race 1 has 5 legs
The race layout is 58.48 nm long
Leg 1 from Mark M1 to M2: direction = 45.0, distance = 14.14 nm
Tack 1: speed = 5.0, direction = 90.0, distance = 10.00 nm
Tack 2: speed = 5.0, direction = 0.0, distance = 10.00 nm
Leg 2 from Mark M2 to M3: direction = 343.3, distance = 10.44 nm
Tack 3: speed = 5.0, direction = 343.3, distance = 10.44 nm
Leg 3 from Mark M3 to M4: direction = 253.6, distance = 17.72 nm
Tack 4: speed = 6.7, direction = 253.6, distance = 17.72 nm
Leg 4 from Mark M4 to M5: direction = 153.4, distance = 11.18 nm
Tack 5: speed = 7.5, direction = 153.4, distance = 11.18 nm
Leg 5 from Mark M5 to M6: direction = 180.0, distance = 5.00 nm
Tack 6: speed = 6.7, direction = 180.0, distance = 5.00 nm
Race 1 was 64.34 nm long with 6 tack legs
Estimated Race Duration is 11.47 hours with 0.50 hours of Tack Penalty
掌握以上要点后,即可顺利通过本题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)