Part 4.1 线性动态规划
线性动态规划,即具有线性阶段划分的动态规划。
P1216 [IOI 1994] 数字三角形 Number Triangles
题目描述
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
在上面的样例中,从 7→3→8→7→5 的路径产生了最大权值。
输入格式
第一个行一个正整数 r ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
输出格式
单独的一行,包含那个可能得到的最大的和。
输入输出样例
输入 #1复制
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5输出 #1复制
30说明/提示
【数据范围】
对于 100% 的数据,1≤r≤1000,所有输入在 [0,100] 范围内。题目翻译来自NOCOW。
USACO Training Section 1.5
IOI1994 Day1T1
import java.util.*; public class Main { public static void main(String args[]) { Scanner in = new Scanner(System.in); int r=in.nextInt(); int a[][]=new int[1005][1005]; for(int i=1;i<=r;i++) for(int j=1;j<=i;j++) a[i][j]=in.nextInt(); for(int i=r-1;i>=1;i--) { for(int j=1;j<=i;j++) { a[i][j]+=Math.max(a[i+1][j], a[i+1][j+1]); } } System.out.println(a[1][1]); } }
P1020 [NOIP 1999 提高组] 导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
一行,若干个整数,中间由空格隔开。
输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入输出样例
输入 #1复制
389 207 155 300 299 170 158 65输出 #1复制
6 2说明/提示
对于前 50% 数据(NOIP 原题数据),满足导弹的个数不超过 104 个。该部分数据总分共 100 分。可使用O(n2) 做法通过。
对于后 50% 的数据,满足导弹的个数不超过 105 个。该部分数据总分也为 100 分。请使用 O(nlogn) 做法通过。对于全部数据,满足导弹的高度为正整数,且不超过 5×104。
此外本题开启 spj,每点两问,按问给分。
NOIP1999 提高组 第一题
upd 2022.8.24:新增加一组 Hack 数据。
import java.util.*; public class Main { // 定义一个常量 N,用于指定数组的最大长度 static final int N = 200005; // 数组 a 用于存储输入的序列,比如导弹的高度 static int[] a = new int[N]; // 数组 g 用于辅助计算最长不上升子序列和最长上升子序列 static int[] g = new int[N]; // n 表示序列的长度,cnt 用于记录当前最长子序列的长度 static int n, cnt; public static void main(String[] args) { // 创建 Scanner 对象,用于从标准输入读取数据 Scanner sc = new Scanner(System.in); // 初始化序列长度为 0 n = 0; // 循环读取输入的整数,直到没有更多输入 while (sc.hasNextInt()) { // 每读取一个整数,将序列长度 n 加 1,并把该整数存储到数组 a 中 a[++n] = sc.nextInt(); } // 第一部分:计算最长不上升子序列的长度 // 初始化 g[0] 为一个很大的数,作为初始边界条件 g[0] = 2000000000; // 遍历数组 a 中的每个元素 for (int i = 1; i <= n; i++) { // 如果当前元素 a[i] 小于等于 g[cnt],说明可以直接添加到当前不上升子序列的末尾 if (a[i] <= g[cnt]) { // 不上升子序列长度加 1,并将当前元素存入 g 数组 g[++cnt] = a[i]; } else { // 如果不能直接添加,需要在 g 数组中找到第一个小于 a[i] 的位置 int l = 1; int r = cnt; // 二分查找满足条件的位置 while (l < r) { int mid = (l + r) / 2; // 如果 g[mid] 小于 a[i],说明目标位置在左半部分 if (g[mid] < a[i]) { r = mid; } else { // 否则,目标位置在右半部分 l = mid + 1; } } // 将 a[i] 替换掉找到的位置的元素 g[l] = a[i]; } } // 输出最长不上升子序列的长度 System.out.println(cnt); // 第二部分:计算最长上升子序列的长度 // 重新初始化 cnt 为 0,准备计算最长上升子序列 cnt = 0; // 初始化 g[0] 为一个很小的数,作为初始边界条件 g[0] = -2000000000; // 再次遍历数组 a 中的每个元素 for (int i = 1; i <= n; i++) { // 如果当前元素 a[i] 大于 g[cnt],说明可以直接添加到当前上升子序列的末尾 if (a[i] > g[cnt]) { // 上升子序列长度加 1,并将当前元素存入 g 数组 g[++cnt] = a[i]; } else { // 如果不能直接添加,需要在 g 数组中找到第一个大于等于 a[i] 的位置 int l = 1; int r = cnt; // 二分查找满足条件的位置 while (l < r) { int mid = (l + r) / 2; // 如果 g[mid] 大于等于 a[i],说明目标位置在左半部分 if (g[mid] >= a[i]) { r = mid; } else { // 否则,目标位置在右半部分 l = mid + 1; } } // 将 a[i] 替换掉找到的位置的元素 g[l] = a[i]; } } // 输出最长上升子序列的长度 System.out.println(cnt); // 关闭 Scanner 对象,释放资源 sc.close(); } }
P1091 [NOIP 2004 提高组] 合唱队形
题目描述
n 位同学站成一排,音乐老师要请其中的 n−k 位同学出列,使得剩下的 k 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 k 位同学从左到右依次编号为 1,2, … ,k,他们的身高分别为 t1,t2, … ,tk,则他们的身高满足 t1<⋯<ti>ti+1> … >tk(1≤i≤k)。
你的任务是,已知所有 n 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入格式
共二行。
第一行是一个整数 n(2≤n≤100),表示同学的总数。
第二行有 n 个整数,用空格分隔,第 i 个整数 ti(130≤ti≤230)是第 i 位同学的身高(厘米)。
输出格式
一个整数,最少需要几位同学出列。
输入输出样例
输入 #1复制
8 186 186 150 200 160 130 197 220输出 #1复制
4说明/提示
对于 50% 的数据,保证有 n≤20。
对于全部的数据,保证有 n≤100。
import java.util.*; public class Main { public static void main(String[] args) { // TODO Auto-generated method stub Scanner in = new Scanner(System.in); int n=in.nextInt(); int a[]=new int[105]; int b[]=new int[105]; int c[]=new int[105]; int d[]=new int[105]; for(int i=1;i<=n;i++) a[i]=in.nextInt(); Arrays.fill(b, 1); Arrays.fill(c, 1); int max=0; for(int i=n-1;i>=1;i--) { for(int j=i+1;j<=n;j++) if(a[i]>a[j]&&c[i]<=c[j]) c[i]=c[j]+1; } for(int i=1;i<=n;i++) { for(int j=i+1;j<=n;j++) if(a[i]<a[j]&&b[i]>=b[j]) b[j]=b[i]+1; } for(int i=1;i<=n;i++) { max=Math.max(max, b[i]+c[i]-1); } System.out.println(n-max); } }
P1095 [NOIP 2007 普及组] 守望者的逃离
题目背景
NOIP2007 普及组 T3
题目描述
恶魔猎手尤迪安野心勃勃,他背叛了暗夜精灵,率领深藏在海底的娜迦族企图叛变。
守望者在与尤迪安的交锋中遭遇了围杀,被困在一个荒芜的大岛上。
为了杀死守望者,尤迪安开始对这个荒岛施咒,这座岛很快就会沉下去。到那时,岛上的所有人都会遇难。
守望者的跑步速度为 17m/s,以这样的速度是无法逃离荒岛的。庆幸的是守望者拥有闪烁法术,可在 1s 内移动 60m,不过每次使用闪烁法术都会消耗魔法值 10 点。守望者的魔法值恢复的速度为 4 点每秒,只有处在原地休息状态时才能恢复。
现在已知守望者的魔法初值 M,他所在的初始位置与岛的出口之间的距离 S,岛沉没的时间 T。你的任务是写一个程序帮助守望者计算如何在最短的时间内逃离荒岛,若不能逃出,则输出守望者在剩下的时间内能走的最远距离。
注意:守望者跑步、闪烁或休息活动均以秒为单位,且每次活动的持续时间为整数秒。距离的单位为米。
输入格式
输入数据共一行三个非负整数,分别表示 M,S,T。
输出格式
输出数据共两行。
第一行一个字符串 Yes 或 No,即守望者是否能逃离荒岛。
第二行包含一个整数。第一行为 Yes 时表示守望者逃离荒岛的最短时间;第一行为 No 时表示守望者能走的最远距离。
输入输出样例
输入 #1复制
39 200 4输出 #1复制
No 197输入 #2复制
36 255 10输出 #2复制
Yes 6说明/提示
对于 30% 的数据,1≤T≤10,1≤S≤100;
对于 50% 的数据,1≤T≤103,1≤S≤104;
对于 100% 的数据,1≤T≤3×105,0≤M≤103,1≤S≤108
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取魔法初值 M,初始位置与岛出口的距离 S,岛沉没的时间 T int m = scanner.nextInt(); int s = scanner.nextInt(); int t = scanner.nextInt(); // 闪烁移动的距离 int fla = 0; // 跑步移动的距离 int run = 0; for (int i = 1; i <= t; i++) { // 如果魔法值足够使用闪烁法术 if (m >= 10) { // 使用闪烁法术,消耗 10 点魔法值,移动 60 米 m -= 10; fla += 60; // 同时跑步移动 17 米 run += 17; } else { // 如果闪烁的距离大于跑步的距离,更新跑步距离为闪烁距离 if (fla > run) { run = fla; } // 恢复 4 点魔法值 m += 4; // 跑步移动 17 米 run += 17; } // 如果闪烁或跑步的距离达到或超过出口距离 if (Math.max(fla, run) >= s) { System.out.println("Yes"); System.out.println(i); return; } } // 时间结束还未逃出 System.out.println("No"); System.out.println(Math.max(fla, run)); } }
P1541 [NOIP 2010 提高组] 乌龟棋
题目背景
NOIP2010 提高组 T2
题目描述
小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。
乌龟棋的棋盘是一行 N 个格子,每个格子上一个分数(非负整数)。棋盘第 1 格是唯一的起点,第 N 格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。
乌龟棋中 M 张爬行卡片,分成 4 种不同的类型(M 张卡片中不一定包含所有 4 种类型的卡片,见样例),每种类型的卡片上分别标有 1,2,3,4 四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。
游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。
很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡片使用顺序使得最终游戏得分最多。
现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到多少分吗?
输入格式
每行中两个数之间用一个空格隔开。
第 1 行 2 个正整数 N,M,分别表示棋盘格子数和爬行卡片数。
第 2 行 N 个非负整数,a1,a2,…,aN,其中 ai 表示棋盘第 i 个格子上的分数。
第 3 行 M 个整数,b1,b2,…,bM,表示 M 张爬行卡片上的数字。
输入数据保证到达终点时刚好用光 M 张爬行卡片。
输出格式
一个整数,表示小明最多能得到的分数。
输入输出样例
输入 #1复制
9 5 6 10 14 2 8 8 18 5 17 1 3 1 2 1输出 #1复制
73说明/提示
每个测试点 1s。
小明使用爬行卡片顺序为 1,1,3,1,2,得到的分数为 6+10+14+8+18+17=73。注意,由于起点是 1,所以自动获得第 1 格的分数 6。
对于 30% 的数据有 1≤N≤30,1≤M≤12。
对于 50% 的数据有 1≤N≤120,1≤M≤50,且 4 种爬行卡片,每种卡片的张数不会超过 20。
对于 100% 的数据有 1≤N≤350,1≤M≤120,且 4 种爬行卡片,每种卡片的张数不会超过 40;0≤ai≤100,1≤i≤N,1≤bi≤4,1≤i≤M。
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 棋盘格子数 int n = scanner.nextInt(); // 爬行卡片数 int m = scanner.nextInt(); // 每种卡片的最大数量上限 final int MAXN = 41; // 四维数组 F 用于存储不同卡片使用组合下的最大得分 int[][][][] F = new int[MAXN][MAXN][MAXN][MAXN]; // 存储棋盘每个格子的分数 int[] num = new int[351]; // 存储每种类型卡片的数量 int[] g = new int[5]; // 读取棋盘每个格子的分数 for (int i = 1; i <= n; i++) { num[i] = scanner.nextInt(); } // 初始化起点分数 F[0][0][0][0] = num[1]; // 读取每张卡片的类型,并统计每种类型卡片的数量 for (int i = 1; i <= m; i++) { int x = scanner.nextInt(); g[x]++; } // 四重循环遍历所有可能的卡片使用组合 for (int a = 0; a <= g[1]; a++) { for (int b = 0; b <= g[2]; b++) { for (int c = 0; c <= g[3]; c++) { for (int d = 0; d <= g[4]; d++) { // 计算当前所在的格子位置 int r = 1 + a + b * 2 + c * 3 + d * 4; // 如果使用了一张 1 号卡片,更新最大得分 if (a != 0) { F[a][b][c][d] = Math.max(F[a][b][c][d], F[a - 1][b][c][d] + num[r]); } // 如果使用了一张 2 号卡片,更新最大得分 if (b != 0) { F[a][b][c][d] = Math.max(F[a][b][c][d], F[a][b - 1][c][d] + num[r]); } // 如果使用了一张 3 号卡片,更新最大得分 if (c != 0) { F[a][b][c][d] = Math.max(F[a][b][c][d], F[a][b][c - 1][d] + num[r]); } // 如果使用了一张 4 号卡片,更新最大得分 if (d != 0) { F[a][b][c][d] = Math.max(F[a][b][c][d], F[a][b][c][d - 1] + num[r]); } } } } } // 输出使用完所有卡片后的最大得分 System.out.println(F[g[1]][g[2]][g[3]][g[4]]); } }
P1868 饥饿的奶牛
题目描述
有一条奶牛冲出了围栏,来到了一处圣地(对于奶牛来说),上面用牛语写着一段文字。
现用汉语翻译为:
有 N 个区间,每个区间 x,y 表示提供的 x∼y 共 y−x+1 堆优质牧草。你可以选择任意区间但不能有重复的部分。
对于奶牛来说,自然是吃的越多越好,然而奶牛智商有限,现在请你帮助他。
输入格式
第一行一个整数 N。
接下来 N 行,每行两个数 x,y,描述一个区间。
输出格式
输出最多能吃到的牧草堆数。
输入输出样例
输入 #1复制
3 1 3 7 8 3 4输出 #1复制
5说明/提示
1≤n≤1.5×105,0≤x≤y≤3×106。
#include<cstdio> #include<algorithm> #include<vector> using namespace std; vector<int>beg[3000010];//有点大,不过并不会 MLE int n,mx,f[3000010];//mx 代表最大的 y,f 就是 dp 用的数组 int main(){ scanf("%d",&n); for(int i=1;i<=n;i++){ int x,y; scanf("%d%d",&x,&y); beg[y].push_back(x-1);//这里保存的是 x-1,后面会比较方便 mx=max(mx,y); } for(int i=1;i<=mx;i++){ f[i]=f[i-1];//先设定为 f[i-1],后面再更新 for(int j=0;j<beg[i].size();j++){ int b=beg[i][j]; f[i]=max(f[i],f[b]+i-b);//这里会比较方便 } } printf("%d\n",f[mx]); return 0; }
P2679 [NOIP 2015 提高组] 子串
题目背景
NOIP2015 Day2T2
题目描述
有两个仅包含小写英文字母的字符串 A 和 B。
现在要从字符串 A 中取出 k 个互不重叠的非空子串,然后把这 k 个子串按照其在字符串 A 中出现的顺序依次连接起来得到一个新的字符串。请问有多少种方案可以使得这个新串与字符串 B 相等?
注意:子串取出的位置不同也认为是不同的方案。
输入格式
第一行是三个正整数 n,m,k,分别表示字符串 A 的长度,字符串 B 的长度,以及问题描述中所提到的 k,每两个整数之间用一个空格隔开。
第二行包含一个长度为 n 的字符串,表示字符串 A。
第三行包含一个长度为 m 的字符串,表示字符串 B。
输出格式
一个整数,表示所求方案数。
由于答案可能很大,所以这里要求输出答案对 1000000007 取模的结果。
输入输出样例
输入 #1复制
6 3 1 aabaab aab输出 #1复制
2输入 #2复制
6 3 2 aabaab aab输出 #2复制
7输入 #3复制
6 3 3 aabaab aab输出 #3复制
7说明/提示
样例解释
所有合法方案如下:(加下划线的部分表示取出的字串)
样例 1:aabaab,aabaab。
样例 2:aabaab,aabaab,aabaab,aabaab,aabaab,aabaab,aabaab。
样例 3:aabaab,aabaab,aabaab,aabaab,aabaab,aabaab,aabaab。数据范围
对于第 1 组数据:1≤n≤500,1≤m≤50,k=1;
对于第 2 组至第 3 组数据:1≤n≤500,1≤m≤50,k=2;
对于第 4 组至第 5 组数据:1≤n≤500,1≤m≤50,k=m;
对于第 1 组至第 7 组数据:1≤n≤500,1≤m≤50,1≤k≤m;
对于第 1 组至第 9 组数据:1≤n≤1000,1≤m≤100,1≤k≤m;
对于所有 10 组数据:1≤n≤1000,1≤m≤200,1≤k≤m。import java.util.Scanner; public class Main { // 定义最大长度 static final int MAXN = 1010; static final int MAXM = 210; // 取模的常量 static final int MOD = 1000000007; public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取字符串 A 的长度、字符串 B 的长度和子串数量 k int n = scanner.nextInt(); int m = scanner.nextInt(); int k = scanner.nextInt(); scanner.nextLine(); // 消耗掉换行符 // 读取字符串 A 和 B,下标从 1 开始 String a = " " + scanner.nextLine(); String b = " " + scanner.nextLine(); // f[i][j] 表示 A 到第 i 个字符,B 到第 j 个字符,取了 k 串的方案数 int[][] f = new int[MAXM][MAXM]; // g[i][j] 是 f 的前缀和 int[][] g = new int[MAXM][MAXM]; // 初始化边界条件 g[0][0] = 1; // 遍历字符串 A 的每个字符 for (int i = 1; i <= n; i++) { // 从后往前遍历字符串 B 的可能位置 for (int j = Math.min(m, i); j >= 1; j--) { // 从后往前遍历取的子串数量 for (int p = Math.min(k, j); p >= 1; p--) { // 如果当前 A 的字符和 B 的字符相等 if (a.charAt(i) == b.charAt(j)) { // 更新 f 数组 f[j][p] = (f[j - 1][p] + g[j - 1][p - 1]) % MOD; } else { // 不相等则方案数为 0 f[j][p] = 0; } // 更新 g 数组 g[j][p] = (g[j][p] + f[j][p]) % MOD; } } } // 输出最终结果 System.out.println(g[m][k]); } // 自定义的求最小值函数 public static int min(int a, int b) { return a < b ? a : b; } }
Part 4.2 背包动态规划
背包动态规划是线性动态规划中特殊的一类,NOIP中考到的次数也不少。
P1048 [NOIP 2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 2 个整数 T(1≤T≤1000)和 M(1≤M≤100),用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。
接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
输入输出样例
输入 #1复制
70 3 71 100 69 1 1 2输出 #1复制
3说明/提示
【数据范围】
- 对于 30% 的数据,M≤10;
- 对于全部的数据,M≤100。
【题目来源】
NOIP 2005 普及组第三题
import java.util.*; public class Main { public static void main(String args[]) { Scanner in = new Scanner(System.in); int t=in.nextInt(); int m=in.nextInt(); int f[]=new int[1005]; int w[]=new int[1005]; int v[]=new int[1005]; for(int i=1;i<=m;i++) { v[i]=in.nextInt(); w[i]=in.nextInt(); } for(int i=1;i<=m;i++) { for(int j=t;j>=v[i];j--) { f[j]=Math.max(f[j], f[j-v[i]]+w[i]); } } System.out.println(f[t]); } }
P1060 [NOIP 2006 普及组] 开心的金明
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N 元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的 N 元。于是,他把每件物品规定了一个重要度,分为 5 等:用整数 1−5 表示,第 5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过 N 元(可以等于 N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第j件物品的价格为 vj,重要度为 wj,共选中了 k 件物品,编号依次为 j1,j2,…,jk,则所求的总和为:
vj1×wj1+vj2×wj2…+vjk×wjk
请你帮助金明设计一个满足要求的购物单。
输入格式
第一行,为 2 个正整数,用一个空格隔开:n,m(n<30000,m<25)其中 n 表示总钱数,m 为希望购买物品的个数。
从第 2 行到第 m+1 行,第 j 行给出了编号为 j−1 的物品的基本数据,每行有 2 个非负整数 v,p(其中 v 表示该物品的价格 (v≤10000),p 表示该物品的重要度(1≤p≤5)。
输出格式
1 个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<100000000)。
输入输出样例
输入 #1复制
1000 5 800 2 400 5 300 5 400 3 200 2输出 #1复制
3900说明/提示
NOIP 2006 普及组 第二题
import java.util.*; public class Main { public static void main(String args[]) { Scanner in = new Scanner(System.in); int n=in.nextInt(); int m=in.nextInt(); int v[]=new int[30005]; int w[]=new int[30005]; int f[]=new int[30005]; for(int i=1;i<=m;i++) { v[i]=in.nextInt(); w[i]=in.nextInt(); w[i]*=v[i]; } for(int i=1;i<=m;i++) { for(int j=n;j>=v[i];j--) { f[j]=Math.max(f[j], f[j-v[i]]+w[i]); } } System.out.println(f[n]); } }
P1855 榨取kkksc03
题目描述
洛谷 2 的团队功能是其他任何 OJ 和工具难以达到的。借助洛谷强大的服务器资源,任何学校都可以在洛谷上零成本的搭建 OJ 并高效率的完成训练计划。
为什么说是搭建 OJ 呢?为什么高效呢?
因为,你可以上传私有题目,团队外别人是无法看到的。我们还能帮你们评测!
你可以创建作业,给组员布置任务,查看组员的完成情况,还可以点评任意一份代码!
你可以创建比赛!既可以是 OI 赛制还可以是 ICPC 赛制!既可以是团队内部的私有比赛,也可以公开赛,甚至可以指定谁可以参加比赛。这样,搞“x 校联赛”最合适不过了。洛谷凭借这个功能,希望能够提供公开及私有比赛的另外一个平台。
值得说明的是,本次比赛就是采用团队私有题目+邀请比赛的机制。
洛谷的运营组决定,如果一名 OIer 向他的教练推荐洛谷,并能够成功的使用(成功使用的定义是:该团队有 20 个或以上的成员,上传 10 道以上的私有题目,布置过一次作业并成功举办过一次公开比赛),那么他可以浪费掉 kkksc03 的一些时间的同时消耗掉 kkksc03 的一些金钱以满足自己的一个愿望。
kkksc03 的时间和金钱是有限的,所以他很难满足所有同学的愿望。所以他想知道在自己的能力范围内,最多可以完成多少同学的愿望?
输入格式
第一行三个整数 n,M,T,表示一共有 n(1≤n≤100)个愿望, kkksc03 的手上还剩 M(0≤M≤200)元,他的暑假有 T(0≤T≤200)分钟时间。
第 2~n+1 行 mi , ti 表示第 i 个愿望所需要的金钱和时间。
输出格式
一行,一个数,表示 kkksc03 最多可以实现愿望的个数。
输入输出样例
输入 #1复制
6 10 10 1 1 2 3 3 2 2 5 5 2 4 3输出 #1复制
4import java.util.*; public class Main { public static void main(String args[]) { Scanner in = new Scanner(System.in); int n=in.nextInt(); int m=in.nextInt(); int t=in.nextInt(); int v[]=new int[3000]; int w[]=new int[3000]; int f[][]=new int[3000][3000]; for(int i=1;i<=n;i++) { w[i]=in.nextInt(); v[i]=in.nextInt(); } for(int i=1;i<=n;i++) { for(int j=m;j>=w[i];j--) { for(int k=t;k>=v[i];k--) { f[j][k]=Math.max(f[j][k], f[j-w[i]][k-v[i]]+1); } } } System.out.println(f[m][t]); } }
P1757 通天之分组背包
题目背景
直达通天路·小 A 历险记第二篇
题目描述
自 01 背包问世之后,小 A 对此深感兴趣。一天,小 A 去远游,却发现他的背包不同于 01 背包,他的物品大致可分为 k 组,每组中的物品相互冲突,现在,他想知道最大的利用价值是多少。
输入格式
两个数 m,n,表示一共有 n 件物品,总重量为 m。
接下来 n 行,每行 3 个数 ai,bi,ci,表示物品的重量,利用价值,所属组数。
输出格式
一个数,最大的利用价值。
输入输出样例
输入 #1复制
45 3 10 10 1 10 5 1 50 400 2输出 #1复制
10说明/提示
0≤m≤1000,1≤n≤1000,1≤k≤100,ai,bi,ci 在
int
范围内。import java.util.Scanner; public class Main{ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取背包的容量 v 和物品的数量 n int v = scanner.nextInt(); int n = scanner.nextInt(); // 用于记录最大的组编号 int t = 0; // 存储每个物品的体积 int[] w = new int[10001]; // 存储每个物品的价值 int[] z = new int[10001]; // 存储每个物品所属的组 int x; // 记录每个组内物品的数量 int[] b = new int[10001]; // 存储每个组内物品的编号 int[][] g = new int[205][205]; // 动态规划数组,dp[j] 表示背包容量为 j 时的最大价值 int[] dp = new int[10001]; // 读取每个物品的信息,并将其归类到相应的组中 for (int i = 1; i <= n; i++) { w[i] = scanner.nextInt(); z[i] = scanner.nextInt(); x = scanner.nextInt(); // 更新最大组编号 t = Math.max(t, x); // 当前组的物品数量加 1 b[x]++; // 记录该组中当前物品的编号 g[x][b[x]] = i; } // 分组背包的动态规划过程 for (int i = 1; i <= t; i++) { for (int j = v; j >= 0; j--) { for (int k = 1; k <= b[i]; k++) { if (j >= w[g[i][k]]) { // 更新 dp[j] 的值,取放入当前物品和不放入当前物品的最大值 dp[j] = Math.max(dp[j], dp[j - w[g[i][k]]] + z[g[i][k]]); } } } } // 输出背包容量为 v 时的最大价值 System.out.println(dp[v]); scanner.close(); } }
Part 4.3 区间动态规划
区间动态规划一般以区间作为动态规划的阶段。
P1880 [NOI1995] 石子合并
题目描述
在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。
输入格式
数据的第 1 行是正整数 N,表示有 N 堆石子。
第 2 行有 N 个整数,第 i 个整数 ai 表示第 i 堆石子的个数。
输出格式
输出共 2 行,第 1 行为最小得分,第 2 行为最大得分。
输入输出样例
输入 #1复制
4 4 5 9 4输出 #1复制
43 54说明/提示
1≤N≤100,0≤ai≤20。
import java.util.Scanner; public class Main{ public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取石子堆数 int n = scanner.nextInt(); // 存储每堆石子的数量,为了处理环形问题,长度设为 2 倍 int[] a = new int[2005]; // 存储前缀和 int[] sum = new int[2005]; // 存储合并的最小得分 int[][] fmi = new int[2005][2005]; // 存储合并的最大得分 int[][] fma = new int[2005][2005]; // 用于记录最小得分的决策点 int[][] smi = new int[2005][2005]; // 读取每堆石子的数量,并计算前缀和 for (int i = 1; i <= n; i++) { a[i] = scanner.nextInt(); // 处理环形问题,复制一份石子堆数据 a[i + n] = a[i]; // 计算前缀和 sum[i] = sum[i - 1] + a[i]; // 初始化决策点 smi[i][i] = i; } // 继续计算扩展部分的前缀和 for (int i = n + 1; i <= 2 * n; i++) { sum[i] = sum[i - 1] + a[i]; smi[i][i] = i; } // 动态规划求解最小和最大得分 for (int i = 2 * n - 1; i >= 1; i--) { for (int j = i + 1; j <= 2 * n; j++) { // 计算最大得分 fma[i][j] = Math.max(fma[i][j - 1], fma[i + 1][j]) + sum[j] - sum[i - 1]; // 初始化最小得分的临时变量 int tmp = Integer.MAX_VALUE; int jc = 0; // 根据决策点的范围进行枚举 for (int k = smi[i][j - 1]; k <= smi[i + 1][j]; k++) { int tt = fmi[i][k] + fmi[k + 1][j] + (sum[j] - sum[i - 1]); if (tt < tmp) { tmp = tt; jc = k; } } // 更新决策点和最小得分 smi[i][j] = jc; fmi[i][j] = tmp; } } // 找出最终的最小和最大得分 int ama = 0; int ami = Integer.MAX_VALUE; for (int i = 1; i <= n; i++) { ama = Math.max(ama, fma[i][i + n - 1]); ami = Math.min(ami, fmi[i][i + n - 1]); } // 输出结果 System.out.println(ami); System.out.println(ama); } }
P3146 [USACO16OPEN] 248 G
题目描述
贝西喜欢在手机上下载游戏来玩,尽管她确实觉得对于自己巨大的蹄子来说,小小的触摸屏用起来相当笨拙。
她对当前正在玩的这个游戏特别感兴趣。游戏开始时给定一个包含 N 个正整数的序列(2≤N≤248),每个数的范围在 1…40 之间。在一次操作中,贝西可以选择两个相邻且相等的数,将它们替换为一个比原数大 1 的数(例如,她可以将两个相邻的 7 替换为一个 8)。游戏的目标是最大化最终序列中的最大数值。请帮助贝西获得尽可能高的分数!
输入格式
第一行输入包含 N,接下来的 N 行给出游戏开始时序列的 N 个数字。
输出格式
请输出贝西能生成的最大整数。
题意翻译
输入输出样例
输入 #1复制
4 1 1 1 2输出 #1复制
3说明/提示
在示例中,贝西首先合并第二个和第三个 1,得到序列 1 2 2,然后将两个 2 合并为 3。注意,合并前两个 1 并不是最优策略。
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 定义常量 N 和 K final int N = 250; final int K = 50; // 初始化动态规划数组 f int[][] f = new int[N][K]; // 读取序列的长度 n int n = scanner.nextInt(); int ans = 0; // 读取序列中的每个数字,并初始化 f 数组 for (int i = 1; i <= n; i++) { int x = scanner.nextInt(); // f[i][x] 表示从第 i 个位置开始能合并出值为 x 的下一个位置 f[i][x] = i + 1; // 更新当前最大数值 ans = Math.max(ans, x); } // 动态规划过程,尝试合并出更大的值 for (int k = 1; k <= 47; k++) { for (int i = 1; i <= n; i++) { // 如果 f[i][k] 为 0,尝试从 f[i][k - 1] 位置继续合并 if (f[i][k] == 0) { f[i][k] = f[f[i][k - 1]][k - 1]; } // 如果能合并出值为 k 的结果,更新最大数值 if (f[i][k] != 0) { ans = Math.max(ans, k); } } } // 输出最大生成的整数 System.out.println(ans); } }