好看到哭
基础理论
递归:
递:大问题分解子问题的过程 ; 归:产生答案
dp:只进行归;用已知的最底层的(递归的边界,搜索树的底),推出未知
一句话:
dp数组(不一定是数组,也可以是有限数间的来回递推)
递推关系:dfs向下递归的公式
dp数组初始化:递归的边界
动态规划题目的基础就是:回溯——记忆化——dp(一层比一层效率高)
例:
例题
打家劫舍
打家劫舍1:LCR 089. 打家劫舍
线性结构,常规做法
#include<iostream>
using namespace std;
#include<vector>
vector<int>nums = { 10,11,7,12 };
vector<int>memo(100, -1);
int dfs(int index) //暴力
{
if (index >= nums.size()) return 0;
return max( dfs(index + 2) + nums[index], dfs(index + 1) ); //这两个dfs分别代表选和不选,两种情况下的max
}
int mem(int index) //记忆化
{
if (memo[index] != -1) return memo[index]; //这个数下面的最大值我们已经记录了,直接返回即可
if (index >= nums.size()) return 0;
return memo[index]= max(mem(index + 2) + nums[index], mem(index + 1)); //记录当前下面子树最大值
}
int main()
{
cout << "暴力:"<< dfs(0) << endl << "记忆化:" << mem(0) << endl;
vector<int>dp(nums.size(), 0); //dp.1 dp[i]就是当前房屋的max
dp[0] = nums[0]; dp[1] = max(nums[0],nums[1]);
for (int i = 2; i < nums.size(); i++)
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); //画图,一定要画图,用: 10 11 7 12等试试看,会很通透
int a = nums[0], b = max(nums[0], nums[1]), c = 0; //dp.2
for (int i = 2; i < nums.size(); i++)
{
c = max(a + nums[i], b);
a = b;
b = c;
}
cout << "dp1:" << dp[nums.size() - 1] << endl;
cout << "dp2:" << b << endl;
return 0;
}
打家劫舍2:LCR 090. 打家劫舍 II
环形结构,转换为线性
#include<iostream>
#include<vector>
using namespace std;
//针对打家劫舍2的核心思想就是:1不考虑首元素,2不考虑尾元素,3首尾都不考虑 这三种情况;把环形问题转换为线性问题
//改进:3可以排除,因为1/2以及包含3了
//dfs dfs dfs
vector<int>nums = { 2,7,9,3,1 };
int dfs1(int index) //最后一家不选
{
if (index >= nums.size() - 1) return 0;
return max(dfs1(index + 2) + nums[index], dfs1(index + 1));
}
int dfs2(int index) //第一家不选
{
if (index >= nums.size()) return 0;
return max(dfs2(index + 2) + nums[index], dfs2(index + 1));
}
int dfs3(int index) //首尾都不选 这个可以忽略
{
if (index >= nums.size() - 1) return 0;
return max(dfs3(index + 2) + nums[index], dfs3(index + 1));
}
int dp_function(int index,int size,vector<int>nums) //dp封装起来
{
vector<int>dp(size);
dp[index] = nums[index];
dp[index + 1] = max(nums[index], nums[index + 1]);
for (int i = index + 2; i < size; i++)
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
return dp[size - 1];
}
int main()
{
//这个要有
if (nums.size() == 1) return nums[0];
if (nums.size() == 2) return max(nums[0], nums[1]);
//dfs
cout << max(dfs1(0), max(dfs2(1), dfs3(1))) << endl;
//dp
cout << max(dp_function(0, nums.size() - 1,nums), dp_function(1, nums.size(),nums));
return 0;
}
打家劫舍3:337. 打家劫舍 III
树形结构
爬楼梯(斐波那契)
递推草稿&视频:动态规划(dp)入门 | 这tm才是入门动态规划的正确方式! | dfs记忆化搜索 | 全体起立!!_哔哩哔哩_bilibili
#include<iostream>
using namespace std;
#include<vector>
#include<chrono>
vector<int>memo(100, -1);
int mem(int index) //记忆化(剪枝)
{
if (memo[index] != -1) return memo[index];
if (index == 1) return 1;
if (index == 2) return 2;
memo[index] = mem(index - 1) + mem(index - 2);
return memo[index];
}
int dfs(int index) //暴力
{
if (index == 1) return 1;
if (index == 2) return 2;
return dfs(index - 1) + dfs(index - 2);
}
int time(int(*k)(int),int n) //用时测试函数
{
auto start = std::chrono::steady_clock::now();
k(n);
auto end = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}
int main()
{
int n; cin >> n;
cout << dfs(n) << endl << mem(n) << endl;
memo.clear(); memo.resize(100,-1);
cout <<"暴力用时:"<< time(dfs, n) << " " <<"记忆化用时:" << time(mem, n) << endl;
vector<int>dp(n, 0); //dp 当然也可以用三个变量来互相推
dp[0] = 1, dp[1] = 2;
for (int i = 2; i < n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
cout << dp[n - 1] << endl;
return 0;
}
数字三角形
#include<iostream>
using namespace std;
#include<vector>
//最大子路径:max 最小min
vector<vector<int>>nums(100, vector<int>(100, 0));
int n;
vector<vector<int>>memo(100, vector<int>(100, -1));
int dfs(int x, int y) //暴力
{
if (x >=n || y >= n ) return 0;
return max(dfs(x + 1, y) + nums[x][y], dfs(x + 1, y + 1) + nums[x][y]);
}
int mem(int x,int y) //记忆化(剪枝)
{
if (memo[x][y]!=-1) return memo[x][y];
if (x >= n || y >= n) return 0;
return memo[x][y] = max(mem(x + 1, y) + nums[x][y], mem(x + 1, y + 1) + nums[x][y]);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j <= i; j++)
cin >> nums[i][j];
cout << "暴力:" << dfs(0, 0) << endl;
cout << "记忆化:" << mem(0, 0) << endl;
vector<vector<int>>dp(n + 1, vector<int>(n + 1, 0)); //注意这个+1;自己思考一下
for (int i = n - 1; i >= 0; i--) //dp 从下往上
for (int j = 0; j < n; j++)
dp[i][j]=max(dp[i+1][j+1]+nums[i][j],dp[i+1][j]+nums[i][j]);
cout << "dp(从下往上递推也就是遵循从上往下递归):" << dp[0][0] << endl;
return 0;
}
数字三角形递推2(上往下也就是遵循下往上递归)
#include<iostream>
using namespace std;
#include<vector>
int main()
{
//1 从1开始
int n; cin >> n;
vector<vector<int>>nums(n + 1, vector<int>(n + 1, 0));
//注意:建议以后动态规划题,原始数据的存储从1开始而不是0(方便),后面有从0开始
for (int i = 1; i <= n; i++) //注意取等
for (int j = 1; j <= i; j++) //注意取等
cin >> nums[i][j];
cout << endl;
vector<vector<int>>dp(n + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dp[i][j] = max(dp[i - 1][j] + nums[i][j], dp[i - 1][j - 1] + nums[i][j]);
for (auto i : dp) //强烈建议打印dp数组,观察数据是否正常
{
for (auto j : i)
cout << j << " ";
cout << endl;
}
//cout << dp[n][n] << endl; //err 如果直接到这里就结束就错了
//这一步是干嘛?:我们从下到上dp最后答案就是dp[0][0],但是现在上到下,所以最后一整行都是和"下到上的dp[0][0]"同性质的,所以我们需要求max
//还有一种方法,就是在上面dp时就应用res
int res = 0;
for (int i = 1; i <= n; i++)
res = max(res, dp[n][i]);
cout << res << endl;
//2 从0开始存储
vector<vector<int>>nums2(100, vector<int>(100, 0));
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j <= i; j++)
cin >> nums2[i][j];
cout << endl;
vector<vector<int>>dp2(n + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
dp2[i][j] = max(dp2[i - 1][j] + nums2[i - 1][j - 1], dp2[i - 1][j - 1] + nums2[i - 1][j - 1]);
for (auto i : dp2)
{
for (auto j : i)
cout << j << " ";
cout << endl;
}
res = 0;
for (int i = 1; i <= n; i++)
res = max(res, dp2[n][i]);
cout << res << endl;
return 0;
}
最小路径
和上一个数字三角形差不多
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int x, y; cin >> x >> y;
vector<vector<int>>nums(x, vector<int>(y, 0));
for (int i = 0; i < x; i++)
for (int j = 0; j < y; j++)
cin >> nums[i][j];
vector<vector<int>>dp(x + 1, vector<int>(y + 1, 0));
for (int i = x - 1; i >= 0; i--)
for (int j = y - 1; j >= 0; j--)
{
if (i + 1 >= x && j + 1 >= y) dp[i][j] = nums[i][j];
else if (i + 1 >= x) dp[i][j] = dp[i][j + 1] + nums[i][j];
else if (j + 1 >= y) dp[i][j] = nums[i][j] + dp[i + 1][j];
else dp[i][j] = max(dp[i + 1][j] + nums[i][j], dp[i][j + 1] + nums[i][j]);
}
cout << dp[0][0];
return 0;
}
买卖股票问题
买卖股票1:121. 买卖股票的最佳时机
暴力dfs:会超时
#include<iostream>
#include<vector>
using namespace std;
int sum = 0;
int dfs(vector<int>nums, int index)
{
if (!index) return 0; //结束条件
for (int i = index-1; i >= 0; i--) //深度
sum = max(sum, nums[index] - nums[i]); //当前抛股最大值。
sum=max(sum,dfs(nums, index - 1)); //宽度
return sum;
}
int main()
{
vector<int>nums = { 7,1,5,3,6,4 };
int n = nums.size();
cout << dfs(nums, n - 1);
return 0;
}
dp1
#include<iostream>
#include<vector>
using namespace std;
//求:花的钱少&回报高
int main()
{
vector<int>nums = { 7,1,5,3,6,4 };
int n = nums.size();
vector<vector<int>>dp(n, vector<int>(2));
dp[0][0] = -nums[0]; //买入 当前状态
dp[0][1] = 0; //卖出回报
for (int i = 1; i < n; i++)
{
dp[i][0] = max(dp[i - 1][0],0 - nums[i]); //入股,之前买入和现在买入(投资) 注意这个0一点也不多余,等到第二题你就明白了。
dp[i][1] = max(dp[i - 1][1], nums[i] + dp[i][0]); //捞钱:之前捞和现在捞(卖出+买入=利润;)
}
cout << dp[n - 1][1];
return 0;
}
贪心
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>nums = { 7,1,5,3,6,4 };
int i = 1e9, j = 0;
for (auto n : nums)
{
j = max(j, n - i);
i = min(i, n);
}
cout << j << endl;
return 0;
}
买卖股票2:122. 买卖股票的最佳时机 II
dp1
#include<iostream>
#include<vector>
using namespace std;
//求:花的钱少&回报高
int main()
{
vector<int>nums = { 7,1,5,3,6,4 };
int n = nums.size();
vector<vector<int>>dp(n, vector<int>(2));
dp[0][0] = -nums[0]; //买入 当前状态
dp[0][1] = 0; //卖出回报
for (int i = 1; i < n; i++)
{
dp[i][0] = max(dp[i - 1][0],dp[i-1][1] - nums[i]); //入股,之前买入和现在买入(投资,上一次赚的钱-这一次投入)
dp[i][1] = max(dp[i - 1][1], nums[i] + dp[i][0]); //捞钱:之前捞和现在捞(卖出+买入=利润;)
}
cout << dp[n - 1][1];
return 0;
}
买卖股票3:123. 买卖股票的最佳时机 III
dp1
#include<iostream>
#include<vector>
using namespace std;
//求:花的钱少&回报高
int main()
{
vector<int>nums = {3,3,5,0,0,3,1,4};
int n = nums.size();
vector<vector<int>>dp(n, vector<int>(5, 0));
dp[0][0] = 100; //原始资金随便你多少
dp[0][1] = dp[0][0] - nums[0]; //第一次买入
dp[0][2] = dp[0][1] + nums[0]; //第一次卖出 0
dp[0][3] = dp[0][2] - nums[0]; //第二次买入
dp[0][4] = dp[0][3] + nums[0]; //第二次卖出 0
for (int i = 1; i < n; i++)
{
dp[i][0] = dp[i - 1][0];
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - nums[i]); //第一次买
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + nums[i]); //第一次卖
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - nums[i]); //第二次买
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + nums[i]); //第二次卖
}
cout << dp[n - 1][4] - 100; //减去原始资金=净利润
return 0;
}
买卖股票4:188. 买卖股票的最佳时机 IV
dp1
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>nums = { 8,6,4,3,3,2,3,5,8,3,8,2,6 };
int k = 2; //买卖几次
int n = nums.size();
vector<vector<int>>dp(n, vector<int>(2*k+1,0));
dp[0][0] = 0; //原始资金
for (int i = 1; i < 2*k+1; i++) //dp数组初始化
if (i % 2) dp[0][i] = dp[0][i - 1] - nums[0]; //买
else dp[0][i] = dp[0][i - 1] + nums[0]; //卖
for (int i = 1; i < n; i++)
{
int x = nums[i];
for (int j = 1; j < 2*k+1; j++)
{
x *= -1;
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + x);
}
}
cout << dp[n - 1][2*k];
//for (auto i : dp)
//{
// for (auto j : i)
// cout << j << " ";
// cout << endl;
//}
return 0;
}
买卖股票5:309. 买卖股票的最佳时机含冷冻期
dp1
#include<iostream>
#include<vector>
using namespace std;
int main()
{
vector<int>nums = { 1,2,3,0,2 };
int n = nums.size();
vector<vector<int>>dp(n, vector<int>(4, 0));
//4个状态都是最大利润(口袋里的钱)
dp[0][0] = -nums[0]; //买入
dp[0][1] = 0; //保持卖出状态 口袋钱状态
dp[0][2] = 0; //今天卖 捞钱
dp[0][3] = 0; //冷冻期
for (int i = 1; i < n; i++)
{
//保持前一天买入状态 今天买:前一天是冷冻期 前一天不是冷冻期,前一天卖出状态
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - nums[i], dp[i - 1][1] - nums[i]));
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); //保持前一天卖出状态 ****前一天是冷冻期:不能是dp[i-1][3]+nums[i]你还没有买呢,不能加
dp[i][2] = dp[i - 1][0] + nums[i]; //当天卖
dp[i][3] = dp[i - 1][2]; //今天是冷冻期,说明昨天卖了股票,所以利润就是昨天卖的
}
cout << max(dp[n - 1][1], max(dp[n - 1][2], dp[n - 1][3]));
return 0;
}
背包问题:
01背包:2. 01背包问题 - AcWing题库
dfs&记忆化: dfs当数据量大就会超时/爆栈(有大量重复)
这里有两种dfs方法,两种思想,有注释,自己分析
// ***************《dfs1》****************
#if 0
#include<iostream>
#include<vector>
using namespace std;
vector<int>S; //记录每一个
int sum_2 = 0; //答案
//v&s:每个物品体积&价值 index:索引,操作哪个物品 V:当累积包体积 sum:当前价值累加
void dfs(vector<int>v,vector<int>s, int index,int V,int sum)
{
if (index > v.size() || V < 0) return; //1这里index不需要设置为>=0 不然会有元素读不到(2)
if (V >= 0) //2
{
S.push_back(sum);
sum_2 = max(sum_2, sum);
}
for (int i = index; i < v.size(); i++) //1哪怕把v.size()放进来,这里也会有一个for约束
dfs(v, s, i + 1, V - v[i], sum + s[i]);
}
int main()
{
int N, V;
cin >> N >> V; //物品数量和背包总体积 4 5
vector<int>v(N); //每个物品的体积 1 2 3 4
vector<int>s(N); //每个物品的价值 2 4 4 5
for (int i = 0; i < N; i++)
cin >> v[i] >> s[i];
dfs(v, s, 0, V, 0);
for (auto i : S)
cout << i << " ";
cout << endl;
cout << sum_2;
return 0;
}
#else //************《dfs2 指数型枚举(选和不选方法)》***************
#include<iostream>
#include<vector>
using namespace std;
int N, V;
vector<int>v;
vector<int>s;
//V:剩余容量 index:当前索引到的物品
int dfs(int V,int index) //dfs暴力
{
if (index >= N)
return 0;
else if (V - v[index] < 0) //当前背包容量装不下这个物品 和指数型枚举的区别也正是这里,加了一个容量判断
return dfs(V, index + 1); //所以跳过当前物品
else if (V - v[index] >= 0) //当前背包装的下这物品
return max(dfs(V - v[index], index + 1) + s[index], dfs(V, index + 1)); //选择这个物品和不选择当前物品
}
vector<int>memo;
int m(int V, int index) //M 记忆化
{
if (memo[V])
return memo[V]; //这里要以体积为记忆化对象,代表着当前容量的最大价值
if (index >= N)
return memo[V] = 0;
else if (V - v[index] < 0)
return memo[V] = dfs(V, index + 1);
else if (V - v[index] > 0)
return memo[V] = max(dfs(V - v[index], index + 1) + s[index], dfs(V, index + 1));
}
int main()
{
cin >> N >> V;
v.resize(N);
s.resize(N);
for (int i = 0; i < N; i++)
cin >> v[i] >> s[i];
cout << dfs(V, 0) << endl; //dfs
memo.resize(V+1); //这里分配空间要+1,因为V这个容量就是我们的答案
cout << m(V, 0) << endl; //M
return 0;
}
#endif
dp:
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int N, V;
cin >> N >> V;
vector<int>v(N);
vector<int>s(N);
for (int i = 0; i < N; i++)
cin >> v[i] >> s[i];
//********dp1二维动态规划********观察可以得出,每一个dp依赖于上方dp&上一行左边某一个dp;
//for顺序可调换,j也可以倒序。注意倒序查看时我们的01背包理解图也要倒过来。
vector<vector<int>>dp(N+1, vector<int>(V + 1)); //注意V和N都要+1
//dp[i][j]:0-i物品中自由选(一个选一次)并且容量不超过j的最大价值
//问题1:为什么N+1,V+1?不加行不行
for (int i = 1; i <= N; i++)
{
for (int j = 0; j <= V; j++)
{
//问题1:因为我们这里i是1开始,如果我们N不取等,就会导致最后一个物品无法加入计算
//自然的j可以取到V是因为我们要考虑容量为V时的情况
if (j - v[i-1] >= 0) //j-v[i]当前容量-当前物品容量
//dp[i-1][j]为不选i物品,所以保持 dp[i - 1][j - v[i-1]] + s[i-1]为选择i物品
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i-1]] + s[i]); //从这个表达式也可以看出,每一个dp依赖上方格子和上方左边某一个格子;
else
dp[i][j] = dp[i - 1][j];
}
}
for (auto i : dp)
{
for (auto j : i)
cout << j << " ";
cout << endl;
}
cout << endl;
//************dp2空间压缩一维动态规划************
//j只能倒序,for循环不可调换
//上一层拷贝到当前层,然后当前层引入了新的物品,再进行比较这个新物品选还是不选
vector<int>dp2(V + 1);
for (int i = 0; i < N; i++)
{
//这个j循环为什么倒序:反正重复,自己正序遍历推导一下。
for (int j = V; j >= v[i]; j--)
{
dp2[j] = max(dp2[j], dp2[j - v[i]] + s[i]);
cout << dp2[j] << " ";
}
cout << endl;
}
cout << endl;
for (auto i : dp2)
cout << i << " ";
return 0;
}
错误dp思想:
/* 错误dp
vector<vector<int>>dp(N+1, vector<int>(2)); //0:价值 1:容量
dp[0][0] = 0;
dp[0][1] = V;
for (int i = 1; i <= N; i++)
{
if (dp[i-1][1] - v[i-1] >= 0)
{
dp[i][0] = max(dp[i-1][0] + s[i-1], dp[i-1][0]);
if (dp[i][0] == dp[i-1][0])
dp[i][1] = dp[i-1][1];
else
dp[i][1] = dp[i-1][1] - v[i-1];
}
else //装不下 保持现状
{
dp[i][0] = dp[i - 1][0];
dp[i][1] = dp[i - 1][1];
}
}
*/
完全背包:3. 完全背包问题 - AcWing题库
物品可以重复选了
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int N, V;
cin >> N >> V;
vector<int>v(N);
vector<int>s(N);
for (int i = 0; i < N; i++)
cin >> v[i] >> s[i];
vector<vector<int>>dp1(N + 1, vector<int>(V + 1));
for (int i = 1; i <= N; i++)
for (int j = 0; j <= V; j++)
{
if (j - v[i - 1] >= 0)
dp1[i][j] = max(dp1[i - 1][j - v[i - 1]] + s[i - 1], dp1[i - 1][j]);
else
dp1[i][j] = dp1[i - 1][j];
}
for (auto i : dp1)
{
for (auto j : i)
cout << j << " ";
cout << endl;
}
cout << endl;
vector<int>dp2(V + 1);
for (int i = 0; i < N; i++)
{
for (int j = 0; j <=V; j++)
{
if (j - v[i] >= 0)
dp2[j] = max(dp2[j - v[i]] + s[i], dp2[j]);
//cout << dp2[j] << " "; 这个可以不要就是拿来观察的
}
cout << endl;
}
for (auto i : dp2)
cout << i << " ";
return 0;
}
多重背包:4. 多重背包问题 I - AcWing题库
01背包思想解决
#include<iostream>
#include<vector>
using namespace std;
int N, V;
vector<int>v; //单个体积
vector<int>w; //单个价值
vector<int>s; //数量
int main()
{
cin >> N >> V;
v.resize(N);
w.resize(N);
s.resize(N);
for (int i = 0; i < N; i++)
cin >> v[i] >> w[i] >> s[i];
vector<int>dp(V + 1);
for (int i = 0; i < N; i++)
for (int k = 0; k < s[i]; k++)
for (int j = V; j >= 0; j--) //看这个就可以区别01和完全
if (j - v[i] >= 0) dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << dp[V];
return 0;
}