C++学习/复习补充记录 --- 动态规划

4 篇文章 0 订阅

动态规划算法:

每个决策都进行求解,放在表格里,最后选择最优解

动态规划,英文:Dynamic Programming,简称DP

如果某一问题很多重叠子问题,使用动态规划最有效的。


所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,

动规思路五部曲:

1、确定dp数组及其下标意义。

2、确定递推公式。

3、确定dp数组如何初始化。

4、确定遍历顺序。

5、举例推导dp数组。

1、

1.1 斐波那契数

 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

class Solution {
public:
	int fib(int n) {
		if (n == 0) return 0;
		if (n == 1) return 1;
		vector<int> dp(n + 1);//dp[i]代表第i个数时的值
		dp[0] = 0;//初始化
		dp[1] = 1;//初始化
		for (int i = 2; i <= n; i++) {
			dp[i] = dp[i - 1] + dp[i - 2];//状态转移方程
		}
		return dp[n];
	}
};

1.2 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。可转到 ==》 2.2.3 爬楼梯(进阶版)

class Solution {
public:
	int climbStairs(int n) {
		/*
		dp[i-1],上i-1层楼梯时,有dp[i-1]种方法,那此时再上一层就是 dp[i] 了
		dp[i-2],上i-2层楼梯时,有dp[i-2]种方法,那此时再上两层就是 dp[i] 了
		因此:递推公式为:dp[i] = dp[i-1] + dp[i-2]
		*/
		if (n <= 1) return 1;//防止空指针
		vector<int> dp(n + 1);//dp[i]代表爬第i个台阶时的方案数
		dp[0] = 1;//在第0个台阶时,不动也是一种方法
		dp[1] = 1;
		for (int i = 2; i <= n; i++) {
			dp[i] = dp[i - 1] + dp[i - 2];
		}
		return dp[n];
	}
};

1.3 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

class Solution {
public:
	int minCostClimbingStairs(vector<int>& cost) {
		/*
		将cost的size全走完,再走一步则到达终点,求走到终点的最少总花费。

		注意!!!
		从第0阶出发时最少花费为cost[0]
		从第1阶出发时最少花费最便宜的那阶花费
		但是!!!
		走到第0阶时和走到第1阶时并不需要任何花费,因此0和1都初始化为0!!!!
		*/
		if (cost.size() < 0) return 0;
		int n = cost.size();
		vector<int> dp(n + 1, 0);//dp[i]表示走到第i阶时所花费的最少费用
		dp[0] = 0;//走到第0阶时 并不需要任何花费
		dp[1] = 0;//      1
		for (int i = 2; i <= n; i++) {
			dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
		}
		//for (int i : dp) cout << i << endl;
		return dp[n];
	}
};

优化空间复杂度版本:

dp[i]就是由前两位推出来的,因此可以不用dp数组,而是直接只用三个变量搞定。

class Solution {
public:
	int minCostClimbingStairs(vector<int>& cost) {
		int dp0 = 0;
		int dp1 = 0;
		for (int i = 2; i <= cost.size(); i++) {
			int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
			dp0 = dp1;
			dp1 = dpi;
		}
		return dp1;
	}
};

1.4 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

动态规划解法:

//法一:
class Solution {
public:
	int uniquePaths(int m, int n) {
		vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
		//将左边缘及上边缘全部元素都直接初始化为1,因为路径都只有一条。
		for (int i = 0; i < m; i++) {
			for (int j = 0; j <n; j++) {
				if (i == 0) dp[i][j] = 1;//相当于 ==》(dp[0][j] = 1;//初始化)
				else if (j == 0) dp[i][j] = 1;//相当于 ==》(dp[i][0] = 1;//初始化)
				else if (i >= 1 && j >= 1) dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
			}
		}
		return dp[m - 1][n - 1];
	}
};

//法二:
class Solution {
public:
	int uniquePaths(int m, int n) {
		vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
		for (int i = 0; i < m; i++) dp[i][0] = 1;//初始化左边缘
		for (int j = 0; j < n; j++) dp[0][j] = 1;//初始化上边缘
		for (int i = 1; i < m; i++) {
			for (int j = 1; j < n; j++) {
				//每个位置的方案数都是其上一个方案数+其左一格的方案数之和
				dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
			}
		}
		return dp[m - 1][n - 1];//返回右下角的方案数
	}
};

深搜解法:(转化为求二叉树叶子节点的个数) 但力扣上提交会超时。

数论解法:

1.5 

2、 背包问题(动态规划)

教程参考链接:

动态规划之背包问题系列

动态规划:01背包理论基础

01:物品只能用一次,常是外层遍历物品,内层遍历价值(容量),遍历价值时需逆向(--)

完全:物品可用多次,遍历价值时需正向(++)

           无顺序要求的组合外层物品,内层价值;(常用的)

           有顺序要求的排列外层价值,内层物品

dp[j]表示背包内物品总价值为j时可以获得的最大价值, 0<=j<=W

2.1 01背包(每个物品只能用一次)-逆向

二维dp数组

dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i最大价值此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

 一维dp数组

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

所以递归公式为:(求限定容量时的最大价值)

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

 (求限定容量时的最大方案数,即组合类的最大方案数)实际应用看2.1.3目标和

dp[0] = 1;//求方案数时一定记得要先初始化第0号元素(啥都不放也是一种方案)
dp[j] += dp[j - nums[i]];

2.1.1 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

是求 给定背包容量,能不能装满这个背包。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
		int sum = 0;
		for (int n : nums) sum += n;
		if (sum % 2 == 1) return false;//元素和必须是奇数
		int w = sum / 2;//期望背包容量,此题中同时也是背包价值

		vector<int> dp(w + 1, 0);//初始化dp数组,往里放进w+1个0
		for (int i = 0; i < nums.size(); i++) {
			for (int j = w; j >= nums[i]; j--) {
				dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);//获得每个容量对应的最大价值
			}
		}
		if (dp[w] == w) return true;//若容量为元素和一半时,其价值刚好为元素和一半
		else return false;
    }
};

2.1.2 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。

是求 给定背包容量,尽可能装,最多能装多少 

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        /*
		就是求出两堆石头的最小差值
		即 将全部石头分成尽可能相等的两堆,求其差值并返回
		*/
		int sum = 0;
		for (int s : stones) sum += s;
		int w = sum / 2;

		vector<int> dp(w + 1, 0);
		for (int i = 0; i < stones.size(); i++) {
			for (int j = w; j >= stones[i]; j--) {
				dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
			}
		}
		return abs(sum - dp[w] - dp[w]);
    }
};

2.1.3 目标和

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

是求 给定背包容量,装满背包有多少种方法。

class Solution {
public:
	int findTargetSumWays(vector<int>& nums, int target) {
		/*
		思路一:
		转换为子集求和问题:

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

		即 找到nums的一个子集 P,使得sum(P) = (target + sum(nums)) / 2
		且 target + sum(nums)必须是偶数,否则输出为0

		-------------------------------------------------------------

		思路二:
		假设加法的总和为x,那么减法对应的总和就是sum - x。
		所以我们要求的是 x - (sum - x) = target
		x = (target + sum) / 2

		此时问题就转化为,装满容量为x的背包,有几种方法。
		这里的x,就是bagSize,也就是我们后面要求的背包容量w。
		*/
		int sum = 0;
		for (int n : nums) sum += n;
        //注意这里的target绝对值,一个是会导致后面的vectoe初始化下标越界,一个是要求的差值大于所有元素和
		if ((target + sum) % 2 == 1 || sum < abs(target)) return 0;
		int w = (target + sum) / 2;

		vector<int> dp(w + 1, 0);
        //容量为0时,啥都不放也是一种方案(容量为0的背包,拿第0件物品,有1种填满方式,那就是不填,因此第0号元素初始化为1)
		dp[0] = 1;
		/*
		此代码是一维dp,如果是二维dp的话:
		dp[0][0] = 1;//意义与一维dp中的 dp[0] = 1;同理
		但 第一列也不能全部初始化为1,原因是 题目中物品的重量可以为0,所以容量为0的背包,不止一种放入方式。
		*/
		for (int i = 0; i < nums.size(); i++) {
			for (int j = w; j >= nums[i]; j--) {
				/*
				遍历到i物品,此时dp[j]有两种来源:
				不放入i物品,填满容量为j的背包的方式为,上一层的值:dp[j]
				放入i物品,填满容量为j的背包的方式为,上一层减掉物品重量的值:dp[j - nums[i]]

				不放第i件物品时,dp[j] = dp[j - 1]
				因此,以下两条式子等价
				dp[j] = dp[j - 1] + dp[j - nums[i]];
				dp[j] = dp[j] + dp[j - nums[i]];

				再简化就可以写成 dp[j] += dp[j - nums[i]];
				*/
				dp[j] += dp[j - nums[i]];//dp中每个元素都是背包容量为j时,可放物品的方案数
			}
		}
		return dp[w];
	}
};

2.1.4  一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

是求 给定背包容量,装满背包最多有多少个物品。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
		/*
		有 m 和 n 两个维度的01背包
		而不同长度的字符串就是不同大小的待装物品。
		dp[i][j]表示使用i个0和j个1时字符串的最大数量(i和j是两个容量,最大数量就是最大价值)
		该题目中每个字符串的价值都为1,即其个数
		*/
		vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));//初始化
		for (string curStr : strs) {//遍历字符串数组(遍历物品)
			int oneNum = 0;
			int zeroNum = 0;
			for (char c : curStr) {//遍历当前字符串中的所有字符
				if (c == '1') oneNum++;
				else zeroNum++;
			}
			//此时已知每个字符串中的 0 和 1 的数量 
			for (int i = m; i >= zeroNum; i--) {//遍历第一个容量m
				for (int j = n; j >= oneNum; j--) {//遍历第二个容量n
					dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
				}
			}
		}
		return dp[m][n];
    }
};

 2.2 完全背包(每个物品可用多次)-正向

**如果求组合数就是外层for循环遍历物品内层for遍历背包**。//平时常用的

**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。//需要排列顺序时用的


需要排列顺序时:

如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!

 2.2.1 零钱兑换 II

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

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 i : coins) {
			for (int j = i; j <= amount; j++) {
				dp[j] += dp[j - i];
			}
		}
		return dp[amount];
    }
};

2.2.2 组合总和 Ⅳ

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

class Solution {
public:
	int combinationSum4(vector<int>& nums, int target) {
		/*
		背包容量为target,物品数量为nums.size(),每个物品价值结尾nums当前的元素
		本题是 需要排列顺序,因此两层循环中:外层容量,内层物品
		*/
		vector<int> dp(target + 1, 0);
		dp[0] = 1;//求的是组合的个数,即方案数。
		for (int j = 0; j <= target; j++) {
			for (int i : nums) {
				if (j < i) continue;
				if (dp[j] >= INT_MAX - dp[j - i]) continue;//C++测试用例有两个数相加超过int的数据,所以需要 加上 该句
				dp[j] += dp[j - i];
			}
		}
		return dp[target];
	}
};

2.2.3 爬楼梯(进阶版)

卡码网:57. 爬楼梯leetcode上并没有原题,是在卡码网上的)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢? 

注意:给定 n 是一个正整数。

#include<iostream>
#include<vector>
using namespace std;
int main() {
	/*
	总台阶数 为背包容量
	每次可走的步数 就是物品价值
	步数可重复走,即完全背包
	1、2 和 2、1 是上三台阶的 两种不同的 方案,即求的是排列问题,有顺序要求

	完全背包 ==》遍历容量时 为正向遍历
	排列问题 ==》两层循环为 外层容量,内层物品
	dp[0]时不走也是一种方案 ==》dp[0]初始化为1
	*/
	int n, m;
	cin >> n >> m;
	vector<int> dp(n + 1, 0);//dp[i]代表总共要走的i阶时的的方案数
	dp[0] = 1;//要走0阶时,直接不走也是一种方案
	for (int i = 0; i <= n; i++) {//遍历背包容量
		for (int j = 1; j <= m; j++) {//遍历物品
			if (i - j >= 0) dp[i] += dp[i - j];
		}
	}
	cout << dp[n] << endl;
}

3、

3.1 无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

class Solution {
public:
	int lengthOfLongestSubstring(string s) {
		/*
		对于第i个字符:

		若在dp[i-1]所代表的子串中出现,
		那么从所出现的位置j的下一个位置到i,构成了以i结尾的不重复子串。
		即dp[i] = i-j;

		若不在前面的dp[i-1]子串中出现,那么i-1子串加上i字符构成了i子串,
		因此 dp[i] = dp[i-1]+1

		无需担心j位置后是否又出现字符i,因为前面的子串本身就是不重复的,不可能存在两个字符i。

		最大值: 返回 max(dp[i]) 即可
		*/
		if (s.size() <= 1) return s.size();
		int maxSize = 1;
		vector<int> dp(s.size(), 1);//dp[i]代表以第i个字符结尾的无重复字符字串的长度
		for (int i = 1; i < s.size(); i++) {
			bool flag = false;//当前字符在i-1的字串中是否出现过重复
			for (int j = i - 1; j >= i - dp[i - 1] && j >= 0; j--) {
				if (s[i] == s[j]) {
					flag = true;
					dp[i] = i - j;//重复,则i重构新子串
					break;
				}
			}
			if (!flag) {//没有重复,则i的子串长度为i-1长度的+1
				dp[i] = dp[i - 1] + 1;
			}
			maxSize = max(maxSize, dp[i]);
			//cout << i << " " << dp[i] << endl;
		}
		return maxSize;
	}
};

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值