每日算法 — 使用java实现调度算法:贪心策略与优先队列的任务 deadlines 最优排序

调度问题是操作系统、项目管理和游戏开发中的经典优化问题。给定一组任务,每个任务有执行时间和截止时间,如何安排执行顺序使得超时任务最少、或总延迟最小?本文用Java实现三种核心调度算法:最早截止时间优先(EDD)、最短处理时间优先(SPT)以及带权重的最小延迟调度,结合贪心策略与优先队列,并通过一个”游戏任务调度器”案例展示其在实际场景中的应用。

一、问题定义

设有n个任务,每个任务i有:
– 处理时间 p[i]:完成该任务需要的时间
– 截止时间 d[i]:任务必须在此时间前完成
– 权重 w[i](可选):任务的重要程度

目标1(可行性调度):找出一个调度方案,使得所有任务都能在截止时间前完成。

目标2(最小化最大延迟):最小化 max(0, C[i] – d[i]),其中C[i]为任务i的完成时间。

目标3(最小化总加权延迟):最小化 Σ w[i] × max(0, C[i] – d[i])。

二、最早截止时间优先(EDD)算法

EDD算法按截止时间升序排列任务,若存在可行调度,则EDD调度必为可行调度。这是调度理论中最经典的贪心结论之一。

package com.algorithm.scheduling;

import java.util.*;

/**
 * 任务类:包含处理时间、截止时间和权重
 */
public class Task implements Comparable<Task> {
    String name;
    int processingTime;  // 处理时间
    int deadline;        // 截止时间
    int weight;          // 权重(用于加权调度)
    int startTime;       // 实际开始时间(调度后填充)
    int completionTime;  // 实际完成时间(调度后填充)

    public Task(String name, int processingTime, int deadline) {
        this(name, processingTime, deadline, 1);
    }

    public Task(String name, int processingTime, int deadline, int weight) {
        this.name = name;
        this.processingTime = processingTime;
        this.deadline = deadline;
        this.weight = weight;
    }

    public int getLateness() {
        return Math.max(0, completionTime - deadline);
    }

    @Override
    public int compareTo(Task other) {
        return Integer.compare(this.deadline, other.deadline);
    }

    @Override
    public String toString() {
        return String.format("%s[p=%d,d=%d,w=%d] -> [%d,%d], lateness=%d",
            name, processingTime, deadline, weight,
            startTime, completionTime, getLateness());
    }
}

/**
 * 最早截止时间优先调度器
 * 贪心策略:按截止时间升序排列
 */
public class EDDScheduler {

    /**
     * EDD调度:按截止时间升序排列
     * 定理:若存在可行调度(所有任务不超时),则EDD调度必可行
     */
    public List<Task> schedule(List<Task> tasks) {
        // 拷贝并排序
        List<Task> scheduled = new ArrayList<>(tasks);
        Collections.sort(scheduled);

        int currentTime = 0;
        for (Task task : scheduled) {
            task.startTime = currentTime;
            task.completionTime = currentTime + task.processingTime;
            currentTime = task.completionTime;
        }

        return scheduled;
    }

    /**
     * 检查调度是否可行(所有任务在截止时间前完成)
     */
    public boolean isFeasible(List<Task> scheduled) {
        for (Task task : scheduled) {
            if (task.completionTime > task.deadline) {
                return false;
            }
        }
        return true;
    }

    /**
     * 计算最大延迟
     */
    public int maxLateness(List<Task> scheduled) {
        int max = 0;
        for (Task task : scheduled) {
            max = Math.max(max, task.getLateness());
        }
        return max;
    }

    public static void main(String[] args) {
        EDDScheduler scheduler = new EDDScheduler();

        List<Task> tasks = Arrays.asList(
            new Task("任务A", 3, 5),
            new Task("任务B", 2, 8),
            new Task("任务C", 1, 4),
            new Task("任务D", 4, 10)
        );

        System.out.println("=== EDD调度演示 ===\n");
        System.out.println("原始任务:");
        tasks.forEach(System.out::println);

        List<Task> result = scheduler.schedule(tasks);
        System.out.println("\nEDD调度结果(按截止时间升序):");
        result.forEach(System.out::println);

        System.out.println("\n是否可行: " + scheduler.isFeasible(result));
        System.out.println("最大延迟: " + scheduler.maxLateness(result));
    }
}

三、带优先队列的在线调度

实际系统中任务往往动态到达,需要使用优先队列(堆)维护就绪任务。

package com.algorithm.scheduling;

import java.util.*;

/**
 * 在线任务调度器:任务动态到达,使用优先队列选择下一个执行的任务
 * 策略:最早截止时间优先(EDF - Earliest Deadline First)
 */
public class OnlineScheduler {

    /**
     * 模拟在线调度过程
     * @param arrivingTasks 按到达时间排序的任务列表
     * @return 调度后的任务执行序列
     */
    public List<Task> simulateEDF(List<Task> arrivingTasks) {
        // 按到达时间排序
        List<Task> sorted = new ArrayList<>(arrivingTasks);
        sorted.sort(Comparator.comparingInt(t -> t.startTime)); // 此处startTime复用为到达时间

        // 优先队列:按截止时间排序的就绪任务
        PriorityQueue<Task> readyQueue = new PriorityQueue<>(
            Comparator.comparingInt(t -> t.deadline)
        );

        List<Task> executionOrder = new ArrayList<>();
        int currentTime = 0;
        int taskIndex = 0;

        while (taskIndex < sorted.size() || !readyQueue.isEmpty()) {
            // 将所有已到达的任务加入就绪队列
            while (taskIndex < sorted.size() &&
                   sorted.get(taskIndex).startTime <= currentTime) {
                readyQueue.offer(sorted.get(taskIndex));
                taskIndex++;
            }

            if (readyQueue.isEmpty()) {
                // 没有就绪任务,跳到下一个任务到达时刻
                if (taskIndex < sorted.size()) {
                    currentTime = sorted.get(taskIndex).startTime;
                }
                continue;
            }

            // 选择截止时间最早的任务执行
            Task next = readyQueue.poll();
            int actualStart = currentTime;
            int actualCompletion = currentTime + next.processingTime;
            next.startTime = actualStart;
            next.completionTime = actualCompletion;
            currentTime = actualCompletion;
            executionOrder.add(next);
        }

        return executionOrder;
    }

    public static void main(String[] args) {
        OnlineScheduler scheduler = new OnlineScheduler();

        // 任务到达时间分别为0,1,2,3
        List<Task> tasks = Arrays.asList(
            new Task("T1", 4, 10) {{ startTime = 0; }},  // 到达时间0,处理4,截止10
            new Task("T2", 2, 5)  {{ startTime = 1; }},  // 到达时间1,处理2,截止5
            new Task("T3", 3, 8)  {{ startTime = 2; }},  // 到达时间2,处理3,截止8
            new Task("T4", 1, 4)  {{ startTime = 3; }}   // 到达时间3,处理1,截止4
        );

        System.out.println("=== 在线EDF调度演示 ===\n");
        List<Task> result = scheduler.simulateEDF(tasks);
        System.out.println("执行顺序:");
        result.forEach(System.out::println);
    }
}

四、最小化加权延迟:基于优先队列的贪心算法

当任务有权重时,简单EDD不再最优。对于单位处理时间的任务,按权重降序排列可最小化总加权延迟。对于一般情况,需要使用更复杂的算法,如分支定界或动态规划。

package com.algorithm.scheduling;

import java.util.*;

/**
 * 带权任务调度:最小化总加权延迟
 * 对于单位处理时间任务,按权重降序贪心即可
 * 对于一般任务,使用基于优先队列的近似/精确算法
 */
public class WeightedScheduling {

    /**
     * 单位处理时间任务的最优调度
     * 贪心策略:按权重降序,在截止时间前尽量靠前安排
     */
    public List<Task> scheduleUnitTimeTasks(List<Task> tasks) {
        // 按权重降序排序
        List<Task> sorted = new ArrayList<>(tasks);
        sorted.sort((a, b) -> Integer.compare(b.weight, a.weight));

        // 使用并查集找到每个任务最晚可安排的时隙
        int maxDeadline = sorted.stream().mapToInt(t -> t.deadline).max().orElse(0);
        UnionFind slots = new UnionFind(maxDeadline + 1);

        List<Task> scheduled = new ArrayList<>();
        int[] slotAssignment = new int[maxDeadline + 1];
        Arrays.fill(slotAssignment, -1);

        for (Task task : sorted) {
            int slot = slots.find(Math.min(task.deadline, maxDeadline));
            if (slot > 0) {
                slotAssignment[slot] = scheduled.size();
                task.startTime = slot - 1;  // 时隙编号从1开始,时间从0开始
                task.completionTime = task.startTime + task.processingTime;
                scheduled.add(task);
                slots.union(slot, slot - 1); // 该时隙被占用,指向前一个可用时隙
            }
        }

        // 按开始时间排序输出
        scheduled.sort(Comparator.comparingInt(t -> t.startTime));
        return scheduled;
    }

    /**
     * 并查集:用于找最晚可用时隙
     */
    static class UnionFind {
        int[] parent;

        UnionFind(int n) {
            parent = new int[n];
            for (int i = 0; i < n; i++) parent[i] = i;
        }

        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        void union(int x, int y) {
            parent[find(x)] = find(y);
        }
    }

    public static void main(String[] args) {
        WeightedScheduling scheduler = new WeightedScheduling();

        // 单位处理时间任务
        List<Task> unitTasks = Arrays.asList(
            new Task("高优先级补丁", 1, 3, 50),
            new Task("日常更新", 1, 2, 10),
            new Task("安全修复", 1, 4, 40),
            new Task("UI优化", 1, 3, 20)
        );

        System.out.println("=== 加权单位任务调度 ===\n");
        List<Task> result = scheduler.scheduleUnitTimeTasks(unitTasks);
        System.out.println("调度结果(权重优先+并查集时隙分配):");
        result.forEach(System.out::println);

        int totalWeightedLateness = result.stream()
            .mapToInt(t -> t.weight * t.getLateness())
            .sum();
        System.out.println("\n总加权延迟: " + totalWeightedLateness);
    }
}

五、游戏任务调度器实战

package com.algorithm.scheduling;

import java.util.*;

/**
 * 游戏任务调度器:管理NPC行为、技能冷却、资源加载等任务的执行顺序
 * 综合EDD、权重调度和优先队列
 */
public class GameTaskScheduler {

    private final PriorityQueue<GameTask> taskQueue;
    private int currentTick = 0;

    public GameTaskScheduler() {
        // 按优先级排序:截止时间早的优先,截止时间相同则权重高的优先
        this.taskQueue = new PriorityQueue<>(Comparator
            .comparingInt((GameTask t) -> t.deadline)
            .thenComparingInt(t -> -t.priority)
        );
    }

    /**
     * 游戏任务内部类
     */
    static class GameTask {
        String name;
        int duration;      // 执行持续时间(tick数)
        int deadline;      // 截止时间(tick)
        int priority;      // 优先级权重
        Runnable action;   // 实际执行逻辑
        int remaining;     // 剩余执行时间
        boolean completed;

        GameTask(String name, int duration, int deadline, int priority, Runnable action) {
            this.name = name;
            this.duration = duration;
            this.deadline = deadline;
            this.priority = priority;
            this.action = action;
            this.remaining = duration;
            this.completed = false;
        }
    }

    /**
     * 添加任务到调度队列
     */
    public void addTask(GameTask task) {
        taskQueue.offer(task);
    }

    /**
     * 执行一帧(一个tick)
     * 返回本帧执行的任务列表
     */
    public List<GameTask> tick() {
        List<GameTask> executed = new ArrayList<>();
        currentTick++;

        // 每帧可以执行总时间为1个tick的任务
        int availableTime = 1;

        // 使用临时队列,因为可能需要中断任务
        PriorityQueue<GameTask> nextQueue = new PriorityQueue<>(taskQueue.comparator());

        while (availableTime > 0 && !taskQueue.isEmpty()) {
            GameTask task = taskQueue.poll();

            if (task.deadline < currentTick && task.remaining > 0) {
                System.out.println("[超时] " + task.name + " 已错过截止时间!");
            }

            // 执行任务
            int executeTime = Math.min(availableTime, task.remaining);
            task.remaining -= executeTime;
            availableTime -= executeTime;

            if (task.remaining == 0) {
                task.completed = true;
                task.action.run();
                executed.add(task);
                System.out.println("[完成] Tick " + currentTick + ": " + task.name);
            } else {
                // 任务未完成,放回队列
                nextQueue.offer(task);
                System.out.println("[执行中] Tick " + currentTick + ": " + task.name +
                                   " 剩余 " + task.remaining + " tick");
            }
        }

        // 将未执行的任务移到下一轮
        nextQueue.addAll(taskQueue);
        taskQueue.clear();
        taskQueue.addAll(nextQueue);

        return executed;
    }

    public int getCurrentTick() {
        return currentTick;
    }

    public static void main(String[] args) {
        GameTaskScheduler scheduler = new GameTaskScheduler();

        System.out.println("=== 游戏任务调度器演示 ===\n");

        // 添加各类游戏任务
        scheduler.addTask(new GameTask("玩家输入响应", 1, 5, 100,
            () -> System.out.println("  -> 处理玩家键盘/鼠标输入")));
        scheduler.addTask(new GameTask("物理模拟", 2, 10, 80,
            () -> System.out.println("  -> 更新刚体碰撞与运动")));
        scheduler.addTask(new GameTask("AI行为树更新", 2, 8, 70,
            () -> System.out.println("  -> 更新所有NPC的AI决策")));
        scheduler.addTask(new GameTask("技能冷却检查", 1, 5, 90,
            () -> System.out.println("  -> 减少所有技能冷却计时器")));
        scheduler.addTask(new GameTask("资源异步加载", 3, 20, 50,
            () -> System.out.println("  -> 加载下一关地图资源")));
        scheduler.addTask(new GameTask("网络同步", 1, 3, 95,
            () -> System.out.println("  -> 发送/接收网络数据包")));

        // 模拟运行20个tick
        for (int i = 0; i < 20; i++) {
            List<GameTask> completed = scheduler.tick();
            if (completed.isEmpty() && scheduler.taskQueue.isEmpty()) {
                System.out.println("所有任务已完成!");
                break;
            }
        }
    }
}

六、复杂度分析

调度类型 时间复杂度 空间复杂度 最优性
EDD离线调度 O(n log n) O(n) 最小化最大延迟(最优)
EDF在线调度 O(n log n) O(n) 动态到达时可行调度(最优)
加权单位任务 O(n log n) O(n + d) 最小化总加权延迟(最优)
加权一般任务 O(n!) 精确 / O(n log n) 近似 视算法而定 NP-hard,需启发式

七、总结

调度算法的核心在于理解不同目标下的贪心选择性质:
– EDD/EDF:最小化最大延迟的最优贪心策略
– 权重优先+并查集时隙:单位时间任务的最小加权延迟解
– 优先队列:在线场景下动态选择最优就绪任务

在游戏开发中,调度思想广泛应用于:帧率稳定(每帧任务时间预算)、技能系统(冷却与优先级)、资源管理(异步加载顺序)以及NPC行为更新(LOD级别调度)。