动态规划_背包问题笔记
https://www.cnblogs.com/31415926535x/p/10403589.html
dp自从知道有这么个东西时,就没有好好的学,,现在一看道dp的题就绕道走,,,但是,很多比赛中的dp问题有很多,,别人都会,自己不会很吃亏啊,,,于是从基础开始一点一点的补ing
背包问题
背包问题是动态规划的经典也是基础,,,下面的东西部分来自 背包九讲;
01背包
01背包指的是对于任意的物品只有 取或不取 两种状态,,
状态转移方程
状态转移方程为:
\(F[i,j]=max(F[i-1,j], F[i-1,j-c_i]+w_i)\)
外层循环枚举物品总数:\(for \ i=1\ to\ n\)
内层循环枚举背包的容量: \(for \ j=c_i \ to \ v\)
空间优化后的状态转移方程:
\(F[j]=max(F[j], F[j-c_i]+w_i)\)
外层循环不变,内层循环变为: \(for \ j=v \ to \ c_i\)
外层循环可以继续优化为: \(for \ j \ to \ max(v-\sum_i^nw_i, \ \ c_i)\)
初始化
- 恰好装满背包:\(F[0]=0,F[1..v]=-\infty\)
- 不必装满: \(F[0..v]=0\)
初始化F数组就是在没有任何物品可以放入背包时的合法状态,所以,前者只有容量为零的背包什么都不装的情况下是恰好装满的,其他容量的背包都是未定义的状态,无合法解;后者因为不必装满,所以什么都不装的时候就是一个合法解,这时的价值为零。
例题
hud-2602
裸的01背包,,直接做,,,注意判断当前物品是否能放入背包,,再选择放与不放,,
还有内层循环容量的遍历是从0开始
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; ++i)
for(int j = 0; j <= v; ++j)
if(c[i] <= j)//能放入时,选择放与不放
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - c[i]] + w[i]);
else
dp[i][j] = dp[i - 1][j];
printf("%d\n", dp[n][v]);
空间优化后的方法:
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; ++i)
for(int j = v; j >= 0; --j)
if(c[i] <= j)//能放入时,选择放与不放
dp[j] = max(dp[j], dp[j - c[i]] + w[i]);
printf("%d\n", dp[v]);
hdu-2546
题意是:一个总钱数为m的钱包,在剩余金额大于等于5的情况下可以购买任何东西,即使买了一个东西后剩余钱数为负,然后给你这n个东西的标价,每种东西只能购买一次,,
这道题按01背包做的话,可以将钱包m看成背包的容量,n道菜就是n种物品, 每种物品的价值和花费都是其菜价,,
这是其中一个点,还有为了尽可能的是利益最大,,我们可以先保留5块,为了最后买那个最贵的菜,,对剩下的n-1个菜选择出价值最大的,,,这样就将这道题转化成了容量为m-5的背包选择一些物品使得总价值最大,,,最后的答案在算上那个最贵的菜就行了,,,
int dp[maxn], c[maxn], w[maxn];
int main()
{
// freopen("233.in" , "r" , stdin);
// freopen("233.out" , "w" , stdout);
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
int n;
while(scanf("%d", &n) && n)
{
for(int i = 1; i <= n; ++i)
scanf("%d", &c[i]);
int m;scanf("%d", &m);
if(m < 5)
{
printf("%d\n", m);
continue;
}
m -= 5;
sort(c + 1, c + 1 + n);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n - 1; ++i)
for(int j = m; j >= c[i]; --j)
dp[j] = max(dp[j], dp[j - c[i]] + c[i]);
printf("%d\n", m + 5 - dp[m] - c[n]);
}
return 0;
}
hdu-1171
题意是:有一些设施,每个设施的价值为 \(w_i\),,然后要分成两堆,这两堆的价值要尽可能的相近
显然分后的价值和 \(sum\) 就是原来的价值和,,然后肯定一个大于等于均值,一个小于等于,,,所以可以将这道题目看成01背包的模型:一个容量为 \(sum/2\) 的背包,选择装一些物品,这些物品的价值的和费用相同,,求最大的价值
int dp[maxn], c[maxn], w[maxn];
int main()
{
// freopen("233.in" , "r" , stdin);
// freopen("233.out" , "w" , stdout);
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
int n;
while(scanf("%d", &n) && n > 0)
{
int tot = 0;
for(int i = 1; i <= n; ++i)
{
int a, b;
scanf("%d%d", &a, &b);
while(b--)w[++tot] = a;
}
memset(dp, 0, sizeof dp);
int sum = 0;
for(int i = 1; i <= tot; ++i)sum += w[i];
for(int i = 1; i <= tot; ++i)
for(int j = sum / 2; j >= w[i]; --j)
dp[j] = max(dp[j], dp[j - w[i]] + w[i]);
printf("%d %d\n", sum - dp[sum / 2], dp[sum / 2]);
}
return 0;
}
完全背包
完全背包就是在01背包的基础上对于物品的限制解除,,物品不再为只能取一件,而是无限件(实际也不可能是无限件,每一个物品最多取 \(\lfloor \frac{v}{c_i} \rfloor\)),,
将完全背包转化为01背包后, 状态转移方程和01背包的类似,,只有对背包容量的枚举也就是内层循环中,完全背包是递增的顺序而01背包的是递减的顺序,,
\(for \ j=c_i \ to \ v\)
0-1背包和完全背包的不同:
从二维数组上区别0-1背包和完全背包也就是状态转移方程就差别在放第i中物品时,完全背包在选择放这个物品时,最优解是F[i][j-c[i]]+w[i]即画表格中同行的那一个,而0-1背包比较的是F[i-1][j-c[i]]+w[i],上一行的那一个。
从一维数组上区别0-1背包和完全背包差别就在循环顺序上,0-1背包必须逆序,因为这样保证了不会重复选择已经选择的物品,而完全背包是顺序,顺序会覆盖以前的状态,所以存在选择多次的情况,也符合完全背包的题意。状态转移方程都为F[i] = max(F[i],dp[F-c[i]]+v[i])。
例题
hdu-1114
题意是:给你一个存钱罐的总质量个单纯存钱罐的质量(也就是差为钱的质量),,以及n种硬币的面值和质量,然后问你最小的金额是多少
差值可以看作背包的容量,每个硬币的质量为物品的代价,面值为其价值,,然后求最小的价值转移方程里就为min,,初始化再改变一下,,
int dp[maxn], c[maxn], w[maxn];
int main()
{
// freopen("233.in" , "r" , stdin);
// freopen("233.out" , "w" , stdout);
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
int t;scanf("%d", &t);
while(t--)
{
int e, f;scanf("%d%d", &e, &f);
int v = f - e;
int k;scanf("%d", &k);
for(int i = 1; i <= k; ++i)scanf("%d%d", &w[i], &c[i]);
memset(dp, inf, sizeof dp);
dp[0] = 0;
for(int i = 1; i <= k; ++i)
for(int j = c[i]; j <= v; ++j)
dp[j] = min(dp[j], dp[j - c[i]] + w[i]);
if(dp[v] >= inf)
printf("This is impossible.\n");
else
printf("The minimum amount of money in the piggy-bank is %d.\n", dp[v]);
}
return 0;
}
多重背包
多重背包就是完全背包的限制版,,每一种物品不再是无限个,,而是给定的个数,最后还是求背包的最大价值什么的,,,
转化成01背包问题就是对于每一种物品取 \(1, 2, 2^2, 2^3,,,2^{k-1},M_i-2^k+1\)件,,
一般的多重背包模板:
int dp[maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
//01背包,该物品的代价,价值
void ZeroOnePack(int C, int W)
{
for(int i = v; i >= C; --i)
dp[i] = max(dp[i], dp[i - C] + W);
return;
}
//完全背包,该物品的代价,价值
void CompletePack(int C, int W)
{
for(int i = C; i <= v; ++i)
dp[i] = max(dp[i], dp[i - C] + W);
return;
}
//一次多重背包,该物品的代价,价值,数量
void OneMuitPack(int C, int W, int M)
{
if(v <= C * M)//物品足够多时用完全背包
{
CompletePack(C, W);
return;
}
else //否则用二进制划分成若干件01背包的物品
{
int k = 1;
while(k < M)
{
ZeroOnePack(k * C, k * W);//某一个划分成01背包的物品
M -= k;
k <<= 1;
}
ZeroOnePack(C * M, W * M);//剩下的一些物品
}
return;
}
例题
hdu-2844
题意是:n种面值的硬币,每种硬币的个数限定,问你能够组成几种面值和不超过m的组成方法,
转化成背包问题就是,一个容量为m的背包装一些价值和代价都为面值的物品,其中物品的个数有限制,,问背包内的价值的可能种类
//cf
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '\n'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, ull> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e5 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
//01背包,该物品的代价,价值
void ZeroOnePack(int C, int W)
{
for(int i = v; i >= C; --i)
dp[i] = max(dp[i], dp[i - C] + W);
return;
}
//完全背包,该物品的代价,价值
void CompletePack(int C, int W)
{
for(int i = C; i <= v; ++i)
dp[i] = max(dp[i], dp[i - C] + W);
return;
}
//一次多重背包,该物品的代价,价值,数量
void OneMuitPack(int C, int W, int M)
{
if(v <= C * M)//物品足够多时用完全背包
{
CompletePack(C, W);
return;
}
else //否则用二进制划分成若干件01背包的物品
{
int k = 1;
while(k < M)
{
ZeroOnePack(k * C, k * W);//某一个划分成01背包的物品
M -= k;
k <<= 1;
}
ZeroOnePack(C * M, W * M);//剩下的一些物品
}
return;
}
int main()
{
// freopen("233.in" , "r" , stdin);
// freopen("233.out" , "w" , stdout);
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
while(scanf("%d%d", &n, &m) && n + m)
{
for(int i = 1; i <= n; ++i)scanf("%d", &w[i]);
for(int i = 1; i <= n; ++i)scanf("%d", &num[i]);
memset(dp, 0, sizeof dp);
v = m;
for(int i = 1; i <= n; ++i)
OneMuitPack(w[i], w[i], num[i]);
int ans = 0;
for(int i = 1; i <= m; ++i)if(dp[i] == i)++ans;
printf("%d\n", ans);
}
return 0;
}
混合背包
混合背包就是n种物品有的只能取一次,有的能取有限次,有的能取无限次,然后问你对于容量为v的背包的可取最大价值是多少
直接判断每个物品的种类,使用不同的背包类型就行了
例题
codevs-3269
题意就是混合背包的定义,,直接做就行
//cf
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '\n'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, ull> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 2e5 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
//01背包,该物品的代价,价值
void ZeroOnePack(int C, int W)
{
for(int i = v; i >= C; --i)
dp[i] = max(dp[i], dp[i - C] + W);
return;
}
//完全背包,该物品的代价,价值
void CompletePack(int C, int W)
{
for(int i = C; i <= v; ++i)
dp[i] = max(dp[i], dp[i - C] + W);
return;
}
//一次多重背包,该物品的代价,价值,数量
void OneMuitPack(int C, int W, int M)
{
if(v <= C * M)//物品足够多时用完全背包
{
CompletePack(C, W);
return;
}
else //否则用二进制划分成若干件01背包的物品
{
int k = 1;
while(k < M)
{
ZeroOnePack(k * C, k * W);//某一个划分成01背包的物品
M -= k;
k <<= 1;
}
ZeroOnePack(C * M, W * M);//剩下的一些物品
}
return;
}
int main()
{
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
scanf("%d%d", &n, &v);
for(int i = 1; i <= n; ++i)scanf("%d%d%d", &c[i], &w[i], &num[i]);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; ++i)
{
if(num[i] == 1)
ZeroOnePack(c[i], w[i]);
else if(num[i] == -1)
CompletePack(c[i], w[i]);
else
OneMuitPack(c[i], w[i], num[i]);
}
printf("%d\n", dp[v]);
return 0;
}
二维费用背包
二维费用指的就是相比之前的背包问题侑多了一个费用的影响因素,,对于一个物品有两个不同的代价以及其容量,,做法和前面的一样,dp数组增加一维就行了,,
例题
hdu-2159
转化成背包问题就是代价一是忍耐度,背包容量为m;代价二就是打怪,容量就是s,,求最大的价值(经验值)与n的大小关系,,,
//cf
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '\n'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, ull> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e3 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[maxn][maxn], c[maxn], w[maxn], num[maxn];
int n, m, v;//n为物品总数,v为背包容量
int main()
{
// freopen("233.in" , "r" , stdin);
// freopen("233.out" , "w" , stdout);
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
int n, m, k, s;
while(~scanf("%d%d%d%d", &n, &m, &k, &s))
{
memset(w, 0, sizeof w);
memset(c, 0, sizeof c);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= k; ++i)
scanf("%d%d", &w[i], &c[i]);
int ans = inf;
for(int i = 1; i <= k; ++i)
for(int j = c[i]; j <= m; ++j)
for(int k = 1; k <= s; ++k)
{
dp[j][k] = max(dp[j][k], dp[j - c[i]][k - 1] + w[i]);
if(dp[j][k] >= n)ans = min(ans, j);
}
if(ans > m)printf("-1\n");
else printf("%d\n", m - ans);
}
return 0;
}
(loading)
分组背包
分组背包就是:一堆物品被划分成了K组,同一组的物品只能选择一个,或者这组不选,其他的条件和其他背包模型一样,,
解决方法,再加一层对每组背包的枚举
伪代码:
\(for \ k=1 \ to \ K\)
\(for \ v=V \ to \ V\)
\(for \ item \ i \ in \ group \ k\)
\(F[v]=max(F[v], F[v-C_i]+W_i)\)
例题
hdu-1712
题意就是有n节课,每一课上几天的价值给你,,一共要上m节课,问最大的价值,,
把这道题看成容量为m的背包,装分为n组的物品最大的价值就行
int dp[maxn];
int main()
{
int n, m;
int a[maxn][maxn];
while(scanf("%d%d", &n, &m) && n + m)
{
memset(a, 0, sizeof a);
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
scanf("%d", &a[i][j]);
memset(dp, 0, sizeof dp);
for(int k = 1; k <= n; ++k)//枚举组数
for(int j = m; j >= 0; --j)//枚举背包的容量
for(int i = 1; i <= m; ++i)//枚举第k组的物品
if(i <= j)//保证能装下
dp[j] = max(dp[j], dp[j - i] + a[k][i]);
printf("%d\n", dp[m]);
}
return 0;
}
hdu-3033
题意就是一堆鞋子,某一些是一个牌子的,然后每一双鞋有一个价格(看作代价),一个价值,每个牌子至少取一双,问最大的价值,,,
与上一道不同的是每一组的物品不再是最多选一个了,,一组可以选多个,每一组都要选一个,,
dp[i][j]表示的是前i组在容量为j的背包所取的最大价值,,当前状态dp[i][j]可以由 前一状态在本组选一个物品 推来,也可以由 当前状态在本组再取一个物品 推来,,
初始化也不同了,,除了那一组都不选的那一行dp为零,,其他都为负,即未定义状态,,由这个判断是否有解,,
//hdu
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string.h>
#include <algorithm>
#define aaa cout<<233<<endl;
#define endl '\n'
#define pb push_back
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
const int inf = 0x3f3f3f3f;//1061109567
const ll linf = 0x3f3f3f3f3f3f3f;
const double eps = 1e-6;
const double pi = 3.14159265358979;
const int maxn = 1e4 + 5;
const int maxm = 2e5 + 5;
const ll mod = 1e9 + 7;
int dp[11][maxn];
pii a[11][maxn];
int num[11];
int main()
{
// freopen("233.in" , "r" , stdin);
// freopen("233.out" , "w" , stdout);
// ios_base::sync_with_stdio(0);
// cin.tie(0);cout.tie(0);
int n, m, K;
while(~scanf("%d%d%d", &n, &m, &K))
{
memset(num, 0, sizeof num);
for(int i = 1; i <= n; ++i)
{
int aa, bb, cc;
scanf("%d%d%d", &aa, &bb, &cc);
++num[aa];
a[aa][num[aa]].first = bb;
a[aa][num[aa]].second = cc;
}
memset(dp, -1, sizeof dp);
//for(int i = 0; i <= m; ++i)dp[0][i] = 0;
memset(dp[0], 0, sizeof dp[0]);
//不能写成memset(dp[0], 0, sizeof dp);
for(int k = 1; k <= K; ++k)
for(int i = 1; i <= num[k]; ++i)
for(int j = m; j >= a[k][i].first; --j)
{
if(dp[k][j - a[k][i].first] >= 0)
dp[k][j] = max(dp[k][j], dp[k][j - a[k][i].first] + a[k][i].second);
if(dp[k - 1][j - a[k][i].first] >= 0)
dp[k][j] = max(dp[k - 1][j - a[k][i].first] + a[k][i].second, dp[k][j]);
}
if(dp[K][m] < 0)printf("Impossible\n");
else printf("%d\n", dp[K][m]);
}
return 0;
}
这道题没怎么理解还,,
(loading)
剩下一些其他的内容,暂时先放放,,