背包

动态规划中的背包问题

01背包

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为v[i],把它放进袋子里会获得w[i]的收益,每种物品至多能用一次,问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

分析: 考虑前i个物品,总体积为j时分为两种情况: 第i个物品没取,问题变成了考虑了前 i−1个物品,总体积为j时的情况; 第i个物品取了,问题变成了考虑了前i −1个物品,总体积为j-v[i]时的情况;

状态:f[i][j]表示考虑了前i个物品,总体积为j时的最大收益。 转移:分别对应第i个物品没取f[i][j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i])和取了两种情况。

错误写法

根据状态转移方程写出了这样的代码:

int n, m;
int f[1010][1010];
int v[1010], w[1010];
signed main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n; i++) {
        for (int j = v[i]; j <= m; j++) {
            f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

在这个写法中,当j<=v[i]时,数组没有被更新,因此发生错误

正确写法

int n, m;
int f[1010][1010];
int v[1010], w[1010];
signed main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            if(j<v[i]) f[i][j] = f[i - 1][j];
            else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

滚动数组

但是当数据过大时,使用二维数组占用的空间太大,由于在计算i行时只用到了i-1行,所以可以用一个g数组存i-1行的数据,循环滚动f和g

int f[1010], g[1010];
int v[1010], w[1010];
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            if (j >= v[i]) g[j] = max(f[j], f[j - v[i]] + w[i]);
            else g[j] = f[j];
        }
        memcpy(f, g, sizeof(g));
    }
    cout << f[m] << endl;
    return 0;
}

一维做法

状态:用f[i]表示总体积为i时的最大收益。 转移:f[j] = max(f[j], f[j - v[i]] + w[i])表示取前i个物品总体积为j时的最大情况

注意在枚举体积时,要从大向小枚举,若小到大枚举,则在枚举过程中可能会多次放入同一物品

int f[1010];
int v[1010], w[1010];
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++) {
        for (int j = v[i]; j <= m; j++) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

完全背包

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为v[i],把它放进袋子里会获得w[i]的收益,每种物品能用无限多次,问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

完全背包的区别就是每个物品可以拿无数次

分析: 考虑前i个物品,总体积为j时分为两种情况: 第i个物品没取,问题变成了考虑了前 i−1个物品,总体积为j时的情况; 第i个物品取了,问题变成了考虑了前i 个物品,总体积为j-v[i]时的情况;

状态:用f[i][j]表示考虑了前i个物品,总体积为j时的最大收益。 转移:f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i])分别对应第i个物品没取和取了两种情况。

普通做法:

int n, m;
int f[1010][1010];
int v[1010], w[1010];
signed main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            if (j < v[i]) f[i][j] = f[i - 1][j];
            else f[i][j] = max(f[i - 1][j], f[i][j - v[i]] + w[i]);
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

一维做法: 因为完全背包就是在01背包的基础上每个物品可以拿无数次,因此在枚举体积时正向枚举,即可在体积允许的情况下,将多个同一物品放入背包。

int f[1010];
int v[1010], w[1010];
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++) {
        for (int j = v[i]; j <=m; j++) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

延伸

如果要让背包里的物品体积正好是m,应该怎么做呢?

只需要把f数组初始化为负无限大,然后让f[0]=0,这样只有在体积刚好凑成的时候f才能数组才能被更新。

int f[1010];
int v[1010], w[1010];
int main() {
    int n, m;
    cin >> n >> m;
    memset(f, 128, sizeof(f));
    f[0] = 0;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for (int i = 1; i <= n; i++) {
        for (int j = v[i]; j <= m; j++) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
}

多重背包

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为v[i],把它放进袋子里会获得w[i]的收益,可以用l[i]次。问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

分析:

i种物品可以使用l[i] 次,我们可以把它拆成l[i]个物品,每个物品只能用一次; 然后就是个01背包问题啦!

int f[1010];
int v[1010], w[1010], l[1010];
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> l[i];
    for (int i = 1; i <= n; i++) {
        for (int k = 1; k <= l[i]; k++) {
            for (int j =m; j >=v[i]; j--) {
                f[j] = max(f[j], f[j - v[i]] + w[i]);
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

但是当数据过大时,这种做法会超时,可以采用二进制分组优化

二进制分组优化

1,2,…,2m中选一些数字相加,可以得出任意[0,2m+1)内的值,每个数字只能用一次;

利用这条结论,我们可以用二进制表示体积m,例如:

5=1+4 6=2+4 7=1+2+4 8=1+1+2+4

因此对于第i种物品,可以使用l[i]次,那么可以把它拆成O(log n)个物品,每个物品只能用一次;

int f[2010];
int v[2010], w[2010], l[2010];
int main() {
	int n, m;
	cin >> n >> m;
	for (int i = 1; i <= n; i++) { 
		cin >> v[i] >> w[i] >> l[i];
	}
	for (int i = 1; i <= n; i++) {
		int res = l[i];
		for (int k = 1; k <= res; res -= k, k *= 2) {
			for (int j = m; j >= v[i] * k; j--) {
				f[j] = max(f[j], f[j - v[i]*k] + k*w[i]);
			}
		}
		for (int j = m; j >= v[i] * res; j--)
			f[j] = max(f[j], f[j - res * v[i]] + res * w[i]);
	}
	cout << f[m] << endl;
	return 0;
}

分组背包

有n种物品要放到一个袋子里,袋子的总容量为m。第i个物品属于第a[i]组,每组物品我们只能从中选择一个。第i种物品的体积为v[i],把它放进袋子里会获得w[i]的收益。问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

分析: 把考虑了前i组物品以后,总体积为0,1,...,m时的最大收益都记下来; 考虑了前i组物品,总体积为j时分为两种情况: 第i组物品一个没取,问题变成了考虑了前 i−1组物品,总体积为j时的情况; 第i组物品取了,我们枚举取了其中的物品k,问题变成了考虑了前 i−1组物品,总体积为 j−v[k]时的情况; 状态:用f[i][j]表示考虑了前i组物品,总体积为j时的最大收益。 转移:f[i][j]= max (f[i-1][j], f[i-1][j-v[k]]+w[k])),其中c[i]表示第i组中物品的编号集合。

int n, m;
int f[1010][1010];
int v[1010], w[1010], a[1010];
vector<int>c[1010];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i] >> v[i] >> w[i];
		c[a[i]].push_back(i);
	}
	for (int i = 1; i <= 1000; i++) {
		for (int j = 0; j <= m; j++) {
			f[i][j] = f[i - 1][j];
			for (auto k : c[i]) {
				if (v[k] <= j)
					f[i][j] = max(f[i][j], f[i - 1][j - v[k]] + w[k]);
			}
		}
	}
	cout << f[1010][m] << endl;
	return 0;
}

一维做法

int n, m;
int f[1010];
int v[1010], w[1010], a[1010];
vector<int>c[1010];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i] >> v[i] >> w[i];
		c[a[i]].push_back(i);
	}
	for (int i = 1; i <= 1000; i++) {
		for (int j = m; j; j--) {
			for (auto k : c[i]) {
				if (v[k] <= j)
					f[j] = max(f[j], f[j - v[k]] + w[k]);
			}
		}
	}
	cout << f[m] << endl;
	return 0;
}

二维背包

有n种物品要放到一个袋子里,袋子的总容量为m,我们一共有k点体力值。第i种物品的体积为v[i],把它放进袋子里会获得w[i]的收益,并且消耗我们t[i]点体力值,每种物品只能取一次。问如何选择物品,使得在物品的总体积不超过m并且花费总体力不超过k的情况下,获得最大的收益?请求出最大收益。

分析: 把考虑了前i个物品以后,总体积为0,1,...,m,总体力为0,1,...k时的最大收益都记下来; 考虑了前i个物品,总体积为j,总体力为x时分为两种情况: 第i个物品没取,问题变成了考虑了前 i−1个物品,总体积为j,总体力为x时的情况; 第i个物品取了,问题变成了考虑了前 i−1个物品,总体积为 j−v[i],总体力为x-t[i]时的情况; 状态:用f[i][j][x]表示考虑了前i个物品,总体积为j,总体力为x时的最大收益。 转移:f[i][j][x]=max(f[i−1][j][x],f[i−1][j−ν[i]][x−t[i]]+w[i])

这里就不贴普通做法的三维数组做法了,直接贴二维

int n, m, k;
int v[1001], w[1001], t[1001];
int f[1001][1001];
int main() {
	cin >> n >> m >> k;
	for (int i = 1; i <= n; i++) {
		cin >> v[i] >> w[i] >> t[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = m; j >= v[i]; j--) {
			for (int x=k; x >= t[i]; x--) {
				f[j][x] = max(f[j][x], f[j - v[i]][x - t[i]] + w[i]);
			}
		}
	}
	cout << f[m][k] << endl;
	return 0;
}

单调队列

有n个生物,第i个生物会在第i到第a[i]天出现,它的攻击力为b[i]。其中对于所有i(1≤i<n),满足a[i]≤a[i]+1。请输出每天出现的生物的攻击力的最大值。

分析: 假设现在考虑到了第i天,对于第 j(j<i)个生物,如果 b[j]<b[i],那么之后第j个生物都不用考虑了(在它存在期间,永远被第i个生物压制) ; 用一个队列,按编号从小到大的顺序,存放到目前为止,哪些生物是需要被考虑的(有可能在未来的某一天成为攻击力最高的生物) ;

int n, a[100010], b[100010], c[100010][2];
int main() {
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%d %d", &a[i], &b[i]);
	}
	int k = 0, l = 1;
	for (int i = 1; i <= n; i++) {
		for (; k >= l && b[i] >= c[k][0]; --k);//删掉队尾攻击力小的
		c[++k][0] = b[i]; c[k][1] = a[i];
		printf("%d\n", c[l][0]);
		for (; k >= l && c[l][1] == i; ++l);//删掉队头已经超过时限的元素
	}
    return 0;
}
//选择数字P
int head = 1, tail = 1;
    for (int i = 1; i <= n; i++) {
        dp[i] = q[head] + a[i];
        while (head <= tail && q[tail] >= dp[i]) tail--;
        q[++tail] = dp[i], p[tail] = i;
        while (head <= tail && p[head] < i - k) head++;
    }

多重背包

有n种物品要放到一个袋子里,袋子的总容量为m,第i种物品的体积为v[i],把它放进袋子里会获得w[i]的收益,可以用l[i]次。问如何选择物品,使得在物品的总体积不超过m的情况下,获得最大的收益?请求出最大收益。

分析: 把考虑了前i种物品以后,总体积为0,1,…,m时的最大收益都记下来; 考虑了前i种物品,总体积为j时分为两种情况:

第i种物品没取,问题变成了考虑了前 i−1种物品,总体积为j时的情况;

第i种物品取了,枚举它取了k次,问题变成了考虑前 i−1 种物品,总体积为 j−v[i]×k时的情况;

状态:用f[i][j]表示考虑了前i种物品,总体积为j时的最大收益。

转移: f[i][j]=max(f[i−1][j],max f[i−1][j−v[i]×k]+w[i]×k)

把j按照 mod v[i]分类,只有同一类的状态间可以进行转移; 例如:当 v[i]=4时,其中一类为1,5,9,13,... 在某一类中下标为k的位置,可以转移到下标在 [k+1,k+l[i]]中的位置; 每个位置取最大值, 下标k,x之间转移的时候,(x−k)*w[i]怎么处理? 要求max(f[k]+(x−k)*w[i]),f[k]表示某一类中下标为k时的最值;因为x*w[i]对于每个位置是定值; 所以把 f[k]−k*w[i]放进单调队列就行啦!

int n, m, v, w, l,t, f[10001], c[10001][2];
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> v >> w >> t;
		for (int j = 0; j < v; j++) {
			int k = 0, l = 1;//k是尾指针,l是头指针
			for (int pos = j, x = 1; pos <= m; pos += v, ++x) {
				int e = f[pos] - x * w, r = x + t;
				for (; k >= l && c[k][0] <= e; --k);
				c[++k][0] = e; c[k][1] = r;
				f[pos] = c[l][0] + x * w;
				for (; k >= l && c[l][1] == x; ++l);
			}
		}
	}
	cout << f[m] << endl;
	return 0;
}

Last updated