【算法详解】飞机降落问题:DFS剪枝解决调度问题

问题描述

N架飞机准备降落到一个只有一条跑道的机场。每架飞机i有三个关键参数:

  • Tᵢ:到达机场上空的时刻

  • Dᵢ:剩余油料可继续盘旋的时间

  • Lᵢ:降落过程所需的时间

约束条件

  • 一架飞机可以在[Tᵢ, Tᵢ + Dᵢ]时间段内开始降落

  • 降落过程需要连续占用跑道Lᵢ个单位时间

  • 一架飞机降落完毕时,另一架飞机可以立即在同一时刻开始降落

  • 但不能在前一架飞机完成降落前开始降落

目标:判断是否所有飞机都能安全降落。

问题分析

这是一个典型的单机调度问题 ,每个任务有释放时间截止时间。问题可以抽象为:给定N个任务,每个任务有最早开始时间Tᵢ,最晚开始时间Tᵢ + Dᵢ,处理时间Lᵢ,判断是否存在一种顺序使得所有任务都能在截止时间前完成。

关键观察

  1. 飞机开始降落的时间必须满足:Tᵢ ≤ start_time ≤ Tᵢ + Dᵢ

  2. 如果当前时间为last(上一架飞机完成的时间),那么下一架飞机i的开始时间为max(last, Tᵢ)

  3. 必须满足:max(last, Tᵢ) ≤ Tᵢ + Dᵢ

解题思路

方法选择

由于N的最大值通常较小(比如N≤10),我们可以采用暴力搜索的方法,枚举所有可能的降落顺序,检查是否存在可行方案。

搜索空间大小为N!,当N=10时,10! = 3,628,800,在可接受范围内。

核心算法:深度优先搜索(DFS)

我们通过DFS枚举所有排列,关键步骤:

  1. 状态表示

    • 已安排的飞机数量

    • 当前时间(上一架飞机完成降落的时刻)

    • 记录每架飞机是否已被安排

  2. 剪枝策略

    • 如果当前飞机的最晚开始时间Tᵢ + Dᵢ小于当前时间last,说明这架飞机已无法安排,跳过

    • 找到一种可行方案后立即终止搜索

  3. 递归公式

    • 选择一架未安排的飞机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+剪枝可以在可接受时间内解决小规模实例。关键点在于:

  1. 理解时间窗约束:Tᵢ ≤ start ≤ Tᵢ + Dᵢ

  2. 正确处理开始时间:max(lastTime, Tᵢ)

  3. 合理剪枝:当Tᵢ + Dᵢ < lastTime时跳过

对于更大规模的问题,可能需要更高效的算法,如动态规划状态压缩(DP with bitmask)或启发式算法。但考虑到比赛和面试中的常见数据范围,本文的DFS解法是简洁有效的解决方案。

相关推荐
奶人五毛拉人一块2 小时前
模板与vector的学习
数据结构·学习·迭代器·vector·模板
I Promise342 小时前
C++ 基础数据结构与 STL 容器详解
开发语言·数据结构·c++
徒 花2 小时前
Python知识学习08
java·python·算法
chushiyunen2 小时前
milvus笔记、常用表结构
笔记·算法·milvus
liliangcsdn2 小时前
ChromaDB距离计算公式示例
人工智能·算法·机器学习
人道领域2 小时前
【LeetCode刷题日记】242.字母异位词
算法·leetcode·职场和发展
卖男孩的小火柴.2 小时前
java内置方法总结及基础算法
java·算法
旖-旎2 小时前
链表(两两交换链表中的节点)(2)
数据结构·c++·学习·算法·链表·力控
XWalnut2 小时前
LeetCode刷题 day8
算法·leetcode·职场和发展