动态规划 ——0-1背包问题(1)

回眸只為那壹抹淺笑 提交于 2019-12-31 21:42:55

0-1背包问题

本文内容来来源于《计算机算法设计与分析》(王晓东著),是笔者的学习笔记,内容不当处,欢迎留言探讨

  • 问题描述:给定n中物品和一背包,物品i的重量是wiw_i,价值为viv_i,背包容量为c。应如何选择装入背包中的物品,使得装入背包中物品的总价值最大。
  • 目标函数:maxi=1nviximax\sum_{i=1}^{n}v_ix_i即装入背包中的物品价值达到最大。
  • 约束条件:i=1nvixi<c\sum_{i=1}^{n}v_ix_i<c,即装入背包中的物品总重量小于背包的容量。
    好吧,其实我看到这里已经要睡着了,脑子里想到的画面是装到背包里的东西,和重量有个线球关系,你该考虑的不该是体积么,反正我装背包,从来不担心重量。不过不要紧,清醒一下,洗把脸,心里默念,去~~~的

1、最优子结构

动态规划的本质是对问题进行分解,对每个子问题求解,从而得到原问题的解,当然,其灵魂是建立一个表格,或者什么都行,存储一部分子问题的解,避免进行重复计算。

  • (y1,y2...yn)(y_1,y_2...y_n)是0-1背包问题的最优解,则(y2,y3,...yn)(y_2,y_3,...y_n)是一个子问题的最优解,该子问题的最优解情况如下:maxi=2nvixi{i=2nwixicw1y1xi{0,1},2inmax\sum_{i=2}^nv_ix_i\\\begin{cases}\sum_{i=2}^{n}w_ix_i\leqslant c-w_1y_1\\x_i\in\{0,1\},2\leqslant i \leqslant n\end{cases}
    书中写的是wiy1w_iy_1,目测笔误,下版建议修改。

2、递归关系

  • 子问题i的问题描述: 背包容量为jj,可选择物品编号为i,i+1,...ni,i+1,...n,对应的价值的最优值为m(i,j)m(i,j)。数学表达:maxk=invkxk{k=inwkxkjxk{0,1},iknmax\sum_{k = i}^{n}v_kx_k\\\begin{cases}\sum_{k=i}^nw_kx_k\leqslant j\\x_k\in\{0,1\},i\leqslant k \leqslant n\end{cases}
    笔者一开始对放入背包中的物品编号从iinn感觉很不适应,觉得如果从1到 ii的话也不是不行。
  • 递归关系构建: m(i,j)={max{m(i+1,j),m(i+1,jwi)+vi},jwim(i+1,j),0j<wi m(i,j)=\begin{cases}max\{m(i+1,j),m(i+1,j-w_i)+v_i\},j\geqslant w_i\\m(i+1,j) ,0\leqslant j<w_i\end{cases}
    m(i,j)={vn,jwn0,0j<wn m(i,j)=\begin{cases}v_n,j \geqslant w_n\\0,0\leqslant j<w_n\end{cases}
    笔者这里看到jwij\geqslant w_i,第一反应是背包剩余容量大于物品i的重量,但这是与jj的定义是相反的,再次强调jj表示的是背包容量,是子问题的背包容量。
    递归关系公式中,第一个式子表示的含义是,物品编号为从iinn,背包容量为jj的子问题的最佳解有两种情况:
    • 背包容量jj小于物品ii的重量wiw_i: 此时物品ii是放不进背包中的,最佳解的值就是我们已经得到i+1...ni+1...n的解。
    • 背包容量jj大于物品ii的重量wiw_i:此时物品ii是可以放到背包中的,那我们要分别作出讨论。**背包中不放入物品ii **,因为不放入物品ii并不影响背包容量,所以此时最佳解的值为m(i+1,j)m(i+1,j); 背包中放入物品ii,放入后,背包中已有物品价值是viv_i,背包原本容量为jj,此时剩余jwij-w_i,所以最佳解的值为m(i+1,jwi)+vim(i+1,j-w_i)+v_i。此时最佳解就是这两个值中的最大值。

递归分析,我们的初始问题肯定是1....n1....n求解,逐层递归,最后一个子问题是物品nn的子问题,再将值逐层返回上层。

3、代码实现

终于开始了,笔者都有些迫不及待了,不过真的要吐槽一下这本书,讲的极为简单,对自学的学渣可能不太友好,代码还没有注释,看得想撕书。

// 0-1背包问题
// m[i][j]记录不同情况下的最佳值
#include<iostream>
#include<algorithm>
using namespace std;
void Knapspack(int v[], int w[], int c, int n, int m[][101]) {
	/* v是价值向量,w是质量向量,c是背包容量,n物品数量,m是最价值矩阵*/
	int jMax = min(w[n] - 1, c);   
	/*从递归关系的公式中,我们可以看到最后一层递归是可选物品为n,此时的子问题中,背包容量有两种可能:
	容量小于物品n的质量,容量大于或等于物品n的质量,这与递归关系中的两种情况相同。
	此处w[n]-1表示的就是小于n的质量,选择w[n]-1和c中小的那个,就是先给容量小于w[n]的情况赋值*/
	for (int j = 0; j <= jMax; j++)        // jMax小于w[n],所以此处对m[n][j]赋值0 
		m[n][j] = 0;
	for (int j = w[n]; j <= c; j++)        // 此处j从w[n]开始增长,
		m[n][j] = v[n];                    //表示的情况是子问题中背包容量足够装下物品n,将n放入背包
	for (int i = n - 1; i > 1; i--) {   
     /*可选物品为n的情况已经确定了,就已经具备了逐层求解的条件。求解从可选物品为n-1,n开始。*/
		jMax = min(w[i] - 1, c);           // 先确定 0<= j <w[i]时的m[i][j]
		for (int j = 0; j <= jMax; j++)
			m[i][j] = m[i + 1][j];
		for (int j = w[i]; j <= c; j++)    // 确定j>=w[i]时的m[i][j]
			m[i][j] = max(m[i + 1][j], m[i + 1][j - w[i]] + v[i]);
	}
	m[1][c] = m[2][c];                     //w[1]>c时的m[1][j]
	if (c >= w[1])                         //c>=w[1]时的m[1][j]
		m[1][c] = max(m[1][c], m[2][c - w[1]] + v[1]);
}

这一部分笔者在看书的时候看了好久,困了看,看了困,理解之后就发现,我去,这么简单!也希望大家在学习的时候不要害怕,不难的,学他!

  • 通过上面的函数,我们可以求出m[i][j]m[i][j]矩阵,但是并没有求出最优解,不过我们可以利用m[i][j]m[i][j]矩阵求出最优解,这一部分是从m[i][j]m[i][j]倒推,比如我们有5个商品,m[1][5]=10m[1][5]=10,如果m[1][5]=m[2][5]m[1][5]=m[2][5],则1未放入背包中,反之放入,依此倒推。
    x[i]={1,m[i][j]m[i+1][j]0,m[i][j]=m[i+1][j]j={j,x[i]=0j=jw[i],x[i]=1 x[i]=\begin{cases} {1, m[i][j] \neq m[i+1][j]}\\{0,m[i][j]=m[i+1][j]}\end{cases}\\ j=\begin{cases}{j,x[i]=0}\\j=j-w[i],x[i]=1\end{cases}
    下面为求最优解的代码:
void Traceback(int m[][101], int w[], int c, int n, int x[]) {
	for (int i = 1; i < n; ++i) {
		if (m[i][c] == m[i + 1][c])
			x[i] = 0;
		else {
			x[i] = 1;
			c -= w[i];
		}
	}
	x[n] = m[n][c] ? 1 : 0;    //m[n][c]不为0,x[n]赋值1,为0,赋值0
}
  • 主函数
int main() {
	int c;
	cout << "输入背包容量: ";
	cin >> c;
	int w[101], v[101], m[101][101], x[101];
	cout << "输入物品数量:  ";
	cin >> w[0];      // w[0]存储物品数量
	v[0] = w[0];
	cout << "输入各物品质量: ";
	for (int i = 1; i <= w[0]; ++i)
		cin >> w[i];
	cout << "输入各物品价值: ";
	for (int i = 1; i <= w[0]; ++i)
		cin >> v[i];
	Knapspack(v, w, c, w[0], m);
	Traceback(m, w, c, w[0], x);
	for (int i = 1; i <= w[0]; ++i)
		cout << x[i] << ' ';
	cout <<endl<< m[1][c];
	return 0;
}

运行结果:在这里插入图片描述

4.时间复杂度分析

上述代码时间复杂度最大的一部分为m[i][j]m[i][j]矩阵赋值二重循环部分,外层循环次数为n-2,内层循环次数为c+1,复杂度为O((n2)(c+1))O((n-2)(c+1)),不考虑常数,复杂度为O(nc)O(nc)

以上是动态规划思想的0-1背包问题的求解过程,有错误之处欢迎留言指出,下一篇是对算法的改进。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!