01背包:
二维更容易理解
dp 代表从0,i索引是否能组成target,01背包
初始化:0行0列,对于0列都为true,因为target=0,默认不选
对于0行,拿到nums0恰好填满时候为true。所以dp0nums\[0]=true,如果没有越界
递推公式:对于当前numsi可以选择拿或者不拿,取到一个或的结果。
class Solution {
public boolean canPartition(int[] nums) {
// 类似01背包
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum += nums[i];
}
if(sum % 2 != 0) return false;
int target = sum /2;
// 从0,i上能否填满target
boolean dp[][] = new boolean[nums.length][target+1];
// dp[i][j] = dp[i-1][j] || dp[i-1][target-nums[i]]
// 当target为0的时候,都不选默认为true
for(int i = 0; i < nums.length; i++){
dp[i][0] = true;
}
// 当i=0的时候,如果恰好填满target为true
if(nums[0]<= target) dp[0][nums[0]]=true;
for(int i = 1; i < nums.length;i++){
for(int j = 1; j <= target; j++){
dp[i][j] = dp[i-1][j];
if(j-nums[i] >= 0){
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
}
}
}
return dp[nums.length-1][target];
}
}
完全背包
完全背包,先遍历物品,内部遍历容量,初始化的最大值注意,按照1的最多数量+1就可以
出错的点:容量错误,用整数最大值会越界
class Solution {
public int coinChange(int[] coins, int amount) {
int dp[] = new int[amount+1];
for(int i = 1; i <= amount; i++){
dp[i] = amount+1;
}
for(int i = 0; i < coins.length;i++){
for(int j = 1; j <= amount;j++){
if(j - coins[i] >=0){
dp[j] = Math.min(dp[j], dp[j-coins[i]]+1);
}
}
}
if(dp[amount] == amount+1) return -1;
return dp[amount];
}
}
完全背包的变种:和零钱兑换几乎一样,先遍历物品,此时物品是i*i,平方数,内部正序循环容量,初始化都用容量+1.
class Solution {
public int numSquares(int n) {
int dp [] = new int[n+1];
dp[0]=0;
for(int i=1; i<=n;i++){
dp[i]=n+1;
}
for(int i = 1; i * i <= n;i++){
int sq = i * i;
for(int j = 1; j <= n;j++){
if(j >= sq){
dp[j] = Math.min(dp[j], dp[j - sq] + 1);
}
}
}
return dp[n];
}
}
类似完全背包:但是不能套用外层遍历物品的模板,字符串位置问题一般是先遍历容量
推理定义:前i个字符能否拆分,要看前j个能否拆分+j,i是否是一个单词
初始化:前0个为true。
公式:要算 dp[i],就把所有 j < i 试一遍,看 dp[j] + s[j..i-1] 是否成立。只要有一个成立,dp[i] 就为 true。
算 dp[i] 时,前面所有 dp[0..i-1] 都已经算好,直接查表复用。不是只复用 dp[i-1]。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// 从前i个字符串能否被worddict填满。
// dp[i] = dp[j] && s[j, i];
boolean dp[] = new boolean[s.length()+1];
dp[0]=true;
//在前i个字符每一种i的情况下,只要有一种j可以成功切分,dp[i]就是true,所以找到了就break
// 内层循环是为了尝试所有切分点,让i可以拆分为j+(j,i)
for(int i = 1; i <= s.length();i++){
for(int j = 0; j <= i;j++){
if(dp[j] && (wordDict.contains(s.substring(j,i)))){
dp[i]=true;
break;
}
}
}
return dp[s.length()];
}
}
连续子数组/子串"问题
maxdpi以i结尾的最大值
mindpi以i结尾的最小值
class Solution {
public int maxProduct(int[] nums) {
// dpmax[i] = dpmin[i-1] * nums[i] if nums[i] < 0
// dpmax[i] = dpmax[i-1] * nums[i] if nums[i] > 0
int maxdp [] = new int[nums.length];
int mindp [] = new int[nums.length];
maxdp[0] = nums[0];
mindp[0]=nums[0];
int result =nums[0];
for(int i = 1; i < nums.length;i++){
maxdp[i] = Math.max(nums[i], Math.max(nums[i] * mindp[i-1], nums[i] * maxdp[i-1]));
mindp[i] = Math.min(nums[i], Math.min(nums[i] * maxdp[i-1], nums[i] * mindp[i-1]));
result = Math.max(result, maxdp[i]);
}
return result;
}
}
推理方程:dp[i] = 以 s[i] 结尾的最长有效括号子串长度。
初始化:dp0 = 0,因为一个字符形成不了,其余也为0
状态转移:s[i]=='(' 时为 0;s[i]==')' 时,若前一个是 ( 则 dp[i]=dp[i-2]+2,否则跳过 dp[i-1] 看 s[j](j=i-dp[i-1]-1)能否配对。答案取 max(dp),全程用 res 记录。
class Solution {
public int longestValidParentheses(String s) {
if(s.length()==0)return 0;
// 以i结尾的字符换的最长连续子串
int dp[]=new int[s.length()];
dp[0]=0; // 初始化
int res = 0;
for(int i = 1; i < s.length();i++){
// i位置以(结尾必然无法拼接
if(s.charAt(i) == '('){
dp[i]=0;
continue;
}
if(s.charAt(i) == ')'){
if(s.charAt(i-1)=='('){
if(i-2<0) {
dp[i]=2;
}else{
dp[i] = dp[i-2]+2;
}
}else{
int j = i-dp[i-1] -1;
if(j-1>= 0 && s.charAt(j) == '('){
dp[i] = dp[i-1]+2+dp[j-1];
}else if(j>= 0 && j<1 &&s.charAt(j) == '('){
dp[i] = dp[i-1]+2;
}
}
if(dp[i] > res){
res = dp[i];
}
}
}
return res;
}
}
错误分析:初始化没初始化全部数组
推理方程:以i位置结束的最大递增长度
初始化:所有dp位置初始为1.因为至少一个
转移:对于每个i,看他前面的所有j位置,如果numsj < numsi,找到一种dpi的可能dpj+1,然后取dpi的最大值。
class Solution {
public int lengthOfLIS(int[] nums) {
// i位置结尾的最大递增子序列是dp[i]
int dp[] = new int[nums.length];
Arrays.fill(dp,1);
int res=dp[0];
for(int i = 1;i<nums.length;i++){
for(int j = 0; j<i; j++){
if(nums[i] > nums[j]){
dp[i]=Math.max(dp[i], dp[j]+1);
}
}
res=Math.max(dp[i],res);
}
return res;
}
}
总结:
01背包:
简化为一维数组之后,外循环物品,内循环容量逆序
完全背包:
简化为一维数组之后,外循环物品,内循环容量正序
连续子串/子数组 ,或者扩展时需要看末尾元素的:
用"以 i 结尾"
前缀的整体性质,扩展时不关心末尾具体是谁的:
用"前 i 个"