LeetCode-动态规划总结(三)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Apple_hzc/article/details/84171711

最长公共子序列

对于两个子序列 S1 和 S2,找出它们最长的公共子序列。

定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:

  • 当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
  • 当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i 个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。

综上,最长公共子序列的状态转移方程为:


对于长度为 N 的序列 S1 和长度为 M 的序列 S2,dp[N][M] 就是序列 S1 和序列 S2 的最长公共子序列长度。

与最长递增子序列相比,最长公共子序列有以下不同点:

  • 针对的是两个序列,求它们的最长公共子序列。
  • 在最长递增子序列中,dp[i] 表示以 Si 为结尾的最长递增子序列长度,子序列必须包含 Si ;在最长公共子序列中,dp[i][j] 表示 S1 中前 i 个字符与 S2 中前 j 个字符的最长公共子序列长度,不一定包含 S1i 和 S2j
  • 在求最终解时,最长公共子序列中 dp[N][M] 就是最终解,而最长递增子序列中 dp[N] 不是最终解,因为以 SN 为结尾的最长递增子序列不一定是整个序列最长递增子序列,需要遍历一遍 dp 数组找到最大者。
public int lengthOfLCS(int[] nums1, int[] nums2) {
    int n1 = nums1.length, n2 = nums2.length;
    int[][] dp = new int[n1 + 1][n2 + 1];
    for (int i = 1; i <= n1; i++) {
        for (int j = 1; j <= n2; j++) {
            if (nums1[i - 1] == nums2[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[n1][n2];
}

0-1 背包

有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。

这张表是至底向上,从左到右生成的。只要你能手工填写出上面这张表就算理解了01背包的动态规划算法。

原理:定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i 件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:

  • 第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i - 1][j]。
  • 第 i 件物品添加到背包中,dp[i][j] = dp[i - 1][j - w] + v。

第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:


public int knapsack(int N, int W, int[] w, int[] v) {
    int[][] dp = new int[N + 1][W + 1];
        for (int i = 1; i <= N; i++) {
            int weight = w[i - 1], value = v[i - 1];
            for (int j = 1; j <= W; j++) {
                if (j >= weight) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight] + value);
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        items = new int[N + 1];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= W; j++) {
                findWhat(dp, i, j, w, v);
            }
        }
        return dp[N][W];
}
//寻找解的组成方式
public void findWhat(int[][] dp, int i, int j, int[] w, int[] v) {
        if (i >= 1) {
            if (dp[i][j] == dp[i - 1][j]) {
                items[i] = 0;
                findWhat(dp, i - 1, j, w, v);
            } else if (j >= w[i - 1] && dp[i][j] == dp[i - 1][j - w[i - 1]] + v[i - 1]) {
                items[i] = 1;
                findWhat(dp, i - 1, j - w[i - 1], w, v);
            }
        }
    }

空间优化

在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,当我们把第二层循环颠倒过来时,当我们要求dp[j]时,dp[j - 1]到dp[1]还保存着下面一行的状态,因此可以采用一维数组解决。其中 dp[j] 既可以表示 dp[i - 1][j] 也可以表示 dp[i][j]。此时,


因为 dp[j - w] 表示 dp[i - 1][j - w],因此不能顺序求,以防将 dp[i - 1][j - w] 覆盖。我们可以考虑一下假如不把第二层循环颠倒,当要求dp[j]时,dp[j - 1]到dp[1]已经是同一行的状态了,因此在求后面的最优价值时前面的状态已经被覆盖,因此应该采用倒序。

public int optimizerBlag(int N, int W, int[] w, int[] v) {
    int[] dp = new int[W + 1];
        for (int i = 1; i <= N; i++) {
            int weight = w[i - 1], value = v[i - 1];
            for (int j = W; j >= 1; j--) {
                if (j >= weight) {
                    dp[j] = Math.max(dp[j], dp[j - weight] + value);
                }
            }
        }
        return dp[W];
}

优化前后算法对比

对于优化前的算法,不足之处在于空间开销大,数据的存储得用到二维数组,但优点是可以找出最优解的组成;优化后,可以使得空间效率从O(n*c)转化为O©,遗憾的是,虽然优化了空间,但优化后只能求出最优解,解组成的方式在该方法运行的时候已经被破坏掉。总之优化前后的算法各有优缺点,可以根据实际问题的需求选择不同的方式。

完全背包

完全背包是指每种物品都有无限件,可以放入也可以不放,求出使得背包装满价值最大的解。

其实对于这个问题,只需要将01背包优化代码中第二层循环中V的次序由倒序变为顺序即可。

首先想想为什么01背包中要按照v=V…0的逆序来循环。这是因为要保证第i次循环中的状态dp[i][j]是由状态dp[i - 1] [j - w]递推而来。换句话说,这正是为了保证每件物品只选一次。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果dp[i][j - w],所以就可以采用v=0…V的顺序循环。

该思路的状态转移方程为:


利用一维数组实现的状态转移方程:


在完全背包中,当计算dp[j]时,dp[1]-dp[j - 1]表示的就不再是前一状态的最大价值,而是在当前状态的基础上,再考虑是否装或者不装。

其它变种

  • 多重背包:物品数量有限制

  • 多维费用背包:物品不仅有重量,还有体积,同时考虑这两种限制

  • 其它:物品之间相互约束或者依赖

划分数组为和相等的两部分

416. Partition Equal Subset Sum (Medium)

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].

可以看成一个背包大小为 sum/2 的 0-1 背包问题。

class Solution {
    public boolean canPartition(int[] nums) {
        if (null == nums || 0 == nums.length) {
            return false;
        }
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 != 0) {
            return false;
        }
        int W = sum / 2;
        boolean[] dp = new boolean[W + 1];
        dp[0] = true;
        for (int i = 1; i <= nums.length; i++) {
            int w = nums[i - 1];
            for (int j = W; j >= 1; j--) {
                if (j >= w) {
                    dp[j] = dp[j] | dp[j - w];
                }
            }
        }
        return dp[W];
    }
}

改变一组数的正负号使得它们的和为一给定数

494. Target Sum (Medium)

Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

There are 5 ways to assign symbols to make the sum of nums be target 3.

该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。

可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:

                  sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
                       2 * sum(P) = target + sum(nums)

因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。

class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        if (null == nums || 0 == nums.length) {
            return 0;
        }
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if ((sum + S) % 2 != 0) {
            return 0;
        }
        int W = (sum + S) / 2;
        int[] dp = new int[W + 1];
        dp[0] = 1;
        for (int i = 1; i <= nums.length; i++) {
            int w = nums[i - 1];
            for (int j = W; j >= 1; j--) {
                if (j >= w) {
                    dp[j] = dp[j] + dp[j - w];
                }
            }
        }
        return dp[W];
    }
}

字符串按单词列表分割

139. Word Break (Medium)

s = "leetcode",
dict = ["leet", "code"].
Return true because "leetcode" can be segmented as "leet code".

dict 中的单词没有使用次数的限制,因此这是一个完全背包问题。

0-1 背包和完全背包在实现上的不同之处是,0-1 背包对物品的迭代是在最外层,而完全背包对物品的迭代是在最里层。

class Solution{
	public boolean wordBreak(String s, List<String> wordDict) {
    	int n = s.length();
    	boolean[] dp = new boolean[n + 1];
    	dp[0] = true;
    	for (int i = 1; i <= n; i++) {
        	for (String word : wordDict) {   // 完全一个物品可以使用多次
            	int len = word.length();
            	if (len <= i && word.equals(s.substring(i - len, i))) {
                	dp[i] = dp[i] || dp[i - len];
            	}
        	}
    	}
    	return dp[n];
	}
}

01 字符构成最多的字符串

474. Ones and Zeroes (Medium)

Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4

Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0"

这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。

class Solution{
	public int findMaxForm(String[] strs, int m, int n) {
    	if (strs == null || strs.length == 0) {
        	return 0;
    	}
    	int[][] dp = new int[m + 1][n + 1];
    	for (String s : strs) {    // 每个字符串只能用一次
        	int ones = 0, zeros = 0;
        	for (char c : s.toCharArray()) {
            	if (c == '0') {
                	zeros++;
            	} else {
                	ones++;
            	}
        	}
        	for (int i = m; i >= zeros; i--) {
            	for (int j = n; j >= ones; j--) {
                	dp[i][j] = Math.max(dp[i][j], dp[i - zeros][j - ones] + 1);
            	}
        	}
    	}
    	return dp[m][n];
	}
}

找零钱的最少硬币数

322. Coin Change (Medium)

Example 1:
coins = [1, 2, 5], amount = 11
return 3 (11 = 5 + 5 + 1)

Example 2:
coins = [2], amount = 3
return -1.

题目描述:给一些面额的硬币,要求用这些硬币来组成给定面额的钱数,并且使得硬币数量最少。硬币可以重复使用。

  • 物品:硬币
  • 物品大小:面额
  • 物品价值:数量

因为硬币可以重复使用,因此这是一个完全背包问题。

class Solution { //二维数组
    public int coinChange(int[] coins, int amount) {
        if (null == coins || 0 == coins.length) {
            return 0;
        }
        int n = coins.length;
        int[][] dp = new int[n][amount + 1];
        for (int i = 0; i < n; i++) {
            Arrays.fill(dp[i], amount + 1);
            dp[i][0] = 0;
        }
        for (int i = 1; i <= amount; i++) {
            dp[0][i] = i % coins[0] == 0 ? i / coins[0] : amount + 1;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i]) {
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i]] + 1);
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n - 1][amount] > amount ? -1 : dp[n - 1][amount];
    }
}
class Solution{ //一维数组
	public int coinChange(int[] coins, int amount) {
    	if (coins == null || coins.length == 0) {
        	return 0;
    	}
    	int[] minimum = new int[amount + 1];
    	Arrays.fill(minimum, amount + 1);
    	minimum[0] = 0;
    	Arrays.sort(coins);
    	for (int i = 1; i <= amount; i++) {
        	for (int j = 0; j < coins.length && coins[j] <= i; j++) {
            	minimum[i] = Math.min(minimum[i], minimum[i - coins[j]] + 1);
        	}
    	}
    	return minimum[amount] > amount ? -1 : minimum[amount];
	}
}

组合总和

377. Combination Sum IV (Medium)

nums = [1, 2, 3]
target = 4

The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

Note that different sequences are counted as different combinations.

Therefore the output is 7.

完全背包。

class Soulution{
	public int combinationSum4(int[] nums, int target) {
    	if (nums == null || nums.length == 0) {
        	return 0;
    	}
    	int[] maximum = new int[target + 1];
    	maximum[0] = 1;
    	Arrays.sort(nums);
    	for (int i = 1; i <= target; i++) {
        	for (int j = 0; j < nums.length && nums[j] <= i; j++) {
            	maximum[i] += maximum[i - nums[j]];
			}
   	 	}
    	return maximum[target];
	}
}

猜你喜欢

转载自blog.csdn.net/Apple_hzc/article/details/84171711