动态规划

概念:

     动态规划DP(dynamic programming,这里的programming表示表格法)。它与分治算法类似,都是通过组合子问题的解来求解原问题。分治算法是将原问题分解为互不相交的子问题,递归的求解子问题,然后将解组合起来。

      动态规划则与之相反,它应用于求解子问题重叠的情况,也就是不同的子问题具有公共的子子问题。 这样,普通的递归方法会反复的求解那些公共子问题,因而浪费了时间,动态规划则是对公共子问题只求解一次,然后将其解保存在表格中,避免了不必要的重复工作


算法应用场景

      如果问题是求一个问题的最优解(通常是求最大值或者最小值),而且该问题能够分解成若干个子问题,并且子问题之间还有重叠的更小的子问题,那么就考虑用动态规划解决问题。

      应用动态规划之前要分析是否能把大问题分解成小问题,分解后的每个小问题也存在最优解。如果把每个小问题的最优解组合起来能够得到整个问题的最优解,那么我们可以应用动态规划解决。整体问题的最优解依赖各个子问题的最优解,并且子问题之间有重叠,一般从上往下分析问题并且从下往上求解问题这是动态规划问题的特点。

      动态规划通常用来解决最优化问题,这类问题通常有很多可行解,每个解法都有一个值,希望找到具有最优值(最小值或者最大值)的解。这样的解为一个最优解,有可能会有多个解都能得到最优值。一般从下往上的顺序先计算小问题的最优解先存储下来,再以此为基础求解大问题的最优解。

      动态规划的最优解特征:

示例:
 //当n=0,1,2等前几个值时,算法最优解的值可以简单推导获知,从而可用于求解n较大时的最优解的值。
 
       f(n)=max{f(n-i)+f(i)},      1<=i<=n;
       
       f(n)=max{f(n-i)*f(i)},      1<=i<=n;

算法步骤

设计动态规划算法的步骤: 最重要的是定义最优解的数学结构
1. 描述一个最优解的结构特征;
2. 递归定义最优解的值;
3. 计算最优解的值,通常采用自底向上的方式计算最优解的值;

4. 利用计算出的信息构造一个最优解。

  • 第1-3步是动态规划求解问题的基础。如果仅需要一个最优解的值,而非最优解本身,则第4步可以忽略。如果需要求得一个最优解,则有时要在第3步的计算中记录一些附加信息,以便用来构造一个最优解。

实现方法

      第一种,带备忘的自顶向下法。过程会保存每个子问题的解(通常保存在一个数组或者散列表中)。当需要一个子问题的解的时候,先查看是否已经保存过了,如果是,则直接使用即可。否则,按常规的递归方式计算子问题。所以称为带备忘的,因为它记住了之前已经计算出的结果。

      第二种,自底向上法,将一个问题分成规模更小的子问题,从小到大进行求解,当求解至原问题时,所需的前提子问题的值都已求解完毕。一般以一个二维数组辅之两个嵌套for循环进行逻辑处理的形式求解。


算法示例

一. 钢条切割问题: 公司购买长钢条,将其切割为短钢条出售。切割工序本身没有成本支出。公司管理层希望知道最佳的切割方案。假定我们知道公司出售一段长为i英寸的钢条的价格为pi(i=1,2,…,单位为美元)。钢条的长度均为整英寸。钢条切割问题是这样的:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,…n),求切割钢条方案,使得销售收益r最大。注意,如果长度为n英寸的钢条的价格p足够大,最优解可能就是完全不需要切割。
      输入:钢条的每英寸的价格表和钢条的总长度。 输出:最大的收益值。分析得最优解结构f(n)=max(p[i]+f(n-i)); 1<=i<=n

#include<iostream>
#include<string>
#include<vector> 
using namespace std;
int getReward(int *p, int length)
{
	int *r = (int*)malloc(sizeof(int)*(length+1));//r[i]为钢条长度为i时的最大收益,p[i]为钢条长度为i时的售价
	r[0] = 0;
	r[1] = p[1];

	//最优解结构f(n)=max(p[i]+f(n-i)); 1<=i<=n

	for (int i = 2; i <= length; i++)
	{
		int temp = 0;
		for (int j = i; j >= 1; j--) 
		{
			if ((p[j] + r[i - j])>temp)
			{
				temp = p[j] + r[i - j];
			}

		}
		r[i] = temp;
	 }
	return r[length];
}
int main()
{
	int p[11] = { 0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30};;//p[i]为钢条长度为i时的售价
	cout << getReward(p, 10) << endl;
	system("pause");
	return 0;
}

二、剪绳子问题: 给你一根长度为n绳子,请把绳子剪成m段(m、n都是整数,n>1并且m>1)。每段的绳子的长度记为k[0]、k[1]、……、k[m]。k[0] * k[1]*…*k[m]可能的最大乘积是多少?例如当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到最大的乘积18。

分析得最优解结构f(n) = max(f(i)*f(n - i)); 1<=i<=n/2

#include<iostream>
#include<string>
#include<vector> 
using namespace std;

//最优解求解式  f(n) = max(f(i)*f(n - i));    1<=i<=n/2
int getReward(int n)
{
	if (n < 2)
		return 0;
	if (n == 2)
		return 1;
	if (n == 3)
		return 2;
	int *r = new int[n+1];
	r[0] = 0;
	r[1] = 1;
	r[2] = 2;
	r[3] = 3;
	int temp = 0;
	for (int i = 4; i <= n; i++)
	{
		temp = 0;
		for (int j = 1; j <= i / 2; j++)
		{
			int value = (r[j] * r[i - j]);
			if (value>temp)
			{
				temp = value;
			}

		}
		r[i] = temp;
	 }
	temp = r[n];
	delete[] r;
	return temp;
}
int main()
{
	
	cout << getReward(8) << endl;
	system("pause");
	return 0;
}

三、最长公共子序列问题:
3.1:给定两个字符串,求解这两个字符串的最长公共子序列(Longest Common Sequence),并且打印。比如字符串1:BDCABA;字符串2:ABCBDAB,则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA
(1)字符子串:指的是字符串中连续的n个字符,如abcdefg中,ab,cde,fg等都属于它的字串。
(2)字符子序列:指的是字符串中不一定连续但先后顺序一致的n个字符,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。
  (3) 公共子序列:如果序列C既是序列A的子序列,同时也是序列B的子序列,则称它为序列A和序列B的公共子序列。如对序列 1,3,5,4,2,6,8,7和序列 1,4,8,6,7,5 来说,序列1,8,7是它们的一个公共子序列。最长公共子序列(LCS):就是A和B的公共子序列中长度最长的(包含元素最多的),最长公共子序列(LCS)也不一定唯一,但是长度一定。


在这里插入图片描述
在这里插入图片描述

用表b中的信息可以构建出X和Y的一个LCS。从b[m,n]开始,沿着箭头的方向追踪,当箭头是斜上的时候,表示Xi=Yj;是LCS中的一个元素。
在这里插入图片描述

#include<iostream>
#include<string>
#include<map> 
using namespace std;
//LCS 是 Longest Common Subsequence 
const int N = 1000;


void Print(int i, int j, int**b, string x)
{
	if (i == 0 || j == 0)///递归终止条件
	{
		return;
	}
	if (b[i][j] == 1)
	{
		Print(i - 1, j - 1, b, x);
		printf("%c", x[i - 1]);
	}
	else if (b[i][j] == 2)
	{
		Print(i - 1, j, b, x);
	}
	else if (b[i][j] == 3)
	{
		Print(i, j - 1, b, x);
	}
}
void getLCS(string p1, string p2){
	if (p1 == "" || p2 == "")
		return;
	const int len1 = p1.size();
	cout << p1.size()<<endl;
	cout << p2.size()<<endl;
	const int len2 = p2.size();
	int **dp = new int*[len1 + 1];  //申请资源,默认各元素初始化为0
	for (int i = 0; i<len1 + 1; i++)
		dp[i] = new int[len2 + 1];
	int **b = new int*[len1 + 1];  //申请资源,默认各元素初始化为0
	for (int i = 0; i<len1 + 1; i++)
		b[i] = new int[len2 + 1];
	for (int i = 0; i < len1 + 1;i++)
	  for (int j = 0; j < len2 + 1; j++)
		dp[i][j] = 0;


	for (int i = 1; i <= len1; i++){

		for (int j = 1; j <= len2; j++){   
			if (p1[i - 1] == p2[j - 1]){  //为了比较配p1[0] p2[0]

				dp[i][j] = dp[i - 1][j - 1] + 1;
				b[i][j] = 1; //左上
			}
			else if (dp[i - 1][j]>dp[i][j - 1]){
				b[i][j] = 2;//左
				dp[i][j] = dp[i - 1][j];
			}
			else{
				b[i][j] = 3;
				dp[i][j] = dp[i][j - 1];//上
			}
		}
	}
	cout << dp[len1][len2] << endl;
	Print(len1, len2, b, p1);
}


int main()
{
	string a = "abcde";
	string b = "ace";
	getLCS(a, b);
	system("pause");
	return 0;
}

3.2:若只统计最长公共子序列的长度:

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int n = text1.length(), m = text2.length();
        vector<vector<int> > f(n+1, vector<int>(m+1, 0));
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                f[i][j] = max(f[i-1][j], f[i][j-1]);
                if(text1[i-1] == text2[j-1])
                    f[i][j] = max(f[i][j], f[i-1][j-1]+1);
            }
        }
        return f[n][m];
    }
};

四、宝石问题:

问题:4.1 小 Q 是一个宝石爱好者。这一天,小 Q 来到了宝石古董店,店家觉得小 Q 是个宝石行家,于是决定和小 Q 玩一个游戏。游戏是这样的,一共有 块宝石,每块宝石在小 Q 心中都有其对应的价值。注意,由于某些宝石质量过于差劲,因此存在只有店家倒贴钱,小 Q 才愿意带走的宝石,即价值可以为负数。​小 Q 可以免费带走一个连续区间中的宝石,比如区间 [1.3]或区间[2,4]中的宝石。请问小 Q 能带走的最大价值是多少?

在这里插入图片描述
问题:4.2 连续子数组的最大和 输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。例如输入的数组为{1,-2,3,10,-4,7,2,-5},和最大的子数组为{3,10,-4,7,2},因此输出为该子数组的和18。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int max = nums[0];
        int tmp = 0;
        for (int i = 0; i < nums.size(); ++i) {
            tmp += nums[i];
            if (tmp > max)
                max = tmp;
            if (tmp < 0)
                tmp = 0;
        }
        return max;
    }
};

五、三步问题:

问题: 三步问题。有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模 1000000007。
在这里插入图片描述

class Solution {
public:
    vector<int> f;
    int mod = 1000000007;
    int waysToStep(int n) {
        f.resize(n+1);
        f[0] = 1;
        for(int i = 1; i <= n; i++) {
            f[i] = f[i-1];
            if(i >= 2) f[i] = (f[i] + f[i-2]) % mod;
            if(i >= 3) f[i] = (f[i] + f[i-3]) % mod;
        }
        return f[n];
    }
};

六、乘积最大子数组:

问题: 给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

示例 : 输入: [2,3,-2,4] 输出: 6 ; 解释: 子数组 [2,3] 有最大乘积 6。

在这里插入图片描述

class Solution {
public:
    vector<int> maxn, minn;
    int maxProduct(vector<int>& nums) {
        int n = nums.size(), ans = nums[0];
        maxn.resize(n);
        minn.resize(n);
        maxn[0] = minn[0] = nums[0];
        for (int i = 1; i < nums.size(); ++i) {
            if(nums[i] > 0) {
                maxn[i] = max(nums[i], maxn[i - 1] * nums[i]);
                minn[i] = min(nums[i], minn[i - 1] * nums[i]);
            }
            else {
                maxn[i] = max(nums[i], minn[i - 1] * nums[i]);
                minn[i] = min(nums[i], maxn[i - 1] * nums[i]);
            }
            ans = max(ans, maxn[i]);
        }
        return ans;
    }
};

七、最长上升子序列(LIS):

问题: 给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例​: 输入: [10,9,2,5,3,7,101,18], 输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

在这里插入图片描述

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int sz = nums.size(), ans = 0;
        vector<int> f(sz, 0);
        for(int i = 0; i < sz; i++) {
            int tmp = 1;
            for(int j = i-1; j >= 0; j--) {
                if(nums[i] > nums[j])
                    tmp = max(tmp, f[j]+1);
            }  
            f[i] = tmp;
            ans = max(ans, tmp);
        }
        return ans;
    }
};

八、打家劫舍:

问题: 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你「不触动警报装置的情况下」,一夜之内能够偷窃到的最高金额。

示例 : 输入:[1,2,3,1], 输出:4。解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

在这里插入图片描述

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 0) return 0;
        vector<int> f(n, 0);
        for(int i = 0; i < n; i++) {
            f[i] = nums[i];
            if(i >= 2) f[i] = max(f[i], f[i-2]+nums[i]);
            if(i >= 1) f[i] = max(f[i], f[i-1]);
        }
        return f[n-1];
    }
};

九、俄罗斯套娃信封问题:

问题: 给定一些标记了宽度和高度的信封,宽度和高度以整数对形式 (w, h) 出现。当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。请计算最多能有多少个信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。

示例:输入: envelopes = [[5,4],[6,4],[6,7],[2,3]]。输出: 3 。解释: 最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
在这里插入图片描述

class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        sort(envelopes.begin(), envelopes.end());
        int n = envelopes.size(), ans = 0;
        vector<int> f(n, 0);
        for(int i = 0; i < n; i++) {
            int tmp = 0;
            for(int j = 0; j < i; j++) {
                if(envelopes[j][1] < envelopes[i][1] && envelopes[j][0] < envelopes[i][0])
                    tmp = max(tmp, f[j]);
            }
            f[i] = tmp + 1;
            ans = max(f[i], ans);
        }
        return ans;
    }
};

十、 把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 S。输入 n,打印出S的所有可能的值出现的概率。

//func:获取n个骰子指定点数和出现的次数
//para:n:骰子个数;sum:指定的点数和
//return:点数和为sum的排列数
/*****************************/
int getNSumCount(int n, int sum)
{
	if(n<1||sum<n||sum>6*n)
	{
		return 0;
	}
	if(n==1)
	{
		return  1;
	}
	int resCount=0;
	resCount=getNSumCount(n-1,sum-1)+getNSumCount(n-1,sum-2)+
			 getNSumCount(n-1,sum-3)+getNSumCount(n-1,sum-4)+
			 getNSumCount(n-1,sum-5)+getNSumCount(n-1,sum-6);
	return resCount;
}


十一、数字翻译为字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", “bwfi”, “bczi”, “mcfi"和"mzi”
在这里插入图片描述

class Solution {
public:
    int getTranslationCount(string s) {
        int len = s.size();
        if(len == 1) return 1;//如果 只有一个字符 只有一种翻译方法
        vector<int>dp(len,0);
        dp[1] = 1;//如果 只有一个字符 只有一种翻译方法
        dp[0] = 1;
        for(int i = 2;i<=len;i++)
        {
            if(s[i - 2] == '0'||(s[i -2] - '0' )*10 + s[i - 1] - '0' >= 26)//特判 出现05这样的情况 还是只有一种翻译方法
            {
                dp[i] = dp[i - 1];
            }
            else dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[len];
    }
};

换钱的最少货币数,货币可重复使用

给定数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种货币都可以使用任意张,再给定一个整数aim代表要找的钱数,求组成aim的最少货币数。
例:
arr=[5, 2, 3], aim=20, 返回4
arr=[5, 2, 3], aim=0, 返回0
arr=[3, 5], aim=2, 返回-1

分析:动态规划的实质就是在计算过程中,根据动态规划表计算下一个目标值,并更新动态规划表在本题中,构造这样二维动态规划表,dp[i][j],其中,i j的含义如下:

  • i: 代表可以使用的货币种类为 arr[0…i]
  • j: 代表需要兑换的面值数,其取值范围为[0…aim],因此实现时,二维数组的列数应该为aim + 1
    构造过程:

构造边界值。边界值是更新规划表的起始值,也是很容易犯错的地方,需要谨慎设置起始边界值。

  • dp[0…N-1][0]:规划表的第一列值,表示当需要兑换0元时,需要的货币数,显然货币数为0,直接设置为0.
  • dp[0][0…aim]:规划表的第一行值,表示只使用arr[0]一种货币兑换[0…aim]时,需要的货币数,因此,只要tmp_aim可以被arr[0]整除,返回整除后的数,即为对应的值。在下面的实现中,我们使用了更为通用的方法来得到规划表的第一行的值,可以根据下面的算法,理解一下实现中的计算方法。
  • 对于dp[i][j],更新的依据有两个值,一个是“不使用当前种类的货币时,组成总数的j的最小方法,即dp[i-1][j]”,另外一个是“使用并且仅使用一张当前种类的货币时,组成总数的j的最小方法,即 dp[i][j-arr[i]] + 1”,取这两个值中的最小值即为dp[i][j]的值。
class Solution
{
public:
    int ExchangeMoney(std::vector<uint32_t>& arr, uint32_t aim);
};

int Solution::ExchangeMoney(std::vector<uint32_t>& arr, uint32_t aim)
{
    // fix: precheck
    if (arr.size() == 0 || aim == 0)
    {
        return 0;
    }
    int arrSize = arr.size();
    int dp[arrSize][aim + 1];
    for (uint32_t lineIndex = 0; lineIndex < arrSize; ++lineIndex)
    {
        dp[lineIndex][0] = 0;
    }
    for (uint32_t rowIndex = 1; rowIndex < aim + 1; ++rowIndex)
    {
        // 动态初始化边界值的方法
        if (int(rowIndex - arr[0]) >= 0 && dp[0][rowIndex - arr[0]] != UINT32_MAX)
        {
            dp[0][rowIndex] = dp[0][rowIndex - arr[0]] + 1;
        }
        else
        {
            dp[0][rowIndex] = UINT32_MAX;
        }
    }
    for (uint32_t lineIndex = 1; lineIndex < arrSize; ++lineIndex)
    {
        for (uint32_t rowIndex = 1; rowIndex < aim + 1; ++rowIndex)
        {
            int subCurArrValue = rowIndex - arr[lineIndex];
            uint32_t upValue = dp[lineIndex - 1][rowIndex];
            uint32_t leftValue = UINT32_MAX;
            if (subCurArrValue >= 0 && dp[lineIndex][subCurArrValue] != UINT32_MAX)
            {
                leftValue = dp[lineIndex][subCurArrValue] + 1;
            }
            dp[lineIndex][rowIndex] = leftValue < upValue ? leftValue : upValue;
        }
    }
    return dp[arrSize - 1][aim] == UINT32_MAX ? -1 : dp[arrSize - 1][aim];
}

矩阵中的最长递增路径

给定一个整数矩阵,找出最长递增路径的长度。

对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。

示例 1:

输入: nums =
[
[9,9,4],
[6,6,8],
[2,1,1]
]
输出: 4
解释: 最长递增路径为 [1, 2, 6, 9]。

动态规划思路,用二维数组dp记忆位置(i,j)的最大递增序列长度。

dp[x][y] = max(dp[x][y], dfs(matrix,x+dx,y+dy,dp)) if matrix[x][y] < matrx[x+dx][y+dy]

/*
 * 动态规划
 * 利用matrix[i'][j'] > matrix[i][j]且dp[i'][j']已经计算过的元素
 */
class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return 0;

        int rows = matrix.size(), cols = matrix[0].size();
        vector<vector<int>> dp(rows, vector<int>(cols,0));  // 矩阵记忆位置 (x, y) 的最大递增序列长度

        int maxLen = 0;
        for (int i = 0; i < rows; ++i) {
            for (int j = 0; j < cols; ++j) {
                maxLen = max(maxLen, dfs(matrix, i, j, dp));
            }
        }
        return maxLen;
    }

    int dfs(vector<vector<int>> &matrix, int x, int y, vector<vector<int>> &dp){
        if (dp[x][y]) return dp[x][y];

        vector<vector<int>> dirs = {{-1,0},{1,0},{0,-1},{0,1}}; // 上下左右四个方向
        dp[x][y] = 1;       // 该坐标访问,长度至少为1
        for (auto dir : dirs){
            int dx = dir[0], dy = dir[1];
            if ( x + dx<0 || x + dx>= matrix.size() || y+dy <0 || y + dy >= matrix[0].size())
                continue;   // 坐标出界
            if (matrix[x][y] <= matrix[x+dx][y+dy])
                continue;
            dp[x][y] = max(dp[x][y], dfs(matrix,x+dx,y+dy,dp) + 1);  // 回溯
        }
        return dp[x][y];
    }
};



01背包问题

物品集合s={1,2,3,4,…,n},物品i的重量为wi,其价值为vi,背包的容量(最大载重量)为W,如何装使物品价值最大。(物品不能分割)

/*
j为bagSize ,w[i]为货物i的重量,dp[i][j] 表示 在面对第 i 件物品,且背包容量为  j 时所能获得的最大价值 

if(j<w[i])
   dp[i][j]=dp[i-1][j];     
else
   dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
*/

//动态规划求解
int zero_one_pack(int total_weight, int w[], int v[], int flag[], int n) {
  int c[MAX_NUM+1][MAX_WEIGHT+1] = {0}; //c[i][j]表示前i个物体放入容量为j的背包获得的最大价值
  // 状态转移方程:c[i][j] = max{c[i-1][j], c[i-1][j-w[i]]+v[i]}
  //状态转移方程的解释:第i件物品要么放,要么不放
  //                 如果第i件物品不放的话,就相当于求前i-1件物体放入容量为j的背包获得的最大价值
  //                 如果第i件物品放进去的话,就相当于求前i-1件物体放入容量为j-w[i]的背包获得的最大价值
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= total_weight; j++) {
      if (w[i] > j) {
        // 说明第i件物品大于背包的重量,放不进去
        c[i][j] = c[i-1][j];
      } else {
        //说明第i件物品的重量小于背包的重量,所以可以选择第i件物品放还是不放
          if (c[i-1][j] > v[i]+c[i-1][j-w[i]]) {
            c[i][j] = c[i-1][j];
          }
          else {
            c[i][j] =  v[i] + c[i-1][j-w[i]];
          }
      }
    }
  }

  //下面求解哪个物品应该放进背包
  int i = n, j = total_weight;
  while (c[i][j] != 0) {
    if (c[i-1][j-w[i]]+v[i] == c[i][j]) {
      // 如果第i个物体在背包,那么显然去掉这个物品之后,前面i-1个物体在重量为j-w[i]的背包下价值是最大的
      flag[i] = 1;
      j -= w[i];
      //--i; 移到外面去
    }--i;
  }
  return c[n][total_weight];
}

分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100. 数组的大小不会超过 200
示例 :
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].

思路: 给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?那么对于这个问题,我们可以先对集合求和,得出sum,把问题转化为背包问题:给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?

根据这个定义,我们想求的最终答案就是dp[N][sum/2],base case 就是dp[…][0] = true和dp[0][…] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。

dp[i][j] = x表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。

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];
}

目标和

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

输入:nums: [1, 1, 1, 1, 1], S: 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
记数组的元素和为sum,添加- 号的元素之和为 neg,则其余添加+ 的元素之和为 sum−neg,得到的表达式的结果(sum−neg)−neg=sum−2⋅neg=target. 由于数组 nums 中的元素都是非负整数,neg 也必须是非负整数,所以上式成立的前提是 sum−target 是非负偶数。若不符合该条件可直接返回 0。若上式成立,问题转化成在数组 nums 中选取若干元素,使得这些元素之和等于 neg,计算选取元素的方案数。我们可以使用动态规划的方法求解。

在这里插入图片描述

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int& num : nums) {
            sum += num;
        }
        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int n = nums.size(), neg = diff / 2;
        vector<vector<int>> dp(n + 1, vector<int>(neg + 1));
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= num) {
                    dp[i][j] += dp[i - 1][j - num];
                }
            }
        }
        return dp[n][neg];
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值