0-1背包问题与数组分割问题

什么是0-1背包问题?

给定两个数组weight[N]和value[N]分别表示物品的重量和价值,现在有一个容量为s的背包,要求在背包中放置若干件物品使得物品的总价值最大。

怎么解决?

采用动态规划,令dp[i][j]表示在前i件产品中做选择且背包中物品重量小于等于j时所选择的物品的最大价值,显然第i件物品的选还是不选完全取决于之前的状态,状态转移方程为dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),如果dp[i][j] == dp[i-1][j]说明不选择第i件物品,背包中物品的重量仍然为j;如果dp[i][j] == dp[i-1][j-weight[i]] + value[i]说明选择第i件物品,背包重量增加了weight[i]且物品总价值增加了value[i]。最终的返回值为dp[N][s]。

没有进行空间优化的代码:

int knapsackSolve1(const vector<int>& weight, const vector<int>& value, int s)
{
	if (weight.size() != value.size())
	{
		return 0;
	}
	int n = weight.size();
	vector<vector<int> > dp(n, vector<int>(s + 1, 0));
	
	for (int i = 1; i < n; ++i)
	{
		for (int j = weight[i]; j <= s; ++j)
		{
			if (dp[i - 1][j - weight[i]] + value[i] > dp[i - 1][j])
				dp[i][j] = dp[i - 1][j - weight[i]] + value[i];
			else
				dp[i][j] = dp[i - 1][j];
		}
	}
	return dp[n - 1][s];
}

进行空间优化的代码:

int knapsackSolve2(const vector<int>& weight, const vector<int>& value, int s)
{
	if (weight.size() != value.size())
	{
		return 0;
	}
	int n = weight.size();
	vector<int> dp(s + 1, 0);
	for (int i = 1; i < n; ++i)
	{
		for (int j = s; j >= weight[i]; --j)//可以节省空间,j必须从大到小
		{
			//dp[j]是本轮的,dp[j - weight[i]]是上一轮的,所以必须保证dp[j]在dp[j - weight[i]]之前更新
			if (dp[j - weight[i]] + value[i] > dp[j])
			{
				dp[j] = dp[j - weight[i]] + value[i];
			}
		}
	}
	return dp[s];
}

用背包思想解决数组分割问题:

有一个无序的、元素个数为2n的正整数数组,要求把这个数组分成两个长度为n的子数组,并使两个子数组的和最接近。
用dp[i][j][c]来表示前面i个元素取j个且这j个元素和不大于c(可以认为是背包的容量)的最佳方案(也就是最接近c的方案)。如果第i个元素不取,那么dp[i][j][c] = dp[i-1][j][c];如果第i个元素被选取,那么dp[i][j][c] = dp[i-1][j-1][c-a[i]]+a[i],因此可知动态规划的递推式为dp[i][j][c] = max(dp[i-1][j-1][c-a[i]]+a[i], dp[i-1][j][c])。

代码已经进行空间优化

int divideArray(const vector<int>& array)//已经去除最外围空间
{
	int n;
	if (array.size() % 2 != 0)
	{
		return 0;
	}
	else
	{
		n = array.size() / 2;
	}
	int sum = std::accumulate(array.begin(), array.end(), 0);
	vector<vector<int>> dp(n + 1, vector<int>(sum / 2 + 2, 0));
	for (int i = 1; i < array.size(); ++i)
	{
		for (int j = 1; j <= i && j <= n; ++j)//最多只需要选取n个元素
		{
			for (int c = sum / 2 + 1; c >= array[i]; --c)//从大到小,为了节约最外围空间
			{
				dp[j][c] = max(dp[j - 1][c - array[i]] + array[i], dp[j][c]);
			}
		}
	}

	return dp[n][sum / 2 + 1];
}

关于数组分割问题《编程之美》上有一个更好的解决方案:

维护一个二维数组,用来存储array子数组和,isOk[j][s]这个bool类型的数组表示array中是否存在j个数其和等于s。通过动态规划求出isOk[n][i]=true其中i是小于sum / 2 + 1的最大的整数。

代码如下

int divideArray2(const vector<int>& array)
{
	int n;
	if (array.size() % 2 != 0)
	{
		return 0;
	}
	else
	{
		n = array.size() / 2;
	}
	int sum = std::accumulate(array.begin(), array.end(), 0);
	vector<vector<bool>> isOk(n + 1, vector<bool>(sum / 2 + 2, 0));
	isOk[0][0] = true;//选取0个元素其和为0这种情况肯定为真
	for (int i = 0; i < array.size(); ++i)
	{
		for (int j = 1; j <= n && j <= i; ++j)
		{
			for (int s = sum / 2 + 1; s >= array[i]; --s)
			{
				if (isOk[j-1][s - array[i]])
				{
					isOk[j][s] = true;
				}
			}
		}
	}

	//选出最接近sum / 2 + 1的和
	for (int s = sum / 2 + 1; s >= 0; --s)
	{
		if (isOk[n][s])
		{
			return s;
		}
	}
	return 0;
}
上面的两种方法都求出了数组分割后子数组的和,但是子数组具体包含哪些元素并不能知道,下面的代码采用深度优先搜索的方法求数组中的N个元素且它们的和为定值。

//从array中获取和为sum且元素个数为n的子序列(子序列不要求连续)
bool fixedSumNSequence(const vector<int>& array, int start, int n, int sum, vector<int>& subArray)
{
	if (subArray.size() >= n)
	{
		if (sum == 0)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	for (int i = start; i < array.size(); ++i)
	{
		subArray.push_back(array[i]);
		if (fixedSumNSequence(array, i + 1, n, sum - array[i], subArray))
		{
			return true;
		}
		subArray.pop_back();
	}
	
	return false;
}

bool fixedSumNSequence(const vector<int>& array, int n, int sum, vector<int>& subArray)
{
	if (n >= array.size())
	{
		return false;
	}
	return fixedSumNSequence(array, 0, n, sum, subArray);
}

测试用例:

int main()
{
	vector<int> weight = { 0, 10, 20, 30 };//在数组前面加上0元素是为了简便代码(动态规划的边界好确定)
	vector<int> value = { 0, 60, 100, 120 };
	cout << knapsackSolve1(weight, value, 50) << endl;
	cout << knapsackSolve2(weight, value, 50) << endl;

	vector<int> array = { 0, 1, 5, 7, 8, 9, 6, 3, 11, 20, 17, 12 };
	cout << divideArray(array) << endl;
	vector<int> subArray;
	if (fixedSumNSequence(array, array.size() / 2, divideArray2(array), subArray))
	{
		for (auto p : subArray)
		{
			cout << p << "  ";
		}
		cout << endl;
	}
	return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值