这周主要还是学习线性dp,但是因为考试这周练习确实不多。还是把一些练习和基本的背包问题整理一下。
01背包一维优化和记忆化搜索
最朴素的写法是把物品数放入递推过程中一起递推了,虽然一维优化后思路上仍然如此,但是没必要在在dp数组里加入物品数。
我们可以把物品数优化,让体积从0-v每层外循环刷新一次dp数组(当前体积的最优解),但即使是把物品数优化dp数组化为一维,外层循环一样起到了枚举每一个背包体积的对应物品数的状态。直白点说,我们还是比较的同一体积下随着物品数增加最优解对应的变化,如果当前的体积下的最优解比下一次外循环(物品数变化)大,我们就更新对应体积的dp数组值(最优解)
但是要注意,一维优化的思路不是很直观
这里的的dp[i]是原值,这个 dp[i-vb[j] 指的是上一个物品数下对应的该体积的最优解。也就是上一层外循环的对应体积dp数组值。所以重点就在这,我们要怎么才能让下一层外循环(物品数)的dp值跟上一层比较?不能从前往后枚举背包体积了,因为从前往后枚举体积,下一层外循环会从前往后覆盖原来的dp数组值,也就是说如果从前往后递推会覆盖上一层的dp数组值,导致没法与之前的最优解取最值。所以这里的解决方案是从后往前枚举体积。
#include <iostream>
#include<ctime>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
int vb[1000001] = { 0 }, w[1000001] = { 0 }, s[1000001] = { 0 };
int dp[100001] = { 0 };
int n, v;
int main()
{
cin >> n >> v;
int k = 0;
for (int j = 1; j <= n; j++)
cin >> vb[j] >> w[j];
for (int j = 1; j <= n; j++) {//物品数
for (int i = v; i >= vb[j]; i--) {//背包体积,从后往前逆向枚举
//保证可以用到上一层外循环的dp值
if (i - vb[j] >= 0)
dp[i] = max(dp[i], dp[i - vb[j]] + w[j]);
}
}
int ans = 0;
for (int j = 0; j <= v; j++) ans = max(ans, dp[j]);
cout << ans;
return 0;
}
记忆化搜索的本质就是dp。按常规的搜索思路,再用dp数组记录每次的状态
#include <iostream>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
#include<queue>
#include <iomanip>
using namespace std;
int object[1001] = { 0 };
int n, v;
int vb[10001] = { 0 }, w[10001] = { 0 };
int mx = 0;
int rec[10000][10000] = { 0 };
int dfs(int dep ,int vnow) {//dep为搜索深度,也就是当前有几个物品
int res=0;
if (rec[dep][vnow] > 0) return rec[dep][vnow];//记忆数组已经记录过就输出
if (dep == n) return 0;//搜索终点,物品都搜完了拿不了,返回0
if (vnow - vb[dep] <0)
res = dfs(dep + 1, vnow); //拿不了的
else if (vnow - vb[dep] >=0)
res= max(dfs(dep + 1, vnow), dfs(dep + 1, vnow - vb[dep]) + w[dep]);//取每个路线的最大值
return rec[dep][vnow]=res;//记忆
}
int main() {
cin >> n >> v;
for (int j = 0; j < n; j++)
cin >> vb[j] >> w[j];
cout << dfs(0, v);
}
完全背包
我是这么理解完全背包的,对于每个体积,我每次都可以选最多当前物品数的物品。所以从中挑出选完直观物品,使价值最大物品即可。写成搜索的形式会更直观
//#include<graphics.h>
#include <iostream>
//#include<fstream>
#include<ctime>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
int dp[10000] = { 0 }, vb[10001] = { 0 }, w[100001] = { 0 };
int main()
{
int n, v;
cin >> n >> v;
for (int j = 1; j <= n; j++) {
cin >> vb[j] >>w[j];
}
for (int j = 1; j <= v; j++) {
for (int i = 1; i <=n; i++) {
if (j - vb[i] < 0)continue;
else
dp[j] = max(dp[j - vb[i]] + w[i], dp[j]);
}
}
cout << dp[v];
return 0;
}
记忆化搜索
#include <iostream>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
#include<queue>
#include <iomanip>
using namespace std;
int object[1001] = { 0 };
int n, v;
int vb[10001] = { 0 }, w[10001] = { 0 };
int mx = 0;
int rec[10000] = { 0 };
int dfs(int vnow) {//dep为搜索深度,也就是当前有几个物品
//rec[vnow]已经初始化为0 ,
//也就是说dfs返回的时候价值沿路的价值为0+沿路的所选物品价值
if (rec[vnow] != 0)return rec[vnow];
for (int j = 0; j < n; j++) {//枚举每一种物品
if(vnow>=vb[j])
rec[vnow] = max(dfs(vnow - vb[j]) + w[j], rec[vnow]);
}
return rec[vnow];
}
int main() {
cin >> n >> v;
for (int j = 0; j < n; j++)
cin >> vb[j] >> w[j];
cout << dfs( v);
}
多重背包
其实就是把选择物品变成了选择物品类,像01背包那样枚举每个类别选不选,再枚举选几件即可
所以同样可以一维优化
#include <iostream>
//#include<fstream>
#include<ctime>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
int vb[100001] = { 0 }, w[100001] = { 0 }, s[100001] = { 0 };
int dp[10000] = { 0 };
int mx = 0;
int n, v;
int main()
{
cin >> n >> v;
for (int j = 1; j <= n; j++) {
cin >> vb[j] >> w[j] >> s[j];
}
for (int j = 1; j <= n; j++) {
for (int i = v; i >=0; i--) {
for (int k = 0; k <= s[j]; k++) {
if (i - vb[j] * k >= 0)
dp[i] = max(dp[i], dp[i-vb[j]*k]+w[j]*k);
}
}
}
cout << dp[v];
return 0;
}
分组背包
按我的理解,我认为分组背包其实就是同一种类内的物品不同的多重背包,所以和多重背包相似枚举组(种类),然后再枚举选择组内哪个物品使价值最大
但是存储相对麻烦,我采用邻接表的形式
#include<bits/stdc++.h>
#pragma warning (disable:4996);
#define ll long long
#define int ll
#define mem(a,b) memset(a,b,sizeof(a))
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
const int mod = 80112002;
int T, n, m, k, q;
int a, b, c;
struct node {
node(int a,int b):v(a),w(b){}
int v,w;
};
vector<node>bag[N];
vector<int>dp(N, 0);
signed main() {
//ios::sync_with_stdio(0); cout.tie(0);
#ifndef ONLINE_JUDGE
//freopen("in.txt", "r", stdin);
//freopen("out.txt", "w", stdout);
#endif
int v;
cin >> n >> v;
for (int j = 0; j <= n; j++) {
cin >> k;
for (int i = 1; i <= k; i++) {
cin >> a >> b;
bag[j].push_back({ a,b });
}
}
for (int j = 0; j < n; j++) {
for (int i = v; i >= 0; i--) {
for (int p = 0; p < bag[j].size();p++) {
node now = bag[j][p];
if (i >= now.v) {
dp[i] = max(dp[i - now.v] + now.w, dp[i]);
}
}
}
}
cout << dp[v];
}
以上是比较基本,但是很重要的背包问题。接下来是本周的一些练习。
P3183 [HAOI2016]食物链 记忆化搜索(dp)
本周就是求图内从入度为0的点(食物链底端)到出度为0的点(顶端)的路径条数。
dp主要记录的是,从该顶点为起点有几条可以到达出度为0的点。
采用搜索的方式遍历图找到底端到顶端的路径会更方便,所以使用记忆化搜索的方式
当能找到一条到顶端的路径时返回1,对每个子节点都遍历求和,即可得到当前起点的食物链条数
设i为入度为0的起点,j为i的子节点,dp表示到终点的路径条数。有状态转移
dp[i]+=dp[j]
#include<bits/stdc++.h>
#pragma warning (disable:4996);
#define ll long long
#define int ll
#define mem(a,b) memset(a,b,sizeof(a))
#define endl '\n'
using namespace std;
const int N = 2e5 + 10;
int T, n, m, k, q;
int a, b, c;
vector<int>gra[N];
vector<int>dp(N, 0);
vector<int>ind(N, 0),oud(N,0);
inline int dfs(int now) {
if (dp[now])return dp[now];
if (oud[now] == 0)return 1;
int res = 0;
for (auto& x : gra[now]) {
res+=dfs(x);
}
return dp[now] = res;
}
signed main() {
// ios::sync_with_stdio(0); cout.tie(0);
#ifndef ONLINE_JUDGE
//freopen("out.txt", "w", stdout);
#endif
cin >> n>>m;
for (int j = 1; j <= m; j++) {
scanf("%d%d", &a, &b);
gra[a].emplace_back(b);
ind[b]++;
oud[a]++;
}
int ans = 0;
for (int j = 1; j <= n; j++) {
if(ind[j]==0&&oud[j]!=0)
ans += dfs(j);
}
printf("%d", ans);
}
213. 打家劫舍 II - 力扣(LeetCode)dp,状态讨论
如果要抢一个房间i,那么他的状态则有i-2转移来。如果不抢,则继承上一个房间的状态。状态转移
dp[x]=max(dp[x-2]+nums[i],dp[x-1])
这题的状态转移非常简单,但是这题首尾成环,一个比较简便的思路就是单独讨论起点和终点的状态。
如果第一个房间抢了,那么第n个房间就不管他了。其值继承dp[n-1]
所以我们从第1个房间计算到第n-1个房间即可。
如果第一个房间不抢,那么第n个房间可抢可不抢,所以放到动态规划过程里递推计算即可。
class Solution:
def r1(self,nums:list[int],s,e):
n=len(nums)
dp=[0 for _ in range(n+10)]
for x in range(s,e+1):
i=x-1
if x==1:
dp[x]=nums[i]
else:
dp[x]=max(dp[x-2]+nums[i],dp[x-1])
return dp[e]
def rob(self, nums: List[int]) -> int:
n=len(nums)
if n==1:
return nums[0]
return max(self.r1(nums,1,n-1),self.r1(nums,2,n))