文章目录
闫氏DP分析法
DP问题主要是靠经验的,所以要多加练习总结
1.01背包问题
f[i][j]表示只看前i个物品,总体积是j的情况下,总价值最大是多少
result=max(f[n][0v])
f[i][j];
1.不选第i个物品,f[i][j]=f[i-1][j];
2.选第i个物品,f[i][j]=f[i-1][j-v[i]]+w[i];
朴素版代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N][N],v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)//默认f[0][0]为0
for(int j=0;j<=m;j++)
{
f[i][j]=f[i-1][j];
if(j>=v[i])//有空间才行
f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);//有i无i作比较
}
int res=0;
for(int i=0;i<=m;i++)
res=max(res,f[n][i]);
cout<<res<<endl;//直接输出f[n[[m]也行;
return 0;
}
优化版
很容易想到吧if判定条件放在循环的时候,可以把数组f变为一维的,因为i=i-1,可以知道,都是从上一个状态转移过来的但是j 不是上一个转移的(本来i=i-1,即j对于的是上一个i,现在把i去掉),j 可以从大到小,这样就上从上一个转移过来的
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N],v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)//从大到小
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
一个循环
其实就是简单变形
#include<iostream>
using namespace std;
int f[10000];
int main()
{
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++)
{
int v,w;
cin>>v>>w;
for(int j=V;j>=v;j--)
f[j]=max(f[j],f[j-v]+w);
}
cout<<f[V]<<"\n";
return 0;
}
2.完全背包
题目链接
与01背包的差别就是这个可以无限选
前i-1项已经定型,第i项选k个i ,选第i个的时候空间还有v-k*i;
可以得出f[i][j]=f[i-1][j],f[i][j]=f[i-1][j-v]+w
推导出
f[i][j] = max(f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2v]+2w,…)
f[i][j-v] = max(f[i-1][j-v],f[i-1][j-2v]+w,f[i-1][j-3v]+2w…)
第一个式子的第2项和第二个式子的第一项作比较,发现只相差一个w
推导为f[i,j] = max(f[i-1][j],f[i][j-v]+w);
朴素代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N][N],v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=0;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]);//
}
cout<<f[n][m]<<endl;
return 0;
}
优化代码
优化思路:先把if判断消去,而且i和j是等价的,所以数组f可以直接变为一维
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N],v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=v[i];j<=m;j++)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
一个循环
#include<iostream>
using namespace std;
int f[10000];
int main()
{
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++)
{
int v,w;
cin>>v>>w;
for(int j=v;j<=V;j++)
f[j]=max(f[j],f[j-v]+w);
}
cout<<f[V]<<"\n";
return 0;
}
3.多重背包 1
题目链接
每个物品随便选,可以想到第i件物品选s[i]个依次比较
可以得到公式**f[i][j]=max(f[i][j],f[i-1][j-v[i]k]+w[i]k)
朴素代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010;
int n,m;
int f[N][N],v[N],w[N],s[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i]>>s[i];
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
f[i][j]=max(f[i][j],f[i-1][j-v[i]*k]+w[i]*k);
cout<<f[n][m]<<endl;
return 0;
}
4.多重背包2
题目链接
其实就是数据变大的多重背包1
优化代码(背包1的优化)
倍增法
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=25000;
int n,m;int v[N],w[N]; int f[N];
int main()
{
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)
{
int a,b,s;
cin>>a>>b>>s;
int k=1;
while(k<=s)
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
for(int i=1;i<=cnt;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}
优化代码(背包1的优化)
单调队列,思路和上面代码一样,用容器和结构体,但是比上面的慢
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 2022;
int n, m, f[N];
struct Good {
int v, w;
};
int main()
{
vector<Good> goods;
cin >> n >> m;
for (int i = 0; i < n; i++)
{
int v, w, s;
cin >> v >> w >> s;
for (int j = 1; j <= s; j++)
{
s -= j;
goods.push_back({v * j, w * j});
}
if (s > 0) goods.push_back({v * s, w * s});
}
for (auto good : goods)
for (int j = m; j >= good.v; j--)
f[j] = max(f[j], f[j - good.v] + good.w);
cout << f[m] << endl;
return 0;
}
5.多重背包3
朴素代码
其实就是多重背包的终极优化
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=20020;
int n,m,f[N],g[N],q[N];
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
int v,w,s;
cin>>v>>w>>s;
memcpy(g,f,sizeof(f));
for(int j=0;j<v;j++)
{
int hh=0,tt=-1;
for(int k=j;k<=m;k+=v)
{
f[k]=g[k];
if(hh<=tt&&k-s*v>q[hh]) hh++;
if(hh<=tt) f[k]=max(f[k],g[q[hh]]+(k-q[hh])/v*w);
while(hh<=tt&&g[q[tt]]-(q[tt]-j)/v*w<=g[k]-(k-j)/v*w) tt--;
q[++tt]=k;
}
}
}
cout<<f[m]<<'\n';
return 0;
}
6.混合背包
题目链接
混合背包就是把01背包,完全背包,多重背包组合起来,判断一下不同情况就行
朴素代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1111;
int n, m, f[N];
struct Thing
{
int kind;
int v, w;
};
vector<Thing> things;
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
{
int v, w, s;
cin >> v >> w >> s;
if (s < 0)
{
things.push_back({-1, v, w});
}
else if (s == 0)
{
things.push_back({0, v, w});//判断不同情况入栈
}
else //多重背包,拆分为多个01背包
{
for (int k = 1; k <= s; k *= 2)
{
s -= k;
things.push_back({-1, v * k, w * k});
}
if(s>0)things.push_back({-1, v * s, w * s});//最后剩余的
}
}
for (const auto& thing : things) //赋值
{
if (thing.kind < 0)
{
for (int j = m; j >= thing.v; j--)
{
f[j] = max(f[j], f[j - thing.v] + thing.w);//01背包求最大值
}
}
else
{
for (int j = thing.v; j <= m; j++)
{
f[j] = max(f[j], f[j - thing.v] + thing.w);//多重背包求最大值
}
}
}
cout << f[m] << '\n';
return 0;
}
7.二维费用的背包问题
题目链接
其实和01背包一样,就加了一个限制条件(重量)
就这个核心公式: f[i][j1][j2] = max (f[i][j1][j2],f[i - 1][j1 - w1][j2 - w2] + v)
朴素代码
#include <iostream>
using namespace std;
const int N = 110;
int n,m1,m2;
int f[1010][N][N];
int main () {
cin >> n >> m1 >> m2;
for (int i = 1;i <= n;i++) {
int w1,w2,v;
cin >> w1 >> w2 >> v;
for (int j1 = m1;j1 >= 0;j1--) {
for (int j2 = m2;j2 >= 0;j2--) {
f[i][j1][j2] = f[i - 1][j1][j2];
if (j1 >= w1 && j2 >= w2)
f[i][j1][j2] = max (f[i][j1][j2],f[i - 1][j1 - w1][j2 - w2] + v);
}
}
}
cout << f[n][m1][m2] << endl;
return 0;
}
优化代码
减少不必要循环
#include<iostream>
#include<algorithm>
using namespace std;
const int N=111;
int n,v,m,f[N][N];
int main()
{
cin>>n>>v>>m;
for(int i=0;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
for(int j=v;j>=a;j--)
for(int k=m;k>=b;k--)
f[j][k]=max(f[j][k],f[j-a][k-b]+c);
}
cout<<f[v][m]<<'\n';
return 0;
}
8.分组背包问题
题目链接
其实就是多个01背包,每一组可以选也可以不选,就是第i组选第k个
式子1:f[i-1][j]
式子2:f[i-1][j-v[i][k]]+w[i][k]
即f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
朴素代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=191;
int n,m,v[N][N],w[N][N],s[N],f[N];
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];
}
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)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
cout<<f[m]<<endl;
return 0;
}
朴素代码
其实和上面前一样
#include<iostream>
#include<algorithm>
using namespace std;
const int N=190;
int n,m,v[N],w[N],s,f[N];
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
cin>>s;
for(int j=0;j<s;j++)
cin>>v[j]>>w[j];
for(int j=m;j>=0;j--)
for(int k=0;k<s;k++)
if(v[k]<=j)
f[j]=max(f[j],f[j-v[k]]+w[k]);
}
cout<<f[m]<<endl;
return 0;
}
9.有依赖的背包问题
题目链接
树形DP和背包问题的结合
朴素代码
#include <iostream>
#include <vector>
using namespace std;
int f[110][110]; // f[x][v] 表示选择以 x 为子树的物品,在容量不超过 v 时所获得的最大价值
vector<int> g[110];//vector<int> g[110] 用于构建树的邻接表,存储每个节点的子节点。
int v[110], w[110];//v[110] 数组存储每个物品(节点)的体积,w[110] 数组存储每个物品的价值。
int n, m, root;
int dfs(int x, int m)
{
for (int i = v[x]; i <= m; i++) f[x][i] = w[x]; // 点 x 必须选,所以初始化 f[x][v[x] ~ m] = w[x]
for (int i = 0; i < g[x].size(); i++)
{
int y = g[x][i];
dfs(y, m - v[x]);//,对于每个子节点 y ,递归调用 dfs(y, m - v[x]) ,即计算子节点在剩余容量下的最大价值。
//两层循环更新当前节点 x 在不同容量 j的最大价值。内层循环枚举分配给子节点 y 的容量 k ,通过比较更新 f[x][j] 的值。
for (int j = m; j >= v[x]; j--)
{ // j 的范围为 v[x] ~ m, 小于 v[x] 无法选择以 x 为子树的物品
for (int k = 0; k <= j - v[x]; k++)
{ // 分给子树 y 的空间不能大于 j - v[x], 不然都无法选根物品 x
if (f[x][j - k] + f[y][k] > f[x][j])
{
f[x][j] = f[x][j - k] + f[y][k];
}
}
}
}
return f[x][m];
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int ff;
cin >> v[i] >> w[i] >> ff;
if (ff == -1)
root = i;
else
g[ff].push_back(i);
}
cout << dfs(root, m);
return 0;
}
10.背包问题求方案数
题目链接
01背包换了一种问法,就是找到最大值之后确定有几个最大值
朴素代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1111,mod=1000000007,INF=1000000;
int n,m;
int f[N],g[N];
int main()
{
cin>>n>>m;
g[0]=1;
for(int i=1;i<=m;i++)
f[i]= -INF;
for(int i=0;i<n;i++)
{
int v,w;
cin>>v>>w;
for(int j=m;j>=v;j--)
{
int t=max(f[j],f[j-v]+w);//选和不选的最大值
int s=0;
if(t==f[j]) s+=g[j];//第一种决策
if(t==f[j-v]+w) s+=g[j-v];//第二种决策
if(s > mod) s -= mod;//取模
f[j]=t;//最优解
g[j]=s;//方案数
}
}
int maxw = 0;
for(int i = 0;i <= m;i ++)
maxw=max(maxw,f[i]);//求最优解,体积不一定用完
int res=0;
for(int i = 0;i <= m;i ++)
if(maxw==f[i])//所有最优解方案
{
res+=g[i];
if(res>=mod) res -= mod;//取模
}
cout<<res<<'\n';
return 0;
}
11.背包问题求具体方案
题目链接
和01完全一样,就是找一下具体是选哪一个,不过要注意一下字典序最小,正常求法是最大,所以逆过来
朴素代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1111;
int n ,m,f[N][N],v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=n;i>=0;i--) //因为要求字典序最小,所以这里逆序
for(int j=0;j<=m;j++)
{
f[i][j]=f[i+1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
}
int i=1,j=m;
while(i<=n)
{
if(j>=v[i]&&f[i+1][j-v[i]]+w[i]>=f[i+1][j])//右边可以取到最大值
{
cout<<i<<' ';
j-=v[i];
i++;
}
else i++;//不能选择第i个物品(左边集合)
}
return 0;
}