AcWing算法提高课【第一章动态规划】
体积不超过j,求max初始化为0,j>=v
体积恰好为j,求max初始化dp[j = 0] = 0,其他为-inf
体积恰好为j,求min初始化dp[j = 0] = 0, 其他为inf, 且j>=v
体积最少为j,求min初始化dp[j = 0] = 0, 其他为inf,且j不做要求
数字三角形模型:
题目:
Hello Kitty想摘点花生送给她喜欢的米老鼠。 她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。 地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。 Hello Kitty只能向东或向南走,不能向西或向北走。 问Hello Kitty最多能够摘到多少颗花生。 1.gif 输入格式 第一行是一个整数T,代表一共有多少组数据。 接下来是T组数据。 每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。 每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M。 输出格式 对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。 数据范围 1≤T≤100, 1≤R,C≤100, 0≤M≤1000 输入样例: 2 2 2 1 1 3 4 2 3 2 3 4 1 6 5 输出样例: 8 16
分析:
线性DP,所求为最大值。 当前值可以从上面或者左边转移而来,选择较大的那一个。
代码:
#include <iostream> #include <algorithm> #include <cstring> using namespace std; const int N = 110; int n, m; int w[N][N]; int dp[N][N]; void work() { memset(dp, 0, sizeof dp); cin >> n >> m; for (int i = 1; i <= n; i ++ ) for (int j = 1; j<= m; j ++ ) cin >> w[i][j]; for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= m; j ++ ) { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); dp[i][j] = max(dp[i][j], dp[i - 1][j] + w[i][j]); dp[i][j] = max(dp[i][j], dp[i][j - 1] + w[i][j]); } cout << dp[n][m] << endl; } int main() { int T; cin >> T; while (T -- ) { work(); } return 0; }
题目:
一个商人穿过一个N×N的正方形的网格,去参加一个非常重要的商务活动。 他要从网格的左上角进,右下角出。 每穿越中间1个小方格,都要花费1个单位时间。 商人必须在(2N-1)个单位时间穿越出去。 而在经过中间的每个小方格时,都需要缴纳一定的费用。 这个商人期望在规定时间内用最少费用穿越出去。 请问至少需要多少费用? 注意:不能对角穿越各个小方格(即,只能向上下左右四个方向移动且不能离开网格)。 输入格式 第一行是一个整数,表示正方形的宽度N。 后面N行,每行N个不大于100的整数,为网格上每个小方格的费用。 输出格式 输出一个整数,表示至少需要的费用。 数据范围 1≤N≤100 输入样例: 5 1 4 6 8 10 2 5 7 15 17 6 8 9 18 20 10 11 12 19 21 20 23 25 29 33 输出样例: 109 样例解释 样例中,最小值为109=1+2+5+7+9+12+19+21+33。
分析:
线性DP
代码:
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 110; int n; int w[N][N]; int dp[N][N]; int main() { cin >> n; for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) cin >> w[i][j]; memset(dp, 0x3f, sizeof dp); dp[1][1] = dp[1][0] = dp[0][1] = 0; for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= n; j ++ ) { if (i == 1) { dp[i][j] = dp[i][j - 1] + w[i][j]; // printf("i = %d, j = %d, dp[i][j] = %d\n", i, j, dp[i][j]); } else if (j == 1) { dp[i][j] = dp[i - 1][j] + w[i][j]; } else { dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + w[i][j]; } } cout << dp[n][n] << "\n"; return 0; }
题目:
设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示: 2.gif 某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。 在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。 此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大。 输入格式 第一行为一个整数N,表示 N×N 的方格图。 接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。 行和列编号从 1 开始。 一行“0 0 0”表示结束。 输出格式 输出一个整数,表示两条路径上取得的最大的和。 数据范围 N≤10 输入样例: 8 2 3 13 2 6 6 3 5 7 4 4 14 5 2 21 5 6 4 6 3 15 7 2 14 0 0 0 输出样例: 67
分析:
走一次后,会将当前格子的数字取出来,格子当前值变为了0,所以,我们可以尝试走两次,第一次走DP的时候 将格子变为0,然后再走第二次。 但是这样走是不可以的,因为走DP的时候,不确定你是选的那一个路径,那么,格子的值也就不确定是否要变为0 所以,我们的方案就是将两次放到一次中走.
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 12; int n; int w[N][N]; int dp[N * 2][N][N];//x1表示第一条路经的x值,x2表示第二条路径的x值,k表示当前一条路径所有的步数(k-x)就是y了 int main() { cin >> n; int x, y, z; while (cin >> x >> y >> z) { if (x == 0 && y == 0 && z == 0) break; w[x][y] = z; // cout << x << ' ' << y << ' ' << z << endl; } //起始位置为(1, 1) for (int k = 2; k <= 2 * n; k ++ ) for (int i = 1; i < k && i <= n; i ++ ) for (int j = 1; j < k && j <= n; j ++ ) { int val = w[i][k - i]; if (i != j) val += w[j][k - j]; int x1 = i, x2 = j, y1 = k - x1, y2 = k - x2; // printf("x1 = %d, y1 = %d, x2 = %d, y2 = %d\n", x1, y1, x2, y2); // printf("(%d, %d), (%d, %d)\n", x1, y1, x2, y2); // printf("value = %d\n", val); if (y1 <= 0 || y1 > n || y2 <= 0 || y2 > n) continue; //一下分为四种路径 //第一种,A从左到右,B从左到右 dp[k][i][j] = max(dp[k][i][j], dp[k - 1][i][j] + val); //第二种,A从左到右,B从上到下 dp[k][i][j] = max(dp[k][i][j], dp[k - 1][i][j - 1] + val); //第三种,A从上到下,B从左到右 dp[k][i][j] = max(dp[k][i][j], dp[k - 1][i - 1][j] + val); //第四种,A从上到下,B从上到下 dp[k][i][j] = max(dp[k][i][j], dp[k - 1][i - 1][j - 1] + val); } cout << dp[2 * n][n][n] << "\n"; return 0; }
题目:
小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。 一次素质拓展活动中,班上同学安排坐成一个 m 行 n 列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。 幸运的是,他们可以通过传纸条来进行交流。 纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标 (1,1),小轩坐在矩阵的右下角,坐标 (m,n)。 从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。 在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。 班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙,反之亦然。 还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用 0 表示),可以用一个 0∼100 的自然数来表示,数越大表示越好心。 小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度之和最大。 现在,请你帮助小渊和小轩找到这样的两条路径。 输入格式 第一行有 2 个用空格隔开的整数 m 和 n,表示学生矩阵有 m 行 n 列。 接下来的 m 行是一个 m×n 的矩阵,矩阵中第 i 行 j 列的整数表示坐在第 i 行 j 列的学生的好心程度,每行的 n 个整数之间用空格隔开。 输出格式 输出一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值。 数据范围 1≤n,m≤50 输入样例: 3 3 0 3 9 2 8 5 5 7 0 输出样例: 34
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 51; int n, m; int w[N][N]; int f[N * 2][N][N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= m; j ++ ) cin >> w[i][j]; for (int k = 2; k <= n + m; k ++ ) for (int i = 1; i < k && i <= n; i ++ ) for (int j = 1; j < k && j <= n; j ++ ) { int x1 = i, y1 = k - i, x2 = j, y2 = k - j; if (y1 <= 0 || y1 > m || y2 <= 0 || y2 > m) continue; int val = w[x1][y1]; if (i != j) val += w[x2][y2]; int &x = f[k][i][j]; x = max(x, f[k - 1][i][j] + val); x = max(x, f[k - 1][i][j - 1] + val); x = max(x, f[k - 1][i - 1][j] + val); x = max(x, f[k - 1][i - 1][j - 1] + val); } cout << f[n + m][n][n] << endl; return 0; }
最长上升子序列模型
题目:
怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。 而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。 有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。 不得已,怪盗基德只能操作受损的滑翔翼逃脱。 假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。 初始时,怪盗基德可以在任何一幢建筑的顶端。 他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。 因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。 他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。 请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)? 输入格式 输入数据第一行是一个整数K,代表有K组测试数据。 每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。 输出格式 对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。 数据范围 1≤K≤100, 1≤N≤100, 0<h<10000 输入样例: 3 8 300 207 155 299 298 170 158 65 8 65 158 170 298 299 155 207 300 10 2 1 3 4 5 6 7 8 9 10 输出样例: 6 6 9
分析:
它可以单向飞行,高度逐渐降低。 那我们可以做两次最长单调上升子序DP,然后找到高度最高的一点。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 110; int n; int h[N]; int dp[N]; int get(int ans) { memset(dp, 0, sizeof dp); for (int i = 1; i <= n; i ++ ) { for (int j = 1; j < i; j ++ ) if (h[i] > h[j]) dp[i] = max(dp[i], dp[j] + 1); ans = max(ans, dp[i]); } return ans; } void work() { cin >> n; for (int i = 1; i <= n; i ++ ) cin >> h[i]; int ans = get(0); reverse(h + 1, h + 1 + n); ans = get(ans); cout << ans + 1 << endl; } int main() { int T; cin >> T; while (T -- ) { work(); } return 0; }
题目:
五一到了,ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。 同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。 队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么? 输入格式 第一行包含整数N,表示景点数量。 第二行包含N个整数,表示每个景点的海拔。 输出格式 输出一个整数,表示最多能浏览的景点数。 数据范围 2≤N≤1000 输入样例: 8 186 186 150 200 160 130 197 220 输出样例: 4
分析:
题目里说不连续浏览海拔相同的两个景点,且出现高度下降的时候就不会再上升。 那么,这就是一个单调上升子序问题,我们先正着求一边单调上升子序,然后再反正求一边。 这样,就是求得最长上升子序和最长下降子序了,然后,我们可以遍历,取一个max即可
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010; int n; int h[N]; int dp[N], f[N]; int main() { cin >> n; for (int i = 1; i <= n; i ++ ) { cin >> h[i]; dp[i] = 1; for (int j = 1; j < i; j ++ ) if (h[i] > h[j]) dp[i] = max(dp[i], dp[j] + 1); } int ans = 0; for (int i = n; i >= 1; i -- ) { for (int j = n; j > i; j -- ) if (h[i] > h[j]) f[i] = max(f[i], f[j] + 1); ans = max(ans, dp[i] + f[i]); } cout << ans << endl; return 0; }
题目:
N 位同学站成一排,音乐老师要请其中的 (N−K) 位同学出列,使得剩下的 K 位同学排成合唱队形。 合唱队形是指这样的一种队形:设 K 位同学从左到右依次编号为 1,2…,K,他们的身高分别为 T1,T2,…,TK, 则他们的身高满足 T1<…<Ti>Ti+1>…>TK(1≤i≤K)。 你的任务是,已知所有 N 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。 输入格式 输入的第一行是一个整数 N,表示同学的总数。 第二行有 N 个整数,用空格分隔,第 i 个整数 Ti 是第 i 位同学的身高(厘米)。 输出格式 输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。 数据范围 2≤N≤100, 130≤Ti≤230 输入样例: 8 186 186 150 200 160 130 197 220 输出样例: 4
分析:
没啥好说的,就是纯模板题嘞
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 110; int n; int h[N]; int f[N], g[N]; int main() { cin >> n; for (int i = 1; i <= n; i ++ ) cin >> h[i]; for (int i = 1; i <= n; i ++ ) { f[i] = 1; for (int j = 1; j < i; j ++ ) if (h[i] > h[j]) f[i] = max(f[i], f[j] + 1); } for (int i = n; i >= 1; i -- ) for (int j = n; j > i; j -- ) if (h[i] > h[j]) g[i] = max(g[i], g[j] + 1); int ans = 0; for (int i = 1; i <= n; i ++ ) ans = max(ans, f[i]+ g[i]); cout << n - ans << "\n"; return 0; }
题目:
Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。 北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。 每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。 编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。 输入格式 第1行,一个整数N,表示城市数。 第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。 输出格式 仅一行,输出一个整数,表示政府所能批准的最多申请数。 数据范围 1≤N≤5000, 0≤xi≤10000 输入样例: 7 22 4 2 6 10 3 15 12 9 8 17 17 4 2 输出样例: 4
分析:
模板,差不多。 因为我们第一维是按照升序排序,所以我们第二维也是要按照升序排序来找到最高上升子序。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 5010; struct Node { int x, y; bool operator< (const Node &W) const { return x < W.x; } }; int n; Node a[N]; int dp[N]; int main() { cin >> n; for (int i = 1; i <= n; i ++ ) { int x, y; cin >> x >> y; a[i] = {x, y}; } sort(a + 1, a + 1 + n); // for (int i = 1; i <= n; i ++ ) // cout << a[i].x << ' ' ; // cout << endl; // for (int i = 1; i <= n; i ++ ) // cout << a[i].y << ' '; // cout << endl; for (int i = 1; i <= n; i ++ ) { dp[i] = 1; for (int j = 1; j < i; j ++ ) if (a[i].y > a[j].y) dp[i] = max(dp[i], dp[j] + 1); } int ans = 0; for (int i = 1; i <= n; i ++ ) ans = max(ans, dp[i]); cout << ans << '\n'; return 0; }
题目:
一个数的序列 bi,当 b1<b2<…<bS 的时候,我们称这个序列是上升的。 对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1<i2<…<iK≤N。 比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。 这些子序列中和最大为18,为子序列(1,3,5,9)的和。 你的任务,就是对于给定的序列,求出最大上升子序列和。 注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。 输入格式 输入的第一行是序列的长度N。 第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。 输出格式 输出一个整数,表示最大上升子序列和。 数据范围 1≤N≤1000 输入样例: 7 1 7 3 5 9 4 8 输出样例: 18
分析:
将维护的信息改为最大值
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010; int n; int a[N]; int dp[N]; int main() { cin >> n; for (int i = 1; i <= n; i ++ ) cin >> a[i]; int ans = 0; for (int i = 1; i <= n; i ++ ) { dp[i] = a[i]; for (int j = 1; j < i; j ++ ) if (a[i] > a[j]) dp[i] = max(dp[i], dp[j] + a[i]); ans = max(ans, dp[i]); } cout << ans << "\n"; return 0; }
题目:
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。 但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。 某天,雷达捕捉到敌国的导弹来袭。 由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。 输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。 输入格式 共一行,输入导弹依次飞来的高度。 输出格式 第一行包含一个整数,表示最多能拦截的导弹数。 第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。 数据范围 雷达给出的高度数据是不大于 30000 的正整数,导弹数不超过 1000。 输入样例: 389 207 155 300 299 170 158 65 输出样例: 6 2
分析:
显然,第一个是最长非上升子序 第二个贪心求最小值
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <sstream> using namespace std; const int N = 100010; int n; int h[N]; int dp[N]; int q[N]; int main() { string s; getline(cin, s); stringstream ssin; ssin << s; while (ssin >> h[++ n]); // cout << n << endl; n --; int ans = 0; for (int i = n; i >= 1; i -- ) { dp[i] = 1; for (int j = n; j > i; j -- ) if (h[i] >= h[j]) dp[i] = max(dp[i], dp[j] + 1); ans = max(ans, dp[i]); } cout << ans << '\n'; int cnt = 0; for (int i = 1; i <= n; i ++ ) { //如果放的下,放在差距最小的那个导弹后面 //如果放不下,就新开一个 if (i == 1) { q[cnt ++ ] = h[i]; } else { int pos = lower_bound(q, q + cnt, h[i]) - q; if (pos < cnt) q[pos] = h[i]; else q[cnt ++ ] = h[i]; } } cout << cnt << '\n'; return 0; }
题目:
为了对抗附近恶意国家的威胁,R 国更新了他们的导弹防御系统。 一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。 例如,一套系统先后拦截了高度为 3 和高度为 4 的两发导弹,那么接下来该系统就只能拦截高度大于 4 的导弹。 给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。 输入格式 输入包含多组测试用例。 对于每个测试用例,第一行包含整数 n,表示来袭导弹数量。 第二行包含 n 个不同的整数,表示每个导弹的高度。 当输入测试用例 n=0 时,表示输入终止,且该用例无需处理。 输出格式 对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。 数据范围 1≤n≤50 输入样例: 5 3 5 2 4 1 0 输出样例: 2 样例解释 对于给出样例,最少需要两套防御系统。 一套击落高度为 3,4 的导弹,另一套击落高度为 5,2,1 的导弹。
分析:
我们上一题可以贪心的去考虑。 但是这题,每个位置都可以选择升还是降,这就不太好弄了。 注意题目中给的n很小,考虑爆搜。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 55; int n; int h[N], up[N], down[N]; int ans; void dfs(int u, int na, int nb) { if (na + nb >= ans) return; if (u == n) { ans = na + nb; return; } int k = 0; while (k < na && up[k] >= h[u]) k ++;//放到一个恰好比他小的数,up是一个单调下降的 if (k >= na) //新开一个 up[na] = h[u], dfs(u + 1, na + 1, nb); else {int t = up[k]; up[k] = h[u]; dfs(u + 1, na, nb); up[k] = t;} k = 0; while (k < nb && down[k] <= h[u]) k ++;//就放到一个恰好比他大的数,down是一个单调上升的 if (k >= nb) down[nb] = h[u], dfs(u + 1, na, nb + 1); else {int t = down[k]; down[k] = h[u]; dfs(u + 1, na, nb); down[k] = t;} } int main() { while (cin >> n, n) { for (int i = 0; i < n; i ++ ) cin >> h[i]; ans = n; dfs(0, 0, 0); cout << ans << endl; } return 0; }
背包模型
0/1背包:
什么是0/1背包? 给定n种物品和一个背包,物品i的重量是wi,价值是为vi,背包的总容量为m。在装入背包的物品时对每种物品i只有两种选择,即装入背包和不装入背包(称为0/1背包)。如何选择装入背包的物品,试的装入的物品的总价值最大? 设xi表示物品i装入背包的情况,当xi= 0时,表示不将当前物品放入背包;当xi=1时,表示将当前物品装入背包。则,有一下的约束条件和目标函数: 约束条件: val = x1w1+x2w2 + ... + xnwn; (val <= m)(其中,xi 属于{0, 1}, 1 <= i<= n) 目标函数: (val)max 用DP求解0/1背包: 引入一个二维表DP[n + 1][m + 1],可以把每个dp[i][j]都看成一个背包,dp[i][j]表示把前i个物品装入容量为j的背包中所获得的最大价值。 相对于当前的物品i,我们有两种选择,可以装也可以不装。 (1)如果装当前物品的话,那就是将当前空间v[i]内放入物品i,产生的价值为w[i]。而剩余的空间m-v[i]可以放能放的最大价值,就是之前处理好的i-1层中,选择一个体积小于等于m-v[i]的价值最大的一个状态和当前状态加起来。又因为我们的dp[i][j]保留的信息是将前i个物品装入容量为j的背包所获得的最大价值,那么,f[i][j - v[i]]就是我们要找的最大值 推导公式为: f[i][j] = f[i - 1][j - v[i]] + w[i] (2)如果不装当前物品的话,那么相当于将前i-1个物品装入容量为j的背包中所获得的最大价值。而这个值我们在递推的时候已经求得了,也就是f[i-1][j] 推导公式为:f[i][j] = f[i - 1][j] 结合这两种情况而看: f[i][j]= max(f[i - 1][j], f[i][j - v[i]] + w[i])
题目:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。 输出格式 输出一个整数,表示最大价值。 数据范围 0<N,V≤1000 0<vi,wi≤1000 输入样例 4 5 1 2 2 4 3 4 4 5 输出样例: 8
代码:
#include <iostream> using namespace std; const int N = 1010; int n, m; int dp[N][N]; int main() { cin >> n >> m; int ans = 0; for (int i = 1; i <= n; i ++ ) { int v, w; scanf("%d%d", &v, &w); // dp[i][0] = dp[i - 1][0]; for (int j = 0; j <= m; j ++ ) { //第一种选择,不选择第i个物品,且背包体积为j的情况下,背包最大价值就是对应体积下,第i-1层的结果 // dp[i][j] = max(dp[i][j], dp[i - 1][j]); dp[i][j] = dp[i - 1][j]; //第二种选择,选择第i个物品,且背包体积为j的情况下,背包最大价值为对应i-1层,且体积为j-v的最大价值加上当前价值 if (j - v >= 0) { dp[i][j] = max(dp[i][j], dp[i - 1][j - v] + w); } } // ans = max(ans, dp[i][m]); } cout << dp[n][m] << "\n"; // cout << ans << endl; return 0; }
求当前背包中选择了哪些物品,代码:
#include <iostream> using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int dp[N][N]; int a[N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) { cin >> v[i] >> w[i]; for (int j = 0; j <= m; j ++ ) { dp[i][j] = dp[i - 1][j]; if (j - v[i] >= 0) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]); } } int val = m; for (int i = n; i >= 1; i -- ) if (dp[i][val] == dp[i - 1][val]) //说明没选 a[i] = 0; else //说明选了 a[i] = 1, val -= v[i]; for (int i = 1; i <= n; i ++ ) cout << a[i] << ' '; return 0; }
题目:
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。 为此,他想拜附近最有威望的医师为师。 医师为了判断他的资质,给他出了一个难题。 医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。” 如果你是辰辰,你能完成这个任务吗? 输入格式 输入文件的第一行有两个整数 T 和 M,用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。 接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。 输出格式 输出文件包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。 数据范围 1≤T≤1000, 1≤M≤100 输入样例: 70 3 71 100 69 1 1 2 输出样例: 3
代码:
//0/1背包模板题 #include <iostream> using namespace std; const int N = 110, M = 1010; int m, n; int dp[N][M]; int main() { cin >> m >> n; for (int i = 1; i <= n; i ++ ) { int v, w; cin >> v >> w; for (int j = 0; j <= m; j ++ ) { dp[i][j] = dp[i - 1][j]; if (j - v >= 0) dp[i][j] = max(dp[i][j], dp[i - 1][j - v] + w); } } cout << dp[n][m] << endl; return 0; }
题目:
有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积(正整数)。 要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。 输入格式 第一行是一个整数 V,表示箱子容量。 第二行是一个整数 n,表示物品数。 接下来 n 行,每行一个正整数(不超过10000),分别表示这 n 个物品的各自体积。 输出格式 一个整数,表示箱子剩余空间。 数据范围 0<V≤20000, 0<n≤30 输入样例: 24 6 8 3 12 7 9 7 输出样例: 0
代码:
#include <iostream> using namespace std; const int N = 20010; int m, n; int dp[N]; int main() { cin >> m >> n; for (int i = 1; i <= n; i ++ ) { int x; scanf("%d", &x); for (int j = m; j >= x; j -- ) dp[j] = max(dp[j], dp[j - x] + x); } cout << m - dp[m] << "\n"; return 0; }
题目:
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。 更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N 元钱就行”。 今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的 N 元。 于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1∼5 表示,第 5 等最重要。 他还从因特网上查到了每件物品的价格(都是整数元)。 他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。 设第 j 件物品的价格为 v[j],重要度为 w[j],共选中了 k 件物品,编号依次为 j1,j2,…,jk,则所求的总和为: v[j1]×w[j1]+v[j2]×w[j2]+…+v[jk]×w[jk] 请你帮助金明设计一个满足要求的购物单。 输入格式 输入文件的第 1 行,为两个正整数 N 和 m,用一个空格隔开。(其中 N 表示总钱数,m 为希望购买物品的个数) 从第 2 行到第 m+1 行,第 j 行给出了编号为 j−1 的物品的基本数据,每行有 2 个非负整数 v 和 p。(其中 v 表示该物品的价格,p 表示该物品的重要度) 输出格式 输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(数据保证结果不超过 108)。 数据范围 1≤N<30000, 1≤m<25, 0≤v≤10000, 1≤p≤5 输入样例: 1000 5 800 2 400 5 300 5 400 3 200 2 输出样例: 3900
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 30, M = 30010; int m, n; int dp[M]; int main() { cin >> m >> n; for (int i = 1; i <= n; i ++ ) { int v, w; cin >> v >> w; for (int j = m; j >= v; j -- ) dp[j] = max(dp[j], dp[j - v] + v * w); } cout << dp[m] << "\n"; return 0; }
0/1背包求方案数
题目:
给定 N 个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。 输入格式 第一行包含两个整数 N 和 M。 第二行包含 N 个整数,表示 A1,A2,…,AN。 输出格式 包含一个整数,表示可选方案数。 数据范围 1≤N≤100, 1≤M≤10000, 1≤Ai≤1000 输入样例: 4 4 1 1 2 2 输出样例: 3
分析:
0/1背包求方案数 假设,从开始节点Start可以到达节点x1,x2, x3现在有三条路x1, x2, x3可以走到当前点x4,有两条路x1,x3可以走到x5,从x4,x5可以走到我们的目标节点END 那么,x4的方案数为3, x5的方案数为2,那么END就会有3 + 2 = 5个方案 这题同样如此。
代码:
//0/1背包,记录方案数 //所求性质为方案数,注意初始化为dp[0] = 1; #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 110, M = 10010; int n, m; int v[N]; int dp[M]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) cin >> v[i]; // memset(dp, -0x3f, sizeof dp); dp[0] = 1; // for (int i = 1; i <= n; i ++ ) // for (int j = m; j >= v[i]; j -- ) // dp[j] = max(dp[j], dp[j - v[i]] + 1); // for (int i = 1; i <= n; i ++ ) cout << dp[i] << ' '; // cout << endl; for (int i = 1; i <= n; i ++ ) for (int j = m; j >= v[i]; j -- ) dp[j] += dp[j - v[i]]; cout << dp[m] << "\n"; return 0; }
完全背包问题
完全背包,相当于求所有前缀0~j-v中的最大值
题目:
小明手里有n元钱全部用来买书,书的价格为10元,20元,50元,100元。 问小明有多少种买书方案?(每种书可购买多本) 输入格式 一个整数 n,代表总共钱数。 输出格式 一个整数,代表选择方案种数。 数据范围 0≤n≤1000 输入样例1: 20 输出样例1: 2 输入样例2: 15 输出样例2: 0 输入样例3: 0 输出样例3: 1
代码:
//这是完全背包问题 #include <iostream> using namespace std; const int N = 1010; int n; int a[4] = {10, 20, 50, 100}; int dp[N]; int main() { cin >> n; dp[0] = 1; for (int i = 0; i < 4; i ++ ) for (int j = a[i]; j <= n; j ++ ) dp[j] += dp[j - a[i]]; cout << dp[n] << endl; return 0; }
题目:
给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。 输入格式 第一行,包含两个整数n和m。 接下来n行,每行包含一个整数,表示一种货币的面值。 输出格式 共一行,包含一个整数,表示方案数。 数据范围 n≤15,m≤3000 输入样例: 3 10 1 2 5 输出样例: 10
代码:
#include <iostream> using namespace std; typedef long long LL; const int N = 15, M = 3010; int n, m; int a[N]; LL dp[M]; int main() { cin >> n >> m; dp[0] = 1; for (int i = 0; i < n; i ++ ) { int x; cin >> x; for (int j = x; j <= m; j ++ ) dp[j] += dp[j - x]; } cout << dp[m] << "\n"; return 0; }
题目:
在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。 为了方便,我们把货币种数为 n、面额数组为 a[1..n] 的货币系统记作 (n,a)。 在一个完善的货币系统中,每一个非负整数的金额 x 都应该可以被表示出,即对每一个非负整数 x,都存在 n 个非负整数 t[i] 满足 a[i]×t[i] 的和为 x。 然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额 x 不能被该货币系统表示出。 例如在货币系统 n=3, a=[2,5,9] 中,金额 1,3 就无法被表示出来。 两个货币系统 (n,a) 和 (m,b) 是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。 现在网友们打算简化一下货币系统。 他们希望找到一个货币系统 (m,b),满足 (m,b) 与原来的货币系统 (n,a) 等价,且 m 尽可能的小。 他们希望你来协助完成这个艰巨的任务:找到最小的 m。 输入格式 输入文件的第一行包含一个整数 T,表示数据的组数。 接下来按照如下格式分别给出 T 组数据。 每组数据的第一行包含一个正整数 n。 接下来一行包含 n 个由空格隔开的正整数 a[i]。 输出格式 输出文件共有 T 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a) 等价的货币系统 (m,b) 中,最小的 m。 数据范围 1≤n≤100, 1≤a[i]≤25000, 1≤T≤20 输入样例: 2 4 3 19 10 6 5 11 29 13 19 17 输出样例: 2 5
代码:
#include <iostream> #include <cstring> using namespace std; const int N = 110, M = 25010; int n; int dp[M]; void work() { memset(dp, -1, sizeof dp); dp[0] = 0; cin >> n; int ans = 0; for (int i = 0; i < n; i ++ ) { int x; cin >> x; for (int j = x; j <= M - 10; j ++ ) if (dp[j - x] >= 0) dp[j] = max(dp[j], dp[j - x] + 1);//如果dp[j - x] >= 0,说明可以被组成 } // for (int i = 1; i <= 99; i ++ ) printf("%3d", i); puts(""); // for (int i = 1; i <= 99; i ++ ) printf("%3d", dp[i]); puts(""); for (int j = 1; j <= M - 10; j ++ ) if (dp[j] == 1) ans ++; //如果等于1,说明只被0更新过,是唯一由自己组成 cout << ans << "\n"; } int main() { int T; cin >> T; while (T -- ) { work(); } return 0; }
多重背包问题
求的是:求长度为S得滑动窗口得最大值,是[j - s * v, j - (s-1)*v, j - v]这S个数得最大值得到j得最大值
题目
有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。 输出格式 输出一个整数,表示最大价值。 数据范围 0<N,V≤100 0<vi,wi,si≤100 输入样例 4 5 1 2 3 2 4 1 3 4 3 4 5 2 输出样例: 10
分析:
两处代码
代码1:将多重背包问题转为0/1背包问题
#include <iostream> using namespace std; const int N = 110; int n, m; int dp[N]; int main() { cin >> n >> m; for (int i = 0; i < n; i ++ ) { int v, w, s; cin >> v >> w >> s; for (int k = 1; k <= s; k ++ ) for (int j = m; j >= k * v; j -- ) //取一个物品的时候,dp[j]已经做过一遍了,那么接下来的时候在此基础上接着做0/1背包就好了 dp[j] = max(dp[j], dp[j - v] + w); } //再解释一遍,当做完第一遍0/1背包的时候(k=1),我们剩下的合法的体积就变成了j-v了,在此基础上在做0/1背包, //重复的部分去了max,相当于第二次的物品取代了第一次物品,没有做第一遍的取物品的操作 cout << dp[m] << "\n"; return 0; }
代码2:多重背包
#include <iostream> using namespace std; const int N = 110; int n, m; int dp[N][N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) { int v, w, s; cin >> v >> w >> s; for (int j = 0; j <= m; j ++ ) for (int k = 0; k <= s && k * v <= j; k ++ ) dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v] + k * w); } cout << dp[n][m] << endl; return 0; }
题目:
有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。 输出格式 输出一个整数,表示最大价值。 数据范围 0<N≤1000 0<V≤2000 0<vi,wi,si≤2000 提示: 本题考查多重背包的二进制优化方法。 输入样例 4 5 1 2 3 2 4 1 3 4 3 4 5 2 输出样例: 10
分析:
现在的数据n为1000,指定不能三重for循环。 我们可以将问题二进制优化,将每个s转化成1, 2, 4, ... , 2 ^ k, s - 2 ^ k。 这些个数(k或k+1)个数,可以组成s中的任意个数。
那么,迭代n次,就将n重背包转化为了0/1背包了,可以两个for来做了。
代码1:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010, M = 2010; int n,m; int dp[M]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) { int v, w, s; cin >> v >> w >> s; int k = 1; while (k < s) { for (int j = m; j >= k * v; j -- ) dp[j] = max(dp[j], dp[j - k * v] + k * w); s -= k; k <<= 1; } if (s) { for (int j = m; j >= s * v; j -- ) dp[j] = max(dp[j], dp[j - s * v] + s * w); } } cout << dp[m] << "\n"; return 0; }
代码2:
//二进制优化,每个S可以由二进制组成,那么就可以生成若干个不同体积且对应价值的新的背包 #include <iostream> using namespace std; typedef long long LL; const int N = 20010, M = 2010; int n, m; LL V[N], W[N]; LL dp[N]; int main() { cin >> n >> m; //二进制优化背包 //不可以将一个数变成分为若干个二进制相加的结果,因为中间份的某些数会特别大,所以,还是变成将一个S分为1,2,4,...,2^k,s-(2^k)的结果
//还有一点,就是我们这种错误的分的方法,不可以凑出S中的任意一个数,这显然是没办法解决本题的 // int cnt = 0; // for (int i = 0; i < n; i ++ ) // { // LL v, w, s; cin >> v >> w >> s; // //类似快速幂的求法搞一下 // while (s) // { // if (s & 1) V[++ cnt] = v, W[cnt] = w; // printf("v=%d, w=%d, s=%d, cnt=%d, ", v, w, s, cnt); // printf("V[%d]=%d, W[%d]=%d\n", i, V[i], i, W[i]); // s >>= 1; // v = v * v; // w = w * w; // } // } // printf("cnt = %d\n", cnt); // for (int i = 1; i <= cnt; i ++ ) // for (int i = 1; i <= cnt; i ++ ) // for (int j = m; j >= V[i]; j -- ) // dp[j] = max(dp[j], dp[j - V[i]] + W[i]); int cnt = 0; for (int i = 1; i <= n; i ++ ) { LL v, w, s; cin >> v >> w >> s; int k = 1; while (k <= s) { cnt ++; V[cnt] = v * k; W[cnt] = w * k; s -= k; k *= 2; } if (s > 0) { cnt ++; V[cnt] = s * v; W[cnt] = s * w; } } for (int i = 1; i <= cnt; i ++ ) for (int j = m; j >= V[i]; j -- ) dp[j] = max(dp[j], dp[j - V[i]] + W[i]); cout << dp[m] << "\n"; return 0; }
单调队列优化嘛
题目:
有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V (0<N≤1000, 0<V≤20000),用空格隔开,分别表示物品种数和背包容积。 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。 输出格式 输出一个整数,表示最大价值。 数据范围 0<N≤1000 0<V≤20000 0<vi,wi,si≤20000 提示 本题考查多重背包的单调队列优化方法。 输入样例 4 5 1 2 3 2 4 1 3 4 3 4 5 2 输出样例: 10
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 20010; int n, m; int f[N], g[N], q[N]; int main() { cin >> n >> m; for (int i = 0; i < n; i ++ ) { int v, w, s; cin >> v >> w >> s; //拿到前一层的信息,也就是i-1层的信息 memcpy(g, f, sizeof f); for (int j = 0; j < v; j ++ ) { int hh = 0, tt = -1; for (int k = j; k <= m; k += v) { //维护的是一个单调递减的长度为s*v的滑动窗口 //维护单调队列的区间长度 if (hh <= tt && q[hh] < k - s * v) hh ++; //维护单调队列的最值性,如果队头元素小于当前值,就不断的弹出队头 while (hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt --; //将当前元素放入队头 q[++ tt] = k; f[k] = g[q[hh]] + (k - q[hh]) / v * w; } } } cout << f[m] << "\n"; return 0; }
题目:
为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。 期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力。 输入格式 第一行二个数n,m,其中n代表希望购买的奖品的种数,m表示拨款金额。 接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和能购买的最大数量(买0件到s件均可)。 输出格式 一行:一个数,表示此次购买能获得的最大的价值(注意!不是价格)。 数据范围 n≤500,m≤6000, v≤100,w≤1000,s≤10 输入样例: 5 1000 80 20 4 40 50 9 30 50 7 40 30 6 20 20 1 输出样例: 1040
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 510, M = 6010; int n, m; int dp[M]; int main() { cin >> n >> m; for (int i = 0; i < n; i ++ ) { int v, w, s; cin >> v >> w >> s; for (int k = 1; k <= s; k ++ ) for (int j = m; j >= k * v; j -- ) dp[j] = max(dp[j], dp[j - v] + w); } cout << dp[m] << "\n"; return 0; }
题目:
有 N 种物品和一个容量是 V 的背包。 物品一共有三类: 第一类物品只能用1次(01背包); 第二类物品可以用无限次(完全背包); 第三类物品最多只能用 si 次(多重背包); 每种体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。 si=−1 表示第 i 种物品只能用1次; si=0 表示第 i 种物品可以用无限次; si>0 表示第 i 种物品可以使用 si 次; 输出格式 输出一个整数,表示最大价值。 数据范围 0<N,V≤1000 0<vi,wi≤1000 −1≤si≤1000 输入样例 4 5 1 2 -1 2 4 1 3 4 0 4 5 2 输出样例: 8
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010; int n, m; int V[N], W[N], cnt; int dp[N]; void print() { for (int j = 0; j <= m; j ++ ) cout << dp[j] << " "; cout << endl; } int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) { int v, w, s; cin >> v >> w >> s; if (s == - 1) //0/1背包 { // V[++ cnt] = v; // W[cnt] = w; for (int j = m; j >= v; j -- ) dp[j] = max(dp[j], dp[j - v] + w); } else if (s == 0) //完全背包 { for (int j = v; j <= m; j ++ ) dp[j] = max(dp[j], dp[j - v] + w); } else //多重背包/S=1000二进制优化 { int k = 1; // cnt = 0; memset(V, 0, sizeof V); memset(W, 0, sizeof W); while (k <= s) { // cnt ++; // V[cnt] = v * k; // W[cnt] = w * k; for (int j = m; j >= k * v; j -- ) dp[j] = max(dp[j], dp[j - k * v] + k * w); s -= k; k *= 2;; } if (s > 0) { for (int j = m; j >= s * v; j -- ) dp[j] = max(dp[j], dp[j - s * v] + s * w); // cnt ++; // V[cnt] = s * v; // W[cnt] = s * w; } // print(); // for (k = 1; k <= cnt; k ++ ) // printf("V[%d] = %d, W[%d] = %d\n", k, V[k], k, W[k]); // for (k = 1; k <= cnt; k ++ ) // for (int j = m; j >= V[i] * k; j -- ) // dp[j] = max(dp[j], dp[j - k * V[k]] + k * W[k]); // print(); } } // // cout << cnt << endl; // for (int i = 1; i <= cnt; i ++ ) // for (int j = m; j >= V[i]; j -- ) // dp[j] = max(dp[j], dp[j - V[i]] + W[i]); cout << dp[m] << "\n"; return 0; }
二维费用背包
题目:
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。 每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。 求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。 接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。 输出格式 输出一个整数,表示最大价值。 数据范围 0<N≤1000 0<V,M≤100 0<vi,mi≤100 0<wi≤1000 输入样例 4 5 6 1 2 3 2 4 4 3 4 5 4 5 6 输出样例: 8
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010, M = 110; int n, m1, m2; int dp[M][M]; int main() { cin >> n >> m1 >> m2;//个数,容量,重量 for (int i = 0; i < n; i ++ ) { int v, m, w; cin >> v >> m >> w;//体积、重量、价值 for (int j = m1; j >= v; j -- ) for (int k = m2; k >= m; k -- ) dp[j][k] = max(dp[j][k], dp[j - v][k - m] + w); } cout << dp[m1][m2] << '\n'; return 0; }
题目:
潜水员为了潜水要使用特殊的装备。 他有一个带2种气体的气缸:一个为氧气,一个为氮气。 让潜水员下潜的深度需要各种数量的氧和氮。 潜水员有一定数量的气缸。 每个气缸都有重量和气体容量。 潜水员为了完成他的工作需要特定数量的氧和氮。 他完成工作所需气缸的总重的最低限度的是多少? 例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量: 3 36 120 10 25 129 5 50 250 1 45 130 4 20 119 如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。 你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。 输入格式 第一行有2个整数 m,n。它们表示氧,氮各自需要的量。 第二行为整数 k 表示气缸的个数。 此后的 k 行,每行包括ai,bi,ci,3个整数。这些各自是:第 i 个气缸里的氧和氮的容量及气缸重量。 输出格式 仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。 数据范围 1≤m≤21, 1≤n≤79, 1≤k≤1000, 1≤ai≤21, 1≤bi≤79, 1≤ci≤800 输入样例: 5 60 5 3 36 120 10 25 129 5 50 250 1 45 130 4 20 119 输出样例: 249
代码:
/* 潜水员为了完成他的工作需要特定数量的氧和氮。 他完成工作所需气缸的总重的最低限度的是多少? 这里就说明了,携带的这两种气体一定要大于等于所要求的体积,而不能小于,可能不一定恰好等于 比如说,我们目标状态为dp[3][3],但是这里只有两个情况,4,4与2,2价值是w1,w2 那么,我们为了能凑到3,3显然一定要选择4,4这种情况,才能满足3,3的要求。 所以,dp[3][3] = dp[1][1] + w1 在算2,2的时候,发现dp[3][3] = min(dp[3][3],dp[1 + 2][1 +2] + w2) 还原到成: dp[3][3] = min(dp[1][1] + w1, dp[1 + 2][1 + 2] + w1 + w2); 指定,我们只选择4,4就完事了 */ #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 55, M = 110; int n, m, k; int dp[N][M]; int main() { cin >> n >> m >> k; memset(dp, 0x3f, sizeof dp); dp[0][0] = 0; while (k -- ) { int a, b, c; cin >> a >> b >> c; for (int i = n; i >= 0; i -- ) for (int j = m; j >= 0; j -- ) dp[i][j] = min(dp[i][j], dp[max(0, i - a)][max(0, j - b)] + c); } cout << dp[n][m] << "\n"; return 0; }
题目:
宠物小精灵是一部讲述小智和他的搭档皮卡丘一起冒险的故事。 一天,小智和皮卡丘来到了小精灵狩猎场,里面有很多珍贵的野生宠物小精灵。 小智也想收服其中的一些小精灵。 然而,野生的小精灵并不那么容易被收服。 对于每一个野生小精灵而言,小智可能需要使用很多个精灵球才能收服它,而在收服过程中,野生小精灵也会对皮卡丘造成一定的伤害(从而减少皮卡丘的体力)。 当皮卡丘的体力小于等于0时,小智就必须结束狩猎(因为他需要给皮卡丘疗伤),而使得皮卡丘体力小于等于0的野生小精灵也不会被小智收服。 当小智的精灵球用完时,狩猎也宣告结束。 我们假设小智遇到野生小精灵时有两个选择:收服它,或者离开它。 如果小智选择了收服,那么一定会扔出能够收服该小精灵的精灵球,而皮卡丘也一定会受到相应的伤害;如果选择离开它,那么小智不会损失精灵球,皮卡丘也不会损失体力。 小智的目标有两个:主要目标是收服尽可能多的野生小精灵;如果可以收服的小精灵数量一样,小智希望皮卡丘受到的伤害越小(剩余体力越大),因为他们还要继续冒险。 现在已知小智的精灵球数量和皮卡丘的初始体力,已知每一个小精灵需要的用于收服的精灵球数目和它在被收服过程中会对皮卡丘造成的伤害数目。 请问,小智该如何选择收服哪些小精灵以达到他的目标呢? 输入格式 输入数据的第一行包含三个整数:N,M,K,分别代表小智的精灵球数量、皮卡丘初始的体力值、野生小精灵的数量。 之后的K行,每一行代表一个野生小精灵,包括两个整数:收服该小精灵需要的精灵球的数量,以及收服过程中对皮卡丘造成的伤害。 输出格式 输出为一行,包含两个整数:C,R,分别表示最多收服C个小精灵,以及收服C个小精灵时皮卡丘的剩余体力值最多为R。 数据范围 0<N≤1000, 0<M≤500, 0<K≤100 输入样例1: 10 100 5 7 10 2 40 2 50 1 20 4 20 输出样例1: 3 30 输入样例2: 10 100 5 8 110 12 10 20 10 5 200 1 110 输出样例2: 0 100
分析:
这里的体积有两个条件,一是精灵球的个数,二是皮卡丘的体力,当体力m2小于等于0的时候,当前的精灵不管精灵球够不够捕捉,都会失败,所以,我们要最少让体力保留1,所以我们从m2-1开始。 还有就是,按照0/1背包的模板来写,但是,需要将背包体积限制分为两层for来写。 注意最后皮卡丘的体力。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010; int m1, m2, n; int v1[N], v2[N]; int dp[N][N]; int main() { cin >> m1 >> m2 >> n; for (int i = 1; i <= n; i ++ ) cin >> v1[i] >> v2[i]; for (int i = 1; i <= n; i ++ ) { for (int j = m1; j >= v1[i]; j -- ) { for (int k = m2 - 1; k >= v2[i]; k -- ) dp[j][k] = max(dp[j][k], dp[j - v1[i]][k - v2[i]] + 1); } } int ans = 0; for (int i = m2 - 1; i >= 1; i -- ) if (dp[m1][i] == dp[m1][i - 1]) ans = m2 - i + 1; else break;//如果一直到最后都相等,那么体力就是剩1了 cout << dp[m1][m2 - 1] << " " << max(ans, 1) << endl; return 0; }
背包问题求方案书
题目:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。 输出格式 输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。 物品编号范围是 1…N。 数据范围 0<N,V≤1000 0<vi,wi≤1000 输入样例 4 5 1 2 2 4 3 4 4 6 输出样例: 1 4
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010; int n, m; int v[N], w[N]; int dp[N][N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; for (int i = n; i >= 1; i -- ) for (int j = 0; j <= m; j ++ ) { dp[i][j] = dp[i + 1][j]; if (j - v[i] >= 0) dp[i][j] = max(dp[i][j], dp[i + 1][j - v[i]] + w[i]); } int j = m; for (int i = 1; i <= n; i ++ ) { if (j >= v[i] && dp[i][j] == dp[i + 1][j - v[i]] + w[i]) { cout << i << " " ; j = j - v[i]; } } return 0; }
题目:
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。 输出格式 输出一个整数,表示 方案数 模 109+7 的结果。 数据范围 0<N,V≤1000 0<vi,wi≤1000 输入样例 4 5 1 2 2 4 3 4 4 6 输出样例: 2
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 1010, mod = 1e9 + 7; int n, m; int f[N], g[N]; int main() { cin >> n >> m; memset(f, -0x3f, sizeof f); f[0] = 0; g[0] = 1; //体积恰好为j的价值,就不一定j=m的时候会取到最大值 for (int i = 0; i < n; i ++ ) { int v, w; cin >> v >> w; for (int j = m; j >= v; j -- ) { int maxv = max(f[j], f[j - v] + w); int s = 0; if (maxv == f[j]) s = g[j]; if (maxv == f[j - v] + w) s = (s + g[j - v]) % mod; f[j] = maxv, g[j] = s; } } int res = 0; for (int i = 1; i <= m; i ++ ) if (f[i] > f[res]) res = i; int sum = 0; for (int i = 1; i <= m; i ++ ) if (f[i] == f[res]) sum = (sum + g[i]) % mod; cout << sum << "\n"; return 0; }
分组背包
分组背包问题:就是在每组物品中最多选择一个。
0/1背包问题:就是对每个物品最多只能选择一个。
那么,0/1背包问题与分组背包问题极为相似。分组背包在每组中可以不选、选一个、选两个、... 、选s个。而0/1背包是对每个物品可以不选、选一个。我们在做选择的时候,在加一维,来判断选的是当前组的哪个物品。
那,0/1背包就是分组背包的一个特例喽~~
有 N 组物品和一个容量是 V 的背包。 每组物品有若干个,同一组内的物品最多只能选一个。 每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。 求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。 输出最大价值。 输入格式 第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。 接下来有 N 组数据: 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量; 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值; 输出格式 输出一个整数,表示最大价值。 数据范围 0<N,V≤100 0<Si≤100 0<vij,wij≤100 输入样例 3 5 2 1 2 2 4 1 3 4 1 4 5 输出样例: 8
分析:
先循环决策再循环体积就错了,这里有什么讲究吗? 如果说先循环决策的话,如果说一个组里面有两个物品,那么你在更新第一个物品的时候f的状态就是包括第一个物品的状态了。这样就做不到一组只选一个了。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 110; int n, m; int v[N], w[N], s; int dp[N][N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) { cin >> s; for (int j = 1; j <= s; j ++ ) cin >> v[j] >> w[j]; for (int j = 0; j <= m; j ++ ) { dp[i][j] = dp[i - 1][j]; for (int k = 1; k <= s; k ++ ) if (j >= v[k]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[k]] + w[k]); } } cout << dp[n][m] << "\n"; return 0; }
题目:
总公司拥有M台 相同 的高效设备,准备分给下属的N个分公司。 各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。 问:如何分配这M台设备才能使国家得到的盈利最大? 求出最大盈利值。 分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数M。 输入格式 第一行有两个数,第一个数是分公司数N,第二个数是设备台数M; 接下来是一个N*M的矩阵,矩阵中的第 i 行第 j 列的整数表示第 i 个公司分配 j 台机器时的盈利。 输出格式 第一行输出最大盈利值; 接下N行,每行有2个数,即分公司编号和该分公司获得设备台数。 答案不唯一,输出任意合法方案即可。 数据范围 1≤N≤10, 1≤M≤15 输入样例: 3 3 30 40 50 20 30 50 20 25 30 输出样例: 70 1 1 2 1 3 1
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 20; int n, m; int w[N][N]; int dp[N][N]; int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) for (int j = 1; j <= m; j ++ ) cin >> w[i][j]; for (int i = n; i >= 1; i -- ) { for (int j = 0; j <= m; j ++ ) { // dp[i][j] = dp[i + 1][j]; for (int k = 0; k <= m; k ++ ) if (j - k >= 0) dp[i][j] = max(dp[i][j], dp[i + 1][j - k] + w[i][k]); } } cout << dp[1][m] << "\n"; int k = m; for (int i = 1; i <= n; i ++ ) for (int j = 0; j <= k; j ++ ) { if (dp[i][k] == dp[i + 1][k - j] + w[i][j]) { // printf("dp[%d][%d] = %d == dp[%d + 1][%d-%d] = %d + w[%d][%d] = %d\n", i, k, dp[i][k], i, k, j, dp[i + 1][k - j], i, j, w[i][j]); cout << i << " " << j << endl; k -= j; break; } } return 0; }
题目:
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。
更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。
今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
如果要买归类为附件的物品,必须先买该附件所属的主件。
每个主件可以有0个、1个或2个附件。
附件不再有从属于自己的附件。
金明想买的东西很多,肯定会超过妈妈限定的N元。
于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。
他还从因特网上查到了每件物品的价格(都是10元的整数倍)。
他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,…,jkj1,j2,…,jk,则所求的总和为:
v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk]v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk](其中*为乘号)
请你帮助金明设计一个满足要求的购物单。
输入格式
输入文件的第1行,为两个正整数,用一个空格隔开:N m,其中N表示总钱数,m为希望购买物品的个数。
从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数v p q,其中v表示该物品的价格,p表示该物品的重要度(1~5),q表示该物品是主件还是附件。
如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号。
输出格式
输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。
数据范围
N<32000,m<60,v<10000N<32000,m<60,v<10000
输入样例:
1000 5 800 2 0 400 5 1 300 5 1 400 3 0 500 2 0
输出样例:
2200
代码:
/* 多重背包 将具有主次关系的放在一组来处理。 因为只有0,1,2这三种情况,所以,一共也就四种情况,在当前组中一下就好了 */ #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; typedef pair<int, int> PII; //第一维表示体积,第二维表示价值 #define x first #define y second const int N = 110, M = 32010; int m, n; PII master[N]; vector<PII> advocate[N]; int dp[M]; int main() { cin >> m >> n; for (int i = 1; i <= n; i ++ ) { int a, b, id; cin >> a >> b >> id; if (id == 0) { master[i] = {a, a * b}; } else { advocate[id].push_back({a, a * b}); } } for (int i = 1; i <= n; i ++ ) for (int j = m; j >= 0; j -- ) for (int k = 0; k < 1 << advocate[i].size(); k ++ ) { int v = master[i].x, w = master[i].y; for (int u = 0; u < advocate[i].size(); u ++ ) if (k >> u & 1) { v += advocate[i][u].x; w += advocate[i][u].y; } if (j - v >= 0) dp[j] = max(dp[j], dp[j - v] + w); } cout << dp[m] << "\n"; return 0; }
题目:
有 NN 个物品和一个容量是 VV 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 ii,体积是 vivi,价值是 wiwi,依赖的父节点编号是 pipi。物品的下标范围是 1…N1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,VN,V,用空格隔开,分别表示物品个数和背包容量。
接下来有 NN 行数据,每行数据表示一个物品。
第 ii 行有三个整数 vi,wi,pivi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1pi=−1,表示根节点。 数据保证所有物品构成一棵树。输出格式
输出一个整数,表示最大价值。
数据范围
1≤N,V≤1001≤N,V≤100
1≤vi,wi≤1001≤vi,wi≤100父节点编号范围:
- 内部结点:1≤pi≤N1≤pi≤N;
- 根节点 pi=−1pi=−1;
输入样例
5 7 2 3 -1 2 2 1 3 5 1 4 7 2 3 6 2
输出样例:
11
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 110; int n, m; int v[N], w[N]; int h[N], e[N], ne[N], idx; int f[N][N]; void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void dfs(int u) { for (int i = h[u]; ~i; i = ne[i]) { int son = e[i]; dfs(son); for (int j = m - v[u]; j >= 0; j -- ) for (int k = 0; k <= j; k ++ ) f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]); } for (int i = m; i >= v[u]; i -- ) f[u][i] = f[u][i - v[u]] + w[u]; for (int i = 0; i < v[u]; i ++ ) f[u][i] = 0; } int main() { cin >> n >> m; memset(h, -1, sizeof h); int root; for (int i = 1; i <= n; i ++ ) { int p; cin >> v[i] >> w[i] >> p; if (p == -1) root = i; else add(p, i); } dfs(root); cout << f[root][m] << endl; return 0; }
状态机模型
题目:
阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。 这条街上一共有 N 家店铺,每家店中都有一些现金。 阿福事先调查得知,只有当他同时洗劫了两家相邻的店铺时,街上的报警系统才会启动,然后警察就会蜂拥而至。 作为一向谨慎作案的大盗,阿福不愿意冒着被警察追捕的风险行窃。 他想知道,在不惊动警察的情况下,他今晚最多可以得到多少现金? 输入格式 输入的第一行是一个整数 T,表示一共有 T 组数据。 接下来的每组数据,第一行是一个整数 N ,表示一共有 N 家店铺。 第二行是 N 个被空格分开的正整数,表示每一家店铺中的现金数量。 每家店铺中的现金数量均不超过1000。 输出格式 对于每组数据,输出一行。 该行包含一个整数,表示阿福在不惊动警察的情况下可以得到的现金数量。 数据范围 1≤T≤50, 1≤N≤105 输入样例: 2 3 1 8 2 4 10 7 6 14 输出样例: 8 24 样例解释 对于第一组样例,阿福选择第2家店铺行窃,获得的现金数量为8。 对于第二组样例,阿福选择第1和4家店铺行窃,获得的现金数量为10+14=24。
代码:
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 100010; int n; int dp[N][2]; void work() { memset(dp, 0, sizeof dp); scanf("%d", &n); for (int i = 1; i <= n; i ++ ) { int x; scanf("%d", &x); dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + x); dp[i][1] = dp[i - 1][0]; } printf("%d\n", max(dp[n][0], dp[n][1])); return; } int main() { int T; scanf("%d", &T); while (T -- ) { work(); } return 0; }
题目:
给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润,你最多可以完成 k 笔交易。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。 输入格式 第一行包含整数 N 和 k,表示数组的长度以及你可以完成的最大交易数量。 第二行包含 N 个不超过 10000 的正整数,表示完整的数组。 输出格式 输出一个整数,表示最大利润。 数据范围 1≤N≤105, 1≤k≤100 输入样例1: 3 2 2 4 1 输出样例1: 2 输入样例2: 6 2 3 2 6 5 0 3 输出样例2: 7 样例解释 样例1:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。 样例2:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。共计利润 4+3 = 7.
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 100010, M = 110; int n, k; int dp[N][M][2]; //dp[i][k][0]表示当前进行第k次交易,0表示已经买入了,也就是手中有货 //dp[i][k][1]表示当前进行第k次交易,1表示已经卖出了,也就是手中无货 int main() { int ans = 0; cin >> n >> k; memset(dp, -0x3f, sizeof dp); for (int i = 1; i <= k; i ++ ) dp[0][i][1] = 0; for (int i = 1; i <= n; i ++ ) dp[i][0][1] = 0; for (int i = 1; i <= n; i ++ ) { int x; scanf("%d", &x); for (int j = 1; j <= k; j ++ ) { dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j - 1][1] - x); dp[i][j][1] = max(dp[i - 1][j][0] + x, dp[i - 1][j][1]); } } for (int i = 1; i <= k; i ++ ) ans = max(ans, dp[n][i][1]); cout << ans << endl; return 0; }
题目:
给定一个长度为 N 的数组,数组中的第 i 个数字表示一个给定股票在第 i 天的价格。 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 输入格式 第一行包含整数 N,表示数组长度。 第二行包含 N 个不超过 10000 的正整数,表示完整的数组。 输出格式 输出一个整数,表示最大利润。 数据范围 1≤N≤105 输入样例: 5 1 2 3 0 2 输出样例: 3 样例解释 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出],第一笔交易可得利润 2-1 = 1,第二笔交易可得利润 2-0 = 2,共得利润 1+2 = 3。
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 100010; int n; int dp[N][3]; int main() { cin >> n; memset(dp, -0x3f, sizeof dp); for (int i = 0; i <= n; i ++ ) dp[i][0] = 0; for (int i = 1; i <= n; i ++ ) { int x; scanf("%d", &x); dp[i][0] = max(dp[i - 1][0], dp[i - 1][2]); dp[i][1] = max(dp[i - 1][0] - x, dp[i - 1][1]); dp[i][2] = dp[i - 1][1] + x; } cout << max(dp[n][0], max(dp[n][1], dp[n][2])); return 0; }
题目:
你现在需要设计一个密码 S,S 需要满足: S 的长度是 N; S 只包含小写英文字母; S 不包含子串 T; 例如:abc 和 abcde 是 abcde 的子串,abd 不是 abcde 的子串。 请问共有多少种不同的密码满足要求? 由于答案会非常大,请输出答案模 109+7 的余数。 输入格式 第一行输入整数N,表示密码的长度。 第二行输入字符串T,T中只包含小写字母。 输出格式 输出一个正整数,表示总方案数模 109+7 后的结果。 数据范围 1≤N≤50, 1≤|T|≤N,|T|是T的长度。 输入样例1: 2 a 输出样例1: 625 输入样例2: 4 cbc 输出样例2: 456924
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 55, mod = 1e9 + 7; int n, m; char s[N]; int ne[N]; int f[N][N]; int main() { cin >> n >> s + 1; m = strlen(s + 1); for (int i = 2, j = 0; i <= m; i ++ ) { while (j && s[i] != s[j + 1]) j = ne[j]; if (s[i] == s[j + 1]) j ++; ne[i] = j; } f[0][0] = 1; for (int i = 0; i < n; i ++ ) for (int j = 0; j < m; j ++ ) for (char k = 'a'; k <= 'z'; k ++ ) { int u = j; while (u && k != s[u + 1]) u = ne[u]; if (k == s[u + 1]) u ++; if (u < m) { f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod; } } int res = 0; for (int i = 0; i < m; i ++ ) res = (res + f[n][i]) % mod; cout << res << endl; return 0; }
状态压缩DP
题目:
求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。 例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。 如下图所示: 2411_1.jpg 输入格式 输入包含多组测试用例。 每组测试用例占一行,包含两个整数 N 和 M。 当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。 输出格式 每个测试用例输出一个结果,每个结果占一行。 数据范围 1≤N,M≤11 输入样例: 1 2 1 3 1 4 2 2 2 3 2 4 2 11 4 11 0 0 输出样例: 1 0 1 2 3 5 144 51205
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; const int N = 12, M = 1 << N; typedef long long LL; int n, m; LL dp[N][M]; // vector<int> v;//记录没有连续奇数个0的二进制数 bool st[M]; vector<int> state[M]; /* 注意:我们是从左往右放的,放格子的时候,都是横着放的,而且放的时候,是从i-1列开始,延伸到第i列 */ int main() { while (cin >> n >> m, n || m) { memset(dp, 0, sizeof dp); // v.clear(); for (int i = 0; i < 1 << n; i ++ ) { state[i].clear(); int cnt = 0; bool flag = true; for (int j = 0; j < n; j ++ ) if (i >> j & 1) { // cout << i << ' ' << j << endl; if (cnt & 1) {flag = false; break;} // v.push_back(i); cnt = 0; } else cnt ++; if (cnt & 1) flag = false; // if (flag) v.push_back(i); st[i] = flag; } // for (int i : v) cout << i << endl; // for (int i = 0; i < 1 << n; i ++ ) // for (int j = 0; j < 1 << n; j ++ ) // if (i & j) state[i].push_back(j); // for (int i : v) // for (int j : v) // if (i & j == 0) state[i].push_back(j); for (int i = 0; i < 1 << n; i ++ ) for (int j = 0; j < 1 << n; j ++ ) if (st[i | j] && (i & j) == 0) state[i].push_back(j); dp[0][0] = 1; for (int i = 1; i <= m; i ++ ) for (int j = 0; j < 1 << n; j ++ ) for (int k : state[j]) dp[i][j] += dp[i - 1][k]; cout << dp[m][0] << "\n"; } return 0; }
题目:
在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。 输入格式 共一行,包含两个整数 n 和 k。 输出格式 共一行,表示方案总数,若不能够放置则输出0。 数据范围 1≤n≤10, 0≤k≤n2 输入样例: 3 2 输出样例: 16
代码:
/* 如果第i-1行,当前列有国王,a代表第i行,b代表第i-1行,那么: 他的下面不能放; b & a == 0,如果b为1,那么a如果为1不合法,a一定是0 他的相邻位置不能放, b & (b - 1) == 0//这个写法是错的 他与他下面的位置的相邻位置不能有1 , b|(a-1) == 0 //这个写法是错的 首先用v预处理出来所有相邻位置不能为1的所有二进制 然后用state记录下所有i的合法方案b */ #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; typedef long long LL; const int N = 12, M = 1 << N, K = N * N; int n, k; LL dp[N][K][M]; vector<int> v; vector<int> state[M]; //判断当前数字二进制下是否存在相邻的两个1,有就false bool check(int a) { // printf("a = %d, a - 1 = %d, a & (a - 1) = %d\n", a, a - 1, a & (a - 1)); // if (a & (a - 1)) return false; // return true; for (int i = 0; i < n; i ++ ) if ((a >> i & 1) && (a >> (i + 1) & 1)) return false; return true; } int count(int a) { int t = 0; for (int i = 0; i < n; i ++ ) if (a >> i & 1) t ++; return t; } int main() { cin >> n >> k; for (int i = 0; i < 1 << n; i ++ ) if (check(i)) v.push_back(i); // for (int i : v) cout << i << " "; // cout << endl; for (int i : v) for (int j : v) if (check(i | j) && (i & j) == 0)//i|j相邻两个位置不能为1,同时i的正下方不能有1 state[i].push_back(j); dp[0][0][0] = 1; for (int i = 1; i <= n + 1; i ++ ) for (int j = 0; j <= k; j ++ ) for (int a : v) for (int b : state[a]) { int c = count(a); if (j - c >= 0) dp[i][j][a] += dp[i - 1][j - c][b]; } cout << dp[n + 1][k][0] << "\n"; return 0; }
题目:
农夫约翰的土地由 M×N 个小方格组成,现在他要在土地里种植玉米。 非常遗憾,部分土地是不育的,无法种植。 而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。 现在给定土地的大小,请你求出共有多少种种植方法。 土地上什么都不种也算一种方法。 输入格式 第 1 行包含两个整数 M 和 N。 第 2..M+1 行:每行包含 N 个整数 0 或 1,用来描述整个土地的状况,1 表示该块土地肥沃,0 表示该块土地不育。 输出格式 输出总种植方法对 108 取模后的值。 数据范围 1≤M,N≤12 输入样例: 2 3 1 1 1 0 1 0 输出样例: 9
代码:
/* 十字形的范围内都不可以。 那么,当前位置为1,正下方不能为1,也就是(a & b) == 0 相邻两个位置不能为1, 如果土地贫瘠即为1的话,有1也是不可以的 */ #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; const int N = 14, M = 1 << 12, mod = 1e8; int n, m; int g[N]; vector<int> v; vector<int> state[M]; int dp[N][M]; bool check(int a) { for (int i = 0; i < m; i ++ ) if ((a >> i & 1) && (a >> (i + 1) & 1)) return false; return true; } int main() { cin >> n >> m; //将图存起来,贫瘠为1,存入每行的二进制中 for (int i = 1; i <= n; i ++ ) for (int j = 0; j < m; j ++ ) { int x; cin >> x; g[i] += (!x) << j; } // for (int i = 1; i <= n; i ++ ) cout << g[i] << endl;//YES //将不存在相邻两个1的合法状态存起来 for (int i = 0; i < 1 << m; i ++ ) if (check(i)) v.push_back(i); // for (int i : v) cout << i << ' ' ; // cout << endl; //将i行状态为a的所有可达状态i-1行的b存起来 for (int i : v) for (int j : v) { // cout << i << " " << j << endl; if ((i & j )== 0) { // cout << i << " " << j << endl; state[i].push_back(j); } } // for (int i : v) // { // printf("i=%d: ", i); // for (int j : state[i]) // printf("%d ", j); // puts(""); // } dp[0][0] = 1; // int cnt = 0; for (int i = 1; i <= n + 1; i ++ ) for (int a : v) if (!(a & g[i])) { // printf("a = %d:", a); // printf("a = %d, g[%d] = %d\n", a, i, g[i]); for (int b : state[a]) { // cout << b << ' '; // cnt ++; // printf("dp[i][a] = %d, dp[i - 1][b] = %d\n", dp[i][a], dp[i - 1][b]); dp[i][a] = (dp[i][a] + dp[i - 1][b]) % mod; // printf("dp[%d][%d] = (dp[%d][%d] + dp[%d - 1][%d]) % mod;\n", i, a, i, a, i, b); // printf("dp[i][a] = %d, dp[i - 1][b] = %d\n", dp[i][a], dp[i - 1][b]); // puts("_________________________________"); } // cout << endl; } // cout << cnt << endl; cout << dp[n + 1][0] << "\n"; return 0; }
题目:
司令部的将军们打算在 N×MN×M 的网格地图上部署他们的炮兵部队。
一个 N×MN×M 的地图由 NN 行 MM 列组成,地图的每一格可能是山地(用
H
表示),也可能是平原(用P
表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
图上其它白色网格均攻击不到。
从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
输入格式
第一行包含两个由空格分割开的正整数,分别表示 NN 和 MM;
接下来的 NN 行,每一行含有连续的 MM 个字符(
P
或者H
),中间没有空格。按顺序表示地图中每一行的数据。输出格式
仅一行,包含一个整数 KK,表示最多能摆放的炮兵部队的数量。
数据范围
N≤100,M≤10N≤100,M≤10
输入样例:
5 4 PHPP PPHH PPPP PHPP PHHP
输出样例:
6
代码:
/* 合法状态满足条件 1、同一行,数的二进制表示下,相邻的两个1之间要隔两块空地 2、a &b == 0 && a & c == 0 && b & c == 0 3、山地不可以放置 */ #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <vector> using namespace std; const int N = 110, M = 11, K = 1 << 10; int n, m; int g[N * M]; int cnt[K]; vector<int> v; int dp[2][K][K]; /* dp的意义:当前为第i&1行,且第i-1行的摆放方案为j,第i行摆放方案为k的方案的摆放炮台的最大值 其中j与k的二进制表示下的1表示当前位置有炮台,而0表示没有炮台。 按照加长十字的炮台影响我们得到了一下二进制方案: 第一,同行,数的二进制表示下,相邻的两个1之间要隔两块空地 第二,如果第i行拜访了炮台,第i-1行与第i-2行当前列不能拜访炮台。也就是(a & b) | (b & c) | (c & a)若为1则舍弃 第三,如果当前位置处于山地,那么不能摆放炮台。 */ bool check(int a) { for (int i = 0; i < m; i ++ ) if ((a >> i & 1) && ((a >> (i + 1) & 1) || (a >> (i + 2) & 1))) return false; return true; } int count(int a) { int ans = 0; for (int i = 0; i < m; i ++ ) if (a >> i & 1) ans ++; return ans; } int main() { cin >> n >> m; for (int i = 1; i <= n; i ++ ) for (int j = 0; j < m; j ++ ) { char op; cin >> op; if (op == 'H') g[i] += 1 << j; } // for (int i = 1; i <= n; i ++ ) cout << g[i] << endl; //将相邻两个1之间要有2块空地的合法方案预处理出来并存在来二进制表示中1的个数 for (int i = 0; i < 1 << m; i ++ ) if (check(i)) { v.push_back(i); cnt[i] = count(i); } //处理数据 for (int i = 1; i <= n + 2; i ++ ) for (int j = 0; j < v.size(); j ++ ) for (int k = 0; k < v.size(); k ++ ) for (int u = 0; u < v.size(); u ++ ) { //a是第i-1行,b是第i行,c是第i-2行 int a = v[j], b = v[k], c = v[u]; if ((a & b) | (b & c) | (c & a)) continue; if ((g[i] & b)) continue; dp[i & 1][j][k] = max(dp[i & 1][j][k], dp[(i - 1) & 1][u][j] + cnt[b]); } // int res = 0; // for (int i = 0; i < v.size(); i ++ ) // for (int j = 0; j < v.size(); j ++ ) // res = max(res, dp[n & 1][i][j]); // cout << res << endl; cout << dp[(n + 2) & 1][0][0] << "\n"; return 0; }
区间DP
题目:
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。 每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。 每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。 例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24; 如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。 问题是:找出一种合理的方法,使总的代价最小,输出最小代价。 输入格式 第一行一个数 N 表示石子的堆数 N。 第二行 N 个数,表示每堆石子的质量(均不超过 1000)。 输出格式 输出一个整数,表示最小代价。 数据范围 1≤N≤300 输入样例: 4 1 3 5 2 输出样例: 22
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 310; int n; int s[N]; int dp[N][N]; int main() { cin >> n; memset(dp, 0x3f, sizeof dp); for (int i = 1; i <= n; i ++ ) { dp[i][i] = 0; cin >> s[i]; s[i] += s[i - 1]; } for (int len = 2; len <= n; len ++ ) for (int i = 1; i + len - 1 <= n; i ++ ) { int l = i, r = i + len - 1; for (int k = l; k <= r; k ++ ) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]); } cout << dp[1][n] << "\n"; return 0; }
题目:
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。 规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。 请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算: 选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。 选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。 输入格式 第一行包含整数 n,表示共有 n 堆石子。 第二行包含 n 个整数,分别表示每堆石子的数量。 输出格式 输出共两行: 第一行为合并得分总和最小值, 第二行为合并得分总和最大值。 数据范围 1≤n≤200 输入样例: 4 4 5 9 4 输出样例: 43 54
代码:
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N = 410; int n; int a[N], s[N]; int f[N][N], g[N][N]; int main() { cin >> n; for (int i = 1; i <= n; i ++ ) { cin >> a[i]; a[i + n] = a[i]; } for (int i = 1; i <= n << 1; i ++ ) s[i] = s[i - 1] + a[i]; for (int len = 2; len <= n; len ++ ) for (int i = 1; i + len - 1 <= n << 1; i ++ ) { int l = i, r = i + len - 1; f[l][r] = 1e9; g[l][r] = -1e9; for (int k = l; k < r; k ++ ) { f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]); g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]); } } int minv = 1e9, maxv = -1e9; for (int i = 1; i <= n; i ++ ) minv = min(minv, f[i][n + i - 1]); for (int i = 1; i <= n; i ++ ) maxv = max(maxv, g[i][n + i - 1]); cout << minv << "\n" << maxv <<"\n"; return 0; }