动态规划判断
① 题目需要求“最值”,可以考虑动态规划
② 有比较明显的状态转移
③ 考虑最优子结构(同时满足重叠子问题是使用DP的必要性)
这道题求总误时惩罚最小的时间安排,也就是求 ∑ i w i \displaystyle\sum_i w_i i∑wi 的最小值,可以尝试动态规划
题目分析
状态是什么?
状态一般是题目中会发生改变(进行状态转移)的量
最终的误时惩罚是本题的状态,任务编号、截止时间都不会发生改变
如何改变状态?
改变误时状态,需要通过每一次的决策,决策可以分为:
选择做 | 不选择做 | |
---|---|---|
时间足够 | ① | ② |
时间不足 | — | ③ |
① 时间够,可以选择做
② 时间够,可以不做
③ 时间不够,只能不做
最后选择哪一种决策的依据:哪一个选择使得误时惩罚最小?
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(i−1, 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(i−1, 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
d⩾t1,比如说现在是第 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<t1d⩾t1
总结状态转移方程
求最小惩罚,因此两个决策的结果取最小值:
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(i−1, min{d, di}−ti),punish(i−1, d)+wi},i=0, d<t1i=0, d⩾t1otherwise
代码
设计
① 每个任务包含三个信息:编号、截止时间、惩罚时间
考虑使用三元组实现;另外,由于贪心策略涉及按照 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 数组
- 填表,按照 任务编号 —— 当前截止时间 的顺序循环遍历
③ main
函数流程
- 输入
- 排序
- 调用状态转移函数
- 输出
实现
#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];
}