动态规划(一)基本问题
动态规划把原问题时作若干个重叠子问题进行逐层递推求解,每个字问题的求解过程构成一个“阶段”。
1. 动态规划概念
动态规划算法的三要素:”状态“,”阶段“,”决策“。
动态规划求解的三个基本条件:”子问题重叠性“,“无后效行”,“最优子结构性质”。
无后效行:已求解的子问题不受后续阶段的影响。
最优子结构性质:下一阶段的最优解应该能够由前面各阶段字问题的最优解导出。
动态规划问题求解的的关键在于DP状态的表示和阶段的划分,并给出最终的状态转移方程、边界(或称为初始条件)和目标。
下面给出一些基本的经典问题。
2. 经典问题
最长上升子序列
描述
Given an unsorted array of integers, find the length of longest increasing subsequence.
Example:
1 | Input: [10,9,2,5,3,7,101,18] |
Note:
- There may be more than one LIS combination, it is only necessary for you to return the length.
- Your algorithm should run in O(n2) complexity.
Follow up: Could you improve it to O(n log n) time complexity?
分析
动态规划解法: $f[x]$ 表示是 $a[i]$ 为结尾的“最长上升子序列”的长度,也就是表示子数组 $a[1] … a[i]$ 的“最长上升子序列”的长度。以子序列的结尾位置进行阶段划分。则转移方程为
$$
f[i] = \max \limits_{0 \leq j<i, a[j]<a[i]} \{f[j]+1\}\
$$
边界 $f[0] = 0$ , 目标 $ \max \limits_{1 \leq i \leq N} \{ f[i]\}$。时间复杂度 $O(n^2)$。
1 | // leetcode-300 注意与上述分析不同,代码中下标从0开始 |
贪心+二分解法:虽然LIS是一道比较经典的动态规划题,但它还有其他更优的解法。从LIS的性质出发,要想得到一个更长的上升序列,该序列前面的数必须尽量的小。$a[i]$ 表示第 $i$ 个元素,$f[i]$ 表示长度为 $i$ 的LIS结尾元素的最小值。初始时 $f[1] = a[1]$, 从 $i = 2$ 时遍历原数列, 将每个遍历的数与 $f[]$ 的末尾进行比较;如果大于末尾, 将 $a[i]$ 放在 $f[]$ 数列的最后,$f[]$ 长度增加1, 如果小于末尾那么替换掉 $f[]$ 数列中大于等于 $a[i]$ 的第一个数。$f[]$ 最终的长度就是答案。时间复杂度 $O(nlogn)$。
1 | int lengthOfLIS(vector<int>& a) { |
扩展:输出任意最长上升子序列;输出所有最长上升子序列。
公共最长子序列
描述
Given two strings text1
and text2
, return the length of their longest common subsequence.
A subsequence of a string is a new string generated from the original string with some characters(can be none) deleted without changing the relative order of the remaining characters. (eg, “ace” is a subsequence of “abcde” while “aec” is not). A common subsequence of two strings is a subsequence that is common to both strings.
If there is no common subsequence, return 0.
Example 1:
1 | Input: text1 = "abcde", text2 = "ace" |
Example 2:
1 | Input: text1 = "abc", text2 = "abc" |
Example 3:
1 | Input: text1 = "abc", text2 = "def" |
Constraints:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
- The input strings consist of lowercase English characters only.
分析
$f[i, j]$ 表示最长前缀子串 $a[1…i]$ 与$b[1…j]$ 的“最长公共子序列”的长度。则有如下转移方程
$$
f[i,j] =
\left\{
\begin{array}{**lr**}
max(f[i-1,j],\; f[i, j-1]), & a[i] \not= b[j]\\
f[i-1, j-1] + 1, & a[i] = b[j]
\end{array}
\right.
$$
边界条件 $f[i, 0] = f[j, 0] = 0$,目标 $f[n, m]$。时间复杂度$O(n^2)$。
1 | int longestCommonSubsequence(string a, string b) { |
数字三角形
描述
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
For example, given the following triangle
1 | [ |
The minimum path sum from top to bottom is 11
(i.e., 2 + 3 + 5 + 1 = 11).
分析
题目中说的是从上到下寻找一条路径,在实际的处理过程中可以选择一条从下到上的路径,这样处理的结果的等效的。状态转移方程如下
$$
f[i,j] = a[i,j]+min(f[i+1][j], \;f[i+1][j+1])
$$
初始条件 $f[n, j] = a[n, j]$,目标 $f[1, 1]$。
1 | // leetcode 120. Triangle |
最大子序列和
描述
Given an integer array nums
, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
Example:
1 | Input: [-2,1,-3,4,-1,2,1,-5,4], |
分析
动态规划: $f[i]$ 表示以 $a[i]$ 结尾”最大子序和”。转移方程如下
$$
f[i] = max(f[i-1], \; 0) + a[i]
$$
1 | int maxSubArray(vector<int>& nums) { |
分治思想:假设 $f[i, j]$ 表示序列 $a[i…j]$ 的最大子序列和,$g[i, j]$ 表示序列 $a[i…j]$ 中以 $a[j]$ 结尾的最大子序列和,$h[i, j]$ 表示序列 $a[i…j]$ 中以 $a[i]$ 开头的最大子序列和。那么假设有 $k=(i+j)/2$ ,$a[i…j]$ 的最大子序列和有三种情况,在子序列 $a[i…k]$ 中,在子序列 $a[(k+1)…j]$ 中,或者横跨 $a[i…k]$ 和$a[(k+1)…j]$。则有公式:
$$
f[i, j] = max(f[i,k],\;f[k+1,j],\;g[i,k]+h[k+1,j])
$$
求解目标:$f[1,n]$,边界条件(初始条件):$f[i,i] = g[i,i] = k[i,i] = a[i]$。
3. 状态的多维表示
上面的经典动态规划问题中状态打都是用一维或者二维数组表示的,这些问题中的DP状态表示方式还是比较简单。下面看一道多维状态表示和状态压缩的题目。
Mobile Service
描述
链接:https://ac.nowcoder.com/acm/contest/1041/D
来源:牛客网
一个公司有三个移动服务员,最初分别在位置 1,2,3 处。 如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个地方去。某一时刻只有一个员工能移动,且不允许在同样的位置出现两个员工。从 $p$ 到 $q$ 移动一个员工,需要花费 $c(p,q)$。这个函数不一定对称,但保证 $c(p,p)=0$。
给出N个请求,请求发生的位置分别为 $p_1\sim p_N$。公司必须按顺序依次满足所有请求,目标是最小化公司花费,请你帮忙计算这个最小花费。$N\leq 1000$,位置是 1~200 的整数。
分析
DP的阶段解释已经完成的请求数量。要计划花费就需要知道三个服务员的位置,那么状态就可以表示为$F[i, x, y, z]$,表示完成了前 $i$ 个请求,三个员工分别位于 $x, y, z$ 时,公司当前最小花费。状态转移如下:
$$
F[i+1][p_{i+1}][y][z] = min(F[i+1][p_{i+1}][y][z],\;F[i][x][y][z] + c(x, p_{i+1})) \\
F[i+1][x][p_{i+1}][z] = min(F[i+1][x][p_{i+1}][z],\;F[i][x][y][z] + c(y, p_{i+1})) \\
F[i+1][x][y][p_{i+1}] = min(F[i+1][x][y][p_{i+1}],\;F[i][x][y][z] + c(z, p_{i+1}))
$$
初始值:$F[0][1][2][3] = 0$ ,目标:$max\{F[n][?][?][?]\}$。
状态压缩:$ F[i][p_i][y][z]$ 在处理好第 $i$ 个请求之后,必有一个员工在位置 $p_i$。因此,状态可以缩小一维。
$F[i][x][y]$ 表示在处理好第 $i$ 个请求后,有一个员工在 $p_i$ 另外两个员工在 $x, y$ 时的最小花费。于是有状态转移如下:
$$
F[i+1][x][y] = min(F[i+1][x][y],\;F[i][x][y] + c(p[i], p[i+1])); //p[i+1] x y 互不相等\\
F[i+1][p_i][y] = min(F[i+1][p_i][y],\;F[i][x][y] + c(x, p[i+1])); //p[i+1] pi y 互不相等\\
F[i+1][x][p_i] = min(F[i+1][x][p_i],\;F[i][x][y] + c(y, p[i+1])); //p[i+1] pi x 互不相等
$$
初始值:$F[0][1][2] = 0$,令$p_0 = 3$,目标:$min\{F[n][?][?]\}$。
1 |
|
Author: Hatton.Liu
Link: http://hattonl.github.io/2020/02/14/dynamic-programming-0/
License: 知识共享署名-非商业性使用 4.0 国际许可协议