动态规划 Dynamic Programming
1. Longest Substring Without Repeating Characters
思路:需要 hashmap 辅助保存各个字符的位置,且随时更新最新位置
若第 i 个位置的字符 c 出现过,则 dp[i] = min(map.get(c)-i,dp[i+1]+1)
若没出现则 dp[i] = dp[i+1] + 1;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) return 0;
Map<Character, Integer> m = new HashMap<Character, Integer>();
int[] dp = new int[s.length()];
dp[s.length() - 1] = 1;
int max = 1; // 初始化dp 数组 与 返回值
m.put(s.charAt(s.length() - 1), s.length() - 1); // 初始化 map
for (int i = s.length() - 2; i >= 0; i--) {
if (m.containsKey(s.charAt(i)))
// 这里要取比较小的一个,避免出现 abbbba 这种情况,虽然间隔大,但是中间都是重复的
dp[i] = Math.min(m.get(s.charAt(i)) - i, dp[i+1]+1);
else
dp[i] = dp[i + 1] + 1;
max = max > dp[i] ? max : dp[i];
m.put(s.charAt(i), i); // 更新位置
}
return max;
}
2. Longest Valid Parentheses
For "(()"
, the longest valid parentheses substring is "()"
, which has length = 2.
Another example is ")()())"
, where the longest valid parentheses substring is "()()"
, which has length = 4.
题目大意:找到最长的合法括号序列 ,两种形式的 "()()" 与 "((()))" 都是合法的。
思路: 若 i == ')' 设置 dp[i] = 0 即可;
若 i == '(' ,不仅考虑 r = i+1 ; s[r]=')' 然后加上 dp[r+1]. 因为还有 "(())" 这种情况
应该考虑 r = i + dp[i+1] + 1; 然后在考虑 dp[r+1] ;这样会考虑到 "(())" 这种情况
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int longestValidParentheses(String s) {
if (s == null || s.length() < 2) return 0;
int max = 0; // 弄个最大值
int []dp = new int[s.length()];
dp[s.length() - 1] =0;
for(int i = s.length() - 2 ; i >= 0 ; -- i){
if(s.charAt(i) == '('){
int ri = i + dp[i+1] + 1;
if(ri < s.length() && s.charAt(ri) == ')'){
dp[i] = dp[ri] + dp[i+1] + 2;
if(ri + 1 < s.length()-1) dp[i] += dp[ri+ 1];
}
//else dp[i] ==0 可以省略,因为本来就 = 0
}
max = max > dp[i] ? max : dp[i];
}
return max;
}
3. 最长公共子串(Longest Common Substirng)
子串是串的一个连续的部分。
4. 最长公共子序列(Longest Common Sequence)
而从序列中去掉任意的元素而获得新的序列,最长子序列则可以不必连续。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//求 最长子序列的长度
public static void LongestCommonSequence(String s1 , String s2){
int m = s1.length();
int n = s2.length();
if(m == 0 || n == 0) return;
int dp[][] = new int[m+1][n+1];
//构建
for(int i = 1 ; i <= m; i ++){
for(int j = 1; j <= n; j ++){
if(s1.charAt(i-1)== s2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
}else
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
//查找
String LCS = ""; int i = m,j = n;
while (i >= 1 && j >= 1) {
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
char c = s1.charAt(i - 1);
LCS = c + LCS;
i--;
j--;
} else if (dp[i][j - 1] > dp[i - 1][j]) {
j--;
} else {
i--;
}
}
System.out.println(LCS);// 打印
}
![](https://images2015.cnblogs.com/blog/743682/201607/743682-20160720125051076-734808779.png)
![](https://images2015.cnblogs.com/blog/743682/201607/743682-20160729092502278-584200265.png)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//类似于 LCS 的动态规划 public boolean isInterleave(String s1, String s2, String s3) { int m = s1.length(),n = s2.length(); if(m + n != s3.length()) return false; //横向放 s1 ,纵向放 s2 boolean dp[][] = new boolean [m+1][n+1]; dp[0][0] = true; // 把左上角元素置 true ,因为有 S1=""、 S2="" 、S3="" 的情况 for(int i = 1 ; i < dp.length; i ++){ if(s1.charAt(i-1) == s3.charAt(i-1)) dp[i][0] = true; else break; } for(int j = 1 ; j < dp[0].length; j ++){ if(s2.charAt(j-1) == s3.charAt(j-1)) dp[0][j] = true; else break; } for(int i = 0 ; i < m ; i ++){ for(int j = 0 ; j < n ; j ++){ if(s2.charAt(j) == s3.charAt(i+j+1) && dp[i+1][j] == true){dp[i+1][j+1] = true;} if(s1.charAt(i) == s3.charAt(i+j+1) && dp[i][j+1] == true){dp[i+1][j+1] = true;} } } return dp[m][n] == true ; }
6. Maximum Subarray (leetcode)
找到和最大的连续子序列,比如对于序列 [−2,1,−3,4,−1,2,1,−5,4],和最大的连续子序列为[4,−1,2,1] ,和的最大值= 6.
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int maxSubArray(int[] nums) { if(nums == null || nums.length == 0) return 0; int dp[] = new int[nums.length];// 动态规划数组 dp[dp.length-1] = nums[nums.length - 1]; int ret = nums[nums.length - 1];// 待返回结果 for(int i = nums.length - 2; i >= 0 ; i --){ if(nums[i] + dp[i+1] <= nums[i]){ dp[i] = nums[i]; }else{ dp[i] = dp[i+1] + nums[i]; } ret = Math.max(dp[i],ret); } return ret; }
7. 蘑菇阵(牛客网)
现在有两个好友A和B,住在一片长有蘑菇的由n*m个方格组成的草地,A在(1,1),B在(n,m)。现在A想要拜访B,由于她只想去B的家,所以每次她只会走(i,j+1)或(i+1,j)这样的路线,在草地上有k个蘑菇种在格子里(多个蘑菇可能在同一方格),问:A如果每一步随机选择的话(若她在边界上,则只有一种选择),那么她不碰到蘑菇走到B的家的概率是多少?
第一行N,M,K(2 ≤ N,M ≤ 20, k ≤ 100),N,M为草地大小,接下来K行,每行两个整数x,y,代表(x,y)处有一个蘑菇。
思路:二维数组的DP,起始点概率为 1 ;
对于内部节点,当前点向右边走的概率为 0.5 ,下边走的概率为 0.5 ;
注意右边界与下边界,边界节点只有一个方向。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import java.util.Scanner; public class Main{ public static void main(String[]args){ Scanner sc = new Scanner(System.in); while(sc.hasNext()){ int n = sc.nextInt(); int m = sc.nextInt(); int k = sc.nextInt(); double[][] dp = new double[n+1][m+1]; // 地图 for(int i = 0 ; i < k ; i ++) dp[sc.nextInt()][sc.nextInt()] = 1; // i -> col ; j -> row for(int i = 1 ; i <= n ; i ++){ for(int j = 1 ; j <= m ; j ++){ //走到蘑菇上了,转移概率置 0 . if(dp[i][j] == 1){ dp[i][j] = 0; // 起始位置 }else if(i == 1 && j == 1){ dp[i][j] = 1; // 最后一个元素等于上左概率相加 }else if(i == n && j == m){ dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 最后一列只能下移 }else if(j == m){ dp[i][j] = dp[i-1][j] + 0.5 * dp[i][j-1]; // 最后一行只能右移 }else if(i == n){ dp[i][j] = 0.5 * dp[i-1][j] + dp[i][j-1]; // 内部元素一半几率来自上,一半几率来自左 }else{ dp[i][j] = 0.5 * dp[i-1][j] + 0.5 * dp[i][j-1]; } } } System.out.println(String.format("%.2f", 1.0 * dp[n][m])); } } }
8.Longest Increasing Path in a Matrix(leetcode)
nums = [[9,9,4], [6,6,8], [2,1,1]]
Return4
The longest increasing path is[1, 2, 6, 9]
.
思路:DP 结合 DFS ,用一个 DP 数组保存当前节点路径的最大值,这样,当再次遍历到该节点,便无需再次计算其值了。以下是一个数组示例及其对应的 DP 数组
[8, 6, 4, 3] [1, 2, 3, 4]
[2, 9, 2, 5] [3, 1, 4, 3]
[7, 4, 1, 6] [2, 3, 5, 2]
[8, 2, 4, 7] [1, 4, 2, 1]
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//本题目就是结合 DFS 的DP public int longestIncreasingPath(int[][] matrix) { if(matrix == null || matrix.length == 0 || matrix[0].length == 0) return 0; int ret = 0; int m = matrix.length, n = matrix[0].length; int[][] dp = new int[m][n]; for(int i = 0 ; i < m ; i ++){ for(int j = 0 ; j< n ; j ++){ int len = dfs(matrix, i, j, m, n, dp); ret = Math.max(ret, len); } } return ret; } public int dfs(int[][] matrix, int i,int j, int m,int n,int[][] dp){ if(dp[i][j] != 0) return dp[i][j]; // 这一步比较关键,用到了 DP数组 if(i < 0 || i > m-1 || j < 0 || j > n-1) return 0; int max = 0; //分别从上下左右搜索变大的方向. if(i > 0 && matrix[i][j] < matrix[i-1][j]){//上 int up = dfs(matrix, i-1, j, m, n, dp); max = Math.max(max, up); } if(i < m-1 && matrix[i][j] < matrix[i+1][j]){//下 int down = dfs(matrix, i+1, j, m, n, dp); max = Math.max(max, down); } if(j > 0 && matrix[i][j] < matrix[i][j-1]){//左 int left = dfs(matrix, i, j-1, m, n, dp); max = Math.max(max, left); } if(j < n-1 && matrix[i][j] < matrix[i][j+1]){//右 int right= dfs(matrix, i, j+1, m, n, dp); max = Math.max(max, right); } dp[i][j] = max + 1; // + 1 表示加上自身的操作 return dp[i][j]; }
9. Combination Sum IV (leetcode)
思路:dp[i]表示当target为i 时,有多少种组合。
状态转移方程:dp[i]=$\sum_{j}$dp[i-nums[j]] ;
时间复杂度 $O(n^2)$
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int combinationSum4(int[] nums, int target) { int[] dp = new int[target + 1]; for(int i = 1 ; i < dp.length ; i ++){ for(int num : nums){ if( num > i ) continue; else if(num == i) dp[i] += 1; else dp[i] += dp[i - num]; } } return dp[target]; }
10. 0-1 背包问题
给定质量 m ,怎么偷走最多的物品来达到赚最多的钱
思路:dp 做,用 dp[i][j]表示前 i个物品,装到容量为j的背包的最大价值
dp[i][j]表示放入前 i 件物品且背包容量为 j 时的最大价值。
当只能放入第一件物品即 i=0 时:若背包容量 j < weight[0],物品不能够被放入背包, 若 j >= weight[0] 时,物品可以放入背包,此时 dp[0][j] = value[0]。
当可以放入前2件物品即 i=1 时,我们需要进行这样的处理:若 j<w[1] 时,说明第 2 件物品不能被放入背包内,此时背包的最大价值为背包中只放入第一件物品的最大价值,即 dp[1][j] =d p[0][j], 若 j >= weight[1] 时,假设此时背包容量 j = 8 ,第二件物品可以被放入背包内,那么便会出现两种情况:
1)将第二件物品放入背包,那么背包中物品的最大价值是多少呢?因为第二件物品重量为 weight[1]=2 ,在将第二件物品放入背包之前,背包的容量应为 j-weight[1] = 8-2 = 6,此时背包的最大价值是 dp[0][6] ,因此若将第二件物品放入背包,其背包的最大价值:
dp[1][j] = dp[0][j-weight[1]] + value[1]
2)不将第二件物品放入背包,那么此时背包中物品的最大价值依然为只放入第一件物品时背包的最大价值,即 dp[1][j] = dp[0][j];
3)选取1)、2)中价值的较大者作为 i=1,j=8 时背包中的最大价值。
i = 2,3,4 时的分析同上,直到背包的容量为10,此时 dp[4][10] 即为背包中物品的最大价值。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import java.util.Arrays; import java.util.Scanner; public class Main{ //0-1 背包问题 public static void main(String args[]){ Scanner sc = new Scanner(System.in); while(sc.hasNext()){// 0-1 背包 String[] arg = sc.nextLine().split(" "); int m = Integer.parseInt(arg[0]); // 总质量不能超过 m int n = Integer.parseInt(arg[1]); // 物品总数为 n int[] weight = new int[n]; int[] value = new int[n]; String[] w = sc.nextLine().split(" "); String[] v = sc.nextLine().split(" "); for(int i = 0 ; i < n ; i ++){ weight[i] = Integer.parseInt(w[i]); value [i] = Integer.parseInt(v[i]); } //dp[i][j]表示前 i个物品,装到容量为j的背包的最大价值 int[][] dp = new int[n][m + 1]; for(int i = 0 ; i <= m ; i ++) //第 0 行初始化 if( i >= weight[0]) dp[0][i] = value[0]; //计算状态转移数组 for(int i = 1; i < n; i ++) { for(int j = 1; j <= m; j ++) { if (j >= weight[i]) //dp[i - 1][j - weight[i]] 上一次不加当前物品的最大利润 dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); // j < weight[i] else dp[i][j] = dp[i - 1][j]; } } int ret = dp[n - 1][m]; System.out.println(String.format("%.1f", ret)); } sc.close(); } }
11. Distinct Subsequences (leetcode)
// dp[i][j]表示:T的前j个字符在S的前i个字符中的合法序列数。
思路:1) 假设S[i] != T[j] ,则 dp[i][j]的值跟dp[i-1][j]是一样的,第 i 个字符加入没有任何用处,不考虑当前这个字符,延续上一次的结果。
2) 假设S[i] == T[j],那么当前这个字母即可以保留也可以抛弃,所以变换方法等于保留这个字母的变换方法加上不用这个字母的变换方法。
3) 上一次的结果为 dp[i-1][j] , 之前的变换结果为 dp[i-1][j-1] ,注意 dp[i][0] = 1 代表任意串转为空串只需做一次变换
例如: 对于 S = rabbbit 与 T = rabit 中的 DP 数组如下:
r a b i t
[ 1, 0, 0, 0, 0, 0]
r [1, 1, 0, 0, 0, 0]
a [1, 1, 1, 0, 0, 0]
b [1, 1, 1, 1, 0, 0]
b [1, 1, 1, 2, 0, 0]
b [1, 1, 1, 3, 0, 0]
i [1, 1, 1, 3, 3, 0]
t [1, 1, 1, 3, 3, 3]
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 序列 T 可以有以多少种不同的方式作为 S 的子序列 public int numDistinct(String s, String t) { if(s == null || t == null) return 0; int m = s.length(), n = t.length(); int[][] dp = new int[m + 1][n + 1]; dp[0][0] = 1; //任何一个串变为空串都只有一种方式 for(int i = 0 ; i <= m ; i ++) dp[i][0] = 1; for(int i = 1 ; i <= m ; i ++){ for(int j = 1 ; j <= n ; j ++){ if(s.charAt(i-1) == t.charAt(j-1)){ dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; }else{ dp[i][j] = dp[i-1][j]; } } } return dp[m][n]; }
12. 幸运数字
给定 一组数字,[1, 2, ..., i, ..., T] ,每个数字的范围是 1 <= i <= n ,计算每个数字 i 的在范围 [1,i] 中幸运数字量。
思路:这个题因为对于一组数字,比如说 1000,1001,两个数字得到的值可能回事一样的,但需要计算两遍,所以数组里每个数字都重新计算一遍的话会超时,需要用一个数组保存一下之前的信息:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import java.util.*; public class Main { public static void main(String[] args){ int size = 1000001; // 题目给的最大值范围 int dp[] = new int [size]; dp(dp); // dp[i] 表示 [1,i]里的幸运数的总数 Scanner sc = new Scanner(System.in); while(sc.hasNext()){ int T = Integer.parseInt(sc.nextLine()); int [] nums = new int[T]; for(int i = 0 ; i < T ; i ++){ int n = Integer.parseInt(sc.nextLine()); nums[i] = dp[n]; } for(int i = 0 ; i < nums.length ; i ++){ System.out.println(nums[i]); } } } public static void dp(int[] dp){ int count = 0; for(int i = 1; i < dp.length ; i ++){ int cur = i; int sum = 0, binsum = 0; while( cur > 0){ sum += cur % 10; cur /= 10; } String binstr = Integer.toBinaryString(i); for(char c : binstr.toCharArray()){ binsum +=c - '0'; } if(sum == binsum) count ++; dp[i] = count; } } }
13. Edit Distance (leetcode)
思路: 动态规划, dp[i][j] 表示字符串 word1 的前 i 个字符与字符串 word2 的前 j 个字符的编辑距离。
如果 word1 的第 i 个字符和 word2 的第 j 个字符相等
(即word1[i-1] == word2[j-1]),那么dp[i][j] = dp[i-1][j-1];
如果不相等,那么当前状态可能来自于三种状态 dp[i-1][j],dp[i][j-1],dp[i-1][j-1]。
从 dp[i-1][j] ,给 word1 添加一个字符即可;
从 dp[i][j-1] ,给 word2 添加一个字符即可;
从 dp[i-1][j-1] ,把俩字符的其中一个字符替换一下即可。
从以上一个选项中 三选一,选最小的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int minDistance(String word1, String word2) { int m = word1.length(); int n = word2.length(); int[][] dp = new int[m + 1][n + 1]; //初始化 dp 数组, for(int i = 1 ; i <= m ; i ++) dp[i][0] = i; for(int j = 1 ; j <= n ; j ++) dp[0][j] = j; //状态转移 for(int i = 1 ; i <= m ; i ++){ for(int j = 1 ; j <= n ; j ++){ if(word1.charAt(i-1) == word2.charAt(j-1)){ dp[i][j] = dp[i-1][j-1]; }else{ int min = Math.min(dp[i-1][j],dp[i][j-1]); //别忘了最后的 + 1 代表加上当前的字符 dp[i][j] = Math.min(dp[i-1][j-1], min) + 1; } } } //但会结果 return dp[m][n]; }
14. 旅行商问题
现在有一个城市销售经理,需要从公司出发,去拜访市内的商家,已知他的位置以及商家的位置,但是由于城市道路交通的原因,他只能在左右中选择一个方向,在上下中选择一个方向,现在问他有多少种方案到达商家地址。
给定一个地图map及它的长宽n和m,其中1代表经理位置,2代表商家位置,-1代表不能经过的地区,0代表可以经过的地区,请返回方案数,保证一定存在合法路径。保证矩阵的长宽都小于等于10。
这里 “他只能在左右中选择一个方向,在上下中选择一个方向”应该理解为:左右中只能选一个方向,若选择左只能一直向左走。上下中只能选择一个方向,若选择下只能一直向下。
首先根据 map 找到出发点与终点,然后找到遍历方向 dirX dirY,开始遍历即可,注意边界上的处理
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int countPath(int[][] map, int n, int m) { // write code here //找到起始点,终止点 int startX = 0,startY = 0,endX = 0,endY = 0; int[][] dp = new int[n][m]; // n 行 m 列 for(int i = 0 ; i < n ; i ++){ for(int j = 0 ; j < m ; j ++){ if(map[i][j] == 1) { startX = i; startY = j; }else if(map[i][j] == 2){ endX = i; endY = j; } } } //根据 start 与 end 坐标找出 X 与 Y 的移动方向 int dirX = startX > endX ? -1 : 1; int dirY = startY > endY ? -1 : 1; for(int x = startX ; x <= endX ; x += dirX ){ for(int y = startY ; y <= endY ; y += dirY ){ if(x == startX && y == startY){ dp[x][y] = 1; }else if(x == startX){ // 在横向边界 dp[x][y] = (map[x][y] == -1) ? 0 : dp[x][y-dirY]; }else if(y == startY){ // 在纵向边界 dp[x][y] = (map[x][y] == -1) ? 0 : dp[x-dirX][y]; }else{ // 正常的走 dp[x][y] = (map[x][y] == -1) ? 0 : dp[x-dirX][y] + dp[x][y-dirY]; } } } return dp[endX][endY]; }
15.Coin Change leetcode 硬币组合问题:
子问题1:给定硬币组合 {1,7,13}, 然后给出指定的金额 n ,求有多少种组合可以产生该金额
思路: dp[i] 保存当前有多少种组合可以产生金额 i ,对种面值 coin $\in$ {1,2,5,10},状态转移为 dp[i] = dp[i - coin]
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 人民币总额为 n ,面值如下,求一共有多少种组合方式 public int coinChange(int n) { int[] coins = {1,7,13}; // dp[i] 代表面值为 i 时有多少种组合 int[] dp = new int[n + 1]; dp[0] = 1; for(int coin : coins){ for(int j = coin ; j <= n ; j ++){ dp[j] += dp[j- coin]; } } return dp[n]; }
子问题2:给定硬币组合 {1,7,13}, 然后给出指定的金额 n ,求如何用最少的金额可以产生该金额
思路: dp[i] 代表以当前最少的数目产生金额 i ,对种面值 coin $\in$ {1,7,13},状态转移为 dp[i] = min(dp[i], dp[i - coin] + 1)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int coinChange(int[] coins, int n) { // 需要用 coins 里的最少硬币 凑出金额 n // 使用 动态规划,dp[i]代表的是金额为i时的最小硬币数 int[] dp = new int[n + 1]; // 设置 dp[i] 为总额值 + 1,为不可能的量 for(int i = 1 ; i <= n ; i ++){ dp[i] = n + 1; } for(int coin : coins){ for(int i = coin; i <= n ; i ++){ dp[i] = Math.min(dp[i], dp[i-coin]+1); } } return dp[n] > n ? -1 : dp[n]; }
16. Unique Binary Search Trees (leetcode)
思路:dp 算法
给定序列 1...n, 构建 BST 时每个节点 i 均可以作为根,[1...i-1] 为左子树, [i+1...n] 为右子树,然后对于节点 i 的左右子树,递归使用上述方法即可,为了计算树的个数,我们需要定义两个函数:
- G(n): 代表了长度为 n 的唯一的 BST 数目
- F(i, n), 1 <= i <= n: 根节点为 i 时的 唯一的 BST 数目
G(n) 为需要计算的值, 通过 F(i,n) 计算出来的,公式如下:
\[G(n) = F(1,n) + F(2,n) +...+ F(n,n)\]
当 n = 0 或 n = 1 时, G(0) = G(1) = 1 代表空树或者单节点树,从序列 [1...n]中选择一个数字 i 作为根,唯一BST的数目为 F(i), 它是左右子树 BST 数目的笛卡尔积.因此规律是:
\begin{aligned}
F(i, n) &= G(i-1) * G(n-i) , \ \ 1 <= i <= n \\
G(n) \ \ &= G(0) * G(n-1) + G(1) * G(n-2) +...+ G(n-1) * G(0)
\end{aligned}
比如说, F(3, 7): 即使用 3 作为 根节点,并且使用序列 [1, 2, 3, 4, 5, 6, 7] 构建 BST。也就是说, 我们需要使用 [1,2]创建一个唯一的 BST ,另一个 BST 即使用 [4, 5, 6, 7] 创建, 并且做一个笛卡尔积,使用序列 [1,2] 得到的总数为 G(2), 使用序列 [4, 5, 6, 7] 得到的总数 G(4). 因此 F(3,7) = G(2) * G(4).
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public int numTrees(int n) { int[] dp = new int[n + 1]; dp[0] = dp[1] = 1; for(int i = 2 ; i <= n ; ++ i){ for(int j = 1 ; j <= i ; ++ j){ dp[i] += dp[j-1] * dp[i-j]; } } return dp[n]; }
16. Apple Trees (BaiDu 笔试题)
描述: n 个几点 n -1 条边 的多叉树,每个树节点可能有苹果,可能没有苹果,找一条最大路径,使得该路径上可以得到最多的苹果。
思路:树的 dp ,只要从每个节点的孩子中选择两条最大的路径 然后跟最大值比较 max = MAX(max, first + second + cur)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import java.util.*; //百度笔试题 apple tree public class Main { static int MAX = 0; public static void main(String args[]) { Scanner sc = new Scanner(System.in); while(sc.hasNext()) { String arg[] = sc.nextLine().split(" "); String app[] = sc.nextLine().split(" "); int N = Integer.parseInt(arg[0]); // 共 N 个节点 int K = Integer.parseInt(arg[1]); // K 个节点有苹果 //代表那个节点有苹果 Set<Integer> set = new HashSet<>(K + 1); // 节点的关系 List<List<Integer>> list = new ArrayList<>(N + 1); for(String n : app) set.add(Integer.parseInt(n)); for(int i = 0 ; i <= N ; ++ i) // 加一个全新的 List { list.add(new ArrayList<Integer>()); } for(int i = 1 ; i < N ; ++ i) //N -1 条边 { String[] line = sc.nextLine().split(" "); int f = Integer.parseInt(line[0]); // 父节点 int c = Integer.parseInt(line[1]); // 子节点 list.get(f).add(c); } dfs(list, set, 1); System.out.println(MAX); } } // 数的动态规划,每个节点求当前节点与 左右子树的最大值 public static int dfs(List<List<Integer>> list, Set<Integer> set, int node) { //list.get(node) 便为节点的后继 int cur = set.contains(node) ? 1 : 0; if( list.get(node).size() == 0) return cur; int len = list.get(node).size(); int first =0, second = 0; //记录第一第二大 for(int i = 0 ; i < len; ++ i) { int num = dfs(list,set, list.get(node).get(i)); if(num > first ){ // 分别找到最大 次大的值 second = first; first = num; }else if(num > second){ second = num; } } MAX = Math.max(MAX, first + cur + second); return first + cur; // 这里返回值很重要,返回的是最大数目路径上的值 } }
17 树形 dp
滴滴面试题,先建立 tire 树然后 dp 求解每个节点的左右最长的路径和,这里简单以二叉树写一下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// 二叉树中可以走的最长的路径 public static int dfs(TreeNode root) { if(root == null) return 0; int left = dfs(root.left ); int right = dfs(root.right); MAX = Math.max(MAX, left + right); return Math.max(left, right) + 1; }
18 判断能否将数组分为两部分,使其和相等
思路:首先想到 dfs , 判断能否有组合 = sum/2
discuss里的 dp 很强大
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public boolean canPartition(int[] nums) { if(nums == null || nums.length == 0) return true; int sum = 0; for(int n : nums) sum += n; if(sum % 2 != 0 ) return false; sum /= 2; boolean[] dp = new boolean[sum + 1]; dp[0] = true; for(int i = 0; i < nums.length; ++ i) { for(int j = sum; j >= nums[i] ; --j) dp[j] = dp[j] || dp[j - nums[i]]; } return dp[sum]; }