问题描述 
N架飞机准备降落到一个只有一条跑道的机场。每架飞机i有三个关键参数:
-
Tᵢ:到达机场上空的时刻
-
Dᵢ:剩余油料可继续盘旋的时间
-
Lᵢ:降落过程所需的时间
约束条件:
-
一架飞机可以在[Tᵢ, Tᵢ + Dᵢ]时间段内开始降落
-
降落过程需要连续占用跑道Lᵢ个单位时间
-
一架飞机降落完毕时,另一架飞机可以立即在同一时刻开始降落
-
但不能在前一架飞机完成降落前开始降落
目标:判断是否所有飞机都能安全降落。
问题分析
这是一个典型的单机调度问题 ,每个任务有释放时间 和截止时间。问题可以抽象为:给定N个任务,每个任务有最早开始时间Tᵢ,最晚开始时间Tᵢ + Dᵢ,处理时间Lᵢ,判断是否存在一种顺序使得所有任务都能在截止时间前完成。
关键观察
-
飞机开始降落的时间必须满足:
Tᵢ ≤ start_time ≤ Tᵢ + Dᵢ -
如果当前时间为
last(上一架飞机完成的时间),那么下一架飞机i的开始时间为max(last, Tᵢ) -
必须满足:
max(last, Tᵢ) ≤ Tᵢ + Dᵢ
解题思路
方法选择
由于N的最大值通常较小(比如N≤10),我们可以采用暴力搜索的方法,枚举所有可能的降落顺序,检查是否存在可行方案。
搜索空间大小为N!,当N=10时,10! = 3,628,800,在可接受范围内。
核心算法:深度优先搜索(DFS)
我们通过DFS枚举所有排列,关键步骤:
-
状态表示:
-
已安排的飞机数量
-
当前时间(上一架飞机完成降落的时刻)
-
记录每架飞机是否已被安排
-
-
剪枝策略:
-
如果当前飞机的最晚开始时间
Tᵢ + Dᵢ小于当前时间last,说明这架飞机已无法安排,跳过 -
找到一种可行方案后立即终止搜索
-
-
递归公式:
-
选择一架未安排的飞机i
-
计算开始时间:
start = max(last, Tᵢ) -
验证条件:
start ≤ Tᵢ + Dᵢ -
递归到下一层,更新当前时间:
last' = start + Lᵢ
-
代码实现
cpp
#include<iostream>
using namespace std;
const int N=10; // 最大飞机数量,题目规定N≤10
typedef struct plane
{
int t; // 到达时间
int d; // 盘旋时间
int l; // 降落所需时间
}plane;
plane p[N]; // 存储飞机信息
int n; // 当前测试用例的飞机数量
int visit[N]={0}; // 访问标记数组,0=未安排,1=已安排
int found =0; // 标记是否找到可行方案
/**
* 深度优先搜索函数
* @param stp 当前已安排的飞机数量
* @param lasttime 当前时间(上一架飞机降落完成的时间)
*
* 算法核心思想:尝试所有可能的飞机降落顺序,检查是否存在可行方案
*/
void dfs(int stp,int lasttime)
{
// 终止条件:已安排完所有飞机
if(stp==n)
{
found=1; // 找到可行方案
return ;
}
int i=0;
for(i=0;i<n;i++) // 尝试每一架飞机
{
if(visit[i]==0) // 如果飞机i未被安排
{
/****************************************************************
* 关键点1:为什么要检查 (p[i].t + p[i].d) >= lasttime?
*
* 这个条件检查飞机i的"最晚开始时间"是否不早于"当前跑道可用时间"。
*
* 解释:
* 1. p[i].t + p[i].d 是飞机i的最晚开始时间
* 2. lasttime 是当前跑道最早可用的时间
* 3. 条件等价于:跑道可用时,飞机i是否还能等待(没有超过它的最长等待时间)
*
* 注意:这里没有检查最早开始时间,因为最早开始时间约束在后面的max中处理
****************************************************************/
if((p[i].t+p[i].d)>=lasttime)
{
visit[i]=1; // 标记飞机i为已安排
/****************************************************************
* 关键点2:为什么开始时间是 max(lasttime, p[i].t)?
*
* 开始时间必须同时满足两个条件:
* 1. 跑道已空闲:开始时间 ≥ lasttime
* 2. 飞机已到达:开始时间 ≥ p[i].t
*
* 所以实际开始时间 = max(跑道空闲时间, 飞机到达时间)
*
* 常见误解:为什么不是直接用 lasttime?
* 因为飞机可能还没有到达!如果lasttime=0,但飞机t=5,那么开始时间应该是5
****************************************************************/
int starttime=max(lasttime,p[i].t);
/****************************************************************
* 关键点3:递归调用
*
* 参数说明:
* stp+1:已安排飞机数+1
* starttime+p[i].l:当前飞机降落完成的时间
****************************************************************/
dfs(stp+1,starttime+p[i].l);
/****************************************************************
* 关键点4:回溯
*
* 为什么要 visit[i] = 0?
*
* 回溯的作用是"撤销选择",以便尝试其他可能性。
* 想象一下探索迷宫:走完一条路后,要退回来尝试其他路。
*
* 如果没有回溯,一旦选择了飞机i,就无法尝试其他顺序了。
****************************************************************/
visit[i]=0;
}
}
/****************************************************************
* 关键点5:为什么这里要检查 if(found) return?
*
* 这是最重要的优化!找到解后立即终止搜索。
*
* 当递归调用返回时,有两种情况:
* 1. 找到了解(found=1):立即返回,不再尝试其他飞机
* 2. 没找到解(found=0):继续尝试下一架飞机
*
* 这个检查放在这里,而不是在dfs开头,是因为:
* - 如果在dfs开头检查,每次递归都要检查一次
* - 在这里检查,只需在回溯后检查一次
* 实际上,两种方式都可以,但这里的方式更高效
****************************************************************/
if(found) return;
}
}
int main()
{
int test=0; // 测试用例数量
cin>>test;
while(test--) // 处理每个测试用例
{
found=0; // 重置标记
int i=0;
cin>>n; // 读取飞机数量
// 重置访问标记数组
for(i=0;i<n;i++)
{
visit[i]=0;
}
// 读取每架飞机的信息
for(i=0;i<n;i++)
{
cin>>p[i].t>>p[i].d>>p[i].l;
}
dfs(0,0); // 从0架已安排、时间0开始搜索
// 输出结果
if(found==1)
{
cout<<"YES"<<endl;
}
else
{
cout<<"NO"<<endl;
}
}
return 0;
}
算法正确性证明
1. 完备性
算法枚举了所有可能的飞机降落顺序(N!种排列),因此不会漏掉任何可能的解决方案。
2. 正确性
对于每种顺序,算法严格按照约束条件检查:
-
每架飞机在安排时检查
Tᵢ + Dᵢ ≥ lastTime -
开始时间取
max(lastTime, Tᵢ),满足"跑道空闲"和"飞机到达"双重约束 -
完成时间计算准确:
startTime + Lᵢ
3. 剪枝有效性
当Tᵢ + Dᵢ < lastTime时,飞机i已无法在当前时间安排,因此可以跳过,避免无效搜索。
时间复杂度分析
-
最坏情况:需要检查所有N!种排列
-
每次递归:O(N)时间检查每架飞机
-
总时间复杂度:O(N! × N)
-
当N=10时,10! × 10 ≈ 3.6 × 10⁷,在合理范围内
示例分析
示例1
cpp
输入:
1
3
0 100 10
10 10 10
0 2 20
处理过程:
尝试各种顺序,找到可行顺序(3,1,2):
- 飞机3: 开始=max(0,0)=0, 完成=0+20=20
- 飞机1: 开始=max(20,0)=20, 完成=20+10=30
- 飞机2: 开始=max(30,10)=30 ≤ 20(10+10)? 不,应该是10+10=20,30>20,这个顺序不行
实际上,顺序(1,3,2)是可行的:
- 飞机1: 开始=0, 完成=10
- 飞机3: 开始=max(10,0)=10 ≤ 2(0+2)? 不,10>2,不行
顺序(2,1,3):
- 飞机2: 开始=max(0,10)=10, 完成=20
- 飞机1: 开始=max(20,0)=20 ≤ 100(0+100) ✓, 完成=30
- 飞机3: 开始=max(30,0)=30 > 2(0+2) ✗
顺序(2,3,1):
- 飞机2: 开始=10, 完成=20
- 飞机3: 开始=max(20,0)=20 > 2 ✗
顺序(3,2,1):
- 飞机3: 开始=0, 完成=20
- 飞机2: 开始=max(20,10)=20 ≤ 20(10+10) ✓, 完成=30
- 飞机1: 开始=max(30,0)=30 ≤ 100 ✓
输出:YES
示例2
cpp
输入:
1
3
0 2 10
0 2 10
8 10 5
分析:
前两架飞机时间窗为[0,2],需要各10单位时间
但跑道一次只能服务一架飞机,第一架0-10,第二架最早10开始但已超过2
因此无法全部安排
输出:NO
算法优化
1. 排序剪枝
可以按最晚开始时间Tᵢ + Dᵢ升序排序,优先尝试最紧急的飞机,能更快找到解或确定无解。
2. 记忆化搜索
使用状态压缩记录已安排飞机的集合和当前时间,避免重复搜索相同状态。
3. 贪心启发
在实际应用中,可结合贪心策略,如优先安排最晚开始时间最早的飞机,但需要注意贪心不一定总是最优。
总结
飞机降落问题是一个典型的排列搜索问题,通过DFS+剪枝可以在可接受时间内解决小规模实例。关键点在于:
-
理解时间窗约束:
Tᵢ ≤ start ≤ Tᵢ + Dᵢ -
正确处理开始时间:
max(lastTime, Tᵢ) -
合理剪枝:当
Tᵢ + Dᵢ < lastTime时跳过
对于更大规模的问题,可能需要更高效的算法,如动态规划状态压缩(DP with bitmask)或启发式算法。但考虑到比赛和面试中的常见数据范围,本文的DFS解法是简洁有效的解决方案。