算法学习------LeetCode力扣动态规划篇3
494. 目标和
描述
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
提示
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
代码解析
这道题根本还是回到了在数组中找到一个集合,使得该集合与其余部分之差为target,通过公式推导:
我们可以知道 该集合的值为:(sum-target)/2;
回溯法
目的是找到和为**(sum-target)/2** 的种类
cpp
class Solution {
public:
int result = 0;
void backtracking(vector<int>& nums, int target ,int deep ,int sum)
{
if(sum > target)return;
if(sum == target)result++;
if(deep == nums.size()) return;
//从任一点开始
for(int i= deep ; i < nums.size() ;i++)
{
backtracking(nums,target , i+1 , sum + nums[i]);
}
return;
}
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0 , diff = 0;
for(auto it:nums) sum += it;
diff = sum - target;
if( diff<0 || diff%2==1 ) return 0;
//回溯找diff/2
backtracking(nums,diff/2,0 ,0);
return result;
}
};
动态规划
- 背包定义: dp[i][j] , i是使用0-i的元素,j是背包容量,dp[i][j]是使用这么多个元素恰好凑成j的情况
- 初始化:dp[0][0]为1,装满容量为0的背包,有一种方法。dp[0][j],看第一个元素的大小情况,进行赋值1(如果第一个元素为0.则dp[0][0]应该为2),其他层的根据第一层改变.
- 遍历顺序:从上往下
- 递推公式: dp[i][j]=dp[i-1][j](不需要num[i]就能够凑出j的情况)+dp[i-1][j-nums[i]];(需要num[i]凑出j空间的情况) 最终就能实现,从0-i元素当中组合,得到target的所有情况。
cpp
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0 , diff = 0;
for(auto it:nums) sum += it;
diff = sum - target;
if( diff<0 || diff%2==1 ) return 0;
vector<vector<int>> dp( nums.size() , vector<int>(diff/2 + 1 , 0) ) ;
dp[0][0] = 1;
for(int j=0 ; j<(diff)/2+1 ; j++)
if(j==nums[0]) dp[0][j] += 1;
for(int i=1 ; i<nums.size() ;i++)
{
for(int j=0 ; j<(diff)/2+1 ; j++)
{
if(j>=nums[i])
dp[i][j] = dp[i-1][j] + dp[i-1][ j - nums[i]] ;
else
dp[i][j] = dp[i-1][j];
}
}
return dp[nums.size()-1][(diff)/2];
}
};
474. 一和零
描述
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
提示
1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 '0' 和 '1' 组成
1 <= m, n <= 100
代码解析
动态规划(01背包,三级数组)
和经典的背包问题只有一种容量不同,这道题有两种容量,即选取的字符串子集中的 0 和 1 的数量上限。
经典的背包问题可以使用二维动态规划求解,两个维度分别是物品和容量。这道题有两种容量,因此需要使用三维动态规划求解,三个维度分别是字符串、0的容量和 1 的容量。
定义三维数组dp,其中dp[i][j][k] 表示在前 i 个字符串中,使用 j 个 0 和 k 个 1 的情况下最多可以得到的字符串数量。
当 0 和 1 的容量分别是 j 和 k 时,考虑以下两种情况:
-
如果 j< zeros 或 k<ones,则不能选第 i 个字符串,此时有 dp[i][j][k] = dp[i−1][j][k];
-
如果 j ≥ zeros 且 k ≥ones,则如果不选第 i个字符串,有dp[i][j][k]=dp[i−1][j][k],如果选第 i个字符串,有 dp[i][j][k]=dp[i−1][j−zeros][k−ones]+1,dp[i][j][k] 的值应取上面两项中的最大值。
因此状态转移方程如下:
cpp
class Solution {
public:
int find_0(string s1)
{
int num = 0;
for(auto it:s1) if(it == '0') num++;
return num;
}
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<vector<int>>> dp( strs.size() ,vector<vector<int>>( m+1 ,vector<int>( n+1,0) ));
int num_0 = 0,num_1 = 0;
num_0 = find_0(strs[0]);
num_1 = strs[0].size() - num_0;
for(int j=0 ; j<= m ;j++)
{
for(int k=0 ; k<= n ;k++)
{
if( j>= num_0 && k>= num_1)
dp[0][j][k] = 1;
}
}
for(int i=1 ; i<strs.size() ; i++)
{
num_0 = find_0(strs[i]);
num_1 = strs[i].size() - num_0;
for(int j=0 ; j<=m ;j++)
{
for(int k=0 ; k<=n ;k++)
{
if( j>= num_0 && k>= num_1)
dp[i][j][k] = max( dp[i-1][j][k], dp[i-1][j - num_0][k - num_1] + 1);
else
dp[i][j][k] = dp[i-1][j][k];
}
}
}
int max_num = 0;
for(int i=0 ; i<strs.size() ; i++)
{
if(dp[i][m][n] > max_num) max_num = dp[i][m][n];
// cout<<dp[i][m][n]<<' ';
}
return max_num ;
}
};
动态规划(滑动数组,二级数组)
由于dp[i][][] 的每个元素值的计算只和dp[i−1][][] 的元素值有关,因此可以使用滚动数组的方式,去掉 dp 的第一个维度,将空间复杂度优化到 O(mn)O(mn)。
实现时,内层循环需采用倒序遍历的方式,这种方式保证转移来的是 dp[i−1][][] 中的元素值。
cpp
class Solution {
public:
int find_0(string s1)
{
int num = 0;
for(auto it:s1) if(it == '0') num++;
return num;
}
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> dp( m+1 ,vector<int>(n+1,0));
int num_0 = 0,num_1 = 0;
for(int i=0 ; i<strs.size() ; i++)
{
num_0 = find_0(strs[i]);
num_1 = strs[i].size() - num_0;
for(int j=m ; j>=num_0;j--)
{
for(int k=n ; k>=num_1 ;k--)
{
dp[j][k] = max( dp[j][k], dp[j - num_0][k - num_1] + 1);
}
}
}
return dp[m][n] ;
}
};
518. 零钱兑换 II
描述
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。
示例 3:
输入:amount = 10, coins = [10]
输出:1
提示
1 <= coins.length <= 300
1 <= coins[i] <= 5000
coins 中的所有值 互不相同
0 <= amount <= 5000
代码解析
完全背包
一看到钱币数量不限,就知道这是一个完全背包。
dp[j]:凑成总金额j的货币组合数为dp[j]
dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。
所以递推公式:dp[j] += dp[j - coins[i]];
首先dp[0]一定要为1,dp[0] = 1 是 递归公式的基础。
从dp[i]的含义上来讲就是,凑成总金额0的货币组合数为1。
cpp
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp( amount+1 , 0 );
dp[0] = 1 ;
for(int i=0 ; i < coins.size() ; i++)
{
for(int j=0 ; j<=amount ; j++ )
{
if( j>=coins[i] )
dp[j] += dp[j-coins[i]] ;
else
dp[j] = dp[j];
}
}
return dp[amount];
}
};