背包九讲 && 题目

Posted stupid_one

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了背包九讲 && 题目相关的知识,希望对你有一定的参考价值。

★、背包求方案数的时候,多重背包是不行的,因为产生重复的背包会有多种情况。

★、背包记录路径的时候,其实是不行的,因为更新了12的最优解,如果它依赖于6这个背包,然后你后面改变了6这个背包,就GG

 

1、01背包问题。

tot:总背包空间,vall[i]:每件物品的价值,w[i]:每件物品的重量

http://acm.hdu.edu.cn/showproblem.php?pid=2602

01背包明显可以只写一维的,所以二维的就不写了。

关于为什么可以只写一维的呢?这就和你枚举的顺序有关了。从tot 枚举 到 w[i]。那么是优先更新dp[比较大的数]

而且是从dp[i - 1][]那里更新过来的。至于后面枚举小的背包容量的时候,较大的背包容量是用不了的了,所以这里就可以避免有重复使用的bug。确保都是从dp[i-  1]枚举过来。而这个顺序反转了的话,刚好的完全背包的最优解。这个后面再说

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define ios ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
const int maxn = 1e3 + 20;
int dp[maxn];
int w[maxn], val[maxn];
void work() {
    memset(dp, 0, sizeof dp);
    int n, tot;
    scanf("%d%d", &n, &tot);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &val[i]);
    }
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &w[i]);
    }
    for (int i = 1; i <= n; ++i) {
        for (int j = tot; j >= w[i]; --j) {
            dp[j] = max(dp[j], dp[j -w[i]] + val[i]);
        }
    }
    printf("%d\\n", dp[tot]);
}

int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    int t;
    scanf("%d", &t);
    while (t--) work();
    return 0;
}
View Code

 

一个常数的优化:

我把tot加大到10000.然后提交就变成了655ms。下面来说说当总背包容量tot比较大的时候,该怎么优化。

对于第n件物品,我们的转移方程是dp[tot] = max(dp[tot], dp[tot - w[n]);  //这个就是答案

其实只需要一步就够了,因为我们需要的是dp[tot],不用再向下枚举了。但是根据上面的代码,是需要枚举到

for (j := tot; j >= w[n]; --j),是需要枚举到w[n]的,为什么呢?其实是为了给后面的做铺垫。因为我们并不知道这个是最后的一个背包,所以还是需要枚举到w[i]的,因为后面的背包可能需要用到dp[w[i]]这个背包的值。来更新最优解

那么我们可以算出一个下限,什么下限呢,就是后面的所有可能的背包中,最多需要用到那一个背包。

对于最后一个背包,他只需要用到dp[tot - w[n]]这个背包就够了。枚举到倒数第二种物品的时候,

他只需要用到dp[tot - w[n] - w[n - 1]]这个背包就够了。那么前面的背包,我们就不需要更新了。

 

感觉还是写张图比较好理解,以免我以后忘记。

现在考虑枚举到了倒数第二种物品,我们只需要更新红色那个区域就行了,因为最后一个物品只需要用到dp[tot - w[n]]

那么同理,需要更新红色那段区域,我们只需要知道[tot - w[n] - w[n - 1], tot]这段区域的最优值是谁就可以了,因为我们为了更新红色那段区域,对于倒数第二种物品,其重量是w[i],下限就是tot - w[n] - w[i],故按照这个思路递推回去第i件物品即可。

 

这个用来优化当tot比较大的时候,是有用的,我把tot和w[]都同时加上了一个fix值,结果TLE,不是TLE就是RE。还是找到合适的题目再写上来吧,

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
const int maxn = 1e3 + 20;
int dp[maxn];
int w[maxn], val[maxn];
int suffix_sum[maxn];
void work() {
    memset(dp, 0, sizeof dp);
    int n, tot;
    scanf("%d%d", &n, &tot);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &val[i]);
    }
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &w[i]);
    }
    suffix_sum[n + 1] = 0;
    for (int i = n; i >= 1; --i) {
        suffix_sum[i] = w[i] + suffix_sum[i + 1];
    }
    for (int i = 1; i <= n; ++i) {
        int toUpdate = max(w[i], tot - suffix_sum[i + 1]);
        for (int j = tot; j >= toUpdate; --j) {
            dp[j] = max(dp[j], dp[j -w[i]] + val[i]);
        }
    }
    printf("%d\\n", dp[tot]);
}

int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    int t;
    scanf("%d", &t);
    while (t--) work();
    return 0;
}
一个常数的优化

 

关于dp的初始化。开始的时候dp[0] = 0表示容量为0的背包,能得到物品的价值是0.后面的就有两类了。

①、需要刚好装满tot个,那么,后面的就是全部都是-inf了,表示刚好刚好装满x个的时候,价值是负的,就是没有价值。

②、不需要的话,就全部都是0.

 

二维01背包,

POJ 1948

http://poj.org/problem?id=1948

给定n根木棒,要求全部用上,组成一个三角形,使得这个三角形的面积最大。

dp[i][j]表示组成的第一根木棒长度是i的时候,第二根木棒长度是j,第三根木棒的长度是dp[i][j]

那么对于周长是固定的话,那么dp数组开bool的就够了。dp[i][j] = 0表示这个方案不可行

比如dp[0][1] = true。表示第一根木棒是0,第二根木棒是1,第三根是all - 0 - 1。那么就全部木棒也用上了。

转移的话,if (dp[i][j]) then dp[i + val][j] = true;  dp[i][j + val] = true;

就是这个物品,可以去两组中的任意一组,都可以。

同样也是枚举顺序的问题,应该倒着来枚举,因为木棒只能用一次。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
const int maxn = 1600 + 20;
bool dp[maxn][maxn];
int a[maxn];
bool check (int a, int b, int c) {
    if (abs(a - b) >= c) return false;
    if (abs(a - c) >= b) return false;
    if (abs(b - c) >= a) return false;
    return true;
}
double calc(double a, double b, double c) {
//    cout << a << "  " << b << "  " << c << endl;
    double p = (a + b + c) / 2.0;
//    cout << p << endl;
    double ans = sqrt(p * (p - a) * (p - b) * (p - c));
    return ans * 100;
}
void work() {
    int n;
    int all = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &a[i]);
        all += a[i];
    }
//    dp[0][0] = dp[a[1]][0] = dp[0][a[1]] = true;
    dp[0][0] = true;
    int en = (all + 1) / 2;
    for (int i = 1; i <= n; ++i) {
        for (int j = en; j >= 0; --j) {
            for (int k = en; k >= j; --k) {
                if (j >= a[i] && dp[j - a[i]][k]) {
                    dp[j][k] = true;
                }
                if (k >= a[i] && dp[j][k - a[i]]) {
                    dp[j][k] = true;
                }
            }
        }
    }
    int ans = -1;
    for (int i = 1; i <= en; ++i) {
        for (int j = i; j <= en; ++j) {
            if (dp[i][j] && check(i, j, all - i - j)) {
                ans = max(ans, (int)calc(i, j, all - i - j));
            }
        }
    }
    printf("%d\\n", ans);
}

int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    work();
    return 0;
}
View Code

 

https://vijos.org/p/1037

一题比较好的,具有很强想象力的01背包问题。

题意就是在n个数中,选出一些数字,分成2组,使得两组的和是相同的,现在需要使得这个和最大。

那么可以dp[i][j]表示前i组数中,这两组东西的差值是j的时候,较大的那组数的和是dp[i][j]。那么dp[n][0]是答案

对于每一个物品a[i],为了产生差值为j时的方案。都有4种情况,

1、不选它,不要了, 那么dp[i][j] = dp[i - 1][j];

2、选择它放去比较矮的那组,那么这个时候,要产生差值是j,需要原本的差值是j + a[i]。而且这个时候,最高的那个值没变化。

3、放去较高的那组,那么这个时候,要产生差值是j,需要原本的差值是j - a[i],而且他变高了,所以是dp[i - 1][j - a[i]] + a[i]

4、放去较矮的那组,而且超越了本来较高的那组,然后现在的差值是j,这个需要画个图,

 

为什么会想到这4总情况

因为它是3大类。

1、不用

2、放了之后,最大高度不改变。

3、放了之后,最大高度改变

这时候是,由dp[i - 1][a[i] - j] + j转移过来。

然后取四个的最大值就好了。

这题不容易想啊。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
const int maxn = 100 + 20;
int a[maxn];
int dp[maxn][4000 + 20];
void work() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        assert(a[i] >= 0);
    }
    memset(dp, -0x3f, sizeof dp);
    dp[0][0] = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j <= 2000; ++j) {
            if (j >= a[i]) {
                dp[i][j] = max(dp[i][j], dp[i - 1][j - a[i]] + a[i]); //放去高的
            }
            if (a[i] >= j) {
                dp[i][j] = max(dp[i][j], dp[i - 1][a[i] - j] + j);
            }
            dp[i][j] = max(dp[i][j], dp[i - 1][j + a[i]]); //放在小的那里
            dp[i][j] = max(dp[i][j], dp[i - 1][j]); //不用
        }
    }
    if (dp[n][0] <= 0) {
        cout << "Impossible" << endl;
    } else cout << dp[n][0] << endl;
}
int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    work();
    return 0;
}
View Code

new:

#include <bits/stdc++.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;
int dp[100 + 2][4000 + 2];
void work() {
    memset(dp, -0x3f, sizeof dp);
    int n;
    scanf("%d", &n);
    int val;
    for (int i = 1; i <= n; ++i) {
        scanf("%d", &val);
        dp[i][val] = val;
        for (int j = 2000; j >= 0; --j) {
            dp[i][j] = max(dp[i][j], dp[i - 1][j]);
            if (j + val <= 2000) dp[i][j + val] = max(dp[i][j + val], dp[i - 1][j] + val);
            if (j >= val) {
                dp[i][j - val] = max(dp[i][j - val], dp[i - 1][j]);
            } else {
                dp[i][val - j] = max(dp[i][val - j], dp[i - 1][j] - j + val);
            }
        }
    }
//    printf("%d\\n", dp[2][3]);
    if (dp[n][0] <= 0) {
        printf("Impossible\\n");
    } else printf("%d\\n", dp[n][0]);
}

int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    work();
    return 0;
}
View Code

 

 

 

 

其实这题有一个很简单的方法的,

就是和上面的三角形一样,dp[i][j]表示第一座的高度是i,第二座的高度是j,是否可能。

唉,一开始怎么想不到,不过这个是水过去的,评测机快吧。870ms

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
const int maxn = 100 + 20;
int a[maxn];
bool dp[1000 + 20][1000 + 20];
void work() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        assert(a[i] >= 0);
    }
    dp[0][0] = true;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1000; j >= 0; --j) {
            for (int h = 1000; h >= 0; --h) {
//                dp[j][h] = dp[j][h] || dp[j - a[i]][h] || dp[j][h - a[i]];
                if (j >= a[i]) {
                    dp[j][h] = dp[j][h] || dp[j - a[i]][h];
                }
                if (h >= a[i]) {
                    dp[j][h] = dp[j][h] || dp[j][h - a[i]];
                }
            }
        }
    }
    for (int i = 1000; i >= 1; --i) {
        if (dp[i][i]) {
            cout << i << endl;
            return;
        }
    }
    cout << "Impossible" << endl;
}
int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    work();
    return 0;
}
View Code

还有这题也不错。 http://www.cnblogs.com/liuweimingcprogram/p/6238454.html

 

https://vijos.org/p/1059

这就是一题暴力题,给定n组数字,每组数字能选出若干个,组成一个和值val。现在需要在这n组中,找出他们共有的和值。

明显对n组都做一次01背包,那么复杂度最坏1e8.但是我还是写了,居然127ms。

这里本来还想用一个常数的优化,但是是不行的,我要生成的是有多少个和值,而不是最优解。

dp[i][j]表示第i组,能否生成j这个和值。然后得到这个数组后,不应该用二分答案。因为有可能有些组没有这个值,然后有一个共同的更大的值。

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
const int maxn = 1e2 + 20;
bool dp[maxn][maxn * maxn];
vector<int>a[maxn];
int mx[maxn];
//int suffix_sum[maxn][maxn];
int n;
bool check(int val) {
    for (int i = 1; i <= n; ++i) {
        if (!dp[i][val]) return false;
    }
    return true;
}
void work() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        int x;
        int sum = 0;
        while (scanf("%d", &x)) {
            if (x == -1) break;
            a[i].push_back(x);
            sum += x;
        }
        mx[i] = sum;
    }
//    for (int i = 1; i <= n; ++i) {
//        for (int j = a[i].size() - 1; j >= 0; --j) {
//            suffix_sum[i][j] = suffix_sum[i][j + 1] + a[i][j];
//        }
//    }
    for (int i = 0; i <= n; ++i) {
        dp[i][0] = true;
    }
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < a[i].size(); ++j) {
//            int toUpdate = max(a[i][j], mx[i] - suffix_sum[i][j + 1]);
            for (int v = mx[i]; v >= a[i][j]; --v) {
                dp[i][v] = dp[i][v] || dp[i][v - a[i][j]];
            }
        }
    }
    for (int i = 100 * 100; i >= 0; --i) {
        if (check(i)) {
            cout << i << endl;
            return;
        }
    }
}

int main() {
#ifdef local
    freopen("data.txt", "r", stdin);
//    freopen("data.txt", "w", stdout);
#endif
    work();
    return 0;
}
View Code

 

https://vijos.org/p/1071

再来一题01背包,这个背包需要检查路径,而且需要检查是否合法。

感觉数据有点水,还不知道我的有没数据卡我的程序。

思路就是看看这n个数字中,有没有一些数字,和值是val。如果有多种情况,就输出-1,不可能,输出0.否则输出方案。

其实记录路径很简单的,这里不说了,主要是怎么确定他有多种解。 

比如

18

6

1 2 3 4 5 6

这个是多种解的,3可以用1和2代替。

我的做法是把唯一解分成一组,另外的分成一组,然后两组再进行一次dp,如果能产生相同的数字,就不行,说明可以相互代替了。

dp[v].id,这个背包选了哪一个数字

dp[v].flag,这个背包可以由多少个背包转移过来。

dp[v].pre,这个背包的上一个背包。

 

说一说这里的小bug

3可以用1 +2代替,也可以用直接一个3来代替。我们选择1 + 2,然后记得标记已经生成了,就是3已经可以生成,不再去记录3的其他路径了。因为,还是上面那个例子。

在更新6的时候,18 = 6 + 12.是可以得。12在前面有被生成过。但是,它再次更新了12.12 = 6 + 6

这是不合法的,选了两次6了。所以,我们要记录唯一的路径。

 

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <assert.h>
#define IOS ios::sync_with_stdio(false)
using namespace std;
#define inf (0x3f3f3f3f)
typedef long long int LL;


#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
int a[111];
struct node {
    int id, pre;
    int flag;
}dp[100 * 1000 + 20];
set<int>ans;
vector<int>one;
vector<int>two;
bool out[100 * 1000 + 20];
bool visone[100 * 1000 + 20];
bool dpone[100 * 1000 + 20];
bool dptwo[100 * 1000 + 20];
void work() {
    int tot, n;
    cin >> tot >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
    }
    dp[0].flag = 1;
    dp[0].id = dp[0].pre = inf;
    int tim = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = tot; j >= a[i]; --j) {
            if (dp[j].flag && dp[j - a[i]].flag) {
                dp[j].flag++;
                背包九讲

转载:《背包九讲》

背包九讲

[转]背包九讲

《背包九讲》

背包九讲(Orz)