上述方法的时间复杂度为\(O(nm)\),时间复杂度无法再优化了,但是空间复杂度可以。
这样的优化叫做滚动数组。
如果用最原始的转移方程有:
cin >> m >> n;
for(int i = 1; i <= n; i++)
scanf("%d%d", &v[i], &w[i]);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
f[i][j] = max(f[i-1][j], f[i][j]);
if(j >= v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]]+w[i]);
}
cout << f[n][m] << endl;
优化一维之后变为:
cin >> m >> n;
for(int i = 1; i <= n; i++)
scanf("%d%d", &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;
复杂度为\(O(nm)\)。
这里有个小问题,就是在第二维循环的时候体积是从大到小枚举的,至于原因在下一节完全背包解释。
完全背包板子题,首先看朴素的写法。
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
f[i][j] = max(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;
滚动数组写法。
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;
复杂度为\(O(nm)\)。
这里给出01背包的滚动数组写法:
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]);
我们可以发现,01背包是逆序循环的,完全背包是顺序循环的。
我们同样可以先用01背包的相似思路思考,考虑每个物品使用1次,2次,3次,...
设\(f(i,j)\)表示
那么就有状态转移方程:\(f(i,j)=max(f(i-1,j)f,(i-1,j-k*v(i))+w(i))\),其中\(1\leq k\leq c_i\)。
附上代码:
for(int i = 1; i <= n; i++)
scanf("%d%d%d", &v[i], &w[i], &c[i]);
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
for(int k = 1; k <= c[i] && k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k*v[i]] + w[i] * k);
cout << f[n][m] << endl;
采用滚动数组优化空间。
for(int i = 1; i <= n; i++)
scanf("%d%d%d", &v[i], &w[i], &c[i]);
for(int i = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--)
for(int k = 1; k <= c[i] && k * v[i] <= j; k++)
f[j] = max(f[j], f[j-k*v[i]]+k*w[i]);
cout << f[m] << endl;
此法较为简单,易于思考,可惜效率不高。
时间复杂度:\(O(nmc)\)
二进制拆分:由二进制的知识可以知道,任意一个整数可以由二次幂数字相加而得,所以可以用此方法把\(c_i\)件物品拆分成\(logc_i\)件物品。
这样的话我们需要先需要将\(n\)件物品先拆分成\(nlogc\)件物品,然后用01背包的方法写出。
代码如下:
scanf("%d%d", &n, &m);
int cnt = 0;
for(int i = 1, vv, ww, cc; i <= n; i++)
{
scanf("%d%d%d", &vv, &ww, &cc);
int k = 1;
while(k <= cc)
{
v[++cnt] = vv * k;
w[cnt] = ww * k;
cc -= k; k *= 2;
}
if(cc > 0)
{
v[++cnt] = vv * cc;
w[cnt] = ww * cc;
}
}
n = cnt;
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;
时间复杂度\(O(nmlogc)\)。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int maxm = 20000 + 10;
int n, m;
int f[maxm], q[maxm], g[maxm];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1, v, w, c; i <= n; i++)
{
scanf("%d%d%d", &v, &w, &c);
memcpy(g, f, sizeof(f));
//滚动数组优化空间 f[] = f[i-1][];
for(int j = 0; j < v; j++)
{
int hh = 0, tt = -1;
for(int k = j; k <= m; k += v)
{
f[k] = g[k];
//如果窗口的内容超过了c[i]个, 出队
if(hh <= tt && k-c*v > q[hh])
hh++;
if(hh <= tt) //max(f(i-1,k), f(i-1,能转移里最大)+个数*w[i])
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] << endl;
return 0;
}
时间复杂度\(O(nm)\)。
就分类讨论就行了,不是那么困难。
但是还是要区分一下,这一题的数据允许使用二进制拆分,二进制拆分后用01背包跑,所以01背包可以和多重背包混成一个条件,但是01和完全背包是不一样的,所以需要记录来区分一下。
当然要注意的是二进制拆分过后点数量多了\(log\)的级别,所以数组要开大一点。
代码:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 10;
int n, m, v[maxn], w[maxn], s[maxn], f[maxn], cnt;
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1, vv, ww, cc; i <= n; i++)
{
scanf("%d%d%d", &vv, &ww, &cc);
if(cc < 0) //01
{
v[++cnt] = vv;
w[cnt] = ww;
s[cnt] = 1;
}
else if(cc == 0) //完全
{
v[++cnt] = vv;
w[cnt] = ww;
s[cnt] = 0;
}
else if(cc > 0) //多重
{
int k = 1;
while(k <= cc)
{
v[++cnt] = vv * k;
w[cnt] = ww * k;
cc -= k; k *= 2;
s[cnt] = 1;
}
if(cc > 0)
{
v[++cnt] = vv * cc;
w[cnt] = ww * cc;
s[cnt] = 1;
}
}
}
n = cnt;
for(int i = 1; i <= n; i++)
{
if(s[i] == 1)
{
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j-v[i]]+w[i]);
}
else
{
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;
}
限制条件增多了一个维度,也就是重量,状态也只需要增加一个维度即可。
设\(f(i,j,k)\)表示前\(i\)件物品中,体积为\(j\),重量为\(k\)的物品能获得的最大的价值。
转移方程有\(f(i,j,k)=max(f(i,j,k),f(i-1,j-v(i),k-m(i))+w(i))\).
滚动一下的话可以取消掉第一维也就是\(i\)这一维,代码如下:
scanf("%d%d%d", &N, &V, &M);
for(int i = 1; i <= N; i++)
scanf("%d%d%d", &v[i], &m[i], &w[i]);
for(int i = 1; i <= N; i++)
for(int j = V; j >= v[i]; j--)
for(int k = M; k >= m[i]; k--)
f[j][k] = max(f[j][k], f[j-v[i]][k-m[i]]+w[i]);
cout << f[V][M] << endl;
分情况讨论,分为不选第\(i\)组的物品和选第\(i\)组的第\(j\)个物品。
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
scanf("%d", &c[i]);
for(int j = 1; j <= c[i]; j++)
scanf("%d%d", &v[i][j], &w[i][j]);
}
for(int i = 1; i <= n; i++)
for(int j = m; j >= 0; j--)
for(int k = 1; k <= c[i]; k++)
if(j >= v[i][k])
f[j] = max(f[j], f[j-v[i][k]]+w[i][k]);
cout << f[m] << endl;
其实这是树型+背包问题的一个结合,也就是树上背包。
假设说\(1\)号节点是\(2\)号节点的父节点,那么对于\(2\)节点及其子树构成一组物品,对于\(2\)号节点有许多种体积的子背包,对于\(1\)号节点而言,他只能从这许多种体积的子背包中选一个加入自己的背包中,而\(1\)号节点有可能有许多个子节点,所以其实是树上分组背包。
设\(f(i,j)\)表示在选节点\(i\)的情况下并且背包体积为\(j\)的情况下,以\(i\)为根的整棵子树的最大收益是多少。
对树进行\(dfs\)即可。
int head[maxn<<1], ver[maxn<<1], nex[maxn<<1], tot;
inline void add_edge(int x, int y){
ver[++tot] = y; nex[tot] = head[x]; head[x] = tot;
}
void dfs(int x)
{
for(int i = head[x]; i; i = nex[i])
{
int y = ver[i]; dfs(y);//递归求出儿子的方案
for(int j = m-v[x]; j >= 0; j--) //分组背包
for(int k = 0; k <= j; k++) //选择一个子节点的k体积的子背包
f[x][j] = max(f[x][j], f[x][j-k]+f[y][k]);
}
//加上根节点的价值
for(int i = m; i >= v[x]; i--) f[x][i] = f[x][i-v[x]]+w[x];
for(int i = 0; i < v[x]; i++) f[x][i] = 0; //体积不够选不了
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1, p; i <= n; i++)
{
scanf("%d%d%d", &v[i], &w[i], &p);
if(p == -1) root = i;
else add_edge(p, i);
} dfs(root);
cout << f[root][m] << endl;
return 0;
}
设\(f(j)\)表示背包体积为\(j\)的最优方案的价值。
设\(d(j)\)表示背包体积为\(j\)的最优解方案数。
初始化\(d(i)=1\),因为什么也不装也是一种方案。
ll f[maxn], d[maxn];
int n, m, v[maxn], w[maxn];
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d%d", &v[i], &w[i]);
for(int i = 0; i <= m; i++) d[i] = 1;
for(int i = 1; i <= n; i++)
{
for(int j = m; j >= v[i]; j--)
{
ll tmp = f[j-v[i]] + w[i];
if(tmp > f[j])
{
f[j] = tmp;
d[j] = d[j-v[i]];
}
else if(tmp == f[j])
d[j] = (d[j] + d[j-v[i]]) % mod;
}
}
cout << d[m] << endl;
return 0;
}
当然如果\(f(n,m)=f(n-1,m-v)+w\)说明最后一个物品被选了。
为了求出字典序最小的,我们逆序推出最优解,输出方案,详见代码。
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d%d", &v[i], &w[i]);
for(int i = n; i >= 1; i--)
{
for(int j = 0; j <= m; j++)
{
f[i][j] = max(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 cnt = m;
for(int i = 1; i <= n; i++)
{
if(cnt - v[i] >= 0 && f[i][cnt] == f[i+1][cnt-v[i]]+w[i])
{
printf("%d ", i);
cnt -= v[i];
}
}
原文:https://www.cnblogs.com/zxytxdy/p/12073415.html