动态规划
这类问题往往需要求一个“最值”,应满足以下三个条件:
- 最优子结构
- 最优解
- 重叠子问题
思路:找出状态转移方程,有base case,根据填表来获得最终解
经典问题
硬币问题
有一个面值为c1,c2,…ck的k枚硬币,数量不限,给一个总金额amount,求最少几枚硬币能刚好凑够?
- 先确定**「状态」**,也就是原问题和⼦问题中变化的变量。由于硬币数量⽆
限,所以唯⼀的状态就是⽬标⾦额 amount 。 - 然后确定 dp 函数的定义:当前的⽬标⾦额是 n ,⾄少需要 dp(n) 个硬
币凑出该⾦额。 - 状态转移方程:
戳气球
有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。每当你戳破一个气球 i 时,你可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100
示例:
输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
- 状态方程:dp[start][end] = Math.max(dp[start][end],dp[start][k]+dp[k][end]+new_nums[start]*new_nums[k]*new_nums[end]);
k表示从start到end之间的气球最后戳破的编号为气球k - 动态填表
if start+1 <= end
dp[start][end] = 0;
从start = 3开始往上填写
当start= 3,end = 5,k = 4时,dp[3][5] = dp[3][4]+dp[4][5]+nums[3]nums[4]nums[5]=0+0+581=40
同理
start= 2,end = 4,k = 3时,dp[2][4]=nums[2]nums[4]nums[3]=581=40
start= 2,end = 5,k = 3,4时,dp[2][5]=max(dp[2][4]+nums[2]nums[4]nums[5],dp[3][5]+nums[2]nums[3]nums[5])=max(40+181,40+151)=48
1 3 1 5 8 1
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0 | 0 | 3 | 20 | 159 | 167 |
1 | 0 | 0 | 15 | 135 | 159 | |
2 | 0 | 0 | 40 | 48 | ||
3 | 0 | 0 | 40 | |||
4 | 0 | 0 | ||||
5 | 0 |
class Solution {
public int maxCoins(int[] nums) {
if(nums.length == 0){
return 0;
}
if(nums.length == 1){
return nums[0];
}
if(nums.length == 2){
return nums[0]*nums[1]+Math.max(nums[0],nums[1]);
}
int n = nums.length+2;
int[][] dp = new int[n][n]; //表示编号从i到j的气球戳破的最大硬币值
int[] new_nums = new int[n];
new_nums[0] = new_nums[n-1] = 1;
for(int i = 0; i < nums.length; i++){ //新数组
new_nums[i+1] = nums[i];
}
for(int start = n-1;start >= 0;start--){
for(int end = start+2;end < n;end++){
for(int k = start+1; k < end;k++)
dp[start][end] = Math.max(dp[start][end],dp[start][k]+dp[k][end]+new_nums[start]*new_nums[k]*new_nums[end]);
}
}
return dp[0][n-1];
}
}
目标和
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
- 递归方法
class Solution {
private int core(int[] nums,int[][] dp, int S,int index){
if(index == 0){
if(0 == nums[0] && S == 0){
return 2;
}
else if(S == nums[0] || S+nums[0] == 0){
return 1;
}
else
return 0;
}
return core(nums,dp,S+nums[index],index-1)+core(nums,dp,S-nums[index],index-1);
}
public int findTargetSumWays(int[] nums, int S) {
int n = nums.length;
int[][] dp = new int[n][2000];
return this.core(nums,dp,S,n-1);
}
}
- 动态规划法
转化为找到一个正子集和一个负子集,其和为S
sum§ - sum(N) = target
sum§ + sum(N) + sum§ - sum(N) = target + sum§ + sum(N)
2 * sum§ = target + sum(nums)
转化为找到一个正子集,使得其和为S+sum的一半,类似于0 1 背包问题
class Solution {
public int findTargetSumWays(int[] nums, int S) {
int n = nums.length;
int sum= 0;
for(int i = 0;i < n;i++){
sum += nums[i];
}
if(sum < S || (S+sum)%2 == 1){
return 0;
}
int m = (S+sum)>>1;
int[] dp = new int[m+1];
dp[0] = 1;
for(int i = 0;i < n;i++){
for(int j = m; j >= nums[i];j--){
dp[j] += dp[j-nums[i]];
}
}
return dp[m];
}
}