动态规划(dynamic programming)与分治方法类似,都是通过组合子问题的解来求解原问题。但是动态规划算法对每个子子问题只求解一次,将其保存在一个表格中,从而无需每次求解一个子子问题时都重复计算,避免来这种不必要对对计算工作。
通常按照一下四个步骤来设计一个动态规划算法:
- 刻画一个最优对结构特征。
- 递归地定义最优解对值。
- 计算最优解的值,通常采用自底向上的方法。
- 利用计算出的信息构造一个最优解。
动态规划的本质就是递归,这样比较容易判断一个问题是否可以用动态规划算法去求解,因为只要思考问题的规模缩小/扩大之后是不是同样的方法求解即可。适用应用动态规划算法求解的最优问题应该具备两个要素:
- 最优子结构
- 子问题重叠
如果一个问题的最优解包含其子问题的最优解,则称此问题具有最优解结构的性质。下面,我们来看一个动态规划经典的问题:求最长递增序列长度LIS(longest increasing subsequence)
重叠子问题其实就是子问题空间必须足够“小”,即问题的递归算法会反复的求解相同的子问题,而不是一直生成新的子问题。
设有由n个不相同的整数组成的数列,记为:a(1)、a(2)、……、a(n)且a(i)<>a(j) (i<>j)问题:一个序列有n个数:a[1],a[2],…,a[N],求出最长非降子序列的长度
若存在i1
首先,我们先理一下思路:
1)我们最终目标是要求n个数的序列中最长非降子序列的长度,如果能先求出a[1],a[2],a[i](i
18,7,14,20,18,23根据前面的状态,我们可以进行推断:
(1)i=1时,序列:18,LIS长度d(1)=1;
(2)i=2时,序列:7,此时7前面没有比7小的了,所以LIS长度d(2)=1;
(3)i=3时,序列:7,14,此时14前面有7比它小,所以LIS长度d(3)=2也可以理解为d(3)=d(2)+1;
(4)i=4时,序列:7,14,20,此时20前面7,14比它小,有LIS长度d(4)=3,即d(4)=d(3)+1;
分析到这里,我们基本上可以找到状态转移的方程了,如果我们已经求出了d(1)到d(i-1),那么d(i)可以用下面的状态转移方程得到:
d(i) = max{1, d(j) + 1},其中要满足条件:j < i && a[j] <= a[i]程序如下:
public static void getLongest(int[] arr, int n){
//存放状态
int[] d = new int[n];
//最长非降子序列长度初始化为1
int length = 1;
//状态初始化为1
d[0] = 1;
for (int i = 1;
i < n;
i++){
//状态初始化为1
d[i] = 1;
for (int j = 0;
j < i;
j++){
if (arr[i] > arr[j] && d[j] + 1 > d[i]){
d[i] = d[j] + 1;
}
if (length < d[i]){
length = d[i];
}
}
}System.out.println(length);
}
上面line11-line18即内层for循环其实就是我们在求a[1],a[2],a[i](i
public class Test {public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入序列:");
String[] strLines = scanner.nextLine().trim().split(" ");
int n = strLines.length;
int[] nums = new int[n + 1];
int[] f = new int[n + 1];
for (int i = 1;
i <= n;
i++){
nums[i] = Integer.parseInt(strLines[i-1]);
f[i] = Integer.MAX_VALUE;
}
scanner.close();
f[1] = nums[1];
//通过记录f数组的有效位数,求得个数
int len = 1;
for (int i = 2;
i <= n;
i++){
int l = 0;
int r = len;
int mid;
//若大于末尾,则向后填充
if (nums[i] > f[len]){
f[len++] = nums[i];
} else {
//二分法查找将时间复杂度降为nlogn
while (l < r){
mid = l + (r-l)/2;
if (f[mid] > nums[i]){
r = mid;
} else {
l = mid + 1;
}
}
f[l] = Math.min(nums[i], f[l]);
}
}
System.out.println(len);
}
}
在这我们在继续求解另外一个关于序列的问题,两个序列中的最长公共子序列( LCS )
譬如给定2个序列:
1 2 3 4 5试求出最长的公共子序列。
3 2 1 4 5
显然长度是 3 ,包含 345三个元素(不唯一)
解析:
可以用 dp[i][j] 来表示第一个串的前 i 位,第二个串的前j位的 LCS的长度,那么我们是很容易想到状态转移方程的:
如果当前的 nums1[i] 和 nums2[j] 相同(即是有新的公共元素) 那么dp[ i ] [ j ] = max(dp[ i ] [ j ], dp[ i-1 ] [ j-1 ] + 1);
如果不相同,即无法更新公共元素,考虑继承:dp[i][j]=max(dp[i?1][j],dp[i][j?1])
代码如下:
public class Test {public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入序列1:");
String[] str1 = scanner.nextLine().trim().split(" ");
int n = str1.length;
int[] nums1 = new int[n+1];
for (int i = 1;
i <= n;
i++){
nums1[i] = Integer.parseInt(str1[i-1]);
}
System.out.println("请输入序列2:");
String[] str2 = scanner.nextLine().trim().split(" ");
int m = str2.length;
int[] nums2 = new int[m+1];
for (int i = 1;
i <= m;
i++){
nums2[i] = Integer.parseInt(str2[i-1]);
}
scanner.close();
//dp[i][j]表示两个字符串从头开始,直到第一个串的第i位和第二个串的第j位最多有多少个公共子元素
int[][] dp = new int[n+1][m+1];
for (int i = 1;
i <= n;
i++){
for (int j = 1;
j <= m;
j++){
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
if (nums1[i] == nums2[j]){
dp[i][j] = Math.max(dp[i][j], dp[i-1][j-1] + 1);
}
}
}
System.out.println(dp[n][m]);
}
}
下面在看两道题目进行练习巩固。
一、首先,看一个导弹拦截的问题(http://codevs.cn/problem/1044/)
题目描述 Description某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入描述 Input Description输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数)
输出描述 Output Description输出这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
样例输入 Sample Input389 207 155 300 299 170 158 65
样例输出 Sample Output6
2
数据范围及提示 Data Size & Hint导弹的高度<=30000,导弹个数<=20
分析:本题其实就是求解最长降序序列长度(每套系统拦截的导弹数量)和最长升序序列长度(系统数量)代码如下:
public class Test {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("输入导弹依次飞来的高度(中间用空格间隔): ");
String[] strs = scan.nextLine().split(" ");
int[] high = new int[strs.length];
for (int i = 0;
i < strs.length;
i++){
high[i] = Integer.parseInt(strs[i]);
}
scan.close();
//需要的系统数量
int[] d1 = new int[high.length];
//每套系统拦截的导弹数量
int[] d2 = new int[high.length];
int len1 = 1, len2 = 1;
d1[0] = 1;
d2[0] = 1;
for (int i = 0;
i < high.length;
i++){
d1[i] = 1;
d2[i] = 1;
for (int j = 0;
j < i;
j++){
//导弹系统数量——最长升序序列长度
if (high[i] > high[j] && d1[i] < d1[j] + 1){
d1[i] = d1[j] + 1;
}
if (len1 < d1[i]){
len1 = d1[i];
}
//每套系统拦截的导弹数量——最长降序序列长度
if (high[i] <= high[j] && d2[i] < d2[j] + 1){
d2[i] = d2[j] + 1;
}
if (len2 < d2[i]){
len2 = d2[i];
}
}
}
System.out.println(len2);
System.out.println(len1);
}
}
二、再看一个数字划分问题(http://codevs.cn/problem/1039/)
题目描述 Description将整数n分成k份,且每份不能为空,任意两种划分方案不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种划分方案被认为是相同的。
1 1 5
1 5 1
5 1 1
问有多少种不同的分法。
输入描述 Input Description【动态规划算法的理解】输入:n,k (6
输出描述 Output Description输出:一个整数,即不同的分法。
样例输入 Sample Input7 3
样例输出 Sample Output4
数据范围及提示 Data Size & Hint{四种分法为:1,1,5; 1,2,4; 1,3,3; 2,2,3; }
分析:代码如下:
还是使用动态规划算法,将划分后的数字分成包含1和不包含1两种情况。
dp[i][j] = dp[i-1][j-1] + dp[i-j][j],
即:dp[i][j]表示把数字 i 划分为 j 部分;
dp[i-1][j-1]:划分的j份中至少有一份为1,把这个1拿出来,剩下的相当于把i-1分为j-1份
dp[i-j][j]:划分的j份中没有一份是1,每一份中都拿走1个1,剩下的相当于把i-j分为j份
dp状态一般由前一个状态转移而来的。
public class DivideNum {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
System.out.println("请输入数字以及划分数目(中间空格隔开):");
String[] strs = scan.nextLine().split(" ");
scan.close();
int n = Integer.parseInt(strs[0]);
int k = Integer.parseInt(strs[1]);
int[][] dp = new int[n+1][k+1];
//初始化dp,先暂时默认成1部分
for (int i = 0;
i < n;
i++){
dp[i][1] = 1;
}
//初始化方案数目为1
int size = 1;
for (int i = 1;
i <= n;
i++){
for (int j = 1;
j <=k;
j++){
if (i >= j){
dp[i][j] = dp[i-1][j-1] + dp[i-j][j];
}
if (size < dp[i][j]){
size = dp[i][j];
}
}
}
System.out.println(size);
}
}
另外,如果还不太理解的童鞋,可以在在继续看看下面两篇博文:
- 算法-动态规划 Dynamic Programming--从菜鸟到老鸟
- 动态规划算法
- 动态规划的本质
- 夜深人静写算法(二) - 动态规划
- 进阶DP专题:序列区间DP与环形区间DP
- 算法讲解之Dynamic Programing —— 区间DP [变形:环形DP]
推荐阅读
- 人工智能|干货!人体姿态估计与运动预测
- 分析COMP122 The Caesar Cipher
- 技术|为参加2021年蓝桥杯Java软件开发大学B组细心整理常见基础知识、搜索和常用算法解析例题(持续更新...)
- C语言学习(bit)|16.C语言进阶——深度剖析数据在内存中的存储
- Python机器学习基础与进阶|Python机器学习--集成学习算法--XGBoost算法
- 数据结构与算法|【算法】力扣第 266场周赛
- 数据结构和算法|LeetCode 的正确使用方式
- leetcode|今天开始记录自己的力扣之路
- 人工智能|【机器学习】深度盘点(详细介绍 Python 中的 7 种交叉验证方法!)
- 网络|简单聊聊压缩网络