动态规划

DP的特点

  1. 无后效性:未来与过去无关。即:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前的各状态的影响。
  2. 最优子结构:大问题的最优解可以由小问题的最优解推出

DP的操作过程

将一个大问题转化为几个小问题,大事化小,小事化了:

  • 求解小问题
  • 推出大问题的解

DP经典题型

但是感觉不管查什么资料都是这些,其实什么也解决不了,根据做题的经验,动态规划最难的地方,一是确定一道题能不能用动态规划解决,二就是怎么确定状态,就是f(n)怎么定义。

  • 线性表: length – length + 1
  • 字符串(线性表)–> 前缀 + 后缀 –> 前缀 true && 后缀 true –> true

leetcode 53. 最大子段和

dp[i] 表示以 i 结尾的子段和的最大值

1
2
3
4
5
6
7
8
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
for(int i=1;i<nums.length;i++)
dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
Arrays.sort(dp);
return dp[dp.length-1];
}

leetcode 139. word break

  • 这道题就是字符串的问题,并且是前缀 + 后缀问题
  • dp[i] : 字符串 s[:i] 能否能否由字典中的词拆分开。(以i结尾的字符串能否被字典拆分开)
  • dp[i] : 如果 dp[j]=true, 并且 s[j:i] 在字典中,那么 dp[i]true
1
2
3
4
Given a non-empty string s and a dictionary wordDict containing a list of non-empty words, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
Note:
1. The same word in the dictionary may be reused multiple times in the segmentation.
2. You may assume the dictionary does not contain duplicate words.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length()+1];
dp[0] = true;
for(int i=1;i<=s.length();i++) {
for(int j=0;j<i;j++) {
String tmp = s.substring(j, i);
if(dp[j]&&wordDict.contains(tmp)) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}

leetcode 300. 最长上升子序列

  • 输入:[10, 9, 2, 5, 3, 7, 101, 18],则输出为:4,因为最长的上升子序列是:[2, 3, 7, 101]
  • dp[i]: 以第i位为结尾的最长上升子序列的长度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int lengthOfLIS(int[] nums) {
if(nums.length==0)
return 0;
int[] dp = new int[nums.length];
for(int i=0;i<nums.length;i++) {
dp[i] = 1;
for(int j=i-1;j>=0;j--) {
if(nums[j]<nums[i])
dp[i] = Math.max(dp[j]+1, dp[i]);
}
}
Arrays.sort(dp);
return dp[nums.length-1];
}

leetcode 1143. 最长公共子序列

  • 输入:text1= “abcde” , text2 = “ace”
  • 输出:3
  • 最长公共子序列不需要连续,只要是共同有的就行了。
  • 最长公共子串是需要连续的。
1
2
3
4
5
6
7
8
9
10
11
12
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length()+1][text2.length()+1];
for(int i=1;i<=text1.length();i++) {
for(int j=1;j<=text2.length();j++) {
if(text1.charAt(i-1)==text2.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]);
}
}
return dp[text1.length()][text2.length()];
}

leetcode 62. 从左上到右下共有多少条路径

  • dp[m][n] 表示走到 m n 位置时的走法数
  • dp[m][n] = dp[m-1][n] + dp[m][n-1]
  • 相似题目:leetcode 63. 62题多了障碍,就是递归的时候多了判断而已。
1
2
3
4
5
6
7
8
9
10
11
12
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i=0;i<m;i++)
dp[i][0] = 1;
for(int i=0;i<n;i++)
dp[0][i] = 1;
for(int i=1;i<m;i++)
for(int j=1;j<n;j++)
dp[i][j] = dp[i-1][j] + dp[i][j-1];

return dp[m-1][n-1];
}

leetcode 134. 环形加油站

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static int canCompleteCircuit(int[] gas, int[] cost) {
int res_index = 0;
for(int i=0;i<gas.length;i++) {
if(gas[i]<cost[i])
continue;
else {
boolean flag = true;
int[] dp = new int[2*gas.length];
dp[i] = gas[i] - cost[i];
for(int j=i%gas.length+1;j<2*gas.length;j++) {
dp[j] = dp[j-1]+gas[j%gas.length]-cost[j%gas.length];
if(dp[j]<0) {
flag = false;
break;
}

}
if(flag) {
res_index = i;
return res_index;
}
}
}
return -1;
}

leetcode 96. 独一无二的二叉搜索树数目

1
2
3
4
5
6
7
8
9
10
Input: 3    //输入:1...n
Output: 5 //有多少独一无二的二叉搜索树
Explanation:
Given n = 3, there are a total of 5 unique BST's:

1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
  • 要分析有多少独一无二的二叉搜索树的个数。那么可以定义f(x):x为根节点时二叉搜索树的个数
  • f(x)的定义很关键。。看到这个定义我就知道怎么做了。但是自己想的时候就想不到这一点。😭
  • 当n为根节点时,那么其他1…n-1都在其左子树。那么f(n)=n-1个节点能够成的二叉搜索树的个数。
  • 当n-1为根节点时,那么其他1…n-2都在其左子树,右子树只有n,那么f(n-1)=n-2个节点能够组成的二叉搜索树的个数*右子树只有一个节点能组成的二叉搜索树的个数。
  • 所以可以定义G(x): 1…x能够组成的二叉搜索树的个数。
1
2
3
4
5
6
7
f(n)=G(n-1)*G(0)
f(n-1) = G(n-2)*G(1)
f(n-2) = G(n-3)*G(2)
...
f(1) = G(0)*G(n-1)
f(1)+f(2)+...+f(n) = G(n)
所以:G(n) = G(n-1)*G(0) + G(n-2)*G(1) + G(n-3)*G(2) + ... + G(0)*G(n-1)
  • 从上面我们就能看出来,要想求G(n), 要先求出 G(0)…G(n-1)
  • G(0) = 1, G(1) = 1;
  • G(2) = G(1)*G(0) + G(0)*G(1) = 2
  • G(3) = G(2)*G(0) + G(1)*G(1) + G(0)*G(2)
1
2
3
4
5
6
7
8
9
10
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
// G(n) = G(n-1)*G(0) + G(n-2)*G(1) + G(n-3)*G(2) + ... + G(0)*G(n-1)
for(int i=2;i<=n;i++)
for(int j=i-1;j>=0;j--)
dp[i] += dp[j]*dp[i-j-1];
return dp[n];
}

leetcode 95. 把96题所有的二叉搜索树都写出来

  • 虽然感觉是在96的基础上做了变形,但是完全不一样啊!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public List<TreeNode> generateTrees1(int n) {
if(n <= 0) return new ArrayList<>();
List<TreeNode> list = greateSubTree(1, n);
return list;
}
private List<TreeNode> greateSubTree(int start, int end) {
List<TreeNode> res = new ArrayList<>();
if(end < start) {
res.add(null);
return res;
}
for(int i = start; i <= end; i++) {
for(TreeNode l : greateSubTree(start, i-1)) {
for(TreeNode r : greateSubTree(i+1, end)) {
TreeNode root = new TreeNode(i);
root.left = l;
root.right = r;
res.add(root);
}
}
}
return res;
}

leetcode 120. 三角形矩阵从上到小的最小和路径

1
2
3
4
5
6
[                        | index:
[2], | 0
[3,4], | 0 1
[6,5,7], | 0 1 2
[4,1,8,3] | 0 1 2 3
] | 0 1 2 3 4
  • 从下到上,第 n 行第 i 个位置 由第 n-1 行的第 i-1个和第 i 个走到,取两个路径和中小的那个。
  • dp[n][i] = nums[n][i] + min(dp[n-1][i-1], dp[n-1][i])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int minimumTotal(List<List<Integer>> triangle) {
int result = Integer.MAX_VALUE;
int m = triangle.size();
int n = triangle.get(m-1).size(); // 最后一行的长度
int[][] dp = new int[m][n];
dp[0][0] = triangle.get(0).get(0);
for(int i=1;i<m;i++) {
int size = triangle.get(i).size();
//三角形最边上的两个值只有一种情况
dp[i][0] = dp[i-1][0] + triangle.get(i).get(0);
dp[i][size-1] = dp[i-1][size-2] + triangle.get(i).get(size-1);
for(int j=1;j<triangle.get(i).size()-1;j++)
dp[i][j] = triangle.get(i).get(j) + Math.min(dp[i-1][j-1], dp[i-1][j]);
}
for(int i=0;i<n;i++)
result = dp[m-1][i] < result ? dp[m-1][i] : result;

return result;
}

leetcode 392. 判断s是不是t的子序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean isSubsequence(String s, String t) {
//判断s是不是t的子序列
//dp[i][j] s的第i位到t的第j位是否是子序列
if(s.length()==0)
return true;
else if(s.length()>t.length())
return false;
boolean[][] dp = new boolean[s.length()][t.length()];
dp[0][0] = (s.charAt(0)==t.charAt(0));
for(int i=1;i<t.length();i++)
dp[0][i] = (s.charAt(0)==t.charAt(i) || dp[0][i-1]);
for(int i=1;i<s.length();i++) {
for(int j=i;j<t.length();j++) {
dp[i][j] = (s.charAt(i)==t.charAt(j)&&dp[i-1][j-1]) || dp[i][j-1];
}
}
return dp[s.length()-1][t.length()-1];
}

leetcode 338. 计算二进制中1的个数

  • 每2的幂开始,就是前面全部的位置+1
  • 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 …….
  • 0 1 1 2 1 2 2 3 1 2 2 3 2 3 3 4 1 2 2 3 2 …….
  • 4的位置,8的位置,16的位置开始,都是从头部遍历,然后+1即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public int[] countBits(int num) {
int[] dp = new int[num+1];
if(num>=0) dp[0] = 0;
if(num>=1) dp[1] = 1;
if(num>=2) dp[2] = 1;
if(num>=3) dp[3] = 2;

if(num<=3) return dp;

int i=4;
int tmp = 2;
while(i<=num) {
int cou = 0;
int start = (int)Math.pow(2, tmp);
int end = (int)Math.pow(2, tmp+1);
int j = start;
while(j<end && j<=num) {
dp[j] = dp[cou] + 1;
j += 1;
cou += 1;
}
i = j;
tmp += 1;
}
return dp;
}

leetcode 714. 带有转移费用的股票最佳买卖时间

1
2
3
4
5
6
7
8
Input: prices = [1, 3, 2, 8, 4, 9], fee = 2
Output: 8
Explanation: The maximum profit can be achieved by:
Buying at prices[0] = 1
Selling at prices[3] = 8
Buying at prices[4] = 4
Selling at prices[5] = 9
The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
  • 股票必须先卖出然后才能再买入。
  • hold[i]:表示第 i 时刻手里有股票的最大收益。那么该状态可由两种情况得到。一种是之前没有股票,在该时刻买入。一种是之前就有股票:hold[i] = max(hold[i-1], unhold[i-1]-prices[i])
  • unhold[i]:表示第 i 时刻手里没有股票最大收益。一种是之前有股票,在该时刻卖出。一种是之前就没有股票: unhold[i] = max(unhold[i-1], hold[i-1]+prices[i]-fee)
  • 最终返回 unhold[len-1]。即最后时刻手里没有股票。
1
2
3
4
5
6
7
8
9
10
public int maxProfit(int[] prices, int fee) {
int[] hold = new int[prices.length];
int[] unhold = new int[prices.length];
hold[0] = 0-prices[0];
for(int i=1;i<prices.length;i++) {
unhold[i] = Math.max(unhold[i-1], hold[i-1]+prices[i]-fee);
hold[i] = Math.max(hold[i-1], unhold[i-1]-prices[i]);
}
return unhold[prices.length-1];
}

区间 DP

思想:先在小区间进行DP得到最优解,然后再利用小区间的最优解合并求大区间的最优解。

1
2
3
4
5
6
7
8
9
10
int[][] dp = new int[n][n];
for(int i=n-1;i>=0;i--) {
for(int j=i+1;j<n;j++) {
dp[i][j] = Integer.MIN_VALUE; // 初始值
for(int k=i;k<j;k++) {
// max 还是 min 根据题目不同要求判断。
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k+1][j] + w[i][j]);
}
}
}

leetcode 5. 最长回文子串

1
2
3
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
  • 方法一:中心扩展法,遍历到数组的某一个元素时,以这个元素为中心,向两边进行扩展,如果两边的元素相同则继续扩展,否则停止扩展。时间复杂度O(n^2),空间复杂度为O(1)
  • 存在问题:但这种方法存在一个问题,当长度为偶数时,例如1221,这是一个回文串,但是并没有中心元素,如果直接用方法1,就会判断其不为回文串。
  • 解决办法:在字符串的中间插入分隔符#,那么1221就会变成1#2#2#1,长度为奇数。而本身长度为奇数的12321,插入分隔符变为1#2#3#2#1,仍然为奇数。那么就可以采用1的方法进行统一判断。插入分隔符后长度为2*n-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public String longestPalindrome(String s) {
if(s.length()<2)
return s;
String res = "";

String new_s = s.charAt(0) + "";
for(int i=1;i<s.length();i++) {
new_s += "#" + s.charAt(i);
}

int begin = 0;
int end = 0;
for(int i=0;i<new_s.length();i++) {
int tmp_begin = i-1;
int tmp_end = i+1;
while(tmp_begin>=0 && tmp_end<new_s.length()) {
if(new_s.charAt(tmp_begin)!=new_s.charAt(tmp_end))
break;

tmp_begin -= 1;
tmp_end += 1;
}
tmp_begin += 1;
tmp_end -= 1;
//不加下面这个if会出现#c#和bab,这种长度是一样的,但是最终结果的长度就不一样了。
if(new_s.charAt(tmp_begin)=='#') {
tmp_begin += 1;
tmp_end -=1;
}
if((tmp_end-tmp_begin)>=(end-begin)) {
begin = tmp_begin;
end = tmp_end;
}
}
res = new_s.substring(begin, end+1);
res = res.replace("#","");
return res;
}
  • 方法二:动态规划:如果用dp[i][j]保存子串从ij是否是回文子串, 如果dp[i][j]为回文,那么dp[i+1][j-1],也一定为回文。
  • dp[i][j]= s[i]==s[j] && dp[i+1][j-1], 要想求dp[i][j],要先求出dp[i+1][j-1]
  • i 要先求 i+1, 说明 i 要从大到小遍历;
  • j 要先求 j-1, 说明 j 要从小到大遍历;
  • dp[i][j]保存子串从ij是否是回文子串, 那么 j>=i
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public String longestPalindrome(String s) {
if (s.isEmpty()) {
return s;
}
int n = s.length();
boolean[][] dp = new boolean[n][n];
int left = 0;
int right = 0;
for (int i = n - 2; i >= 0; i--) {
dp[i][i] = true;
for (int j = i + 1; j < n; j++) {
//小于2是因为abbc, 两个元素相邻
//dp[1][2] 要判断 dp[2][1],但是这样的dp是没有的,或者说初始化的时候恒为false
dp[i][j] = s.charAt(i) == s.charAt(j) &&( j-i<2||dp[i+1][j-1]);
if(dp[i][j]&&right-left<j-i){
left=i;
right=j;
}
}
}
return s.substring(left,right+1);
}

leetcode 1130. Minimum Cost Tree From Leaf Values

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
给你一个正整数数组 arr,考虑所有满足以下条件的二叉树:
1. 每个节点都有 0 个或是 2 个子节点。
2. 数组 arr 中的值与树的中序遍历中每个叶节点的值一一对应。
3. 每个非叶节点的值等于其左子树和右子树中叶节点的最大值的乘积。
在所有这样的二叉树中,返回每个非叶节点的值的最小可能总和。这个和的值是一个 32 位整数。

输入:arr = [6,2,4]
输出:32
解释:有两种可能的树,第一种的非叶节点的总和为 36,第二种非叶节点的总和为 32

24 24
/ \ / \
12 4 6 8
/ \ / \
6 2 2 4
  • dp[i][j] :从 ij 的非叶子节点的最小和。
  • dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + max(A[i..k])*max(A[k+1..j]))
  • 想求 dp[i][j], 要先知道 dp[i][k]dp[k+1][j]k 是从 ij 的一个值。
  • 要求 dp[1][4] ,要先求 dp[1][2] dp[3][4] , 所以 i 要从大到小遍历,j 从小到大遍历。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int mctFromLeafValues(int[] arr) {
// max数组是先把arr中区间i到j的最大值保存下来,后面max[i][k]*max[k+1][j]要用。
// 不保存,直接调用函数查找最大值也行,但是比较耗时。
int[][] max = new int[arr.length][arr.length];
for(int i=0;i<arr.length;i++) {
max[i][i] = arr[i];
for(int j=i+1;j<arr.length;j++) {
max[i][j] = Math.max(arr[j], max[i][j-1]);
}
}

int[][] dp = new int[arr.length][arr.length];
for(int i=arr.length-1;i>=0;i--) {
for(int j=i+1;j<arr.length;j++) {
dp[i][j] = Integer.MAX_VALUE;
for(int k=i;k<j;k++) {
int tmp = dp[i][k] + dp[k+1][j] + max[i][k]*max[k+1][j];
dp[i][j] = Math.min(dp[i][j], tmp);
}
}
}
// 其实根据返回值也能确定i是从大到小,j是从小到大,因为返回的位置一定是最后一个得到结果的值。
return dp[0][arr.length-1];
}

poj 1738. 石子合并 (直线型)

1
2
3
4
有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,每次合并花费的代价为这两堆石子的和,经过N-1次合并后成为一堆。求出总的代价最小值。

输入:13 7 8 16 21 4 18
输出:239
  • dp[i][j]:从 i 堆石子到 j 堆石子进行合并最小代价。
  • dp[i][j] = dp[i][k] + dp[k+1][j] + w[i][j], 其中w[i][j]表示 ij 的石子的和。
  • 为了方便我们定义 sum[i] 为从第 0 堆石子到第 i 堆石子的和。那么 w[i][j]=sum[j]-sum[i-1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int stoneSum(int[] arr) {
int[] sum = new int[arr.length+1];
// sum[1] 表示第0堆石子。
for(int i=1;i<=arr.length;i++)
sum[i] = sum[i-1] + arr[i-1];

int[][] dp = new int[arr.length][arr.length];
for(int i=arr.length-1;i>=0;i--) {
for(int j=i+1;j<arr.length;j++) {
dp[i][j] = Integer.MAX_VALUE;
for(int k=i;k<j;k++) {
int tmp = dp[i][k] + dp[k+1][j] + sum[j+1]-sum[i];
dp[i][j] = Math.min(dp[i][j], tmp);
}
}
}
return dp[0][arr.length-1];
}

石子合并(环形)

1
有n堆石子排成一圈,每次选择相邻的两堆石子,将其合并为一堆,记录该次合并的得分为两堆石子个数之和。已知每堆石子的石子个数,求当所有石子合并为一堆时,最小的总得分。
  • 注:3题是排成一列,该题是排成一圈。
  • 方法一:将石子复制一遍,由n个堆的环形变成2n个堆的直线形。最终不再是求 dp[0][n-1], 而是在 dp[0][n-1], dp[1][n], dp[2][n+1]...dp[n][2n-1]中取最小值。
  • 相当于我起始位置不一定从 0 开始,可以从任意位置开始,到长度为 n 的地方结束。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static int stoneSum(int[] stone) {
int[] arr = new int[stone.length*2];
for(int i=0;i<stone.length;i++) {
arr[i] = stone[i];
arr[i+stone.length] = stone[i];
}

int[] sum = new int[arr.length+1];
// sum[1] 表示第0堆石子。
for(int i=1;i<=arr.length;i++)
sum[i] = sum[i-1] + arr[i-1];

int[][] dp = new int[arr.length][arr.length];
for(int i=arr.length-1;i>=0;i--) {
for(int j=i+1;j<arr.length;j++) {
dp[i][j] = Integer.MAX_VALUE;
for(int k=i;k<j;k++) {
int tmp = dp[i][k] + dp[k+1][j] + sum[j+1]-sum[i];
dp[i][j] = Math.min(dp[i][j], tmp);
}
}
}

int min_val = Integer.MAX_VALUE;
int n = stone.length;
for(int i=0;i<n;i++)
min_val = dp[i][n-1+i]<min_val?dp[i][n-1+i]:min_val;
return min_val;
}
  • 方法二: 直接利用环形的动态规划,取余即可。这个暂时不太懂怎么来的。
  • dp[i][j] = dp[i][k] + dp[(i+k−1)%n+1][j−k] + w[i][j]
1
不太懂原理。。

poj 2955. 括号匹配问题:最长匹配子序列的长度

1
2
Given the initial sequence ([([]])], the longest regularbrackets subsequence is[([])].
找到符合括号匹配的最长子序列的长度。
  • 这道题和回文串的动规方法很像,如果 s[i]==s[j],那么 dp[i][j] = 2 + dp[i+1][j-1]
  • 但仅仅做上面一个判断是不够的。
  • 比如 s="()()"dp[0][3] = 2 + dp[1][2] = 2; 显然是不对的。
  • dp[0][3] = max(dp[0][3], dp[0][0]+dp[1][3], dp[0][1]+dp[2][3], dp[0][2]+dp[3][3])
  • 也就是,我到底是把最外面两个括号当作一个匹配,然后加上里面。还是把最左括号和左面一部分匹配+最右括号和右面一部分匹配。然后看哪种得到的结果最大就是哪种情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static int longestValidParentheses(String s) {
if(s.length()==0)
return 0;
int[][] dp = new int[s.length()][s.length()];
for(int i=s.length()-1;i>=0;i--) {
dp[i][i] = 0;
for(int j=i+1;j<s.length();j++) {
if((s.charAt(i)=='(' && s.charAt(j)==')')||(s.charAt(i)=='[' && s.charAt(j)==']'))
dp[i][j] = Math.max(dp[i][j], 2 + dp[i+1][j-1]);
for(int k=i;k<j;k++)
dp[i][j] = Math.max(dp[i][j], dp[i][k]+dp[k+1][j]);
}
}
return dp[0][s.length()-1];
}

⭐ 整数划分使乘积最大

1
2
3
给出两个整数 n, m, 要求在 n 中加入 m - 1 个乘号,将 n 分成 m 段,求出这 m 段的最大乘积。
输入:1112 输出:11
输入:11112 输出:121
1
2
3
4
5
6
7
8
我的思考过程:
1. dp[i][j][m]: 从位置i到位置j,分成m段的乘积最大值。
2. 那么dp[i][j][m] = dp[i][k][p]*dp[k+1][j][m-p-1],但是这样参数太多。
3. 我们最终求dp[0][len][m] = dp[0][k][p]*dp[k+1][len][m-p-1]
4. 可以定义:dp[i][j]:从0位置到i位置插入了j个乘号得到的最大值。那么原dp[0][k][p]可表示为dp[k][p]
5. 那么dp[k+1][len][m-p-1]又没办法表示了。那么我们只能让后一段只有一个数,没有乘号,那么后一段就不需要这么复杂,直接num[k+1...len]就行了。
6. dp[i][j] = max(dp[i][j], dp[k][j-1]*num[k+1...len])
7. 最终求:dp[len][m-1], 所以i和i都要从小到大遍历。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static int maxResult(int n, int m) {
String s = n + "";
int[][] dp = new int[s.length()][m];
// 如果不分割,那么0到i子串的值为多少。
for(int i=0;i<s.length();i++)
dp[i][0] = Integer.parseInt(s.substring(0, i+1));

// 循环的顺序也很重要,把j写在最外层训练位置,因为j的值会决定i的起始位置。
for(int j=1;j<m;j++) {
for(int i=j;i<s.length();i++) { // 前2个位置分割3次是不可能的,所以i从j开始取即可。
for(int k=j-1;k<i;k++) { // 同理,前k个位置分割j-1次,所以k从j-1开始取即可。
int tmp = dp[k][j-1]*Integer.parseInt(s.substring(k+1, i+1));
dp[i][j] = Math.max(dp[i][j], tmp);
}
}
}
return dp[s.length()-1][m-1];
}

凸多边形三角划分问题

1
给定一个具有N(N<=50)个顶点(从1到N编号)的凸多边形,每个顶点的权值已知。问如何把这个凸多边形划分成N-2个互不相交的三角形,使得这些三角形顶点的权值的乘积之和最小。
  • dp[i][j]:表示从顶点 i 到顶点 j 组成的凸多边形划分之后乘积之和的最小值。
  • dp[i][j] = dp[i][k] + dp[k][j] + arr[i]*arr[j]*arr[k]
  • 哎😔,,边界太难控制了。。

1
2
3
4
5
6
7
8
9
10
11
12
public int triangleMin(int[] arr) {
int[][] dp = new int[arr.length][arr.length];
// 至少得有三条边
for(int i=arr.length-3;i>=0;i--) {
for(int j=i+2;j<arr.length;j++) {
dp[i][j] = Integer.MAX_VALUE;
for(int k=i+1;k<j;k++)
dp[i][j] = Math.min(dp[i][j], dp[i][k]+dp[k][j] + arr[i]*arr[k]*arr[j]);
}
}
return dp[0][arr.length-1];
}

leetcode 375. 猜数字,使花费最小

1
2
3
4
5
6
7
8
9
10
我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。

示例: n = 10, 我选择了8.
第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。
游戏结束。8 就是我选的数字。
你最终要支付 5 + 7 + 9 = 21 块钱。

给定 n ≥ 1,计算你至少需要拥有多少现金才能确保你能赢得这个游戏。
  • 就是要怎样进行猜数字,才能让自己花费的钱最少。比如n=10,那么一开始猜7,如果大了就猜9,那么需要花费7+9=16块钱赢得游戏。如果一开始猜5,那么大了还要猜7和9,那么就要花费5+7+9=21。所以我们要求最少花费多少钱赢得游戏。

  • 通过上面的解释可以看出这不是二分查找的题,二分查找虽然能保证一定找到你想要的数字,但却不是花费最少的方法。所以是一道动态规划的题。

  • 分析:

    1
    2
    3
    4
    1到n里面,随便猜一个数i,那么此时赢得游戏的最少花费为: 
    truetruetruedp[1..n] = arr[i] + max(dp[1..i-1], dp[i+1..n]),
    而 dp[1..i+1] 又可以继续分割,随机猜一个数字k, 那么
    truetruetruedp[1..i+1] = arr[k] + max(dp[1..k], dp[k..i+1])
  • 麻卖批。。分析到这明显是区间dp啊!!!为什么自己就不能从这么简单的地方开始分析呢。。

  • 定义 dp[i][j]:从位置 i 到位置 j 猜出数字的最小花费。那么我们最终要求的为 dp[1][n]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int getMoneyAmount(int n) {
if(n==1) return 0;
// 定义长度为 n+1, 0位置不使用,为了让index和值对应,要不然还要减一,不方便看。
int[][] dp = new int[n+1][n+1];

for(int i=n;i>0;i--) {
dp[i][i] = 0;
for(int j=i+1;j<=n;j++) {
dp[i][j] = Integer.MAX_VALUE;
if(j==i+1) dp[i][j] = i;
for(int k=i+1;k<j;k++)
dp[i][j] = Math.min(dp[i][j], k + Math.max(dp[i][k-1], dp[k+1][j]));
}
}
return dp[1][n];
}