暑假N天乐 —— 01背包及变形
暑假集训的第一天 第二天了。
决定还是先把基础巩固一下好了,万一后面训练赛出事故更尴尬。
所以今天的主题是 —— 01背包及变形
动态规划概念
动态规划(Dynamic programming,DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
动态规划问题满足三大重要性质
最优子结构性质:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
子问题重叠性质:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
无后效性:将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
以上转自 https://blog.csdn.net/qq_37774171/article/details/81188690
第一个 dp 相关的专题,所以就直接把概念贴在这方面以后查阅。
题解
[HDU-2602 Bone Collector] 01背包裸题
http://acm.hdu.edu.cn/showproblem.php?pid=2602
就大概是个纯板子,就放这了。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 1e5+5;
struct node
{
int a, c;
}x[maxn];
int dp[maxn];
int main() {
int t;
scanf("%d", &t);
while(t--) {
int n, v;
scanf("%d%d", &n, &v);
for(int i = 1; i <= n; i++) {
scanf("%d", &x[i].a);
}
for(int i = 1; i <= n; i++) {
scanf("%d", &x[i].c);
}
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++) {
for(int j = v; j >= x[i].c; j--) {
dp[j] = max(dp[j], dp[j-x[i].c] + x[i].a);
}
}
printf("%d\n", dp[v]);
}
return 0;
}
[POJ-3624 Charm Bracelet] 01背包裸题
http://poj.org/problem?id=3624
同上,就不贴代码了。
[HDU-2546 饭卡] 01背包常规
http://acm.hdu.edu.cn/showproblem.php?pid=2546
和上边两道裸题很类似,唯一不同的地方大概就是饭卡内的余额小于5的话不能用,多于5时也可买超过5元的物品。因此需要排序一下,把最大的物品最后“赊账”买掉,大概就是这样。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 1e4+5;
int a[maxn];
int dp[maxn];
int main() {
int n, m;
while(~scanf("%d", &n) && n) {
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
scanf("%d", &m);
if(m < 5) {
printf("%d\n", m);
continue;
}
memset(dp, 0, sizeof(dp));
sort(a+1, a+1+n);
m = m - 5; // 用来吃掉最大的一个
for(int i = 1; i < n; i++) {
for(int j = m; j >= a[i]; j--) {
dp[j] = max(dp[j], dp[j-a[i]]+a[i]);
}
}
printf("%d\n", m+5-a[n]-dp[m]);
}
return 0;
}
[UVA-624 CD] 01背包常规
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=565
同样是常规题型,需要输出背包所取的物品的“路径”,用到递归。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 2e3+5;
int a[maxn];
int dp[maxn][maxn];
void dfs(int n, int m) {
if(n == 0) {
return ;
}
if(dp[n][m] == dp[n-1][m]) {
dfs(n-1, m);
}
else {
dfs(n-1, m-a[n]);
printf("%d ", a[n]);
}
}
int main() {
int n, m;
while(~scanf("%d%d", &m, &n)) {
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= m; j++) {
if(j < a[i]) {
dp[i][j] = dp[i-1][j];
}
else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-a[i]]+a[i]);
}
}
}
dfs(n, m);
printf("sum:%d\n", dp[n][m]);
}
return 0;
}
[UVA-562 Dividing coins] 01背包常规
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=503
给定一些硬币,要把这些硬币分给两个人,要求差值最小,输出差值。转化为01背包做,先求出总和,然后对 \(sum/2\) 进行背包。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 100*500+5;
int dp[maxn];
int a[105];
int main() {
int t;
scanf("%d", &t);
while(t--) {
int n;
int sum = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
sum += a[i];
}
int m = sum / 2;
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++) {
for(int j = m; j >= a[i]; j--) {
dp[j] = max(dp[j], dp[j-a[i]]+a[i]);
}
}
printf("%d\n", abs(sum-2*dp[m]));
}
return 0;
}
[HDU-2955 Robberies] 01背包常规
http://acm.hdu.edu.cn/showproblem.php?pid=2955
问在“抢劫”被抓的概率不超过给定值的前提下,能总共抢到多少的银行储蓄。
本题需要把被抓的概率先转化成不被抓的概率,这样的独立事件发生概率就可以用乘法来求解。
\(dp[x] = y\) 表示的是 得到 x 的钱、不被抓概率为 y
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 1e4+5;
struct node {
int val;
double pp;
}a[maxn];
double dp[maxn];
int main() {
int t;
scanf("%d", &t);
while(t--) {
int sum = 0;
memset(dp, 0, sizeof(dp));
double p;
int n;
scanf("%lf%d", &p, &n);
p = 1.0 - p;
for(int i = 1; i <= n; i++) {
scanf("%d%lf", &a[i].val, &a[i].pp);
a[i].pp = 1.0 - a[i].pp;
sum += a[i].val;
}
dp[0] = 1.0;
for(int i = 1; i <= n; i++) {
for(int j = sum; j >= a[i].val; j--) {
dp[j]= max(dp[j], dp[j-a[i].val]*a[i].pp);
}
}
int ans = 0;
for(int i = sum; i >= 0; i--) {
if(dp[i] - p >= eps) {
ans = i;
break;
}
}
printf("%d\n", ans);
}
return 0;
}
[POJ-2184 Cow Exhibition] 01背包变形
http://poj.org/problem?id=2184
就两个属性总和都不能是负的,然后在这个前提下求二者总和最大。因为有负数的存在,所以我们可以数组多开一倍,然后把负数翻成正数做就行了,二者可以任取一个做为体积。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 2e5+5;
int a[maxn], b[maxn];
int dp[maxn];
int main() {
int n;
while(~scanf("%d", &n)) {
for(int i = 1; i <= n; i++) {
scanf("%d%d", &a[i], &b[i]);
}
for(int i = 0; i < maxn; i++) {
dp[i] = -inf;
}
dp[100000] = 0;
for(int i = 1; i <= n; i++) {
if(a[i] < 0 && b[i] < 0) {
continue;
}
if(a[i] > 0) {
for(int j = 200000; j >= a[i]; j--) {
dp[j] = max(dp[j], dp[j-a[i]]+b[i]);
}
}
else {
for(int j = a[i]; j <= 200000+a[i]; j++) {
dp[j] = max(dp[j], dp[j-a[i]]+b[i]);
}
}
}
int ans = -inf;
for(int i = 100000; i <= 200000; i++) {
if(dp[i] >= 0) {
ans = max(ans, dp[i]+i-100000);
}
}
printf("%d\n", ans);
}
return 0;
}
[HDU-2639 Bone Collector II] 第K大01背包
http://acm.hdu.edu.cn/showproblem.php?pid=2639
求价值第K大的01背包问题,技巧是多加一维表示第k大时的价值,转移之前需要利用归并排序把所有可能情况进行排序。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 1e3+5;
int val[maxn], cast[maxn];
int dp[maxn][35];
int ta[maxn], tb[maxn];
int main() {
int n, m, k;
int t;
scanf("%d", &t);
while(t--) {
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= n; i++) {
scanf("%d", &val[i]);
}
for(int i = 1; i <= n; i++) {
scanf("%d", &cast[i]);
}
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++) {
for(int j = m; j >= cast[i]; j--) {
for(int l = 1; l <= k; l++) {
ta[l] = dp[j][l];
tb[l] = dp[j-cast[i]][l] + val[i];
}
ta[k+1] = tb[k+1] = -1;
int x, y, z;
x = y = z = 1;
while((ta[x] != -1 || tb[y] != -1) && z <= k) {
if(ta[x] > tb[y]) {
dp[j][z] = ta[x++];
}
else {
dp[j][z] = tb[y++];
}
if(dp[j][z] != dp[j][z-1]) {
z++;
}
}
}
}
printf("%d\n", dp[m][k]);
}
return 0;
}
[POJ-2923 Relocation] 01背包 + 状态压缩
http://poj.org/problem?id=2923
这就涉及到我的知识盲区了。
用到状态压缩思想的01背包。先枚举选若干个时的状态,总状态量为1<<n,判断集合里的物品能否一次运走,如果能运走,那就把整个状态看成一个物品。预处理完找到 cnt 个物品,再对这 cnt 个物品进行01背包处理,每个物品的体积是state[i],价值是1,求必选n个物品的最少价值。状态转移方程:$$dp[j|k] = min(dp[j|k], dp[k]+1) 【k为st[i,1\leq j\leq (1<<n)-1]】$$
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 2e3+5;
int a[15];
int dp[maxn];
int st[maxn];
int vis[maxn];
int cnt;
int n, v1, v2;
int check(int x) {
memset(vis, 0, sizeof(vis));
vis[0] = 1;
int sum = 0;
for(int i = 0; i < n; i++) {
if(x & (1<<i)) {
// 判断从右到左第 i 位是不是 1(是否被取)
sum += a[i];
for(int j = v1; j >= a[i]; j--) {
if(vis[j-a[i]]) { // 上一个状态是否存在
vis[j] = 1;
}
}
}
}
if(sum > v1+v2) {
return 0;
}
for(int i = 0; i <= v1; i++) {
if(vis[i] && sum-i <= v2) {
return 1;
}
}
return 0;
}
void init() {
cnt = 0;
memset(dp, inf, sizeof(dp));
for(int i = 0; i < (1<<n); i++) {
if(check(i)) {
st[cnt++] = i;
}
}
}
int main() {
int t, cas = 1;
scanf("%d", &t);
while(t--) {
scanf("%d%d%d", &n, &v1, &v2);
for(int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
init();
int m = (1<<n)-1; // 全部取走的状态
dp[0] = 0; // 取 0 个物品需要 0 次
for(int i = 0; i < cnt; i++) {
for(int j = m; j >= 0; j--) {
if(dp[j] == inf)
continue;
dp[j|st[i]] = min(dp[j|st[i]], dp[j]+1);
}
}
printf("Scenario #%d:\n", cas++);
printf("%d\n\n", dp[m]);
}
return 0;
}
[HDU-3466 Proud Merchants] 01背包思维
http://acm.hdu.edu.cn/showproblem.php?pid=3466
题意是有m块钱,要买价值总和最高的几个东西,然后商家看你前不够 qi ,就直接不卖给你了,每个东西花费是 pi。问最多价值
需要先排序的01背包,排序依据是 q-p。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 1e4+5;
struct node{
int p, q, v;
bool operator < (const node &x) const {
return (q-p) < (x.q-x.p);
}
}a[maxn];
int dp[maxn];
int main() {
int n, m;
while(~scanf("%d%d", &n, &m)) {
for(int i = 1; i <= n; i++) {
scanf("%d%d%d", &a[i].p, &a[i].q, &a[i].v);
}
sort(a+1, a+1+n);
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++) {
for(int j = m; j >= a[i].q; j--) {
dp[j] = max(dp[j], dp[j-a[i].p]+a[i].v);
}
}
printf("%d\n", dp[m]);
}
return 0;
}
[HDU-2126 Buy the souvenirs] 01背包思维
http://acm.hdu.edu.cn/showproblem.php?pid=2126
问在买到最多件物品的情况下,能有多少种选择方法。
这种情况需要加一维。
#include <map>
#include <set>
#include <list>
#include <cmath>
#include <ctime>
#include <deque>
#include <stack>
#include <queue>
#include <bitset>
#include <cctype>
#include <cstdio>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double PI = acos(-1.0);
const double eps = 1e-6;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int maxn = 5e2+5;
int dp[maxn][35];
int a[35];
int main() {
int t;
scanf("%d", &t);
while(t--) {
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
int temp = 0;
for(int i = 1; i <= n; i++) {
for(int j = m; j >= a[i]; j--) {
for(int k = n; k >= 1; k--) {
dp[j][k] = dp[j][k] + dp[j-a[i]][k-1];
if(dp[j-a[i]][k-1] > 0) {
temp = max(temp, k);
}
}
}
}
if(temp == 0) {
printf("Sorry, you can't buy anything.\n");
}
else {
int ans = 0;
for(int i = 0; i <= m; i++) {
ans += dp[i][temp];
}
printf("You have %d selection(s) to buy with %d kind(s) of souvenirs.\n", ans, temp);
}
}
return 0;
}
[HDU-4281 Judges' response] 01背包 + 状态压缩 + mTSP
http://acm.hdu.edu.cn/showproblem.php?pid=4281
待填坑... mTSP我不会就是了