0-1背包记录路径:
不要求字典序:
- 法一:逆推状态,看这个状态是由哪个状态推过来的,从而找到上一个状态即可。可用额外的标记数组,也可不标记直接用f[][]数组求解。此时如果f数组被滚动优化成了一维,就无法倒推了。
for(int i=1;i<=n;i++){
for(int j=V;j>=0;j--){
f[i][j] = f[i-1][j];
if(j < c[i]) continue;
f[i][j] = max(f[i-1][j], f[i-1][j-c[i]]+w[i]);
}
}
int j = V, q[MAXN], cnt = 0;
for(int i=n;i>=1&&j>0;i--){
if(f[i][j] == f[i-1][j-c[i]]+w[i]){
q[cnt++] = i;
j -= c[i];
}
}
for(int i=cnt-1;i>=0;i--) cout << q[i] << " \n"[i==0];
- 法二:用path[i][j]记录状态j下物品i是否被选中。path[i][j] = 1表示被选,初始化为全 0. 这样就可以得到f[i][j]状态的上一个状态。如果f是一维时可以用path标记,但这样优化f数组也就没什么意义了…
for(int i=1;i<=n;i++){
for(int j=V;j>=0;j--){
f[i][j] = f[i-1][j];
if(j < c[i]) continue;
if(f[i-1][j] < f[i-1][j-c[i]]+w[i]){
f[i][j] = f[i-1][j-c[i]]+w[i];
path[i][j] = 1;
}
}
}
int q[MAXN],cnt = 0, i = n, j = V;
while(i > 0 && j > 0){
if(path[i][j]==1){
q[cnt++] = i;
j -= c[i];
}
i--;
}
for(int i=cnt-1;i>=0;i--) cout << q[i] << " \n"[i==0];
注意: 如果 f 数组是一维,只用f数组是无法推出路径的!比如下面这样,是错误的。
/*这是错误的*/
for(int i=n;i>=1;i--){
if(f[V]==f[V-c[i]]+v[i]){
cout<<i<<' ';
V-=c[i];
}
}
字典序最小:
字典序最小背景下,通过上面的方法是无法得到正确答案的。在尝试的过程中,进入过如下误区:
-
误区一:访问第i个物品时,如果选上它不会使结果更糟,即 f[i-1][j] == f[i-1][j-c[i]]+w[i], 那就尽可能选上它,标记path[i][j]=1。因为i是从小到大遍历的,越早选上字典序越小。
给出两个反例:
- 如果有相同的两个物品,这样会选中相同的物品中靠后的。比如样例:
n = 4, V = 6
2 2
2 2
3 3
4 4
答案应为 1 4 而非 2 4
- 因为“越早选上字典序越小”这句话本身就是错的。有可能因为i被选了,破坏了前面的小字典序。样例如下:
n = 4, V = 6
1 1
2 2
3 3
5 5
答案应为1 2 3 ,并非1 4
- 误区二:访问第i个物品时,如果选上它可以使结果更优,即 f[i-1][j] < f[i-1][j-c[i]]+w[i], 那就选上它,标记path[i][j]=1。从统计f[n][V]答案的角度来说这必然正确,但从用方法一记录最小字典序路径的角度来说就未必正确了。比如样例:
n = 4, V = 5
1 1
2 2
3 3
4 4
答案应为1 4, 而非2 3. 在访问f[4][5]时,看由于f[3][5] == f[3][1]+4,所以并没有更新答案,即第4件物品没有被标记选上。但实际选上第4件物品才能选第1件,是最优解。
此外,对于这样的样例,无论等号是否取到,都无法取到答案。
n = 4, V = 5
1 1
1 1
5 5
4 4
若取等号,则会输出 2 4;若不取等号,则会输出3。 而实际答案是1 4.
综上,无论转移条件取不取等号,都无法得到正确答案。
从原理上究其原因:
所谓的”字典序最小“就是按字典排序方式,1到N号物体的选择方案排列最靠前。很明显的1肯定比2靠前,也就是说我们尽量要选择前面的物体,由于我们原先的子问题”1…i个物体,背包容量j“会分解为”1…,i-1,背包容量j“和”1…i-1,背包容量j-C[i]“两个子问题并不符合字典序的假设,因为即使第i个物体服从字典序,也无法确定前i-1个物体是否服从字典序,因为1…i-1这个子问题已经被处理过了。
于是我们把循环顺序倒过来,子问题变成了“ i…1个物体,背包容量 j ”会分解为“i-1…,1,背包容量 j” 和 “i-1…1,背包容量 j−C[i] ”两个子问题,这样只要i做了服从字典序的选择,那我们只要去继续探究 i-1…1 这个子问题就好了。 所以我们把状态转移方程改成这样
F
[
i
,
v
]
=
m
a
x
{
F
[
i
+
1
]
[
v
]
,
F
[
i
+
1
]
[
v
−
C
i
]
+
W
i
}
F[i,v]=max\lbrace F[i+1][v],F[i+1][v−C_i]+W_i \rbrace
F[i,v]=max{F[i+1][v],F[i+1][v−Ci]+Wi}
代码也只要把i逆序,i-1变成i+1即可。在输出的时候,如果 F [ i + 1 ] [ v ] = = F [ i + 1 ] [ v − C i ] + W i F[i+1][v]==F[i+1][v−C_i]+W_i F[i+1][v]==F[i+1][v−Ci]+Wi相等,尽量选加入 i 的策略,因为物品 i 必定比 i+1 靠前。
/*读入时正常顺序读*/
for(int i=n;i>=1;i--){
for(int j=V;j>=0;j--){
f[i][j] = f[i+1][j];
if(j<c[i]) continue;
f[i][j] = max(f[i+1][j],f[i+1][j-c[i]]+w[i]);
}
}
for(int i=1,j=V;i<=n&&j>=0;i++){
if(f[i][j]==f[i+1][j-c[i]]+w[i])
{
cout<<i<<" ";
v=v-c[j];
}
}