背包路径记录方法总结

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是从小到大遍历的,越早选上字典序越小。

    给出两个反例:

  1. 如果有相同的两个物品,这样会选中相同的物品中靠后的。比如样例:

n = 4, V = 6
2 2
2 2
3 3
4 4
答案应为 1 4 而非 2 4

  1. 因为“越早选上字典序越小”这句话本身就是错的。有可能因为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][vCi]+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][vCi]+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];
	}
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值