4-13 非单位时间任务安排问题(算法设计与分析)

image-20240412182040582

动态规划判断

① 题目需要求“最值”,可以考虑动态规划

② 有比较明显的状态转移

③ 考虑最优子结构(同时满足重叠子问题是使用DP的必要性)

这道题求总误时惩罚最小的时间安排,也就是求 ∑ i w i \displaystyle\sum_i w_i iwi 的最小值,可以尝试动态规划


题目分析

状态是什么?

状态一般是题目中会发生改变(进行状态转移)的量

最终的误时惩罚是本题的状态,任务编号、截止时间都不会发生改变


如何改变状态?

改变误时状态,需要通过每一次的决策,决策可以分为:

选择做不选择做
时间足够
时间不足

① 时间够,可以选择做

② 时间够,可以不做

③ 时间不够,只能不做

最后选择哪一种决策的依据:哪一个选择使得误时惩罚最小?


DP数组的定义 & 状态转移方程

DP 要和题目所求的状态相关,因此 DP 数组记录的是最小误时惩罚时间

现在有一个问题:DP 数组应该设定为几个维度的?——看状态与哪些量相关

由于截止时间 d i d_i di 的限制,当处在某一个时间点 d d d 的时候,有的任务可能无法完成,导致了误时惩罚

另一方面,每一个时间点,任务 i i i 做还是不做的误时惩罚状态是不同的

由此,定义二维 DP 数组 dp[i][d],其中 i 表示第 i 项任务,d 表示当前的时间点

dp[i][d] 表示当前截至时间点为d 时任务 i 的最小误时惩罚


求什么?

定义完 DP 数组之后,这道题求的是 DP 数组的哪一个量?

首先,任务可能有的没做,但最终都是要走到最后一个任务,表示任务完成。最后一个任务是 n - 1

对于时间点,取这些任务截止时间的最大值 d = m a x ( d i ) \rm d=max(d_i) d=max(di),在此之后就没有任务了。

因此,题目求的是 dp[n - 1][d],通过每一次决策转移到这个状态上


贪心策略

根据经验,DDL 在前的任务都是要先完成的,DDL 在后面的任务可以先拖一拖()

如果先把 DDL 晚的任务先做,很可能使得大部分 DDL 早的任务都做不完,误时惩罚可能会很大

考虑将任务信息数组按照 d i \rm d_i di 进行非递减排序,排在最后的是 DDL 最晚的任务

先处理 DDL 早的任务,然后将状态转移到 DDL 最晚的任务


p u n i s h ( i , d i ) \rm punish(i, d_i) punish(i,di) 表示上面的 dp[i][d],如何求出状态转移方程?

状态要转移,和决策密不可分。也就是说,要转移状态,要看有几种决策,和每一种决策的结果。

决策1. 选择做

在当前时间点 d d d ,如果任务 i i i 能做,那么根据贪心策略:

让这项任务越早开始越好,用 m i n ( d , d i ) − t i \rm min(d, d_i)-t_i min(d,di)ti 表示最早开始时间

m i n ( d ,   d i ) \rm min(d,\ d_i) min(d, di) 含义:

① 如果d < di,那么任务i必须在d之前完成,因为 d 是当前的截止时间点

② 如果d >= di,则任务i至少要在di之前完成,因为过了 di 将会惩罚

此时
p u n i s h ( i ,   d ) = p u n i s h ( i − 1 ,   m i n { d ,   d i } − t i ) \rm punish(i,\ d)=punish(i-1,\ min\{d,\ d_i\}-t_i) punish(i, d)=punish(i1, min{d, di}ti)

决策2. 不选择做

不选择做有两种情况:① 可以做,但是放弃了(可能有更好的选择);② 根本来不及做

但不论哪一种情况,不选择做都会惩罚,也就是在前一项任务完成之后,加上当前任务的罚时:
p u n i s h ( i ,   d ) = p u n i s h ( i − 1 ,   d ) + w i \rm punish(i,\ d)=punish(i-1,\ d)+w_i punish(i, d)=punish(i1, d)+wi

初始化 / 特殊情况

填表之前首先要初始化一些特殊情况

按照 DDL 排序之后,第一项任务的DDL 最早,对于当前截止时间 d d d

① 如果 d < t 1 d < t_1 d<t1,比如说现在是第 1 天,而任务 1 需要 3 天的时间完成,那么任务 1 将在第 1 天无法完成,从而罚时

② 如果 d ⩾ t 1 d\geqslant t_1 dt1,比如说现在是第 1 天,而任务 1 需要 1 天的时间完成,那么任务 1 直接完成,误时惩罚记为 0
p u n i s h ( 0 ,   d ) = { w 1 , d < t 1 0 , d ⩾ t 1 \rm punish(0,\ d)=\begin{cases}w_1,& d<t_1\\\\0,&d\geqslant t_1\end{cases} punish(0, d)= w1,0,d<t1dt1

总结状态转移方程

求最小惩罚,因此两个决策的结果取最小值:
p u n i s h ( i ,   d ) = { w 1 , i = 0 ,   d < t 1 0 , i = 0 ,   d ⩾ t 1 m i n { p u n i s h ( i − 1 ,   m i n { d ,   d i } − t i ) , p u n i s h ( i − 1 ,   d ) + w i } , o t h e r w i s e \rm punish(i,\ d)=\begin{cases}\rm w_1,&i=0,\ d<t_1\\\\0,&i=0,\ d\geqslant t_1\\\\\rm min\big\{punish(i-1,\ min\{d,\ d_i\}-t_i),\quad punish(i-1,\ d)+w_i\big\},& otherwise\end{cases} punish(i, d)= w1,0,min{punish(i1, min{d, di}ti),punish(i1, d)+wi},i=0, d<t1i=0, dt1otherwise

代码

设计

① 每个任务包含三个信息:编号、截止时间、惩罚时间

考虑使用三元组实现;另外,由于贪心策略涉及按照 DDL 排序,还需要定义compare函数(为了调用sort函数)

struct TASK {
    int t_i;
    int deadline;
    int punishment;
};

bool compareTasks(const TASK& task1, const TASK& task2) {
    return task1.deadline < task2.deadline;
}

② 状态转移函数 dp()

  • 初始化 DP 数组
  • 填表,按照 任务编号 —— 当前截止时间 的顺序循环遍历

image-20240412205807157

main函数流程

  1. 输入
  2. 排序
  3. 调用状态转移函数
  4. 输出

实现

#include <iostream>
#include <vector>
#include <algorithm>

using std::cin;
using std::cout;
using std::min;
using std::vector;

struct TASK {
    int t_i;
    int deadline;
    int punishment;
};

bool compareTasks(const TASK& task1, const TASK& task2) {
    return task1.deadline < task2.deadline;
}

int n, d_max;
vector<TASK> Task;
vector<vector<int>> DP;
// 设置一个无穷大的惩罚时间
const int MAX = 0x3f3f3f3f;

// 状态转移函数
void dp() {
    for (int i = 0; i <= d_max; i++) {
        if (Task[0].t_i <= i)
            DP[0][i] = 0;
        else
            DP[0][i] = Task[0].punishment;
    }

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= d_max; j++) {
            DP[i][j] = DP[i - 1][j] + Task[i].punishment;
            int earlierTime = min(j, Task[i].deadline);
            if (earlierTime >= Task[i].t_i)
                DP[i][j] = min(DP[i][j], DP[i - 1][earlierTime - Task[i].t_i]);
        }
	}
}

int main() 
{
    // 1. 输入
    cin >> n;
    Task.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> Task[i].t_i >> Task[i].deadline >> Task[i].punishment;
    }
    // 2. 排序
    sort(Task.begin(), Task.end(), compareTasks);
    
    // 3. 状态转移
    d_max = Task[n - 1].deadline;
    DP.resize(n, vector<int>(d_max + 1, MAX));
    dp();

    // 4. 输出
    cout << DP[n - 1][d_max];
}
  • 27
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值