01背包:
1.P1048 [NOIP2005 普及组] 采药(裸的01背包板子)
Code:(这里用了滚动数组)
#include <bits/stdc++.h>
using namespace std ;
int t , m ;
int val[500] , w[500] ;
int dp[2][10000] ;
int main() {
cin >> t >> m ;
for(int i = 1 ; i <= m ; i ++) cin >> w[i] >> val[i] ;
for(int i = 1 ; i <= m ; i ++)
for(int j = 0 ; j <= t ; j ++)
{
if(w[i] > j) dp[i%2][j] = dp[(i+1)%2][j] ;
else dp[i%2][j] = max(dp[(i+1)%2][j] , dp[(i+1)%2][j-w[i]] + val[i]) ;
}
cout << dp[m%2][t] ;
}
2.P1060 [NOIP2006 普及组] 开心的金明(和上面那题类似)
3 P1855 榨取kkksc03(两个限制条件)
Code:
#include <bits/stdc++.h>
using namespace std ;
int n , m , t ;
int v[101] , w[101] ;
int dp[2][201][201] ;
int main() {
cin >> n >> m >> t ;
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 ++)
for(int k = 0 ; k <= t ; k ++)
{
if(v[i] <= j && w[i] <= k) dp[i%2][j][k] = max(dp[(i+1)%2][j][k] , dp[(i+1)%2][j-v[i]][k-w[i]] + 1) ;
else dp[i%2][j][k] = dp[(i+1)%2][j][k] ;
}
cout << dp[n%2][m][t] ;
}
完全背包 :
1.P5020 [NOIP2018 提高组] 货币系统
这道题基本一眼就能看出是让求集合中有多少个数不能被其它数表示。
典型的搜索题(但是剪枝不好可能会TLE)
我们还有一种更安全的方法------完全背包。
核心思路就是如果大小为m的货币能被表示 ,那么大小为m+a[i]的货币也能被表示 , 因为货币是无限的 , 所以跑一遍完全背包就可以算出来有多少货币能被其它数表示了。
Code:
#include <bits/stdc++.h>
using namespace std ;
int t ;
int dp[25100] ;
int a[101] ;
int main() {
cin >> t ;
while(t --) {
memset(dp , 0 , sizeof(dp)) ;
int n , maxn = 0 ;
cin >> n ;
for(int i = 1 ; i <= n ; i ++) cin >> a[i] ;
sort(a + 1 , a + 1 + n) ;
int ans = n ;
dp[0] = 1 ;
for(int i = 1 ; i <= n ; i ++) {
if(dp[a[i]]) {
ans -- ;
continue ;
}
for(int j = a[i] ; j <= a[n] ; j ++)
dp[j] = dp[j] | dp[j-a[i]] ;
}
cout << ans << endl ;
}
}
插入:
完全背包和01背包在一维数组更新答案时循环顺序不同:
完全背包是顺序 , 01背包是逆序 。
因为顺序下 , 后面的答案更新可能会和前面的有交叉 , 但在逆序下, 后面的答案更新不会和前面的交叉 , 对应了无限个和只有一个 。
分组背包
P1757 通天之分组背包
分组背包就是把多个物品分为多个组 , 每个组里面只能选择一件物品 。
分组背包题的做法:
第一重for循环枚举每一组 , 第二重for循环枚举背包容量 ,第三重for循环枚举某一组的所有物品 , 然后和01背包一样更新答案。
解释一下循环的顺序:
背包dp需要背包空间大小循环完一遍才能表示把一个物品放进去了 , 因此在循环背包空间的过程中枚举物品 , 就相当于只放一种物品 。
Code:
#include <bits/stdc++.h>
using namespace std ;
int n , m ;
int dp[1001] ;
int w[1001] , val[1001] ;
vector<int> num[1001] ;
int maxc = 0 ;
int main() {
cin >> m >> n ;
for(int i = 1 ; i <= n ; i ++) {
int c ;
cin >> w[i] >> val[i] >> c ;
num[c].push_back(i) ;
maxc = max(maxc , c) ;
}
for(int k = 1 ; k <= maxc ; k ++) { // 组数
for(int j = m ; j >= 0 ; j --) { // 背包容量
for(int i = 0 ; i < num[k].size() ; i ++) // 枚举每组的物品
if(w[num[k][i]] <= j) dp[j] = max(dp[j] , dp[j-w[num[k][i]]] + val[num[k][i]]) ;
}
}
cout << dp[m] ;
return 0 ;
}
例题1:
P1064 [NOIP2006 提高组] 金明的预算方案 (本质是01背包)
这里与普通的01背包不同的是 , 多了物品之间的从属关系 , 即买某个物品可能需要连带着买其它的物品 。
我们可以把这些物品按照从属关系分成若干组 ,然后在某一组中枚举选哪几个 (拿这题来讲 , 如果某个主件有两个附件 , 那么我们可以只拿主件 , 也可以拿主件和第一个附件 , 也可以拿主件与第二个附件 , 还可以拿主件和两个附件), 有点像分组背包 , 不同的是每组拿的数量没有限制 。
Code:
#include <bits/stdc++.h>
using namespace std ;
int dp[32100] , n , m ;
int v[65] , p[65] ;
vector<int> son[65] ;
bool vis[65] ;
int main() {
cin >> m >> n ;
for(int i = 1 ; i <= n ; i ++) {
int q ;
cin >> v[i] >> p[i] >> q ;
if(q) son[q].push_back(i) ;
else vis[i] = true ;
}
for(int i = 1 ; i <= n ; i ++) {
for(int j = m ; j >= v[i] ; j --) {
if(!vis[i]) continue ;
dp[j] = max(dp[j] , dp[j-v[i]] + v[i] * p[i]) ;
if(son[i].size() == 1 && j >= v[i] + v[son[i][0]]) dp[j] = max(dp[j] , dp[j-v[i]-v[son[i][0]]] + v[i] * p[i] + v[son[i][0]] * p[son[i][0]]) ;
if(son[i].size() == 2) {
if(j >= v[i] + v[son[i][0]]) dp[j] = max(dp[j] , dp[j-v[i]-v[son[i][0]]] + v[i] * p[i] + v[son[i][0]] * p[son[i][0]]) ;
if(j >= v[i] + v[son[i][1]]) dp[j] = max(dp[j] , dp[j-v[i]-v[son[i][1]]] + v[i] * p[i] + v[son[i][1]] * p[son[i][1]]) ;
if(j >= v[i] + v[son[i][1]] + v[son[i][0]])
dp[j] = max(dp[j] , dp[j - v[i] - v[son[i][1]] - v[son[i][0]]] + v[i] * p[i] + v[son[i][1]] * p[son[i][1]] + v[son[i][0]] * p[son[i][0]]) ;
}
}
}
cout << dp[m] ;
return 0 ;
}
例2:
P2946 [USACO09MAR]Cow Frisbee Team S
这道题如果是求能力总和的最大值就是个01背包的板子了 , 但是往往事与愿违 , 出题人让我们求能力总和能整除某个数的所有方案数 。
但其实我们换个思路想 , 我们可以把幸运数字F看成背包的容量 ,由此我们设dp[i][j]表示选择前i个物品余数为j时的最大方案数 ,由此可得状态转移方程:dp[i][j] = dp[i][j] + dp[i-1][j] + dp[i-1][(j-r[i]+f)%f]
Code:
#include <bits/stdc++.h>
using namespace std ;
const int mod = 1e8 ;
long long dp[2001][1001] , r[2001] , n , f ;
int main() {
cin >> n >> f ;
for(int i = 1 ; i <= n ; i ++) cin >> r[i] , r[i] %= f ;
for(int i = 1 ; i <= n ; i ++) dp[i][r[i]] = 1 ;
for(int i = 1 ; i <= n ; i ++)
for(int j = 0 ; j < f ; j ++)
dp[i][j] = (dp[i][j] + dp[i-1][j] + dp[i-1][(j-r[i]+f)%f]) % mod ;
cout << dp[n][0] ;
}
注:显然可以利用滚动数组优化。
例3:
P1156 垃圾陷阱
这道题可以抽象成一个背包。其中垃圾井的深度代表背包的容量 , 垃圾的高度代表物品的重量 , 吃掉垃圾所能维持的生命代表物品的价值 。由此我们可以猜测dp[i][j]代表处理了前i个垃圾 , 高度堆到j时 的最大生命 , 那么对于每个垃圾 , 都有吃与不吃两种选择 , 对应了背包中的选与不选 。
但是 , 这里不选会产生一个影响 ---- 垃圾会堆上导致高度增加 , 因此 , 我们在转移堆垃圾的状态时要改变一下 。
Code :
#include <bits/stdc++.h>
using namespace std ;
int dp[101][101] , m , n ; // dp数组表示处理了前i个垃圾高度达到j后所能拥有的最大生命
struct node {
int t = 0 , f , h ;
}a[1001] ;
bool cmp(node x , node y) {return x.t < y.t ;} // 重载
int main() {
cin >> m >> n ;
memset(dp , -1 , sizeof dp) ; // 刚上来dp数组一定要初始化为一个小于0的数 , 因为dp = 0时处于频死状态 , 还是可以操作的
for(int i = 1 ; i <= n ; i ++) cin >> a[i].t >> a[i].f >> a[i].h ;
sort(a + 1 , a + 1 + n , cmp) ;
dp[0][0] = 10 ; // 初始化 , 刚开始有10点生命
for(int i = 1 ; i <= n ; i ++) {
for(int j = 0 ; j <= m ; j ++) {
if(dp[i-1][j] < a[i].t - a[i-1].t) continue ; // 如果当前状态撑不到下一个垃圾的到来 , 直接跳过这个状态
if(j + a[i].h >= m) { // 如果堆上这个垃圾超过m了, 直接输出这个垃圾到来的时间
cout << a[i].t ;
return 0 ;
}
dp[i][j+a[i].h] = max(dp[i][j+a[i].h] , dp[i-1][j] - (a[i].t - a[i-1].t)) ; // 堆
dp[i][j] = max(dp[i][j] , dp[i-1][j] + a[i].f - (a[i].t - a[i-1].t)) ; // 不堆
}
}
int res = 10 ; // 用来输出走不出去的情况
for(int i = 1 ; i <= n ; i ++) {
if(a[i].t - a[i-1].t > res) {
cout << a[i-1].t + res ;
return 0 ;
}
res -= a[i].t - a[i-1].t ;
res += a[i].f ;
}
cout << a[n].t + res ;
return 0 ;
}
例4:
P5322 [BJOI2019]排兵布阵
这道题还是比较好想的 , 根据题意可以抽象出一个类似分组背包的东西来 。n座城堡表示n个物品 ,每名玩家拥有的士兵数表示背包的容量 , 每个物品的价值就是城堡的编号 ,派出的士兵数表示物品的重量 , s名玩家的相同的城堡看做一组。乍一看每一组我们可以选择多个物品呀 , 但是我们可以这样想 , 我们给每一组的物品重量排一个序 , 那么我们在选择了一个物品后 , 是不是重量比它小的物品就都选上了 , 价值就增加选上的物品数*这一组的价值 ,这样处理后就是一个完完全全的分组背包了 , 代码貌似都差不多 。
Code:
#include <bits/stdc++.h>
using namespace std ;
long long s , n , m ;
long long a[101][101] ;
long long dp[20001] ;
int main() {
cin >> s >> n >> m ;
for(int i = 1 ; i <= s ; i ++)
for(int j = 1 ; j <= n ; j ++)
cin >> a[j][i] ;
for(int i = 1 ; i <= n ; i ++) {
sort(a[i] + 1 , a[i] + 1 + s) ;
for(int j = m ; j >= 0 ; j --) {
for(int k = s ; k >= 1 ; k --) {
if(2 * a[i][k] + 1 <= j) dp[j] = max(dp[j] , dp[j-2*a[i][k]-1] + k * i) ;
}
}
}
cout << dp[m] ;
return 0 ;
}
其实这道题我刚开始是二维数组来dp的 , 但是wa了几次没有找到原因 , 就转一维数组去做了 。
现在找到错在哪了 , 先上AC代码:
#include <bits/stdc++.h>
using namespace std ;
long long s , n , m ;
long long a[101][101] ;
long long dp[101][20001] ;
int main() {
cin >> s >> n >> m ;
for(int i = 1 ; i <= s ; i ++)
for(int j = 1 ; j <= n ; j ++)
cin >> a[j][i] ;
for(int i = 1 ; i <= n ; i ++) {
sort(a[i] + 1 , a[i] + 1 + s) ;
for(int j = m ; j >= 0 ; j --) {
for(int k = s ; k >= 1 ; k --) {
if(2 * a[i][k] + 1 <= j) dp[i][j] = max(max(dp[i][j] , dp[i-1][j]) , dp[i-1][j-2*a[i][k]-1] + k * i) ;
else dp[i][j] = dp[i-1][j] ;
}
}
}
cout << dp[n][m] ;
return 0 ;
}
错在了这一行:
if(2 * a[i][k] + 1 <= j) dp[i][j] = max(max(dp[i][j] , dp[i-1][j]) , dp[i-1][j-2*a[i][k]-1] + k * i) ;
最初我写的转移方程是 :
dp[i][j] = max(dp[i][j] , dp[i-1][j-2*a[i][k]-1] + k * i) ;
能放就放了 , 没考虑到不放的价值可以更大 , emmm ,引以为鉴吧 。
补充:
L3-001 凑零钱 (30 分)(输出序列的处理方法)
注意几点:
对序列从大到小排序:这样可以使小数覆盖大数 , 从而满足题目中输出最小序列的条件。
#include <bits/stdc++.h>
using namespace std ;
const int N = 1e4 + 10 ;
int dp[N] , a[N] ;
bool vis[N][N/2] ;
bool cmp(const int &a , const int &b) {return a > b ;}
int main()
{
int n , m ;
scanf("%d%d" , &n , &m) ;
for(int i = 1 ; i <= n ; i ++) scanf("%d" , &a[i]) ;
sort(a + 1 , a + 1 + n , cmp) ;
for(int i = 1 ; i <= n ; i ++)
for(int j = m ; j >= a[i] ; j --)
{
if(dp[j] <= dp[j-a[i]] + a[i])
{
dp[j] = dp[j-a[i]] + a[i] ;
vis[i][j] = true ;
}
}
if(dp[m] == m)
{
int w = m , id = n ;
while(w)
{
if(vis[id][w])
{
if(w != m) printf(" ") ;
printf("%d" , a[id]) ;
w -= a[id] ;
}
id -- ;
}
}
else
{
printf("No Solution") ;
}
return 0 ;
}
当然 , 这题也可以搜索加剪枝去做。