文章目录
一、案例引入:数塔问题
1.DFS深搜求解
思路:dfs枚举所有从起点走到最后一层的路径,最终取和最大的路径
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500 + 10;
int a[N][N];
int n;
int ans = -1e9;
//从起点走到最后一层的路径的和
void dfs(int x, int y, int sum)
{
if(x > n)// 递归出口
{
ans = max(ans, sum);
return ;
}
dfs(x + 1, y, sum + a[x + 1][y]);// 向下
dfs(x + 1, y + 1, sum + a[x + 1][y + 1]);// 右下
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
dfs(1, 1, a[1][1]);
cout << ans;
return 0;
}
缺点:
虽然是一种可行的方案,但是当数据庞大,递归的层数就越多,重重计算也越多,很容易就超时了。本题,n=30时就over了。
因此,dfs一般适用于数据量不是很庞大的情况!
2.动规:递推求解
如何优化上述dfs搜索遇到的大量重复计算问题呢?递推就是一个不错的选择,将计算过的数值用一个数组记录下来,避免重复计算了!(动态规划也是利用了这一思想,俗称存表)。
定义一个二维数组d[x][y]
,表示由(x,y)
点到最后一层的值的最大和是多少。
从后往前推,即答案为d[1][1]
,表示由(1,1)
点到最后一层的值的最大和。
- 不难发现,最后一层的点到最后一层的最大距离即为自己对应的值
a[n - 1][y]
,这个就是问题的边界。 - 从后往前推,观察发现当前点的状态只与正下方和右下方的状态有关,因此得出递推式(状态转移方程):
d[i][j] = a[i][j] + max(a[i+1][j],a[i + 1][j + 1])
【代码实现】
时间复杂度:O(n*n)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500 + 10;
int a[N][N];
int d[N][N];
int n;
int ans = -1e9;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
//最后一层
for(int j = 1; j <= n; j ++) d[n][j] = a[n][j];
// 从后往前推
for(int i = n - 1; i >= 1; i --)
for(int j = 1; j <= i; j ++)
{
d[i][j] = a[i][j] + max(d[i + 1][j], d[i + 1][j + 1]);
}
cout << d[1][1];
return 0;
}
3.动规:递归求解
既然有了上述的递推式,我们直到递归和递推其实是相互的,因此递推可以改写成递归的形式。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500 + 10;
int a[N][N];
int d[N][N];
int n;
int ans = -1e9;
// 求从点(x,y)开始,走到最后一层,经过数字的最大和。
int fun(int x, int y)
{
if(x == n) return a[x][y];// 最后一层的解就是自己
return a[x][y] + max(fun(x + 1, y), fun(x + 1, y + 1));// 如果不是最后一行就递归计算
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
cout << fun(1, 1);
return 0;
}
缺点:
效率太低了,并不是因为递归就效率低,而是因为存在了大量的重复计算。(类比递归式的斐波那契数列)
递归存在着大量不必要的重复计算,,递归的层数就越多,算的就越多,重复的计算更多!
4.动规:记忆化搜索
上述方法3,存在着大量的重复计算,那我们如何在使用递归的情况下去优化,免去那些不必要的重复计算呢?
如上图,我们在求d[2][1]
的时候就把d[3][2]
计算过了,而我们再求d[2][2]
的时候,又把d[3][2]
再计算了一次,这就造成了重复计算?那如何解决呢?
在计算d[2][1]
的时候就把d[3][2]
计算过了,可以把d[3][2]
用一个数组存下来,当再次需要d[3][2]
时,就不需要再去递归求解了,直接用数组中备份过的数据就行——这就是记忆化搜索!
动规:记忆化搜索:
- 求解每一个点的值,先判断该点的值是否曾经求解过,如果曾经求解过,直接拿过来使用;如果没求解过,递归求解,并存储该解!
- 将计算过的值存储到一个数组中
- 如何判断是否求解过呢?——做标记判断
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 500 + 10;
int a[N][N];
int d[N][N];
int n;
int ans = -1e9;
//动规,记忆化搜索:先将d数组初始化为-1,方便判断有没有求解过
int fun(int x, int y)
{
if(x == n) return a[x][y];// 最后一层的解就是自己
else
{
if(d[x][y] != -1) return d[x][y];// 曾经求解过
else
{
//求解(x,y)点走到底层经过的数字和的最大值,并存储
d[x][y] = a[x][y] + max(fun(x + 1, y), fun(x + 1, y + 1));
return d[x][y];
}
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i; j ++)
cin >> a[i][j];
memset(d, -1, sizeof d);
cout << fun(1, 1);
return 0;
}
总结
二、基本介绍
三、巩固练习
1.前缀最大值问题
题目描述
求一个数列的所有前缀最大值之和。
即:给出长度为n的数列a[i],求出对于所有1<=i<=n,max(a[1],a[2],…,a[i])的和。
比如,有数列:666 304 692 188 596,前缀最大值为:666 666 692 692 692,和为3408。
对于每个位置的前缀最大值解释如下:对于第1个数666,只有一个数,一定最大;对于第2个数,求出前两个数的最大数,还是666;对于第3个数,求出前3个数的最大数是692……其余位置依次类推,最后求前缀最大值得和。由于读入较大,数列由随机种子生成。
其中a[1]=x,a[i]=(379*a[i-1]+131)%997。输入
一行两个正整数n,x,分别表示数列的长度和随机种子。(n<=100000,x<997)
输出
一行一个正整数表示该数列的前缀最大值之和。
样例
输入复制
5 666
输出复制
3408
说明
数列为{666,304,692,188,596},前缀最大值为{666,666,692,692,692},和为3408。
思路:
解题步骤:
- 划分阶段:求前
i
个数的最大前缀值取决于前i-1
个数的最大前缀值为多少 - 确定状态和状态变量:状态:前
i
项的最大前缀值是多少,dp[i]
- 确定决策和决策方程:
dp[i] = max(dp[i - 1], a[i])
; - 寻找边界条件:
dp[1] = a[1]
最后将dp[i]
求和,即为答案!
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int dp[N];// dp[i]:表示包含i之前的最大值是多少
int main()
{
int n, x;
cin >> n >> x;
a[1] = x;
for(int i = 2; i <= n; i ++)
{
a[i] = (379 * a[i - 1] + 131) % 997;
}
dp[1] = a[1];// 边界
int ans = dp[1];
for(int i = 2; i <= n; i ++)
{
dp[i] = max(a[i], dp[i - 1]);
ans += dp[i];
}
cout << ans;
return 0;
}
2.取数问题
题目描述
设有N 个正整数(1 <= N <= 50),其中每一个均是大于等于1、小于等于300的数。
从这N个数中任取出若干个数(不能取相邻的数),要求得到一种取法,使得到的和为最大。
例如:当N=5时,有5个数分别为:13,18,28,45,21
此时,有许多种取法,如: 13,28,21 和为62
13, 45 和为58
18,45 和为63
……….和为63应该是满足要求的一种取法
输入
第一行是一个整数N
第二行有N个符合条件的整数。输出
一个整数,即最大和
样例
输入复制
5 13 18 28 45 21
输出复制
63
题意:求前n个数,在不连续的情况下求最大和是多少。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int dp[N];// dp[i]:表示第i个数选不选的情况下,最大不连续数的和
int n;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
dp[1] = a[1];
dp[2] = max(a[1], a[2]);
for(int i = 3; i <= n; i ++)
{
dp[i] = max(dp[i - 1], a[i] + dp[i - 2]);
}
cout << dp[n];
return 0;
}
3.最大子段和(连续部分和)
【做法一】
暴力做法:枚举每一个区间,求出最大区间和。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int a[N];
int sum , ans = -1e9;
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
//从每个数开始连续数的和
for(int i = 1; i <= n; i ++)
{
sum = 0;
for(int j = i; j <= n; j ++)
{
sum += a[j];
ans = max(ans, sum);
}
}
cout << ans;
return 0;
}
缺点:时间复杂度较高,容易超时
【解法二】动态规划法
dp[N]:存储状态,表示包括下标i之前的最大连续子序列和为dp[i]
- 1
- 2
- 3
- 4
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int a[N];
int dp[N];// dp[i]:表示包含i之前的最大连续子段和
int ans = -1e9;
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
dp[1] = a[1];// 边界
ans = a[1];
for(int i = 2; i <= n; i ++)
{
dp[i] = max(a[i], dp[i - 1] + a[i]);
ans = max(ans, dp[i]);
}
cout << ans;
return 0;
}
4.最长上升子序列问题
(LIS)最长上升子序列 I
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
−109≤数列中的数≤109输入样例:
7 3 1 2 1 8 5 6
输出样例:
最长递增序列为:1 2 5 6
4
这题与上一题的明显区别就是,数可以跳着取,且保证序列是严格递增的!
【代码实现】
时间复杂度:O(n * n)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int dp[N];// dp[i]:表示包含i之前的最长递增子序列的长度
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
dp[i] = 1;// 初始化
}
int ans = -1e9;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= i - 1; j ++)// 遍历1~i-1的数,找到能接的(递增)且长度最大的序列
{
if(a[i] > a[j])
{
dp[i] = max(dp[j] + 1, dp[i]);
ans = max(ans, dp[i]);
}
}
cout << ans;
return 0;
}
(LIS)最长递增子序列 II
上述第一种LIS解法存在那些不足呢?时间复杂度为O(n*n)
,当数据量一大就爆炸了!为了解决这一烦恼,我们可以贪心加二分的思想来进行优化!
题目描述
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入
第一行包含整数N。
第二行包含N个整数,表示完整序列。
1≤N≤100000,−109≤数列中的数≤109输出
输出一个整数,表示最大长度。
样例
输入复制
6 1 3 2 8 5 6
输出复制
4
思路:
【代码实现】
时间复杂度:O(nlongn)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int dp[N];
int a[N];
int n, cnt;
//二分:找到dp数组中第一个大于等于a[i]的数
int find(int x)
{
int l = 1, r = cnt;
while(l < r)
{
int mid = (l + r) / 2;
if(dp[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
dp[++ cnt] = a[1];// 边界:dp[1] = a[1]
for(int i = 2; i <= n; i ++)
{
// 如果a[i]大于dp数组的最后一位,LIS长度增加,续到最后一位
if(a[i] > dp[cnt]) dp[++ cnt] = a[i];
else // 在dp数组中二分找到第一个大于等于a[i]的数,进行替换
{
int pos = find(a[i]);
dp[pos] = a[i];
}
}
cout << cnt;
return 0;
}
LIS练习:合唱队形
题目描述
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。
合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,则他们的身高满足T1 < T2 < … < Ti , Ti > Ti+1 > … > TK (1 <= i <= K)。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。输入
输入的第一行是一个整数N(2 <= N <= 100),表示同学的总数。
第一行有n个整数,用空格分隔,第i个整数Ti(130 <= Ti <= 230)是第i位同学的身高(厘米)。输出
输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
样例
输入复制
8 186 186 150 200 160 130 197 220
输出复制
4
思路:求最长上升子序列的扩展应用题,以每一个数为中心,求取它各个数的左右两边的最长递增子序列是多少,剩下的就是要筛掉的最少人数。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int dpa[N], dpb[N];//dpa[i] : 存储包含i的左边的各个数的最长递增子序列;dpb[i]:右边
bool st[N];
int n;
int ans = -1e9;
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
dpa[i] = dpb[i] = 1;
}
// 以每一个数为中心,求出它左后两边的最长递增子序列长度
// 最多留下的人数:dpa[i] + dpb[i] - 1
// 剔除最少人数 n - max
for(int i = 1; i <= n; i ++)
{
for(int j = 1; j < i; j ++)
{
if(a[i] > a[j]) dpa[i] = max(dpa[i], dpa[j] + 1);
}
}
for(int i = n; i >= 1; i --)
{
for(int j = n; j > i; j --)
{
if(a[i] > a[j]) dpb[i] = max(dpb[i], dpb[j] + 1);
}
}
//求解最多留下来的人数
for(int i = 1; i <= n; i ++)
ans = max(ans, dpa[i] + dpb[i] - 1);
cout << n - ans; // 需要剔除的最少人数
return 0;
}
LIS练习:导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹的枚数和导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,每个数据之间至少有一个空格),计算这套系统最多能拦截多少导弹。输入
第1行有1个整数n,代表导弹的数量。(n<=1000)
第2行有n个整数,代表导弹的高度。(雷达给出的高度数据是不大于30000的正整数)输出
输出这套系统最多能拦截多少导弹。
样例
输入复制
8 389 207 155 300 299 170 158 65
输出复制
6
思路:要拦截的导弹越多,前面拦截的导弹要高,从左到右求取最长递增子序列即为答案!
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int dp[N];
int a[N], b[N], c[N];
int n, cnt;
int ans = -1e9;
int find(int x)
{
int l = 1, r = cnt;
while(l < r)
{
int mid = (l + r) / 2;
if(dp[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
//从右往左求最长递增子序列
dp[++ cnt] = a[n];
for(int i = n - 1; i >= 1; i --)
{
if(a[i] > dp[cnt]) dp[++ cnt] = a[i];
else
{
int pos = find(a[i]);
dp[pos] = a[i];
}
}
cout << cnt;
return 0;
}
LIS练习:最少的修改次数
题目描述
现有整数 A1,A2,…An,修改最少的数字为实数(整数或者小数),使得数列严格单调递增。
输入
第一行,一个整数n。(n≤10^5)
第二行,n个整数Ai。(Ai≤10^9)输出
1个整数,表示最少修改的数字的数量。
样例
输入复制
3 1 3 2
输出复制
1
思路:
由于修改可以变为整数或者小数,所以求出最长LIS,剩下数的个数即为要修的最少个数!
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int dp[N];
int a[N], b[N], c[N];
int n, cnt;
int ans = -1e9;
int find(int x)
{
int l = 1, r = cnt;
while(l < r)
{
int mid = (l + r) / 2;
if(dp[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
//从左往右最长递增子序列
dp[++ cnt] = a[1];
for(int i = 2; i <= n; i ++)
{
if(a[i] > dp[cnt]) dp[++ cnt] = a[i];
else
{
int pos = find(a[i]);
dp[pos] = a[i];
}
}
cout << n - cnt;
return 0;
}
5.最长公共子序列
(LCS)最长公共子序列 I
题目描述
给出1-n的两个排列P1和P2,求它们的最长公共子序列。
输入
第一行是一个数n;(n是5~1000之间的整数)
接下来两行,每行为n个数,为自然数1-n的一个排列(1-n的排列每行的数据都是1-n之间的数,但顺序可能不同,比如1-5的排列可以是:1 2 3 4 5,也可以是2 5 4 3 1)。输出
一个整数,即最长公共子序列的长度。
样例
输入复制
5 3 2 1 4 5 1 2 3 4 5
输出复制
3
思路:
【代码实现】
时间复杂度:O(n*n)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N], b[N];
int dp[N][N];
int n, m;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= m; i ++ ) cin >> b[i];
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1;// a数组和b数组同时扔掉相同的那个数
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);// a数组扔掉一个数,b数组扔掉一个数,那种情况更大
}
cout << dp[n][m];
return 0;
}
(LCS)最长公共子序列 II
给出1-n的两个排列P1和P2,求它们的最长公共子序列。
和最长公共子序列(LCS)(1)问题不同的是,本题的n在5-100000之间。输入
第一行是一个数n;(n是5-100000之间的整数)
接下来两行,每行为n个数,为自然数1-n的一个排列(1-n的排列每行的数据都是1-n之间的数,但顺序可能不同,比如1-5的排列可以是:1 2 3 4 5,也可以是2 5 4 3 1)。输出
一个整数,即最长公共子序列的长度。
样例
输入复制
5 3 2 1 4 5 1 2 3 4 5
输出复制
3
说明
对于50%的数据,n≤1000
对于100%的数据,n≤100000
版本一求最长LCS的时间复杂度为O(n*n)
,当数据量庞大就为超时,而且二维空间也有限。那我们如何改进呢?
这种算法主要是将最长公共子序列转为最长上升子序列,然后用最长上升子序列的O(nlogn)
算法来做.
【代码实现】
时间复杂度:O(nlogn)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int dp[N];
int a[N], b[N], c[N];
int n, cnt;
int ans = -1e9;
int find(int x)
{
int l = 1, r = cnt;
while(l < r)
{
int mid = (l + r) / 2;
if(dp[mid] >= x) r = mid;
else l = mid + 1;
}
return l;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
c[a[i]] = i;
}
for(int i = 1; i <= n; i ++) cin >> b[i];
//求b数组的每一个数在a数组的位置(c[b[i]])的LIS
dp[++ cnt] = c[b[1]];
for(int i = 2; i <= n; i ++)
if(c[b[i]] > dp[cnt]) dp[++ cnt] = c[b[i]];
else
{
int pos = find(c[b[i]]);
dp[pos] = c[b[i]];
}
cout << cnt;
return 0;
}
6.背包问题
01背包
01背包问题:每种物品只有一件,要么取要么不取
(1)状态dp[i][j]
定义:前i
个物品,背包容量j
下的最优解(最大价值):
- 当前的状态依赖于之前的状态,可以理解为从初始状态
dp[0][0] = 0
开始决策,有N
件物品,则需要N
次决 策,每一次对第i
件物品的决策,状态dp[i][j]
不断由之前的状态更新而来。
(2)当前背包容量不够(j < v[i])
,没得选,因此前 i
个物品最优解即为前 i−1
个物品最优解:
- 对应代码:
dp[i][j] = dp[i - 1
][j]。
(3)当前背包容量够,可以选,因此需要决策选与不选第 i
个物品:
- 选:
dp[i][j] = dp[i - 1][j - v[i]] + w[i]
。 - 不选:
dp[i][j] = dp[i - 1][j]
。 - 我们的决策是如何取到最大价值,因此以上两种情况取
max()
。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, c;
int w[N], v[N];
int dp[N][N]; // dp[i][j]数组含义:前i件物品装入容量为j的背包的最大价值是多少
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i ++ ) cin >> w[i] >> v[i];
for (int i = 1; i <= n; i ++ ){
for (int j = 1; j <= c; j ++ ){
// 如果j < w[i](当前背包的容量不足以装下物品i):不放入,则最大价值为dp[i - 1][j]
if(j < w[i]) dp[i][j] = dp[i - 1][j];
else{// 如果j > w[i]背包容量足够装下w[i],存在放和不放两种情况,取这两种情况的最大价值
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
}
}
}
// 输出最大价值
cout << dp[n][c];
return 0;
}
01背包优化:一维滚动数组优化
将状态dp[i][j]
优化到一维dp[j]
,实际上只需要做一个等价变形。
为什么可以这样变形呢?我们定义的状态dp[i][j]
可以求得任意合法的i
与j
最优解,但题目只需要求得最终状态dp[n][m]
,因此我们只需要一维的空间来更新状态。
(1)状态dp[j]
定义:N
件物品,背包容量j
下的最优解。
(2)注意枚举背包容量j必须从逆序开始。
(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态dp[i
][j]是由上一轮i - 1
的状态得来的,dp[i][j]
与f[i - 1][j]
是独立的。而优化到一维后,如果我们还是正序,则有dp[较小体积]
更新到dp[较大体积]
,则有可能本应该用第i-1
轮的状态却用的是第i
轮的状态。
(4)简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。
状态转移方程为:dp[j] = max(dp[j], dp[j - v[i]] + w[i]
。
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n, c;
int w[N], v[N];
int dp[N]; // dp[j]数组含义:容量为j的背包能存放物品的最大价值
//二维:
//dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]); 都只与上一行(i-1)的物品有关,因此这一项可优化省略
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i ++ ) cin >> w[i] >> v[i];
for (int i = 1; i <= n; i ++ )
for (int j = c; j >= w[i]; j -- )// 倒着过来循环!
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
// 输出最大价值
cout << dp[c];
return 0;
}
完全背包问题
完全背包问题:每种物品可以取无限次,在背包容量一定的情况下求取最大价值。
(1)01背包:
- 二维:
dp[i][j] = max(dp[i - 1][j], dp[
i - 1][j - w[i]] + v[i])` - 一维:**dp[j] = max(dp[j], d[j - w[i]] + v[i])滚动优化:从背包容量
c
,循环降序**到当前物品重量w[i]
(2)完全背包:
-
三维:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - k * w[i]] + k * v[i])
,三维非常容易就炸了 -
二维:由上述三维变形得:
dp[i][j] = max(dp[i - 1][j], dp[
i][j - w[i]] + v[i])
,(注意与01的二分及其相似但又存在区别) -
一维:**dp[j] = max(dp[j], dp[j - w[i]] + v[i])滚动优化:从当前物品重量
w[i]
,循环正序**升到背包容量c
原理可以不会证明,但状态转移一定要记熟!
我们列举一下更新次序的内部关系(三维到二维):
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w , …)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v] + w , f[i-1,j-3v]+2*w , …),我们发现少了一个w
由上两式,可得出如下递推关系:
f[i][j]=max(f[i-1][j], f[i,j-v]+w)
二维版本代码如下:
for(int i = 1 ; i <= n ;i++)
for(int j = 1 ; j <= m ;j++)
{
f[i][j] = f[i-1][j];
if(j >= v[i])
f[i][j]=max(f[i][j], f[i][j-v[i]] + w[i]);
}
一维优化代码如下:
for (int i = 1; i <= n; i ++ )
for (int j = w[i]; j <= m; j ++ )// 正序循环
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
题目描述
仙岛上种了无数的不同种类的灵芝,小芳跟着爷爷来到仙岛采摘灵芝。由于他们带的食物和饮用水有限,必须在时间t内完成采摘。
假设岛上有m种不同种类的灵芝,每种灵芝都有无限多个,已知每种灵芝采摘需要的时间,以及这种灵芝的价值;
请你编程帮助小芳计算,在有限的时间t内,能够采摘到的灵芝的最大价值是多少?输入
输入第一行有两个整数T(1 <= T <= 100000)和M(1 <= M <= 2000),用一个空格隔开,T代表总共能够用来采灵芝的时间,M代表岛上灵芝的种类数。接下来的M行每行包括两个在1到10000之间(包括1和10000)的整数,分别表示采摘某种灵芝的时间和这种灵芝的价值。
输出
输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的灵芝的最大总价值。
样例
输入复制
70 3 71 100 69 1 1 2
输出复制
140
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int n, c;
int w[N], v[N];
int dp[N];
int main()
{
cin >> c >> n;
for (int i = 1; i <= n; i ++ ) cin >> w[i] >> v[i];
for (int i = 1; i <= n; i ++ )
for (int j = w[i]; j <= c; j ++ )// 正序循环
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
// 输出最大价值
cout << dp[c];
return 0;
}
多重背包问题
多重背包问题:每种物品有si件(有限次数)
思路:将多重
背包转化为01
背包
将si件物品都存起来,转换为有si个物品,每个物品有一件
题目描述
有N种物品和一个容量是V的背包。
第i种物品最多有si件,每件体积是vi,价值是wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
0<N,V≤100
0<vi,wi,si≤100输出
输出一个整数,代表最大价值。
样例
输入复制
4 10 3 2 2 4 3 2 2 2 1 5 3 4
输出复制
8
【解法一】将多重背包的si
件物品,装入w和v
数组中,直接转换为01
背包!(第一种比较好理解!)
时间复杂度:O(n * v * s)~O(n^3)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int dp[N], v[N], w[N];
int vi, wi, si;
int n, c;
int k; // k代表数组下标
int main()
{
cin >> n >> c;
for (int i = 1; i <= n; i ++ )
{
cin >> vi >> wi >> si;
//将第i个物品有si件,都存入数组中
for(int j = 1; j <= si; j ++)
{
k ++;
v[k] = vi;
w[k] = wi;
}
}
//01背包
for(int i = 1; i <= k; i ++)
for(int j = c; j >= v[i]; j --) // 逆序
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
// 输出最大价值
cout << dp[c];
return 0;
}
【解法二】与解法一大同小异,换一种形式。在做01背包时,体现一下每一件物品有si件。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int v[N], w[N], s[N];
int dp[N];
int n, c;
int main()
{
cin >> n >> c;
//有n件物品
for (int i = 1; i <= n; i ++ )
{
cin >> v[i] >> w[i] >> s[i];
//第i件物品有si件,01背包
for(int k = 1; k <= s[i]; k ++)
for(int j = c; j >= v[i]; j --)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
// 输出最大价值
cout << dp[c];
return 0;
}
多重背包优化:二进制优化(状态压缩)
第一种方式直接将si件物品暴力放入数组中,当数据量比较大时就爆炸了,那我们如何有效转换到01背包问题呢?
题目描述
有N种物品和一个容量是V的背包。
第i种物品最多有si件,每件体积是vi,价值是wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出
输出一个整数,表示最大价值。
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000样例
输入复制
4 5 1 2 3 2 4 1 3 4 3 4 5 2
输出复制
10
【代码实现】
时间复杂度:O(n*v*log(s)) ~ O(nlongn)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int v[N], w[N], s[N];
int dp[N];
int n, c;
int vi, wi, si;
int cnt;
int main()
{
cin >> n >> c;
//有n件物品(枚举第i件物品选多少个)
for (int i = 1; i <= n; i ++ )
{
cin >> vi >> wi >> si;
// 对si进行二进制化 比如有10件一样的物品
// 我们转换为4组不同物品 1 2 4 3
// 这4组物品对应的体积分别为:1*v1 2*v2 4*v3 3*v4
int t = 1; // 进位
while(t <= si)
{
cnt ++;
v[cnt] = t * vi;
w[cnt] = t * wi;
si -= t;
t *= 2;
}
if(si > 0)// 如果si还有剩余,加上最后一位
{
cnt ++;
v[cnt] = si * vi;
w[cnt] = si * wi;
}
}
// 01背包
for(int i = 1; i <= cnt; i ++)
for(int j = c; j >= v[i]; j --)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
// 输出最大价值
cout << dp[c];
return 0;
}
分组背包问题
分组背包问题:有n组物品,每组物品里边有若干个物品,同样一组物品里边最多选择一个物品!
分析:
【代码实现】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N][N], w[N][N], s[N];
int dp[N];
int n, m;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> s[i];
for(int j = 0; j < s[i]; j ++)
cin >> v[i][j] >> w[i][j];
}
//类似01背包的处理
for(int i = 1; i <= n; i ++)
for(int j = m; j >= 0; j --)// 逆序
for(int k = 0; k < s[i]; k ++)
if(v[i][k] <= j)
dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
cout << dp[m];
return 0;
}
四、总结
上述这些都是经典的dp问题,要理解并熟记状态转移方程,并能快速码出代码。多练、多总结,见得多了自然就熟了,QAQ~~