动态规划是求解决策过程最优化的数学方法。如果一个问题可以分解成若干个子问题,并且子问题之间还有重叠的更小的子问题,就可以考虑用动态规划来解决这个问题。
应用动态规划之前要分析能否把大问题分解成小问题,分解后的每个小问题也存在最优解。如果将小问题的最优解组合起来能够得到整个问题的最优解,那么就可以使用动态规划解决问题。
可以应用动态规划求解的问题主要由四个特点:
1. 问题是求最优解
2. 整体问题的最优解依赖于各个子问题的最优解
3. 大问题分解成若干小问题,这些小问题之间还有相互重叠的更小的子问题
4. 从上往下分析问题,从下往上求解问题
例:给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim代表要找的钱数,求换钱有多少种方法。
思路及优化方法:
arr={5、10、25、1},aim=1000
1、用0张5元的货币,让[10,25,1]组成剩下的1000,最终方法数记为res1
2、用1张5元的货币,让[10,25,1]组成剩下的995,最终方法数记为res2
3、用2张5元的货币,让[10,25,1]组成剩下的990,最终方法数记为res3
……
201、用200张5元的货币,让[10,25,1]组成剩下的0,最终方法数记为res201
最终结果为res1+res2+…+res201。
定义递归函数:int p1(arr, index, aim),返回用arr[index]至arr[N-1]的货币面值组成面值aim的方法数。
public class DynamicProgramming {
public int coins1(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
return process(arr, 0, aim);
}
/**
* 即上述p1方法
*/
public int process(int[] arr, int index, int aim) {
int res = 0;
if (arr.length == index) {
return (aim == 0 ? 1 : 0);
}
for (int i = 0; arr[index] * i <= aim; i++) {
// arr[index]选i张时,让剩下的货币组成aim-arr[index]*i面额的方法数,即res_i
// 总的方法数即为res_0+res_1+...+res_(aim/arr[index])
res += process(arr, index + 1, aim - arr[index] * i);
}
return res;
}
}
优点:简单方便
缺点:重复计算导致多余的递归,最终导致效率低下
如果已经使用0张5元和1张10元的情况下,后续将求:process(arr, 2, 990);
但是如果已经使用了2张5元和0张十元时,也将要求:process(arr, 2, 990);
就会造成重复计算。
思路:使用HashMap记录计算结果。
public class DynamicProgramming {
/**
* 二维数组map[i][j]的结果代表process(arr, i, j)的返回结果
*/
private int[][] map;
public int coins1(int[] arr, int aim) {
if (arr == null || arr.length == 0 || aim < 0) {
return 0;
}
map = new int[arr.length + 1][aim + 1];
return process(arr, 0, aim);
}
/**
* 即上述p1方法
*/
public int process(int[] arr, int index, int aim) {
int res = 0;
if (arr.length == index) {
return (aim == 0 ? 1 : 0);
}
for (int i = 0; arr[index] * i <= aim; i++) {
int mapValue = map[index + 1][aim - arr[index] * i];
/**mapValue为0表示还没有往当前map中对应位置保存值*/
if (mapValue != 0) {
res += (mapValue == -1 ? 0 : mapValue);
} else {
// arr[index]选i张时,让剩下的货币组成aim-arr[index]*i面额的方法数,即res_i
// 总的方法数即为res_0+res_1+...+res_(aim/arr[index])
res += process(arr, index + 1, aim - arr[index] * i);
}
}
//计算完毕,将计算结果保存至map,由于res可能为0,这里当map=0时表示map中的值还没有计算,等于-1时表示当前值为0
map[index][aim] = (res == 0 ? -1 : res);
return res;
}
public static void main(String[] args) {
int[] arr = {5, 10, 25, 1};
int cnt = new DynamicProgramming().coins1(arr, 1000);
System.out.println(cnt);
}
}
如果arr长度为N,生成行数为N,列数为aim+1的矩阵dp。dp[i][j]的含义是在使用arr[0…i]货币的情况下,组成钱数j有多少种方法。
求每一个位置都需要枚举,时间复杂度为O(aim)。dp一共有N*(aim+1)个位置,对于每个位置要枚举该位置上一行左侧(包括正上)的所有位置的值,因此总体的时间复杂度为O(N*aim^2)。
code:
public class DynamicProgramming {
private int[][] dp;
public int coins(int[] arr, int aim) {
dp = new int[arr.length][aim + 1];
for (int i = 0; i < arr.length; i++) {
dp[i][0] = 1;
}
for (int i = 0; i < aim + 1; i++) {
if (i % arr[0] == 0) {
dp[0][i] = 1;
}
}
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j < aim + 1; j++) {
calDp(arr, i, j); //计算dp[i][j]
}
}
return dp[arr.length - 1][aim];
}
private int calDp(int[] arr, int i, int j) {
int dp_ij = 0;
for (int m = 0; j - m * arr[i] >= 0; m++) {
dp_ij += dp[i - 1][j - m * arr[i]];
}
dp[i][j] = dp_ij;
return dp_ij;
}
public static void main(String[] args) {
int[] arr = {5, 10, 25, 1};
int cnt = new DynamicProgramming().coins(arr, 1000);
System.out.println(cnt);
}
}
上述算法的时间复杂度为O(N*aim^2)。
因为由上述分析可以dp[i][j]=dp[i-1][j]+dp[i-1][j-1*arr[i]]+…,即在上述图中所示,dp[i][j]的值即为上一行的列下标为j-k*arr[i]且>=0,k=0,1,2…的值之和。
而dp[i][j-1*arr[i]]的值同理即为dp[i-1][j-1*arr[i]] + dp[i-1][j-2*arr[i]] + …
因此dp[i][j] = dp[i-1][j] + dp[i][j-arr[i]]。
经过这样化简后的动态规划方法时间复杂度为O(N*aim)。
优化后的代码:
public class DynamicProgramming {
private int[][] dp;
public int coins(int[] arr, int aim) {
dp = new int[arr.length][aim + 1];
for (int i = 0; i < arr.length; i++) {
dp[i][0] = 1;
}
for (int i = 0; i < aim + 1; i++) {
if (i % arr[0] == 0) {
dp[0][i] = 1;
}
}
for (int i = 1; i < arr.length; i++) {
for (int j = 1; j < aim + 1; j++) {
calDp(arr, i, j);
}
}
return dp[arr.length - 1][aim];
}
private void calDp(int[] arr, int i, int j) {
// 在这里进行优化
if ((j - arr[i]) >= 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - arr[i]];
return;
}
int dpIj = 0;
for (int m = 0; j - m * arr[i] >= 0; m++) {
dpIj += dp[i - 1][j - m * arr[i]];
}
dp[i][j] = dpIj;
}
public static void main(String[] args) {
int[] arr = {5, 10, 25, 1};
int cnt = new DynamicProgramming().coins(arr, 1000);
System.out.println(cnt);
}
}
1、最优化原理,也就是最优子结构性质。这指的是一个最优化策略具有这样的性质:不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简单来说就是一个最优化策略的子策略总是最优的,如果一个问题满足最优化原理,就称其具有最优子结构性质。
2、无后效性。指的是某状态下决策的收益,只与状态和决策相关,与到达该状态的方式无关。
3、子问题的重叠性,动态规划将原来具有指数级时间复杂度的暴力搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这时动态规划算法的根本目的。
Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000.
Example 1:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
Example 2:
Input: "cbbd"
Output: "bb"
code:
class Solution {
boolean dp[][];
public String longestPalindrome(String s) {
dp = new boolean[s.length()][s.length()];
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j < s.length(); j++) {
if (i > j) {
continue;
}
if (i == j) {
dp[i][j] = true;
} else if (j == (i + 1)) {
dp[i][j] = (s.charAt(i) == s.charAt(j));
}
}
}
for (int i = s.length() - 1; i >= 0; i--) {
for (int j = 0; j < s.length(); j++) {
if (i >= j || j == (i + 1)) {
continue;
}
dp[i][j] = (dp[i + 1][j - 1] && s.charAt(i) == s.charAt(j));
}
}
int maxLength = 0, maxI = 0, maxJ = 0;
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j < s.length(); j++) {
if (i > j) {
continue;
}
if (dp[i][j] && (j - i + 1) > maxLength) {
maxLength = j - i + 1;
maxI = i;
maxJ = j;
}
}
}
return s.substring(maxI, maxJ + 1);
}
}