最近毕业面临找工作的问题,在刷各大厂笔试题的时候发现大厂特别爱考一类问题,就是动态规划问题。于是决定将我见到的各类常见的动态规划问题做个总结,方便后期复习。
动态规划即Dynamic Programming,简称DP,初接触动态规划问题会觉得这类问题很抽象,晦涩难懂,不同的问题的求解思路也是千差万别,但是从本质上来看动态规划问题,最核心的思想就是将一个大的问题拆分成一个一个的子问题,通过定义问题与子问题之间的状态转移关系,从而使得问题可以递推的解决下去。(绝大部分递归的问题都可以用动态规划的方式来解决,而且可以大大减小时间复杂度和空间复杂度)
最长递增子序列(来自牛客网)
对于一个数字序列,请设计一个复杂度为O(nlogn)的算法,返回该序列的最长上升子序列的长度,这里的子序列定义为这样一个序列U1,U2…,其中Ui < Ui+1,且A[Ui] < A[Ui+1]。
给定一个数字序列A及序列的长度n,请返回最长上升子序列的长度。
测试样例:
[2,1,4,3,1,5,6],7
返回:4
这里用dp[i]表示以标识为i的元素为递增序列结尾元素的最长递增子序列的长度,由于这里的递增序列不要求严格相邻,因此A[i]需要和每一个A[j] (i>j)比较,如果存在A[i]>A[j],说明第i个元素可以接在第j个元素后面作为新的递增序列的结尾,因此dp[i] = max(dp[j]) + 1;否则,说明第i个元素比前面所有的数都小,以i元素作为结尾的d递增p序列长度为1,因此dp[i] = 1。最后只要取出dp中最大的值就是最长递增子序列的长度。将状态分析好了以后,问题就迎刃而解,这里附上手动撸的java版代码:
public static int[] forward(List<Integer> list){
int[] dp = new int[list.size()];
dp[0] = 1;
//max标记前面j个数中最大的子序列的长度
int max;
for(int i = 1; i < list.size();i++){
max = 0;
for(int j = 0; j < i; j++){
if(list.get(i) > list.get(j)){
max = max < dp[j] + 1 ? dp[j] + 1 : max;
}else{
max = max < 1 ? 1 : max;
}
}
dp[i] = max;
}
return dp;
}
最长公共子序列(来自牛客网)
对于两个字符串,请设计一个高效算法,求他们的最长公共子序列的长度,这里的最长公共子序列定义为有两个序列U1,U2,U3…Un和V1,V2,V3…Vn,其中Ui<Ui+1,Vi<Vi+1。且A[Ui] == B[Vi]。
给定两个字符串A和B,同时给定两个串的长度n和m,请返回最长公共子序列的长度。保证两串长度均小于等于300。
测试样例:
“1A2C3D4B56”,10,”B1D23CA45B6A”,12
返回:6
我们用dp[i][j]来表示A串中的前i个字符与B串中的前j个字符的最长公共子序列长度,倘若A[i] = B[j],我们可以得到转移方程dp[i][j] = dp[i – 1][j – 1] + 1,如果A[i] != B[j],dp[i][j] = max(dp[i][j-1],dp[i-1][j])。
public static int findLCS(String A, int n, String B, int m) {
// write code here
int[][] dp = new int[n + 1][m + 1];
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(A.charAt(i - 1) == B.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = Math.max(dp[i - 1][j - 1],Math.max(dp[i - 1][j],dp[i][j - 1]));
}
}
}
return dp[n][m];
}
最长公共子串(来自牛客网)
对于两个字符串,请设计一个时间复杂度为O(m*n)的算法(这里的m和n为两串的长度),求出两串的最长公共子串的长度。这里的最长公共子串的定义为两个序列U1,U2,..Un和V1,V2,…Vn,其中Ui + 1 == Ui+1,Vi + 1 == Vi+1,同时Ui == Vi。
给定两个字符串A和B,同时给定两串的长度n和m。
测试样例:
“1AB2345CD”,9,”12345EF”,7
返回:4
这个问题与上面的问题类似,区别点在于这里是子串,是连续的,令dp[i][j]表示A串中的以第i – 1个字符与B串中的以第j – 1个字符结尾的最长公共子串的长度,但是仅当A[i-1] = B[j-1]且dp[i][j] = dp[i -1][j -1] + 1;A[i-1] != B[j-1]时,以他们结尾的公共子串长度必然为0;最后从dp中取出长度最大的值即为最长公共子串。
public static int findLongest(String A, int n, String B, int m) {
// write code here
int[][] dp = new int[n + 1][m + 1];
int max = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(A.charAt(i - 1) == B.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}else{
dp[i][j] = 0;
}
if(max < dp[i][j]) max = dp[i][j];
}
}
return max;
}
最小编辑代价问题(来自牛客网重点内容)
对于两个字符串A和B,我们需要进行插入、删除和修改操作将A串变为B串,定义c0,c1,c2分别为三种操作的代价,请设计一个高效算法,求出将A串变为B串所需要的最少代价。
给定两个字符串A和B,及它们的长度和三种操作代价,请返回将A串变为B串所需要的最小代价。保证两串长度均小于等于300,且三种代价值均小于等于100。
测试样例:
“abc”,3,”adc”,3,5,3,100
返回:8
这个问题相比前面的几个问题更加抽象一些,我们需要定义四种情况,首先令dp[i][j]表示将A串中的前i个字符转换成B串中的前j个字符所需要的代价,第一种是A[i] = B[j]时,我们不需要做任何操作可以进入下一字符的比较,状态转移方程为dp[i][j] = dp[i – 1][j – 1];第二种情况是删除A串中的最后一个字符,使得A串和B串相等,状态转移为dp[i][j] = dp[i-1][j] + c1 ;第三种是在B串中插入一个字符使得A串和B串相等,即B串中去掉最后一个字符与A现在的A串相等,转移方程为dp[i][j] = dp[i][j – 1] + c0;第四种是将A串中的一个字符修改从而使得A串和B串相等,即A串和B串最后一个字符不相等,其他位都相等,状态状态转移为dp[i][j] = dp[i-1][j-1] + 1。在后三种情况中,我们都可以将A字符串变为B字符串,但是我们这里要求的是最小代价,因此dp[i][j] = min(情况2,3,4)。
java版本的代码如下:
public static int findMinCost(String A, int n, String B, int m, int c0, int c1, int c2) {
// write code here
int[][] dp = new int[n+1][m+1];
for(int i = 0; i <=n; i++) dp[i][0] = i*c0;
for(int j = 0; j <=m; j++) dp[0][j] = j*c1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(A.charAt(i - 1) == B.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1];
}else{
dp[i][j] = Math.min(dp[i - 1][j - 1] + c2,Math.min(dp[i][j - 1] + c0,dp[i - 1][j] + c1));
}
}
}
return dp[n][m];
}
0/1背包问题
0/1背包问题是动态规划问题中的经典问题
问题描述:
给定N中物品和一个背包。物品i的重量是Wi,其价值位Vi ,背包的容量为C。问应该如何选择装入背包的物品,使得转入背包的物品的总价值为最大??
在选择物品的时候,对每种物品i只有两种选择,即装入背包或不装入背包。不能讲物品i装入多次,也不能只装入物品的一部分。因此,该问题被称为0-1背包问题。
对于这个问题,我们定义最大价值为Max(N,C),这里我们可以分两种情况来考虑
情况一:包中放了第N个物品
那么我们现在还剩下N-1个物品,以及C-cn的背包容量,这种情况下我们得到的最大的价值为Max(N-1,C-cn) + N * vn,这即是上面问题的子问题
情况二:包中没有放第N个物品
那么我们还剩下N-1个物品以及C的背包容量,在这种情况下,我们得到的最大的价值为Max(N-1,C)
由上面的分析我们可以定义dp[i][j]表示有i个物品和j的容量的情况下可以得到的最大的价值,很容易通过情况一二得到状态转移方程式如下:
dp[i][j] = Max(dp[i-1][j-ci] + vi,dp[i-1][j])
为了方便理解,这里给出一个具体的0/1背包实例
王强今天很开心,公司发给N元的年终奖。王强决定把年终奖用于购物,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件。王强想买的东西很多,为了不超出预算,他把每件物品规定了一个重要度,分为 5 等:用整数 1 ~ 5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 元的整数倍)。他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j 件物品的价格为 v[j] ,重要度为 w[j] ,共选中了 k 件物品,编号依次为 j 1 , j 2 ,……, j k ,则所求的总和为:
v[j 1 ]w[j 1 ]+v[j 2 ]*w[j 2 ]+ … +v[j k ]*w[j k ] 。(其中 为乘号)
这里直接给出java版本的源码
import java.util.*;
/** * Created by 梅晨 on 2017/3/26. */
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
String[] moneyAndNum = in.nextLine().split(" ");
int money = Integer.valueOf(moneyAndNum[0]);
int n = Integer.valueOf(moneyAndNum[1]);
int[][] weight = new int[60][3];
int[][] val = new int[60][3];
int[][] dp = new int[n+1][money+1];
int index = 1;
for(int i = 0; i < n; i++){
String[] strs = in.nextLine().split(" ");
int p = Integer.valueOf(strs[0]);
int w = Integer.valueOf(strs[1]);
int q = Integer.valueOf(strs[2]);
if(q == 0){
weight[index][0] = p;
val[index][0] = w * p;
index++;
}else{
if(weight[index - 1][1] == 0){
weight[index - 1][1] = p;
val[index - 1][1] = w * p;
}else{
weight[index - 1][2] = p;
val[index - 1][2] = w * p;
}
}
}
for(int i = 1; i < n + 1; i++){
for(int j = 1; j < money + 1; j++){
//j空间够放第i个物品
if(weight[i][0] <= j){
//dp[i - 1][j]表示没有放第i个物品,dp[i - 1][j - weight[i][0]]表示放了第i个物品
dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - weight[i][0]] + val[i][0]);
}
if(weight[i][0] + weight[i][1] <= j){
dp[i][j] = Math.max(dp[i][j],Math.max(dp[i - 1][j],dp[i - 1][j - weight[i][0] - weight[i][1]] + val[i][0] + val[i][1]));
}
if(weight[i][0] + weight[i][2] <= j){
dp[i][j] = Math.max(dp[i][j],Math.max(dp[i - 1][j],dp[i - 1][j - weight[i][0] - weight[i][2]] + val[i][0] + val[i][2]));
}
if(weight[i][0] + weight[i][1] + weight[i][2] <= j){
dp[i][j] = Math.max(dp[i][j],Math.max(dp[i - 1][j],dp[i - 1][j - weight[i][0] - weight[i][1] - weight[i][2]] + val[i][0] + val[i][1] + val[i][2]));
}
}
}
System.out.println(dp[n][money]);
}
}
}
股票收益最大化问题(来自leetcode)
股票收益最大化问题1
股票收益最大化问题2
有一个数组,数组中的第i个数表示第i天股票的价格,现在要求设计一种算法,来取得最大化的股票收益,这里你可以进行尽可能多次的交易,但是交易不能交叉,即买股票之前必须先把上一只股票卖出。
这里我们来分析状态转移的问题,在第i天,我们观察股票的价格,如果price[i] >price[i – 1],表明第i天相对第i-1天是有收益的,倘若我们第i天卖出股票,则相比第i-1天收益增加,方程式为dp[i] = dp[i-1] + price[i] – price[i-1],如果price[i]<=price[i-1],如果我们在第i天卖出股票,则是亏损,因此我们选在继续持有股票,即方程式为dp[i] = dp[i-1]。有的朋友可能会困惑,这里只有卖出操作,那怎么知道它什么时候买进股票呢,我们仔细的分析一下,可以发现在股票下跌的时候,是不进行任何操作的,也没有任何收益,只有在股票价格最低,并且开始上升的时候,我们开始买进,并且在股价最高的时候抛出。
下面给出参考的java版本代码:
public class Solution {
public int maxProfit(int[] prices){
int[] dp = new int[prices.length];
if(prices.length == 0) return 0;
dp[0] = 0;
for(int i = 1; i < prices.length;i++){
if(prices[i] > prices[i - 1]){
dp[i] = dp[i - 1] + prices[i] - prices[i - 1];
}else{
dp[i] = dp[i - 1];
}
}
return dp[prices.length - 1];
}
}
**称砝码问题**
现有一组砝码,重量互不相等,分别为m1,m2,m3…mn;
每种砝码对应的数量为x1,x2,x3…xn。现在要用这些砝码去称物体的重量,问能称出多少中不同的重量。
注:
称重重量包括0
输入描述:
输入包含多组测试数据。
对于每组测试数据:
第一行:n — 砝码数(范围[1,10])
第二行:m1 m2 m3 … mn — 每个砝码的重量(范围[1,2000])
第三行:x1 x2 x3 …. xn — 每个砝码的数量(范围[1,6])
这里的状态转移方程很容易得到,在可用砝码数量不为0的情况下,如果w-mi的重量可以称出来,则w的重量也可以称出来。基于这个状态转移方程,我们可以定义一个大小为总重量的数组,用于标记每个重量是否可以称出来,即maxWeight = mi * xi ( 0
import java.util.Scanner;
/** * Created by 梅晨 on 2017/4/23. */
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
while(in.hasNext()){
int n = in.nextInt();
int[] m = new int[n];
int[] x = new int[n];
for(int i = 0; i < n;i++){
m[i] = in.nextInt();
}
for(int i = 0; i < n; i++){
x[i] = in.nextInt();
}
int max = 0;
for(int i = 0; i < n;i++){
max += m[i] * x[i];
}
int[] dp = new int[max + 1];
dp[0] = 1;
for(int i = 0; i < n;i++){
for(int k = max; k >= 1;k--){
for(int j = x[i]; j >= 0;j--){
if((k-j * m[i] >= 0) && (dp[k-j * m[i]] == 1)){
dp[k] = 1;
}
}
}
}
int num = 0;
for(int i = 0; i <= max; i++){
if(dp[i] == 1){
num++;
}
}
System.out.println(num);
}
}
}