动态规划——背包问题

背包问题 (Knapsack problem x ) 有很多种版本,常见的是以下三种:

**0-1 背包问题 (0-1 knapsack problem):**每种物品只有一个
**完全背包问题 (UKP, unbounded knapsack problem):**每种物品都有无限个可用
**多重背包问题 (BKP, bounded knapsack problem):**第 i 种物品有 n[i] 个可用

0-1背包

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

定义一个二维数组 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 背包的状态转移方程为:
公式

int knapsack(int W,int N,vector<int>& wt,vector<int>& val)
{	
	//base case初始化
	vector<vector<int>> dp(N+1,vector<int>(W+1,0));	//价值全为0
	
	for(int i=1;i<=N;i++)	//注意初始条件
	{
		int w = wt[i-1],v = value[i-1];
		for(int j=1;j<=W;j++){
			if(j-w<0)	dp[i][j] =	dp[i-1][j];	//无法装包,直接等于之前的包内价值
			else{
				dp[i][j] = max(dp[i-1][j],dp[i-1][j-w]+v;
			}
		}
		return dp[N][W];
	}
}

空间优化

在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,

在这里插入图片描述

因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
int knapsack(int W,int N,vector<int>& wt,vector<int>& val)
{	
	//base case初始化
	vector<int> dp(W+1,0);	//价值全为0
	
	for(int i=1;i<=N;i++)	//注意初始条件
	{
		int w = wt[i-1],v = value[i-1];
		for(int j=W;j>=1;--j){
			if(j>=w)	dp[j] = max(dp[j],dp[j-w]+v);
		}
		return dp[W];
	}
}

完全背包

  1. 零钱兑换1
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //if(amount==0)   return 0;
        vector<int>  dp(amount+1,amount+1);
        dp[0]=0;	//base case
        for(int i=0;i<amount+1;i++){
            for(int coin:coins){
                if(i-coin<0)    continue;
                dp[i]=min(dp[i],dp[i-coin]+1);	
            }
        }
        return dp[amount]==amount+1 ? -1 :dp[amount];
    }
};
  1. 零钱兑换2
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
       int n=coins.size();
       vector<vector<int>> dp(n+1,vector<int>(amount+1,0));
       dp[0][0] = 1;
       for(int i=1;i<=n;i++){
       		dp[i][0] = 1;
       		for(int j=1;j<amount+1;j++){
       			dp[i][j] = dp[i-1][j] + (j>=coins[i-1] ? dp[i-1][j-coins[i-1]] : 0);
       		}
       }
        return dp[n][amount];
    }
};

将二维 dp 数组压缩为一维,时间复杂度 O(N*amount),空间复杂度 O(amount)。

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        int N = coins.size();
        vector<long> dp(amount + 1, 0);
        dp[0] = 1;
        for (int i = 1; i <= N; ++i) {
            for (int j = coins[i - 1]; j <= amount; ++j) {
                dp[j] += dp[j - coins[i - 1]];
            }
        }
        return dp[amount];
    }
};

多重背包

  1. 字符构成最多的字符串
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        int S = strs.size();
        vector<vector<int> > dp(m + 1, vector<int>(n + 1, 0));
        for (int l = 0; l < S; ++l) {
            int zero = 0;
            int one = 0;
            //计算对应string所含0 1 个数
            for (int i = 0; i < strs[l].size(); ++i) {
                if (strs[l][i] == '0') ++zero;
                else ++one;
            }
            //查看选取/不选取该string对应的dp最大值
            for (int i = m; i >= zero; --i) {
                for (int j = n; j >= one; --j) {
                    dp[i][j] = max(dp[i][j], 1 + dp[i - zero][j - one]);
                }
            }
        }
        return dp[m][n];
    }
};
  1. 单词拆分
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        int n=s.size();
        vector<bool> dp(n+1,false);
        dp[0]=true;
        for(int i=1;i<=n;i++){
            for(string word:wordDict){
                int len = word.size();
                if(len<=i && s.substr(i-len,len)==word)
                    dp[i]=dp[i] || dp[i-len];
            }
        }
        return dp[n];
    }
};
  1. 组合总和
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) 
    {
        if(nums.size() == 0)
            return 0;

        vector<unsigned int> dp(target + 1, 0);
        dp[0] = 1;

        for(int i = 0; i <= target; i++)
        {
            for(int j = 0; j < nums.size(); j++)
            {
                if(i - nums[j] >= 0)
                    dp[i] += dp[i - nums[j]];
            }
        }
        return dp[target];
    }
};

子集背包

  1. 分割等和子集
bool canPartition(vector<int>& nums) {
    int sum = 0;
    for (int num : nums) sum += num;
    // 和为奇数时,不可能划分成两个和相等的集合
    if (sum % 2 != 0) return false;
    int n = nums.size();
    sum = sum / 2;
    vector<vector<bool>> 
        dp(n + 1, vector<bool>(sum + 1, false));
    // base case
    for (int i = 0; i <= n; i++)
        dp[i][0] = true;

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= sum; j++) {
            if (j - nums[i - 1] < 0) {
               // 背包容量不足,不能装入第 i 个物品
                dp[i][j] = dp[i - 1][j]; 
            } else {
                // 装入或不装入背包
                dp[i][j] = dp[i - 1][j] | dp[i - 1][j-nums[i-1]];
            }
        }
    }
    return dp[n][sum];
}

时间复杂度 O(n*sum),空间复杂度 O(sum)

//唯一需要注意的是 j 应该从后往前反向遍历,因为每个物品(或者说数字)只能用一次,以免之前的结果影响其他的结果。
class Solution {
public:
    bool canPartition(vector<int>& nums) {
		int sum = sum(nums);
		if(sum==0 || sum&1)	return false;	//剪枝,去掉不满足的奇数情况
		sum /=2;
		vector<bool> dp(nums.size()+1,false);
		dp[0] = true;	//初始化
		for(int i=1;i<nums.size();i++){
			for(int j=sum;j>=nums[i];j--){
				dp[j] = dp[j] || dp[j-nums[i]];
				if(dp[j])	return true;
			}
		}
		return false;
    }

	//求和函数
	int sum(vector<int>& nums){
		int sum=0;
		for(int i=0;i<nums.size();i++)
			sum += nums[i]; 
		return sum;
	}
};
  1. 目标和
    给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
    返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
    动态规划——背包问题_第1张图片
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        int sum = sumNumber(nums);
        if(sum<S || (sum+S)%2)   return 0;
        int arr = (sum+S)/2;
        vector<int> dp(arr+1,0);
        dp[0]=1;
        for(int num:nums){
            for(int j=arr;j>=num;j--){
                dp[j] = dp[j] + dp[j-num];
            }
        }
        return dp[arr];

    }

    int sumNumber(vector<int>& nums){
        int sum=0;
        for(int i=0;i<nums.size();i++){
            sum += nums[i];
        }
        return sum;
    }
};

递归方法

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int S) {
        return dfs(nums, S, 0);
    }
//前序遍历二叉树的实现,递归地+或-每个元素,到所有元素都遍历完成的时候,最后那个判断target是否等于零。
    int dfs(vector<int> &nums, uint target, int left) {	//返回值是对应目标和的个数
        if (target == 0 && left == nums.size()) return 1;	//终止条件
        if (left >= nums.size()) return 0;
        int ans = 0;
        ans += dfs(nums, target - nums[left], left + 1);
        ans += dfs(nums, target + nums[left], left + 1);
        return ans;
    }
};

你可能感兴趣的:(数据结构与算法总结)