试题 D: 飞机降落
时间限制: 2.0s 内存限制: 256.0MB 本题总分: 10 分
【问题描述】
N 架飞机准备降落到某个只有一条跑道的机场。其中第 i 架飞机在 Ti 时刻 到达机场上空, 到达时它的剩余油料还可以继续盘旋 Di 个单位时间, 即它最早 可以于 Ti 时刻开始降落, 最晚可以于 Ti + Di 时刻开始降落。降落过程需要 Li 个单位时间。
一架飞机降落完毕时, 另一架飞机可以立即在同一时刻开始降落, 但是不 能在前一架飞机完成降落前开始降落。
请你判断 N 架飞机是否可以全部安全降落。
【输入格式】
输入包含多组数据。
第一行包含一个整数 T,代表测试数据的组数。
对于每组数据,第一行包含一个整数 N。
以下 N 行,每行包含三个整数: Ti ,Di 和 Li。
【输出格式】
对于每组数据,输出 YES 或者 NO,代表是否可以全部安全降落。
【样例输入】
2
3
0 100 10
10 10 10
0 2 20
3
试题D: 飞机降落 6
0 10 20
10 10 20
20 10 20
【样例输出】
YES
NO
【样例说明】
对于第一组数据, 可以安排第 3 架飞机于 0 时刻开始降落, 20 时刻完成降 落。安排第 2 架飞机于 20 时刻开始降落, 30 时刻完成降落。安排第 1 架飞机 于 30 时刻开始降落, 40 时刻完成降落。
对于第二组数据,无论如何安排,都会有飞机不能及时降落。
【评测用例规模与约定】
对于 30% 的数据, N ≤ 2。
对于 100% 的数据, 1 ≤ T ≤ 10 ,1 ≤ N ≤ 10 ,0 ≤ Ti ; Di ; Li ≤ 105。
我的答案:
1. 信息
- 飞机数量 N:需要降落的飞机总数。
- 到达时间 Ti:第 i 架飞机到达跑道上空的时间。
- 盘旋时间 Di:第 i 架飞机可以盘旋的时间。
- 降落时间 Li:第 i 架飞机降落所需的时间。
2. 分析
- 目标:判断所有飞机是否都可以在它们的盘旋时间内安全降落。
- 关键思路:飞机必须在 Ti 到 +Ti+Di 的时间内开始降落。我们需要找到一种降落顺序,使得每架飞机都能在这个时间窗口内降落。
- 策略:将飞机按照它们最晚开始降落的时间Ti+Di 进行排序,然后按此顺序尝试安排它们降落。
3. 算法设计
- 将所有飞机按照 Ti+Di 升序排序。
- 遍历排序后的飞机列表,记录当前时间 currentTime。
- 对于每架飞机,检查是否可以在 Ti 到Ti+Di 时间内降落。
- 如果 currentTime≤Ti,则该飞机可以在 Ti 时刻开始降落。
- 否则,检查 currentTime 是否在 Ti 和 Ti+Di 之间。如果是,则该飞机可以降落;如果不是,则返回 "NO"。
- 更新 currentTime 为当前飞机降落结束的时间。
- 如果所有飞机都能安排降落,则返回 "YES"。
4. 代码实现(用C++)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Plane {
int T, D, L;
};
bool canLandAll(vector<Plane>& planes) {
sort(planes.begin(), planes.end(), [](const Plane& a, const Plane& b) {
return a.T + a.D < b.T + b.D;
});
int currentTime = 0;
for (const auto& plane : planes) {
if (currentTime < plane.T) currentTime = plane.T;
if (currentTime > plane.T + plane.D) return false;
currentTime += plane.L;
}
return true;
}
int main() {
int T, N;
cin >> T;
while (T--) {
cin >> N;
vector<Plane> planes(N);
for (int i = 0; i < N; ++i) {
cin >> planes[i].T >> planes[i].D >> planes[i].L;
}
cout << (canLandAll(planes) ? "YES" : "NO") << endl;
}
return 0;
}
5. 实现代码过程中可能遇到的问题
- 排序复杂度:对飞机进行排序的时间复杂度是 O(NlogN),对于大量飞机的情况可能会有性能影响。
- 时间管理:在更新
currentTime
时需要正确处理每架飞机的降落时间。 - 边界情况处理:需要确保所有边界情况都被正确处理,例如当
currentTime
正好等于T_i + D_i
时的情况。
失败了,😄没通过所有案例,有读者知道为什么吗?
官方题解:
1. 信息
- 飞机数量 N:需要降落的飞机总数。
- 到达时间 Ti:第 i 架飞机到达跑道上空的时间。
- 盘旋时间 Di:第 i 架飞机可以盘旋的时间。
- 降落时间 Li:第 i 架飞机降落所需的时间。
2. 分析
- 目标:判断所有飞机是否都可以在它们的盘旋时间内安全降落。
- 关键思路:考虑所有可能的飞机降落顺序,以确保每架飞机都能在其可盘旋的时间窗口内安全降落。
- 决策过程:使用深度优先搜索(DFS)遍历所有可能的飞机降落顺序。对于每一种顺序,检查是否满足每架飞机的最早和最晚降落时间要求。
3. 算法设计
- 使用DFS遍历所有可能的飞机降落顺序。
- 对于每一种顺序,记录当前时间,并检查每架飞机是否能在其允许的时间内降落。
- 如果所有飞机都能安排降落,则返回“是”;如果任何一架飞机无法在规定时间内降落,则返回“否”。
4. 代码实现(用C++)
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 15; int n; int a[N], b[N], c[N]; // 分别存储到达时间、盘旋时间和降落时间 bool st[N]; // 标记某架飞机是否已被安排降落 bool dfs(int u, int last, int cnt) { if (b[u] < last) return false; // 检查是否满足最晚降落时间要求 if (cnt == n) return true; // 所有飞机都已安排降落 for (int i = 0; i < n; ++i) { if (!st[i]) { st[i] = true; if (dfs(i, max(a[u], last) + c[u], cnt + 1)) return true; st[i] = false; } } return false; } int main() { int T; cin >> T; while (T--) { memset(st, 0, sizeof st); cin >> n; for (int i = 0; i < n; ++i) cin >> a[i] >> b[i] >> c[i], b[i] += a[i]; bool flag = false; for (int i = 0; i < n; ++i) { st[i] = true; if (dfs(i, 0, 1)) { flag = true; break; } st[i] = false; } cout << (flag ? "YES" : "NO") << endl; } return 0; }
5. 实现代码过程中可能遇到的问题
- 时间复杂度:由于DFS算法遍历了所有可能的飞机降落顺序,其时间复杂度为指数级,可能导致在大数据量时运行时间过长。
- 栈溢出:递归深度过大可能会导致栈溢出,尤其是在处理较大规模问题时。
- 初始化状态:需要确保每次开始新的DFS前,所有状态(如
st[]
数组)都已正确初始化。 - 边界情况处理:在实现DFS过程中,需要仔细处理每一架飞机的时间窗口边界情况,以避免错误的判断。
六、学到了什么?
-
问题分析和理解:首先,要准确理解问题的需求和限制条件。这个问题需要我们安排一系列飞机在特定的时间窗口内降落,考虑到每架飞机的到达时间、盘旋时间和降落时间。正确理解这些参数及其相互关系是解决问题的第一步。
-
算法选择的重要性:这个问题展示了选择合适的算法来解决特定问题的重要性。最初尝试的贪心算法虽然简单高效,但可能无法处理所有情况,特别是那些需要全局最优解的复杂情况。而深度优先搜索(DFS)虽然可能效率较低,却能提供全局最优解,确保所有飞机都能在规定时间内安全降落。
-
递归和深度优先搜索(DFS):这个问题是深度优先搜索的一个很好的应用实例。通过递归地探索所有可能的降落顺序,DFS算法可以找到一个符合所有限制条件的解决方案。
-
边界条件和状态管理:在实现DFS的过程中,我们需要仔细处理边界条件(如飞机的最早和最晚降落时间)并妥善管理状态(如标记哪些飞机已被安排降落)。这对于防止错误和确保算法正确性是至关重要的。
-
时间复杂度和效率考虑:此问题还强调了考虑算法的时间复杂度和运行效率的重要性。对于有限的数据集,DFS是可行的,但随着数据规模的增大,效率可能会成为一个问题。
-
实际应用中的决策制定:这个问题模拟了现实世界中的一种决策情景,即如何在有限资源(此处为跑道和时间)和严格限制(飞机的盘旋时间)下进行有效的调度。这类问题在许多领域,如物流、交通管理等,都非常常见。
思想和方法:
-
数学建模:将现实世界的问题转化为数学模型是解决复杂问题的第一步。在这个例子中,我们将飞机的降落问题转化为一个调度问题,其中涉及时间窗口、排序和优化。
-
排序和优化:通过分析问题,我们理解到排序飞机降落时间可以帮助找到解决方案。这是一种数学思维方式,即通过组织和排序数据来简化问题。
-
递归思维:深度优先搜索(DFS)算法的应用展示了递归思维的力量。递归是一种重要的数学思维方式,它允许我们将一个复杂问题分解为更小、更易管理的子问题。
-
探索所有可能性:使用DFS算法来探索所有可能的降落顺序,这是一种穷尽搜索的方法。这种方法体现了一种数学思维,即在某些情况下,最好的方式是检查所有可能的情况以找到解决方案。
-
时间和资源的约束处理:在这个问题中,我们必须在特定的时间和资源(跑道可用性)约束下进行调度。学习如何在有限的约束条件下优化结果是数学中的一个重要方面。
-
算法的时间复杂度分析:理解不同算法(如贪心算法和DFS)的时间复杂度对于选择正确的方法来解决问题至关重要。这是数学中对算法效率的分析。
-
权衡和决策:在选择使用贪心算法还是DFS时,我们进行了一种权衡,这体现了数学中的决策制定过程。权衡算法的复杂性和解决问题的准确性是数学思维的一个重要方面。
七、犯了什么错误?
1. 贪心算法的局限性
-
局部最优:贪心算法通常寻找局部最优解,这在某些问题中可能并不等同于全局最优解。在这个问题中,我建议根据飞机的最晚开始降落时间来排序,这在一些情况下可能无法考虑所有飞机的最佳降落顺序。
-
忽略全局上下文:我没有充分考虑所有飞机的全局降落时间表,这可能导致无法找到适应所有飞机的最佳降落顺序。
2. 没有考虑更复杂的算法
在最初的解答中,我没有考虑到更复杂但能够提供全局解的算法,如深度优先搜索(DFS)。DFS能够考虑所有可能的降落顺序,从而更有可能找到满足所有条件的解决方案。
3. 对问题复杂性的低估
这个问题比最初看起来的更复杂,因为它涉及到对多个有时间限制的事件进行调度。在这种情况下,一个简单的贪心策略可能不足以解决所有潜在的复杂情况。
结论
这个错误强调了在解决复杂问题时对问题的全面理解的重要性,以及选择合适算法的重要性。在解决实际问题时,理解问题的所有细节和复杂性,并选择一个能够全面解决问题的算法是至关重要的。同时,这也显示了计算机科学和算法设计中常见的挑战,即如何在算法的复杂性和解决问题的有效性之间找到平衡。