面试题101:
问题:
给定一个非空的正整数数组,判断能否将这些数字分成和相等的两部分。
解决方案:
- 如果有n个物品,每步判断一个物品是否要放入背包,也就是说解决这个问题需要n步,并且每步都面临放入或不放入两个选择,这看起来是一个能用回溯法解决的问题。但这个题目没有要求列出所有可能的放满背包的方法,而是只要求判断是否存在放满背包的方法,也就是判断方法的数量是否大于0。因此,这个问题更适合用动态规划解决。
- 用函数f(i,j)表示能否从前i个物品(物品标号分别为0,1,…,i-1)中选择若干物品放满容量为j的背包。如果总共有n个物品,背包的容量为t,那么f(n,t)就是问题的解。
- 当j等于0时,即背包的容量为0,不论有多少个物品,只要什么物品都不选择,就能使选中的物品的总重量为0,因此f(i,0)都为true。当i等于0时,即物品的数量为0,肯定无法用0个物品来放满容量大于0的背包,因此当j大于0时f(0,j)都为false。
- 当判断能否从前i个物品中选择若干物品放满容量为j的背包时,对标号为i-1的物品有两个选择。一个选择是将标号为i-1的物品放入背包中,如果能从前i-1个物品(物品标号分别为0,1,…,i-2)中选择若干物品放满容量为j-nums[i-1]的背包(即f(i-1,j-nums[i-1])为true),那么f(i,j)就为true。另一个选择是不将标号为i-1的物品放入背包中,如果从前i-1个物品中选择若干物品放满容量为j的背包(即f(i-1,j)为true),那么f(i,j)也为true。
源代码(递归):
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num:nums){
sum += num;
}
if(sum % 2 == 1){
return false;
}
return subsetSum(nums,sum/2);
}
private boolean subsetSum(int[] nums,int target){
Boolean[][] dp = new Boolean[nums.length+1][target+1];
return dfs(nums,dp,nums.length,target);
}
//i表示物品数量,j表示背包容量
private boolean dfs(int[] nums,Boolean[][] dp,int i,int j){
if(dp[i][j] == null){
//当背包容量为0时,不选择任何物品就可以将背包填满,故dp[i][j] = true
if(j == 0){
dp[i][j] = true;
//当物品数量为0时,没有物品了还怎么填满背包,故dp[i][j] = false
}else if(i == 0){
dp[i][j] = false;
}else{
//情况一:不取该物品
dp[i][j] = dfs(nums,dp,i-1,j);
//情况二:取该物品,但是背包容量必须大于等于该物品重量
if(!dp[i][j] && j >= nums[i-1]){
dp[i][j] = dfs(nums,dp,i-1,j-nums[i-1]);
}
}
}
return dp[i][j];
}
}
源代码(迭代):
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num:nums){
sum += num;
}
if(sum % 2 == 1){
return false;
}
return subsetSum(nums,sum/2);
}
private boolean subsetSum(int[] nums,int target){
//i表示物品数量,j表示背包容量
//因为这题所用数组与上题不太一样,Boolean数组的默认值是null,而boolean数组的默认值是false,因此不需要再对当物品数量为0时,没有物品了还怎么填满背包,故dp[i][j] = false的情况进行赋值
boolean[][] dp = new boolean[nums.length+1][target+1];
//当背包容量为0时,不选择任何物品就可以将背包填满,故dp[i][j] = true
for(int i = 0;i <= nums.length;i++){
dp[i][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(!dp[i][j] && j >= nums[i-1]){
dp[i][j] = dp[i-1][j-nums[i-1]];
}
}
}
return dp[nums.length][target];
}
}
优化空间效率思路:
在计算f(i,j)的值时需要用到f(i-1,j)、f(i-1,j-nums[i-1])、因为f(i-1,j-nums[i-1])保存在f(i,j)之前,那么如果从左到右计算,那么会将f(i-1,j-nums[i-1])覆盖为f(i,j-nums[i-1]),所以只能从右到左计算。而且在f(i,j)计算完后就不需要用到了。
源代码:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num:nums){
sum += num;
}
if(sum % 2 == 1){
return false;
}
return subsetSum(nums,sum/2);
}
private boolean subsetSum(int[] nums,int target){
//i表示物品数量,j表示背包容量
boolean[] dp = new boolean[target+1];
//当背包容量为0时,不选择任何物品就可以将背包填满,故dp[j] = true
dp[0] = true;
for(int i = 1;i <= nums.length;i++){
for(int j = target;j > 0;j--){
//因为压缩为一维数组了,dp[i][j]没赋值前就是dp[i-1][j],所以省去赋值操作
if(!dp[j] && j >= nums[i-1]){
dp[j] = dp[j-nums[i-1]];
}
}
}
return dp[target];
}
}
面试题102:
问题:
给定一个非空的正整数数组和一个目标值S,如果为每个数字添加“+”或“-”运算符,请计算有多少种方法可以使这些整数的计算结果为S
解决方案:
- 假设数组有n个,每一个数字都有两种情况+或-,看起来适合使用回溯法,但是题目没有要求我们求出每一种方法的情况,而是让我们求出总共有多少种方法,故使用动态规划。
- 为输入的数组中的有些数字添加“+”,有些数字添加“-”。如果所有添加“+”的数字之和为p,所有添加“-”的数字之和为q,按照题目的要求,p-q=S。如果累加数字中的所有数字,就能得到整个数组的数字之和,记为sum,即p+q=sum。将这两个等式的左右两边分别相加,就可以得到2p=S+sum,即p=(S+sum)/2。
- 用函数f(i,j)表示在数组的前i个数字(即nums[0…i-1])中选出若干数字使和等于j的方法的数目。如果数组的长度为n,目标和为t,那么 f(n,t)就是整个问题的解。
- 当j0时,也就是在i个数字选出n个数,使它们的和等于j,只要一个不选就等于0了,故f(i,0) = 1,当i0,j > 0时,当数字个数为0,不管怎么选它们的和都不可能大于0,故f(0,j) = 0,当i >0 并且 j > nums[i]时,f(i,j) = f(i-1,j) + f(i-1,j-nums[i])。
源代码:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num:nums){
sum += num;
}
if(target < 0){
target = -target;
}
if((sum + target) % 2 == 1 || sum < target){
return 0;
}
target = (sum+target)/2 ;
return targetSum(nums,target);
}
private int targetSum(int[] nums,int target){
int[] dp = new int[target+1];
dp[0] = 1;
//因为f(i,j)依赖于f(i-1,j)、f(i-1,j-nums[i-1]),如果从左到右顺序f(i-1,j-nums[i-1])在f(i,j)前面计算会给f(i,j-nums[i-1])覆盖,故从右到左进行计算,又因为f(i-1,j)和f(i,j)保存在一起,但是还没计算完f(i,j)的时候,dp[j]保存的是f(i-1,j)的值,故使用累加:dp[j] += dp[j-num]。
for(int num:nums){
for(int j = target;j >= num;--j){
dp[j] += dp[j-num];
}
}
return dp[target];
}
}
面试题103:
问题:
给定正整数数组coins表示硬币的面额和一个目标总额t,请计算凑出总额t至少需要的硬币数目。
解决方案:
- 如果将每种面额的硬币看成一种物品,而将目标总额看成背包的容量,那么这个问题等价于求将背包放满时物品的最少件数。值得注意的是,这里每种面额的硬币可以使用任意多次,因此这个问题不再是0-1背包问题,而是一个无界背包问题(也叫完全背包问题)。
- 用函数f(i,j)表示用前i种硬币(coins[0,…,i-1])凑出总额为j需要的硬币的最少数目。当使用0枚标号为i-1的硬币时,f(i,j)等于 f(i-1,j)(用前i-1种硬币凑出总额j需要的最少硬币数目,再加上1枚标号为i-1的硬币);当使用1枚标号为i-1的硬币时,f(i,j)等于f(i-1,j-coins[i-1])加1(用前i-1种硬币凑出总额j-coins[i-1]需要的最少硬币数目,再加上1枚标号为i-1的硬币);以此类推,当使用k枚标号为i-1的硬币时,f(i,j)等于f(i-1,j-k×coins[i-1])加k(用前i-1种硬币凑出总额j-k×coins[i-1]需要的最少硬币数目,再加上k枚标号为i-1的硬币)。
源代码:
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
Arrays.fill(dp,amount+1);
dp[0] = 0;
for(int num:coins){
for(int j = amount;j >= num;j--){
for(int k = 1;k*num <= j;k++){
dp[j] = Math.min(dp[j],dp[j - k*num] + k);
}
}
}
return dp[amount] > amount?-1:dp[amount];
}
}
面试题104:
问题:
给定一个非空的正整数数组nums和一个目标值t,数组中的所有数组都是唯一的,请计算数字之和等于t的所有排列的数目。
解决方案:
- 如果将每个数字看成一种物品,而将目标值看成背包的容量,那么这个问题等价于求将背包放满时物品的最少件数。值得注意的是,这里每个数字可以在排列中出现任意次,因此这个问题不再是0-1背包问题,而是一个无界背包问题(也叫完全背包问题)。
- 用f(i)表示和为i的排列的数目。为了得到和为i的排列,有如下选择:在和为i-nums[0]的排列中添加标号为0的数字,此时f(i)等于f(i-nums[0]);在和为i-nums[1]的排列中添加标号为1的数字,此时f(i)等于f(i-nums[1])。以此类推,在和为i-nums[n-1]的排列中添加标号为n-1的数字(n为数组的长度),此时f(i)等于 f(i-nums[n-1])。因为目标是求出所有和为i的排列的数目,所以将上述所有情况全部累加起来。
源代码:
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
for(int i = 1;i <= target;i++){
for(int num:nums){
if(i >= num){
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
}