动态规划概念
动态规划主要用来解决多阶段决策过程中的最优化问题,把多阶段过程转化为一系列子问题,不同的子问题通常具有公共的子子问题,即不同的子问题互相重叠。
动态规划通常对每个子子问题只求解一次,将其求解结果保存起来,无需每次求解一个子子问题时都重新计算。
如 背包问题,对于 个物体、容量是 的背包来说,能取得的最大价值记为子问题 ;对于 个物体、容量是 的背包来说,能取得的最大价值记为子问题 。两个子问题可能都会涉及到对于 个物体、容量是 的背包来说能取得的最大价值(子问题 )。所以在求解子问题 时,由于子问题 中已经包含了子问题 ,从而可以避免子问题 的重复计算。
动态规划解题过程
- 刻画一个最优解的结构特征 —— 设计状态;
- 定义最优解的值 —— 推导状态转移方程;
- 计算最优解的值 —— 记忆化搜索或递推;
- 利用计算出的信息构造最优解。
例如,在求解最长上升子序列长度的问题中,朴素的想法是对于长度为 的序列,其解如何来求。在推导状态转移的过程中,发现还需要知道上升序列的最后元素,所以需要返回来设计、修改状态 ,来表示以 结尾的最长上升子序列的长度,从而得出最优解的值为: $(\ a_j
动态规划适用条件
常见动规问题
动态规划的优化
动态规划的应用
【例 】过河问题 第一轮
【题目描述】 个人要从 点坐一条船过河到 点,该船一次最多可坐两人。已知每人独自坐船过河的时间,且两个人坐船过河的时间等于船上渡河时间较长的那个人所用的时间。
【输入格式】 第一行一个整数 ;接下来 行,每行一个整数,代表每个人独自过河的时间。
【输出格式】 一个数,表示所有人都渡过河到 点的最少渡河时间。
【样例输入】
4
1 2 4 8
【样例输出】
15
【题目分析】 首先将 个人按渡河时间排序,设这 个人的渡河时间分别为 。设 为前 个人渡河所需的最小时间,则有两种方案。
方案一:让渡河最快的人带着最慢的人走,这样返程速度是最快的,即 ;
方案二: 把两个比较慢的放在一起,因为如果把他们分开,会对总时间造成更大的影响,即 。
从而可以得到:
【代码片段】
memset(f, 0, sizeof(f));
f[1] = a[1]; f[2] = a[2];
for (int i = 3; i <= n; ++i)
f[i] = min(a[1] + a[i] + f[i - 1], a[1] + 2 * a[2] + a[i] + f[i - 2]);
printf("%d\n", f[n]);
【例 】方格取数 第二轮
【题目描述】 设有 的方格图,每个方格中都有一个整数。现有一只小熊,想从图的左上角走到右下角,每一步只能向上、向下或向右走一格,并且不能重复经过已经走过的方格,也不能走出边界。小熊会取走所有经过方格中的整数,求它能取到的整数之和的最大值。
【输入格式】 第一行两个整数 ,。接下来 行,每行 个整数,依次代表每个方格中的整数。
【输出格式】 一个整数,表示小熊能取到的整数之和的最大值。
【样例输入】
3 4
1 -1 3 2
2 -1 4 -1
-2 2 -3 -1
【样例输出】
9
【题目分析】 普通的方格取数问题只有两个方向,用 表示到格子 能取到的最大值。而在本题中这样表示的话,可以发现就不满足最优子结构的性质,所以需要在普通二维基础上再加一个方向维度,用三维数组进行状态转移,即:
表示从当前格子的左边走到当前格子能取到的最大整数之和;
表示从当前格子的上边走到当前格子能取到的最大整数之和;
表示从当前格子的下边走到当前格子能取到的最大整数之和。
先进行第 列的填值,因为其处在整个地图的最左侧,所以只有从当前格子的上边走到当前格子才可以取到格子里的数的。即:
然后再进行第 到第 列的填值,先填向右走到当前格子和向下走到当前格子的值,再从下往上填向上走到当前格子的值。
最后答案为:
【例 】上升点阵 第二轮
【题目描述】 在一个二维平面内,给定 个整数点 ,此外你还可以自由添加 个整数点。你在自由添加 个点后,还需要从 个点中选出若干个整数点并组成一个序列,使得序列中任意相邻两点间的欧几里得距离恰好为 ,而且横坐标、纵坐标均单调不减,即 或 。请给出满足条件的序列的最大长度。
【输入格式】 第一行两个正整数 , 分别表示给定的整点个数、可自由添加的整点个数。接下来 行,第 行两个正整数 , 表示给定的第 个点的横纵坐标。
【输出格式】 一个整数,表示满足要求的序列的最大长度。
【样例输入】
8 2
3 1
3 2
3 3
3 6
1 2
2 2
5 5
5 3
【样例输出】
8
【题目分析】 对于一维不添加点的上升点列问题,其实就是经典的最长不下降序列问题。同样,对于二维平面上如果不添加点的上升点列问题,也可以借助一维的方法实现,即用 来表示以第 个点结尾的上升点列的最大长度,那么有: 当点 和点 的欧几里得距离为 时 。
在此基础上,如果可以添加点,那我们可以在 的状态基础上在加上一维 ,即用 表示以点 结尾、添加 个点的满足要求的序列的最大长度,那么有
【代码片段】
int dist(int ax, int bx, int ay, int by) { return abs(ax - bx) + abs(ay - by); }
int main() {
int n, K, ans = 0, a[maxn];
scanf("%d %d", &n, &K);
for (int i = 1; i <= n; ++i) scanf("%d %d", &a[i].x, &a[i].y);
sort(a + 1, a + n + 1, cmp);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j < n; ++j) {
for (int k = 0; k <= K; ++k) {
int d = dist(a[i].x, a[j].x, a[i].y, a[j].y) - 1;
if (d <= K - k && a[i].x >= a[j].x && a[i].y >= a[j].y && i != j)
f[i][k + d] = max(f[i][k + d], f[j][k] + d);
}
}
for (int k = 0; k <= K; ++k) f[i][k] += 1;
}
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= K; ++j)
ans = max(ans, f[i][j] + K - j);
printf("%d\n", ans);
return 0;
}
【例 】数列
【题目描述】 给定整数 ,, 和一个长度为 的正整数数组 。对于一个长度为 ,下标从 开始且每个元素均不超过 的非负整数序列 ,我们定义它的权值为 。当这样的序列 满足整数 的二进制表示中 的个数不超过 时,我们认为 是一个合法序列。计算所有合法序列 的权值和对 取模的结果。
【输入格式】 输入第一行是三个整数 ,,。第二行 个整数,分别是 。
【输出格式】 一行一个整数,表示所有合法序列的权值和对 取模的结果。
【样例输入】
5 1 1
2 1
【样例输出】
40
【样例解释】 由于 ,而且由 知道 ,合法的 只有一种可能:,这要求 中必须有 个 和 个 ,于是有 种可能的序列,每种序列的贡献都是 ,权值和为 。
【数据范围】 对所有测试点保证 ,,。
【题目分析】 我们发现, 的二进制表示位中 的数量会涉及进位的问题。由于进位是从低位向高位进行的,所以考虑在 中从低位到高位按位 DP(最低位为第 位)。
设计状态 表示:讨论了 从低到高的前 位,已经确定了 个序列 中的元素, 从低到高前 位中有 个 ,要向当前讨论位的下一位进位 。因为从上一个状态转移到 细节太多,所以考虑用 往后转移。
接下来讨论第 位(位从 开始编号)。假设序列 中有 个元素为 ,那么就相当于给 的第 位贡献了 个 ,再加上上一位进过来的 个 ,总共有 个 。因为当前位每两个 可以向下一位进一个 ,所以 的结果即为全部进位后当前位是否为 。同理,向下一位进的 的个数即为 。所以 往后转移的状态应该是
由于乘积之和的形式满足乘法分配律,所以不难想到 对新状态的贡献应该是
初始化 。统计答案:对于 这一维,由于 ,所以 只用看总共 位,所以是 ;对于 这一维,最终 个元素都要确定,所以是 ;对于 这一维,应该是在 之间的(后面这个 是输入的 ); 这一维就随便了。
细节:在 的第 位有可能还要往后面进位,所以可以使用一个 的函数快速统计出进完位后第 位往后的 的个数,再加上 这一维的数量,如果小于等于输入的 ,就可以统计进答案。 函数很简单,就是一直 res += x & 1, x >>= 1
。并且注意要预处理每个 的幂次方结果与组合数,代码过程中的取模问题等。
时间复杂度时间复杂度为 。
【代码片段】
f[0][0][0][0] = 1;
for (int i = 0; i <= m; i++)
for (int j = 0; j <= n; j++)
for (int k = 0; k <= K; k++)
for (int p = 0; p <= n >> 1; p++)
for (int t = 0; t <= n - j; t++) {
int res = f[i][j][k][p] * pv[i][t] % mod * C[n - j][t] % mod;
f[i + 1][j + t][k + (t + p & 1)][t + p >> 1] += res;
f[i + 1][j + t][k + (t + p & 1)][t + p >> 1] %= mod;
}
for (int k = 0; k <= K; k++)
for (int p = 0; p <= n >> 1; p++)
if (k + popcnt(p) <= K)
ans = (ans + f[m + 1][n][k][p]) % mod;
printf("%lld\n", ans);
【例 】 家今天的饭 第二轮
【题目描述】 是个擅长做菜的高中生,他共掌握 种烹饪方法,且会使用 种主要食材做菜。为了方便叙述,我们对烹饪方法从 编号,对主要食材从 编号。
做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地, 会做 道不同的使用烹饪方法 和主要食材 的菜(),这也意味着 总共会做 道不同的菜。
今天要准备一桌饭招待 和 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 道菜的搭配方案而言:
这里的 为下取整函数,表示不超过 的最大整数。
这些要求难不倒 ,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。
找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 取模的结果。
【输入格式】 第 行两个用单个空格隔开的整数 ,。第 行至第 行,每行 个用单个空格隔开的整数,其中第 行的 个数依次为 。
【输出格式】 一行一个整数,表示所求方案数对 取模的结果。
【样例输入】
2 3
1 0 1
0 1 1
【样例输出】
3
【样例解释】 由于在这个样例中,对于每组 ,, 都最多只会做一道菜,因此我们直接通过给出烹饪方法、主要食材的编号来描述一道菜。
符合要求的方案包括:
做一道用烹饪方法为 、主要食材为 的菜和一道用烹饪方法为 、主要食材为 的菜;
做一道用烹饪方法为 、主要食材为 的菜和一道用烹饪方法为 、主要食材为 的菜;
做一道用烹饪方法为 、主要食材为 的菜和一道用烹饪方法为 、主要食材为 的菜。
因此输出结果为 。需要注意的是,所有只包含一道菜的方案都是不符合要求的,因为唯一的主要食材在超过一半的菜中出现,这不满足 的要求。
【数据范围】 对于所有测试点,保证 ,,。
【题目分析】 首先考虑列的限制,发现若有不合法的列,则必然有且只有一列是不合法的:因为不可能有不同的两列数量都超过总数的一半。于是发现列的限制容易容斥计算:每行选不超过一个的方案数 减 每行选不超过一个、且某一列选了超过一半的方案数。
考虑列的限制,发现若有不合法的列,则必然有且只有一列是不合法的:因为不可能有不同的两列数量都超过总数的一半。那么考虑枚举不合法的一列,假设我们已经枚举了不合法的列为 ,接下来会发现我们只关心一个数的位置是否在当前列;如果属于其他列的情况,那么它具体在哪一列对当前列的合法性并无影响,并不需要考虑。
所以可以用 表示对于 这一列,前 行在 列中选了 个,在其他列中选了 个,令 为第 行的总和,则有转移
状态数 ,转移 ,加上枚举 ,复杂度为 。统计下式的值并对每一列求和即可得到不合法的方案数:
接下来考虑计算总方案数,只需设 为前 行共选了 个数的方案数,则有转移
通过统计可以求得总方案数,这一步是 的,所以我们可以在 的总复杂度内完成这道题,获得 。
继续优化状态表示,我们注意到,在 的转移过程中,我们实际上并不关心 , 的具体数值,而只关心相对的大小关系;所以我们可以将状态变为 ,表示前 行,当前列的数比其他列的数多了 个,则有转移
总复杂度降为 ,从而可以获得 。
【代码片段】
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
scanf("%d", &a[i][j]);
sum[i][0] = (sum[i][0] + a[i][j]) % mod;
}
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
sum[i][j] = (sum[i][0] - a[i][j] + mod) % mod;
long long ans = 0;
for (int col = 1; col <= m; ++col) {
memset(f, 0, sizeof(f));
f[0][n] = 1;
for (int i = 1; i <= n; ++i)
for (int j = n - i; j <= n + i; ++j)
f[i][j] = (f[i - 1][j] + f[i - 1][j - 1] * a[i][col] % mod + f[i - 1][j + 1] * sum[i][col] % mod) % mod;
for (int j = 1; j <= n; ++j)
ans = (ans + f[n][n + j]) % mod;
}
g[0][0] = 1;
for (int i = 1; i <= n; ++i)
for (int j = 0; j <= n; ++j)
g[i][j] = (g[i - 1][j] + (j > 0 ? g[i - 1][j - 1] * sum[i][0] % mod : 0)) % mod;
for (int j = 1; j <= n; ++j)
ans = (ans - g[n][j] + mod) % mod;
printf("%lld\n", ans * (mod - 1) % mod);
【例 】跳房子
【题目描述】 跳房子的游戏规则如下:在地面上确定一个起点,然后在起点右侧画 个格子,这些格子都在同一条直线上。每个格子内有一个整数,表示到达这个格子能得到的分数。玩家第一次从起点开始向右跳,跳到起点右侧的一个格子内。第二次再从当前位置继续向右跳,依此类推。规则规定:玩家每次都必须跳到当前位置右侧的一个格子内。玩家可以在任意时刻结束游戏,获得的分数为曾经到达过的格子中的数字之和。
现在小 研发了一款弹跳机器人来参加这个游戏。但是这个机器人有一个非常严重的缺陷,它每次向右弹跳的距离只能为固定的 。小 希望改进他的机器人,如果他花 个金币改进他的机器人,那么他的机器人灵活性就能增加 ,但是需要注意的是,每次弹跳的距离至少为 。具体而言,当 $g
现在小 希望获得至少 分,请问他至少要花多少金币来改造他的机器人。
【输入格式】 第一行三个正整数 ,,,分别表示格子的数目,改进前机器人弹跳的固定距离,以及希望至少获得的分数。相邻两个数之间用一个空格隔开。接下来 行,每行两个整数 ,,分别表示起点到第 个格子的距离以及第 个格子的分数。两个数之间用一个空格隔开。保证 按递增顺序输入。
【输出格式】 一行一个整数,表示至少要花多少金币来改造他的机器人。若无论如何他都无法获得至少 分,输出 。
【样例输入】
7 4 10
2 6
5 -3
10 3
11 -3
13 1
17 6
20 2
【样例输出】
2
【样例解释】 花费 个金币改进后,小 的机器人依次选择的向右弹跳的距离分别为 ,先后到达的位置分别为 ,对应 这 个格子。这些格子中的数字之和 即为小 获得的分数。
【数据范围】 对于全部的数据满足 ,,,。
【题目分析】 显然需要二分金币花费的值,然后需要考虑,怎样判断二分的这个答案可不可以,显然使用动态规划。
表示跳到第 个格子时所能得到的分数最大值,若跳不到该格,则 ,否则 。时间复杂度 ,对于 的数据可以过。
优化决策选择 —— 单调队列。对于上式,可以进行参数分离,即 ,所以时间复杂度主要是因为需要 的时间来找最大的 ,这显然可以通过单调队列来实现。
【例 】摆渡车
【题目描述】 有 名同学要乘坐摆渡车从人大附中前往人民大学,第 位同学在第 分钟去等车。只有一辆摆渡车在工作,但摆渡车容量可以视为无限大。摆渡车从人大附中出发、把车上的同学送到人民大学、再回到人大附中(去接其他同学),这样往返一趟总共花费 分钟(同学上下车时间忽略不计)。摆渡车要将所有同学都送到人民大学。
凯凯很好奇,如果他能任意安排摆渡车出发的时间,那么这些同学的等车时间之和最小为多少呢?
注意:摆渡车回到人大附中后可以即刻出发。
【输入格式】 第一行包含两个正整数 ,以一个空格分开,分别代表等车人数和摆渡车往返一趟的时间。第二行包含 个正整数,相邻两数之间以一个空格分隔,第 个非负整数 代表第 个同学到达车站的时刻。
【输出格式】 一行一个整数,表示所有同学等车时间之和的最小值(单位:分钟)。
【样例输入】
5 1
3 4 4 3 5
【样例输出】
0
【样例解释】 过程如下:
同学 和同学 在第 分钟开始等车,等待 分钟,在第 分钟乘坐摆渡车出发。摆渡车在第 分钟回到人大附中。
同学 和同学 在第 分钟开始等车,等待 分钟,在第 分钟乘坐摆渡车出发。摆渡车在第 分钟回到人大附中。
同学 在第 分钟开始等车,等待 分钟,在第 分钟乘坐摆渡车出发。自此所有同学都被送到人民大学。总等待时间为 。
【数据范围】 对于 的数据,。
【题目分析】 设 表示在第 分钟发出一班车时,所需要等待的最小时间。最后一个人到车站的时间为 ,则有
$$f_i=\min{f_k+\sum_{k
最终答案 ,时间复杂度为 。
前缀和优化。对于 ,设 表示从 到 时间为止到达车站的人数和,设 表示从 到 时间为止到达车站的人的时间总和,则
即状态转移方程为
时间复杂度降为 。
决策选择优化。由于在最坏情况下,在 时刻发出了一辆车,若有同学在 时刻到达了车站,摆渡车将会在 时刻返回,考虑到等待其他学生的情况,摆渡车最晚会在 时刻发出(不然还不如在 时刻和 时刻各发出一辆),所以我们可以得到结论:没有一个同学会等待超过 分钟。依此,状态转移方程为
时间复杂度为 。
去除冗余阶段。根据上一条优化的结论,当顺序相邻的两位同学的时间间隔超过 的时候,其中的状态都是无用的,可以直接压缩至 。即对 排序后,若 ,则对后续所有的 。另外,可以将动规的起点定为第一个同学到达的时间,则最后到达的时间 最大为 ,最终的时间复杂度为 。
PS: 如有错误请在聊天区 @ 我哦