多重背包的各种优化

一、朴素做法

最朴素的解法就是枚举这种物品具体要选多少个,时间复杂度为O(n*v*m),n为物品种类,v为背包体积,m为物品个数。

以4. 多重背包问题 I为例,贴一个代码:

#include 
using namespace std;

int w[105], v[105], num[105], dp[105];

signed main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> w[i] >> v[i] >> num[i];
    for(int i = 1; i <= n; i++)
        for(int j = m; j >= 1; j--){
            for(int k = 1; k <= num[i]; k++){
                if(j-k*w[i] >= 0)
                    dp[j] = max(dp[j], dp[j-k*w[i]]+k*v[i]);
            }
        }
    cout << dp[m];
    return 0;
}

二、二进制优化

之前的做法中对于每种物品选几个都是一个个枚举的,现在考虑对每种物品进行二进制打包,也就是如果有13个某物品,那么可以打包为1个、2个、4个、6个这样的多个物品包,由于打包前任意选法总能在打包后某种选法中出现,且打包后任意选法也能在打包前某种选法中出现,这就说明两种选法实际上是等价的,于是我们就可以用打包后的01背包选法来解决这个问题,由于打包后减少了需要枚举的物品个数,所以也就减少了时间复杂度,总的时间复杂度为O(n*v*logm)。

以5. 多重背包问题 II为例,贴一个代码:

#include 
using namespace std;

int w[1005], v[1005], num[1005], dp[2005];

signed main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> w[i] >> v[i] >> num[i];
    for(int i = 1; i <= n; i++){
    	for(int j = 1; num[i]; j<<=1){//二进制划分 
    		if(num[i] < j) j = num[i];//如果最后不够2的倍数 
    		num[i] -= j;
    		for(int k = m; k >= j*w[i]; k--)
    			dp[k] = max(dp[k], dp[k-j*w[i]]+j*v[i]);
		}
	}        	
    cout << dp[m];
    return 0;
}

三、单调队列优化

这是多重背包的终极优化了,它可以使其时间复杂度降为O(n*v),对于这种优化我们可以先观察一下朴素做法,可以发现朴素做法中最内层循环就是求个max值,而且这个max值的区间是在移动的,那这显然可以用滑动窗口的做法来优化一下,关键是具体实现时按照滚动数组来写需要倒序遍历,而倒序遍历要多滑动一个窗口的大小,窗口最终会滑动v+m的长度,最坏情况下时间复杂度为O(n*(v+m)),另外这道题数据也卡的比较死,就会导致TLE了。

为了避免上面的这种情况我们需要正序滑动窗口,在正序时窗口滑动的长度就是小于v的了,为了防止覆盖掉i-1时的dp值,可以新建一个拷贝数组或者是用模2的滚动数组。

以6. 多重背包问题 III为例,贴一个代码:

#include 
using namespace std;

int w[1005], v[1005], num[1005], dp[20005], q[20005], cpy[20005];
int n, m; 

signed main()
{
	cin >> n >> m;
	for(int i = 1; i <= n; i++)
		cin >> w[i] >> v[i] >> num[i];
	for(int i = 1; i <= n; i++){
		memcpy(cpy, dp, sizeof dp);
		for(int j = 0; j < w[i]; j++){//枚举余数
			int head = 0, tail = -1;
			for(int k = j; k <= m; k+=w[i]){
				while(tail >= head && cpy[k] > cpy[q[tail]]+(k-q[tail])/w[i]*v[i]) 
					tail--;
				q[++tail] = k;
				//区间长度超出窗口长度 
				if(q[tail]-q[head] > num[i]*w[i])
					head++;
				//记录答案
				dp[k] = cpy[q[head]]+(k-q[head])/w[i]*v[i]; 
			} 
		}
	}
	cout << dp[m];
    return 0;
}

你可能感兴趣的:(算法学习,背包,算法)