一、基本动态规划
和贪心算法一样,在动态规划中,可将一个问题的解决方案视为一系列决策的结果。不同的是,在贪心算法中,每用一次贪心准则便做出一次不可撤回的决策,而在动态规划中,还要考察每个最优决策序列中是否包含一个最优子序列。当一个问题具有最优子结构时,我们会想到用动态规划去解她,虽然有些问题存在最简单,有效的办法,但只要我们总是做出当前看来最好的选择就可以了。
贪心算法所作的选择可以依赖于以往做过的选择,但绝不依赖于将来的选择,也不依赖于子问题的解,这使其具有一定的速度优势。解决动态规划问题首先要确定子问题,判断哪些变量与问题的规模有关,其次是确定状态,推出状态转移方程。
在动态规划问题中,状态转移方程是核心,要判断其确定是不是满足所有的条件,在处理好边界问题。
二、背包问题
1.01背包01背包是在M件物品中取出若干件放在空间为W的背包,每件物体的体积为W1,W2,…Wn,与之对应的价值为P1,P2,…Pn。
1.01背包
是背包中最简单的问题。01背包的约束条件是给定几种物品,每种物品有且只有一个,并且有权值和体积两个属性。在01背包问题中,因为每种物品只有一个,对于每个问题只需要考虑选与不选两种情况。如果不选将其放入背包中,则不需要处理。如果选择将其放入背包中,由于不清楚之前收入的物品占据了多大的空间,需要枚举将这个物品放入背包后可能占据背包空间的所有情况。
2.01背包例题
例题1:现有四个物品,小偷背包总容量为8,怎么可以偷得价值最多的物品?如:物品编号:1 2 3 4物品重量:2 3 4 5物品价值:3 4 5 8记f(k,w):当背包容量为w,现有k件物品可以偷,所能偷到最大价值
1. #include<cstdio>
2. #include <iostream>
3. #include<cstdlib>
4. #include<algorithm>
5. #include <cstring>
6. using namespace std;
7. int f[5][9] = {0};
8. int w[5] ={0,2,3,4,5};
9. int v[5] ={0,3,4,5,8};
10. int main(){
11. int i,j;
12. memset(f,0,sizeof(f));
13. for(int i = 1; i < 5; i++){
14. for(int j = 1; j < 9; j++){
15. if(w[i] > j){
16. f[i][j] = f[i - 1][j];
17. }else{
18. f[i][j] = max(f[i - 1][j],f[i - 1][j - w[i]] + v[i]);
19. }
20. }
21. }
22. for(int i = 0; i < 5; i++){
23. for(int j = 0; j < 9; j++){
24. cout <<"f["<< i << "]" <<"[" << j<<"]= " << f[i][j] << endl;
25. }
26. }
27.
28. }
3.01背包空间优化
滚动数组:根据后无效性原则,下一状态只与上一个状态有关,可以压缩成一维数组。每运行一遍就让让dp数组刷新一遍;注意求新的dp数组时必须倒着,背包剩余容量要从容量最大值开始,不然物体的小容量会被这个物品的小容量值覆盖。比如dp[8] = max(dp[8],dp[4] + 6)此时这一层的dp[4]还未更新还为上一层的,直接使用即可,如果正着推,此时dp[4]已经覆盖为这一层的,无法正确求出后面的;
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
输入格式
第一行有 22 个整数 TT(1 \le T \le 10001≤T≤1000)和 MM(1 \le M \le 1001≤M≤100),用一个空格隔开,TT 代表总共能够用来采药的时间,MM 代表山洞里的草药的数目。
接下来的 MM 行每行包括两个在 11 到 100100 之间(包括 11 和 100100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
如果你是辰辰,你能完成这个任务吗?
1).未进行压缩的动态规划
#include<iostream>
#include<algorithm>
using namespace std;
const int n = 10001;
int dp[n][n];
int t[n];
int v[n];
int main(){
int T,m,i,j;
cin >> T >> m;
for(i = 1; i <= m; i++){
cin >> t[i] >> v[i];
}
for(i = 1; i <= m; i++){
for(j = 1; j <= T; j++){
if(t[i] > j){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - t[i]] + v[i]);
}
}
}
cout << dp[m][T];
}
2).压缩后的动态规划(利用滚动数组)
#include<bits/stdc++.h>
using namespace std;
const int n = 10001;
int dp[n];
int t[n];
int v[n];
int main(){
int T,m;
cin >> T >>m;
for(int i = 1; i <= m; i++){
cin >> t[i] >> v[i];
}
for(int i = 1; i <= m; i++){
for(int j = T; j >= 1; j--){ //递推j必须倒着循环,否则会被覆盖
if(j >= t[i]){
dp[j] = max(dp[j],dp[j - t[i]] + v[i]);
}
}
}
cout << dp[T];
}
例题2:许多前年,在泰迪的家乡,有一个人被称为古收藏家,他喜欢收集不同的骨头。这个人有一个大袋子(袋子的体积为V),以及很多收集的骨头,很明显,不同的骨头有不同的价值和不同的体积,现考虑到每个骨头的价值以及他的袋子,请计算出这个人能收集到骨头的最大总价值?
输入:
开始输入一个数t,表示有t组测试数据。每个测试数据开始输入两个数n和v,表示骨头的总数和袋子的体积。接下一行输入n个数,表示n个骨头的价值。最后一行输入n个数,表示n个骨头的体积。
输出:
每个测试数据输出一个数,表示最大的价值。样例输入:15 101 2 3 4 55 4 3 2 1样例输出14
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
const int n = 1000;
int dp[n][n];
int val[n];
int vo[n];
int main()
{
int t;
cin >> t;
while (t--)
{
int n, v;
memset(dp, 0, sizeof(dp));
cin >> n >> v;
for (int i = 1; i <= n; i++)
{
cin >> val[i];
}
for (int i = 1; i <= n; i++)
{
cin >> vo[i];
}
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= v; j++)
{
if (vo[i] > j)
{
dp[i][j] = dp[i - 1][j]; //放不下
}
else
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - vo[i]] + val[i]); //将第i件物品放入背包之后的总价值
}
}
}
cout << dp[n][v] << endl;
}
}
三、硬币问题
现有若干枚硬币,硬币面值分别为1,5,10,20,50.100要凑出价值为w,至少需要多少枚硬币。
比如w= 15按照贪心的思维:我们每次选取面值最大的硬币去尝试。15 = 1*11 + 4 * 1;至少需要5枚但是实际上是需要3枚面值为5的硬币即可解决
方案1)贪心思维,面临鼠目寸光的境地
2)暴力枚举,复杂度太高
3)DP当我们要凑出价值为15的硬币,我们面临3种情况当我们取了价值为1的硬币时,将面临凑出价值为14的情况当我们取了价值为5的硬币时,将面临凑出价值为10的情况当我们取了价值为11的硬币是,将面临凑出价值为4的情况记凑出价值n所需要的最少硬币数为f(n)
原理:f(n)只与f(n-1),f(n-5),f(n-11)有关f(n) = min{1 + f(n -1),1 + f(n - 5), 1 + f(n - 11)}
总结:最优子结构大问题的最优解可以由小问题的最优解推出无后效性一旦f(n)请确定,那么之后直接调用它的值就可以,不要再去关心f(n)的计算过程
#include<bits/stdc++.h>
using namespace std;
int min(int a,int b){
return a <b ?a:b;
}
int main(){
int dp[100],i;
dp[0] = 0;
int cost;
for(int i = 1; i <= 15; i++){
cost = 0x3f3f3f3f;
if(i - 1 >= 0) cost = min(cost,dp[i - 1] + 1);
if(i - 5 >= 0) cost = min(cost,dp[i - 5] + 1);
if(i - 11 >= 0) cost = min(cost,dp[i - 11] + 1);
dp[i] = cost;
cout <<"dp[" <<i <<"] = " <<" "<< dp[i]<<endl;
}
}
四、最长上升子序列(LIS)
最长上升子序列给定长度为n的序列,从中选取一个子序列,这个子序列需要单调递增。问最长上升子序列的长度。
例如:1 5 2 3 11 7 9最长上升子序列为:1 2 3 7 9记f(x)为以a[x]结尾的Lis的长度,所以LIS = max{f(x)}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int a[N];
int dp[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
for (int i = 1; i <= n; i++)
{
dp[i] = 1;
for (int j = 1; j < i; j++)
{
if (a[j] < a[i])
{
dp[i] = max(dp[i], dp[j] + 1);
}
}
cout << i << " " << dp[i] << endl;
}
}
例题:给出一个长度为N的数组,数组中每个数在-1000和1000之间。求出这个数组最大的连续区间和。
例如(6,-1,5,4,-7),则这个数组中最大的连续区间和为14
输入:
第一行输入T,表示T组数据每组数据的开始为一个整数n,表示数组的长度。接下来有n个数,表示这个数组的n个数
输出:
对于每组数据,输出最大的连续区间和,并给出开始位置和结束位置推出状态转移方程为dp[i] = max(dp[i],a[i])
#include<iostream>
using namespace std;
int const N = 1e6 + 10;
int dp[N];
int arr[N];
int main(){
int t;
cin >> t;
while(t--){
int n;int begin ,end ,pos = 1;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> arr[i];
}
dp[1] = arr[1];
int maxn = -0x3f3f3f3f;
for(int i = 1; i <= n; i++){
if(dp[i - 1] + arr[i] >= arr[i]){
dp[i] = dp[i - 1] +arr[i];
} else{
dp[i] = arr[i];
pos = i;
}
if(dp[i] > maxn){
maxn = dp[i];
begin = pos;
end = i;
}
}cout << maxn << " "<<begin <<" " <<end<<" "<<endl;
} }
借助vector实现
1. #include<iostream>
2. #include<vector>
3. using namespace std;
4. const int N = 1e6 + 10;
5. int dp[N];
6. vector<int> arr;
7. int main(){
8. int t;
9. cin >> t;
10. while(t--){
11. int n;
12.
13. int begin, end, pos = 1;
14. cin >> n;
15. arr.push_back(0); //vector自动从0开始使用,无法人为改变
16. for(int i = 1; i <= n; i++){
17. int a;
18. cin >> a;
19. arr.push_back(a);
20. }
21. dp[1] = arr[1];
22. int maxn = -0x3f3f3f3f;
23. for(int i = 1; i <= n; i ++){
24. if(dp[i - 1] + arr[i] >= arr[i]){
25. dp[i] = dp[i - 1] + arr[i];
26. }
27. else{
28. dp[i] = arr[i];
29. pos = i;
30. }
31.
32. if(dp[i] > maxn){
33. maxn = dp[i];
34. begin = pos;
35. end = i;
36. }
37. }
38. cout << maxn << " " << begin << " " << end << " " << endl;
39. arr.clear(); //注意:vector必须全部删除,要不会影响下一组数据,数组可以实现覆盖
40. }
41. }