三种基本背包问题
一 0/1背包问题
-
问题描述:
有n件物品和容量为m的背包 给出i件物品的重量以及价值求解让装入背包的物品重量不超过背包容量 且价值最大 。 -
特点:<br1.这是最简单的背包问题,特点是每个物品只有一件供你选择放还是不放。
① 二维解法
设f[i][j]表示前 i 件物品 总重量不超过 j 的最大价值 可得出状态转移方程
- f[i][j]=max{f[i-1][j-a[i]]+b[i], f[i-1][j]}
代码:
for(int i=1;i<=n;i++)
for(int j=m;j1.0;j--){
if(a[i]<=j)
f[i][j]=max(f[i-1][j],f[i-1][j-a[i]]+b[i]);
else f[i][j]=f[i-1][j];
}
在一些情况下 题目的数据会很大 因此f数组不开到一定程度是没有办法ac。
②一维解法
设f[j]表示重量不超过j公斤的最大价值 可得出状态转移方程
- f[j]=max{f[j], f[j−a[i]]+b[i]}
代码:
for(int i=1;i<=n;i++){
for(int j=m;j1.=a[i];j--)
f[j]=max(f[j], f[j-a[i]]+b[i]);
}
二、完全背包问题
问题描述:有n件物品和容量为m的背包 给出i件物品的重量以及价值 求解让装入背包的物品重量不超过背包容量 且价值最大 。
特点:题干看似与01一样 但它的特点是每个物品可以无限选用。
设f[j]表示重量不超过j公斤的最大价值 可得出状态转移方程
- f[j] = maxj{f[j], f[j−a[i]]+b[i]}
代码:
for(int i=1;i<=n;i++)
for(int j = a[i];j <= m;j++){
f[j] = max(f[j], f[j-a[i]]+b[i]);
}
三、多重背包问题
问题描述:
有n件物品和容量为m的背包 给出i件物品的重量以及价值 还有数量 求解让装入背包的物品重量不超过背包容量 且价值最大 。
特点 :
它与完全背包有类似点 特点是每个物品都有了一定的数量。状态转移方程为:
f[j] = max{f[j], f[j−k∗a[i]]+k∗b[i]}
代码
for(int i=1;i<=n;i++)
for(int j=m;j1.=a[i];j--)
for(int k=0;k<=c[i];k++){
if(j-k*a[i]<0)break;
f[j] = max(f[j], f[j-k*a[i]]+k*b[i]);
}
实战
拆硬币问题:
链接:https://leetcode-cn.com/problems/coin-change-2/ 力扣(LeetCode)<br1.
题目:给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
class Solution {
public int change(int amount, int[] coins) {
int []dp=new int[amount+1];
dp[0]=1;
for(int coin:coins){
for(int i=coin;i<=amount;i++){
dp[i]=dp[i]+dp[i-coin];
}
}
return dp[amount];
}
}
最长回文子序列
一个字符串有许多子序列,比如字符串cabbeaf,它的子序列有c、abb、e、a、f,可以通过删除某些字符而变成回文字符串,字符串“cabbeaf”,删掉‘c’、‘e’、‘f’后剩下的子串“abba”就是回文字符串
//递归方法,求解最长回文子序列
int lps(char *str, int i, int j)
{
if (i == j)
return 1; //只有一个元素,回文长度为1
if (i > j) return 0; //因为只计算序列str[i....j]
//如果首尾相同
if (str[i] == str[j])
return lps(str, i + 1, j - 1) + 2;
//如果首尾不同
return max(lps(str, i, j - 1), lps(str, i + 1, j));
}
char str[] = "cabbeaf";
int n = strlen(str);
int res = lps(str, 0, n - 1);
cout << res<< endl;
//动态规划求解最长回文子序列,时间复杂度为O(n^2)
int lpsDp(char *str, int n)
{
int dp[10][10], tmp;
memset(dp, 0, sizeof(dp));
for (int i = 0; i < n; ++i) dp[i][i] = 1;
for (int i = 1; i < n; ++i)
{
tmp = 0;
//考虑所有连续的长度为i+1的子串,str[j....j+i]
for (int j = 0; j + i < n; j++)
{
//如果首尾相同
if (str[j] == str[j + i])
tmp = dp[j + 1][j + i - 1] + 2;
//如果首尾不同
else
tmp = max(dp[j + 1][j + i], dp[j][j + i - 1]);
dp[j][j + i] = tmp;
}
}
return dp[0][n - 1]; //返回字符串str[0...n-1]的最长回文子序列长度
}
最长公共子序列
伪代码
##暴力递归
func LCSLength(x []byte, y []byte, m int, n int) int {
if m == 0 || n == 0 {
return 0
}
if x[m-1] == y[n-1] {
return LCSLength(x, y, m-1, n-1) + 1
}
len1 := LCSLength(x, y, m, n-1)
len2 := LCSLength(x, y, m-1, n)
if len1 > len2 {
return len1
}
return len2
}
## map法
func LCSLength(x []byte, y []byte, m int, n int, lookup map[string]int) int {
if m == 0 || n == 0 {
return 0
}
var key string = fmt.Sprintf("%s|%s", string(x[:]), string(y[:]))
// Contains?
_, ok := lookup[key]
if ! ok {
if x[m-1] == y[n-1] {
lookup[key] = LCSLength(x, y, m-1, n-1, lookup) + 1
} else {
len1 := LCSLength(x, y, m, n-1, lookup)
len2 := LCSLength(x, y, m-1, n, lookup)
if len1 > len2 {
lookup[key] = len1
} else {
lookup[key] = len2
}
}
}
return lookup[key]
}
func main() {
x := "ABCBDAB"
y := "BDCABA"
// lookup := make(map[string]int)
lookup := map[string]int{}
fmt.Printf("The length of LCS is %d\n",
LCSLength([]byte(x), []byte(y), len(x), len(y), lookup))
}
## 数组法
func LCSLength(x []byte, y []byte, m int, n int, lookup [][]int) int {
if m == 0 || n == 0 {
return 0
}
// no found in cache
if lookup[m][n] == 0 {
if x[m-1] == y[n-1] {
lookup[m][n] = LCSLength(x, y, m-1, n-1, lookup) + 1
} else {
len1 := LCSLength(x, y, m, n-1, lookup)
len2 := LCSLength(x, y, m-1, n, lookup)
if len1 > len2 {
lookup[m][n] = len1
} else {
lookup[m][n] = len2
}
}
}
return lookup[m][n]
}
func main() {
x := "ABCBDAB"
y := "BDCABA"
// init cache array
m := len(x)
n := len(y)
lookup := make([][]int, m+1)
for i := range lookup {
lookup[i] = make([]int, n+1)
}
fmt.Printf("The length of LCS is %d\n",
LCSLength([]byte(x), []byte(y), m, n, lookup))
}
#动态规划
func LCSLength(x []byte, y []byte) int {
var m, n int
m = len(x)
n = len(y)
lookup := make([][]int, m+1)
for i := range lookup {
lookup[i] = make([]int, n+1)
}
var i, j int
for i = 0; i <= m; i++ {
lookup[i][0] = 0;
}
for j =0; j <= n; j++ {
lookup[0][j] = 0;
}
for i=1; i <=m; i++ {
for j = 1; j <=n; j++ {
if x[i-1] == y[j-1] {
lookup[i][j] = lookup[i-1][j-1] + 1
} else {
len1 := lookup[i-1][j]
len2 := lookup[i][j-1]
if len1 > len2 {
lookup[i][j] = len1
} else {
lookup[i][j] = len2
}
}
}
}
return lookup[m][n]
}
最大连续子序列和-动态规划
描述:
给定K个整数的序列{ N1, N2, …, NK },其任意连续子序列可表示为{ Ni, Ni+1, …, Nj },
其中 1 <= i <= j <= K。最大连续子序列是所有连续子序中元素和最大的一个,
例如给定序列{ -2, 11, -4, 13, -5, -2 },其最大连续子序列为{ 11, -4, 13 },最大和为20。
注意:
最大连续子序列和如果为负,则返回0;而本题目中的最大连续子序列和并不返回0,如果是全为负数,则返回最大的负数即可。
思路分析:
具有最优子结构,和重叠子问题, 动态规划的算法思路
最大连续子序列和只可能是以位置0~n-1中某个位置结尾。当遍历到第i个元素时,判断在它前面的连续子序列和是否大于0,如果大于0,则以位置i结尾的最大连续子序列和为元素i和前门的连续子序列和相加;否则,则以位置i结尾的最大连续子序列和为元素i。
状态转移方程: sum[i]=max(sum[i-1]+a[i],a[i])
代码:
func maxSumOfSubArray(a []int) int {
// 初始化最大和为数组的第一个元素
maxSum := a[0]
n := len(a)
// 第 i 遍搜索从第 i 个元素开始往后搜索
for i := 0; i < n; i++ {
// sum用于记录从第 i 个元素到第 k 个元素的和,i <= k
sum := 0
for k := i; k < n; k++ {
sum += a[k]
if sum > maxSum {
//找到一个比之前找到的最大值更大的连续子序列和
maxSum = sum
}
}
}
return maxSum
}
func MaxSubArray(arr []int) int {
currSum := 0
maxSum := arr[0]
for _, v := range arr {
if currSum > 0 {
currSum += v
} else {
currSum = v
}
if maxSum < currSum {
maxSum = currSum
}
}
return maxSum
}
#py
class Solution(object):
def maxSubArray(self, nums):
for i in range(1, len(nums)):
nums[i]= nums[i] + max(nums[i-1], 0)
return max(nums)
问题
- 输入两个整数 n 和 m,从数列1,2,3…n 中 随意取几个数,使其和等于 m ,要求将其中所有的可能组合列出来.
物品称重获得最大价值,最小体积
分析
- 由该题可知是典型的背包问题,根据该数是否加入进行递归运算。
解法
- 采用0-1背包的思想,使用递归方法:当选择n时,就用剩下的n-1填满 m-n;
当不选择n是,就用剩下的n-1填满m;
注意的是,当m=n时,即找到了符合条件的解
<?php
function bestPackage($w, $v, $maxWeight)
{
$num = count($w);
$res = [];
//初始化背包
for($i=0;$i<$maxWeight;$i++) {
$res[0][$i] = 0;
}
for($i=0;$i<$num;$i++) {
for($j=$maxWeight;$j1.=$w[$i];$j--) {
$res[$i][$j] = max($res[$i-1][$j], $res[$i-1][$j - $w[$i]] + $v[$i]);
}
for ($j = $w[$i] - 1; $j 1.= 0; $j--) {
$res[$i][$j] = $res[$i-1][$j];
}
}
return $res[$num-1][$maxWeight];
}
$maxWeight = 10;//背包最大称重
$w = [2, 2, 6, 5, 4];//物品重量
$v = [6, 3, 5, 4, 6];//物品价值
//求最大容量
$result = bestPackage($w, $v, $maxWeight);
echo $result;
echo PHP_EOL;
#最小体积
void solve() {
init();
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= NUM; ++j) {
if (j < v[i]) {
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
}
}
}
for (int i = n; i >= 1; ++i) {
if (dp[n][i] <= V) {
cout << i << endl;
break;
}
}
}
爬楼梯问题
问题描述
1.假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
函数推导
- 可以推出函数f(n)=f(n-1)+f(n-2),其中n1.2
编程实现
- 递归
function fun($n) {
if($n<=0) {
return 0;
} else if($n<=2) {
return $n;
} else {
return fun($n-1)+fun($n-2);
}
}
- 递归+中间值存储
function fun($n) {
if($n <= 0) {
return 0;
}
$temp = [1,2];
for($i=2;$i<$n;$i++) {
$temp[$i] = $temp[$i-1]+$temp[$i-2];
}
return $temp[$n - 1];
}
路径问题
问题描述
- 一个机器人位于一个 m X n 网格的左上角 (起始点在下图中标记为“Start” )
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
问题思路
- 如果当前位置没有障碍物,那么可以得出 w[m][n] = w[m-1][n]+w[m][n-1];
- 如果当前位置存在障碍物,那么w[m][n]=0;
- 只能向下或者向右走。所以当在i=0 或者 j = 0时 等于1
//方法1
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m,vector<int>(n,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];
}
//方法2
int uniquePaths(int m, int n) {
int total = m + n -2; //一共要走的步数
int down = m - 1; //向下要走的步数
double res = 1;
for(int i = 1 ; i <= down; i++){
res =res * (total - down + i) / i;
}
return (int)res;
}
求证数数字和
问题描述
- 给定整数n,取若干个1到n的整数可求和等于整数m,编程求出所有组合的个数。比如当n=6,m=8时,有四种组合:[2,6], [3,5], [1,2,5], [1,3,4]。限定n和m小于120
1.输入:
整数n和m
1.输出:
求和等于m的所有组合的个数。
1.样例:
输入:
6 8
输出
4
#include<iostream1.
#include<vector1.
using namespace std;
int main()
{
int N, M;
cin 1.1. N 1.1. M;
vector<vector <int1.1. dp(N + 1, vector<int1.(M + 1, 0));
dp[1][0] = 1;
dp[1][1] = 1;
for (int i = 2; i <= N; i++)
{
for (int j = 1; j <= M; j++)
{
if (j < i)
{
dp[i][j] = dp[i - 1][j];
}
else if (j == i)
{
dp[i][j] = dp[i - 1][j] + 1;
}
else
{
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - i];
}
}
}
cout << dp[N][M] << endl;
return 0;
}
数字三角形
听说是最水的动态规划,
大体思路
每次第一次计算出MaxSum(i,j)的值时,把该值保存起来,以后再遇到MaxSum(i.j)时直接取出之前第一次调用时已经存放的值即可,不必再次调用MaxSum函数作递归计算。
这样每个MaxSum(i,j)都只需要计算一次,计算次数为数字三角形中的数字总数。因此,不需要写递归函数,从第N-1行开始向上逐行递推,就可以求得a[1][1]的值。
题目描述
给定一个由n行数字组成的数字三角形如下图所示。试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。
对于给定的由n行数字组成的数字三角形,计算从三角形的顶至底的路径经过的数字和的最大值。
输入
输入数据的第1行是数字三角形的行数n,1≤n≤100。接下来n行是数字三角形各行中的数字。所有数字在0…99之间。
输出
出数据只有一个整数,表示计算出的最大值。
示例输入
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
示例输出 30
#include <stdio.h>
#define MAX 110
int d[MAX][MAX];
int a[MAX][MAX];
int n;
int main()
{
int i,j;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
for(j=1;j<=i;j++)
{
scanf("%d",&d[i][j]);
}
}
for(j=1;j<n;j++)
{
a[n][j]=d[n][j];
}
for(i=n;i>1;i--)
{
for(j=1;j<=i;j++)
{
if(a[i][j]>a[i][j+1])
{
a[i-1][j]=a[i][j]+d[i-1][j];
}
else
{
a[i-1][j]=a[i][j+1]+d[i-1][j];
}
}
}
printf("%d\n",a[1][1]);
return 0;
}
经典问题
一. 0/1 Knapsack,0/1背包问题
-
Equal Subset Sum Partition,相等子集划分问题
-
Subset Sum,子集和问题
-
Minimum Subset Sum Difference,子集和的最小差问题
-
Count of Subset Sum,相等子集和的个数问题
-
Target Sum,寻找目标和的问题
二. Unbounded Knapsack,无限背包,5个题
-
Unbounded Knapsack,无限背包
-
Rod Cutting,切钢条问题
-
Coin Change,换硬币问题
-
Minimum Coin Change,凑齐每个数需要的最少硬币问题
-
Maximum Ribbon Cut,丝带的最大值切法
三. Fibonacci Numbers,斐波那契数列,6个题
-
Fibonacci numbers,斐波那契数列问题
-
Staircase,爬楼梯问题
-
Number factors,分解因子问题
-
Minimum jumps to reach the end,蛙跳最小步数问题
-
Minimum jumps with fee,蛙跳带有代价的问题
-
House thief,偷房子问题
四. Palindromic Subsequence,回文子系列,5个题
-
Longest Palindromic Subsequence,最长回文子序列
-
Longest Palindromic Substring,最长回文子字符串
-
Count of Palindromic Substrings,最长子字符串的个数问题
-
Minimum Deletions in a String to make it a
Palindrome,怎么删掉最少字符构成回文 -
Palindromic Partitioning,怎么分配字符,形成回文
五. Longest Common Substring,最长子字符串系列,13个题
-
Longest Common Substring,最长相同子串
-
Longest Common Subsequence,最长相同子序列
-
Minimum Deletions & Insertions to Transform a String into
another,字符串变换 -
Longest Increasing Subsequence,最长上升子序列
-
Maximum Sum Increasing Subsequence,最长上升子序列和
-
Shortest Common Super-sequence,最短超级子序列
-
Minimum Deletions to Make a Sequence Sorted,最少删除变换出子序列
-
Longest Repeating Subsequence,最长重复子序列
-
Subsequence Pattern Matching,子序列匹配
-
Longest Bitonic Subsequence,最长字节子序列
-
Longest Alternating Subsequence,最长交差变换子序列
-
Edit Distance,编辑距离
-
Strings Interleaving,交织字符串
1、线性 DP
最经典单串:
300. 最长上升子序列
最经典双串:
1143. 最长公共子序列
经典问题:
120. 三角形最小路径和
53. 最大子序和
152. 乘积最大子数组
887. 鸡蛋掉落(DP+二分)
354. 俄罗斯套娃信封问题
打家劫舍系列: (打家劫舍3 是树形DP)
198. 打家劫舍
213. 打家劫舍 II
股票系列:
121. 买卖股票的最佳时机
122. 买卖股票的最佳时机 II
123. 买卖股票的最佳时机 III
188. 买卖股票的最佳时机 IV
309. 最佳买卖股票时机含冷冻期
714. 买卖股票的最佳时机含手续费
字符串匹配系列
72. 编辑距离
44. 通配符匹配
10. 正则表达式匹配
2、区间 DP
516. 最长回文子序列
730. 统计不同回文子字符串
1039. 多边形三角剖分的最低得分
664. 奇怪的打印机
312. 戳气球
3、背包 DP
416. 分割等和子集 (01背包-要求恰好取到背包容量)
494. 目标和 (01背包-求方案数)
322. 零钱兑换 (完全背包)
518. 零钱兑换 II (完全背包-求方案数)
474. 一和零 (二维费用背包)
4、树形 DP
124. 二叉树中的最大路径和
1245. 树的直径 (邻接表上的树形DP)
543. 二叉树的直径
333. 最大 BST 子树
337. 打家劫舍 III
5、状态压缩 DP
464. 我能赢吗
526. 优美的排列
935. 骑士拨号器
1349. 参加考试的最大学生数
6、数位 DP
233. 数字 1 的个数
902. 最大为 N 的数字组合
1015. 可被 K 整除的最小整数
7、计数型 DP
计数型DP都可以以组合数学的方法写出组合数,然后dp求组合数
62. 不同路径
63. 不同路径 II
96. 不同的二叉搜索树 (卡特兰数)
1259. 不相交的握手 (卢卡斯定理求大组合数模质数)
8、递推型 DP
所有线性递推关系都可以用矩阵快速幂做,可以O(logN),最典型是斐波那契数列
70. 爬楼梯
509. 斐波那契数
935. 骑士拨号器
957. N 天后的牢房
1137. 第 N 个泰波那契数
9、概率型 DP
求概率,求数学期望
808. 分汤
837. 新21点
10、博弈型 DP
策梅洛定理,SG 定理,minimax
翻转游戏
293. 翻转游戏
294. 翻转游戏 II
Nim游戏
292. Nim 游戏
石子游戏
877. 石子游戏
1140. 石子游戏 II
井字游戏
348. 判定井字棋胜负
794. 有效的井字游戏
1275. 找出井字棋的获胜者
11、记忆化搜索
本质是 dfs + 记忆化,用在状态的转移方向不确定的情况
329. 矩阵中的最长递增路径
576. 出界的路径数
作者:FennelDumplings
链接:https://leetcode-cn.com/circle/article/NfHhXD/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
总结
解动态规划的一般方法:从终点逐段向始点方向寻找最小(大)的方法。