蓝桥杯C++ AB组辅导课题单:第九讲
鉴于近几年蓝桥杯DP题目数目增加,一定要对DP有较高的熟练度,并且能够秒杀经典题目。
一、复杂DP
1050、鸣人的影分身(中等)
注意N是包含鸣人自己在内的,所以不存在000的情况,M=7,N=3,可以分为:007,106,115,124,133,223,205,304,这个一看不就是搜索吗?DFS搜一下,过了。
import java.util.*;
import java.io.*;
public class Main {
static int cnt = 0;
public static void main(String[] args) throws IOException {
Scanner scan = new Scanner(System.in);
int t = scan.nextInt();
while (t-- > 0) {
int m = scan.nextInt();
int n = scan.nextInt();
cnt = 0;
dfs(0, m, n);
System.out.println(cnt);
}
}
static void dfs(int start, int m, int n) {
if (m < 0 || n < 0) return;
if (n == 0 && m == 0) {
cnt++;
return;
}
for (int i = start; i <= m; i++) {
dfs(i, m - i, n - 1);
}
}
}
本题更好的解法是DP,当然也更不好理解
把问题看作是放苹果问题,查克拉相当于苹果,影分身数量相当于盘子数。
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
Scanner scan = new Scanner(System.in);
int t = scan.nextInt();
int[][] dp;
while (t-- > 0) {
int m = scan.nextInt();
int n = scan.nextInt();
dp = new int[m + 1][n + 1];
// m个苹果放n个盘子
dp[0][0] = 1;
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 当前盘子为空
dp[i][j] = dp[i][j - 1];
if (i >= j) {
// 如果苹果数大于盘子数,那每一个盘子都能放一个,剩余i - j个苹果
dp[i][j] += dp[i - j][j];
}
}
}
System.out.println(dp[m][n]);
}
}
}
1047、糖果(简单)
题目很简单,给你N个物品(每个物品装有不同的糖果,也就是重量),问你如何选择物品,能够得到最多的糖果(糖果数必须是K的倍数,也就是要求重量最大且重量是K的倍数)
一看这样子,先试试搜搜:
import java.util.*;
import java.io.*;
public class Main {
static int[] candy;
static LinkedList<Integer> tmp = new LinkedList<>();
static int n, k;
static int max = 0;
public static void main(String[] args) throws IOException {
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
k = scan.nextInt();
candy = new int[n];
for (int i = 0; i < n; i++) candy[i] = scan.nextInt();
dfs(0, 0);
System.out.println(max);
}
static void dfs(int start, int sum) {
if (sum != 0 && sum % k == 0) {
max = Math.max(max, sum);
return;
}
if (tmp.size() > n) return;
for (int i = start; i < n; i++) {
tmp.add(candy[i]);
dfs(i + 1, sum + candy[i]);
tmp.removeLast();
}
}
}
不行,超时,再来想想DP,看似背包问题,但是有k倍数的限制,该怎么处理呢?
难在对取余的处理,f[i][j]表示从 1 - i 包糖果里选,%k 为 j 的所有方案集合,不选第 i 包糖果好说:f[i - 1][j],拿了呢?应该转换为f[i - 1][j-a[i]],但是怎么处理余数,按照上面来。到最后:
(
j
−
a
[
i
]
)
%
k
,
j
本
来
就
是
k
的
余
数
,
所
以
还
是
等
于
j
(
也
可
以
就
写
成
那
样
)
转
换
为
j
−
a
[
i
]
%
,
但
是
j
−
a
[
i
]
%
k
可
能
小
于
0
我
们
的
数
组
下
标
又
不
能
小
于
0
,
所
以
必
须
转
换
成
正
余
数
,
也
就
是
(
j
+
k
−
a
[
i
]
%
k
)
%
k
(j-a[i])\%{k},j本来就是k的余数,所以还是等于j(也可以就写成那样)\\转换为j - a[i] \%,但是j - a[i] \% k可能小于0\\我们的数组下标又不能小于0,所以必须转换成正余数,也就是(j + k - a[i] \% k) \% k
(j−a[i])%k,j本来就是k的余数,所以还是等于j(也可以就写成那样)转换为j−a[i]%,但是j−a[i]%k可能小于0我们的数组下标又不能小于0,所以必须转换成正余数,也就是(j+k−a[i]%k)%k
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int k = scan.nextInt();
int[] candy = new int[n + 1];
for (int i = 0; i < n; i++) candy[i + 1] = scan.nextInt();
int[][] dp = new int[n + 1][k + 1];
// dp[i][j] : 前i个糖果,总和%k为j的最大糖果数
for (int i = 0; i < n; i++) {
Arrays.fill(dp[i], Integer.MIN_VALUE);
}
dp[0][0] = 0;
// dp[0][1] dp[0][2]...都没有意义
for (int i = 1; i <= n; i++) {
for (int j = 0; j < k; j++) {
// 余数最多取到k-1
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][(j - candy[i] % k + k) % k] + candy[i]);
}
}
System.out.println(dp[n][0]);
}
}
1222、密码脱落(中等)(最长回文子序列)
这道题是:给你一个字符串,让你找里面最长的回文子串长度,然后用总长度 - 该回文子串的长度就知道了需要脱落多少字符。
之前刷过的题目全都忘了…刚好把动态规划专题再复习一遍! 复习归来!
这道题就是求最长回文子序列的长度,然后用密码长度 - 最长回文子序列的长度,就可以得到需要脱落的字符:
import java.io.*;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String code = scan.nextLine();
int n = code.length();
int[][] dp = new int[n + 1][n + 1];
// 找最长回文子串的长度(可以删除)
for (int i = n; i >= 1; i--) {
for (int j = i; j <= n; j++) {
if (code.charAt(i - 1) == code.charAt(j - 1)) {
if (i == j) dp[i][j] = 1;
else if (j - i == 1) dp[i][j] = 2;
else {
dp[i][j] = dp[i + 1][j - 1] + 2;
}
} else {
// 可以选择删除i、j
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
System.out.println(n - dp[1][n]);
}
}
1220、生命之树(中等)(树状DP)
这是一道典型的树型DP问题,我们先来对树型DP问题有一个了解:
P1352、没有上司的舞会(树状DP)
题目很简单,如果父节点参加舞会,所有该父节点的子节点都不能参加舞会,每个人参加舞会都有一个快乐指数,问最大的快乐指数可以是多少?
对于树中某一个节点,它有两种状态:选它、不选它
站在根节点的位置,我们必须要直到儿子的情况,结合之前二叉树的相关知识,我们知道必须要在搜索完成儿子节点后才能考虑根节点值的变化。 还要使用vis数组避免重复访问树中节点。
还需要考虑初始情况,对于每一个节点,如果只考虑自身,那么f[node][1] = 该节点的快乐指数,因为选了该节点就是它自己了。
import java.io.*;
import java.util.*;
public class Main {
static int[][] f;
static boolean[] vis;
static List<Integer>[] graph;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
f = new int[n + 1][2];
vis = new boolean[n + 1];
for (int i = 2; i <= n + 1; i++) {
// f[i][1] 代表 选择当前节点的快乐指数,目前是只考虑节点 i 的情况
f[i - 1][1] = scan.nextInt();
}
boolean[] notRoot = new boolean[n + 1];
graph = new LinkedList[n + 1];
// 建树
for (int i = 0; i < n + 1; i++) {
graph[i] = new LinkedList<>();
}
for (int i = n + 2; i <= 2 * n; i++) {
int l = scan.nextInt();
notRoot[l] = true;
int k = scan.nextInt();
// k 是 l 的直接上司
graph[k].add(l);
}
for (int i = 1; i <= n; i++) {
if (notRoot[i] == false) {
// 找到根节点,从根节点开始遍历
dfs(i);
System.out.println(Math.max(f[i][1], f[i][0]));
}
}
}
static void dfs(int k) {
// 避免重复遍历
vis[k] = true;
for (int next : graph[k]) {
// 遍历该节点的每个子节点
if (vis[next]) continue;
// 搜索
dfs(next);
// 我们需要直到f[next]的结果,所以必须要在搜索完成之后进行,也即后序位置进行
// 如果根节点选了,子节点就只能不选
f[k][1] += f[next][0];
// 如果根节点没选,子节点就可以选、不选,由于有多个子结点,所以结果是累加
f[k][0] += Math.max(f[next][0], f[next][1]);
}
}
}
再次强调DFS搜索的更新顺序:
P1122、最大子树和(树状DP)
对于当前节点,我们可以遍历于它相连接的节点,连接的节点可以选,也可以不选,考虑到节点的值可能为负数,所以当前节点的子树的值,要么就只含自己,要么就与其它节点相连接做累加。为避免负数的影响,需要每次和0相比。注意,至少要取一朵花。
还需要注意的是,本题并没有树的概念,更多表达的是一种图,所以建图是要注意双向边,并且从任意图的节点开始搜索都可以。
import java.io.*;
import java.util.*;
public class Main {
static boolean[] vis;
static List<Integer>[] graph;
static int[] dp;
static int max = Integer.MIN_VALUE;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
dp = new int[n + 1];
vis = new boolean[n + 1];
// 记录每个花的漂亮值,也即是dp的初始值
for (int i = 1; i <= n; i++) {
dp[i] = scan.nextInt();
}
// 建树
graph = new LinkedList[n + 1];
for (int i = 0; i < n + 1; i++) {
graph[i] = new LinkedList<>();
}
for (int i = 1; i <= n - 1; i++) {
int a = scan.nextInt();
int b = scan.nextInt();
// 注意无向边是双向边
graph[a].add(b);
graph[b].add(a);
}
// 开始求解,从任意一个存在的节点开始求解都可以
dfs(1);
System.out.println(max);
}
static void dfs (int k) {
vis[k] = true;
for (int next : graph[k]) {
if (vis[next]) continue;
dfs(next);
// 如果为负值,那就不要选了,剪枝掉
dp[k] += Math.max(0, dp[next]);
}
max = Math.max(max, dp[k]);
}
}
其实抛掉树型结构,抛掉一切外貌,只看本质,就是一个求最大子序列和的问题。
再回到本题,该如何求解?
和上一题类似,我们需要站在每个节点来看待问题,站在每个节点,看由当前节点作为根节点构成的子树的最大值,然后遍历完所有的可能子树,得到全局最大值。
import java.util.*;
public class Main {
static List<Integer>[] graph;
static long[] dp;
static boolean[] vis;
static long max = Long.MIN_VALUE;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
dp = new long[n + 1];
vis = new boolean[n + 1];
for (int i = 1; i <= n; i++) {
// 假设每个节点只选择自己,dp数组的值就是自己
dp[i] = scan.nextInt();
}
graph = new LinkedList[n + 1];
for (int i = 0; i < n + 1; i++) {
graph[i] = new LinkedList<>();
}
for (int i = 0; i < n - 1; i++) {
int u = scan.nextInt();
int v = scan.nextInt();
graph[u].add(v);
graph[v].add(u);
}
// 相当于图,可以从任意节点开始搜索
dfs(1);
System.out.println(Math.max(0, max));
}
static void dfs(int k) {
vis[k] = true;
for (int next : graph[k]) {
if (vis[next]) continue;
dfs(next);
// 对于每个父节点,可以选择选、不选子节点(选一定要能够增大子树和才选,所以需要和0比)
dp[k] += Math.max(0L, dp[next]);
}
max = Math.max(max, dp[k]);
}
}
树状DP将DP和DFS融合,比较考验对DP的推导,以及DFS的使用。遇到这类型树状DP,可以站在每个节点的角度进行思考,把每个节点作为根节点,建立当前跟节点的子树。
包子凑数
欧几里得定理:对于不完全为 0 的整数 a,b,gcd(a,b)表示 a,b 的最大公约数。那么一定存在整
数 x,y 使得 gcd(a,b)=ax+by。
扩展:如果有的包子种类的最大公约数不是1 那么凑不出来的情况就有无限多种。
当结论记住就行,如果需要凑数,可以用的数的gcd(两两求),不等于1的话,就不能凑出所有数。
import java.util.*;
public class Main {
static int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
// 有多少数目的包子是凑不出来的
int[] bz = new int[n];
int flag = 0;
boolean[] dp = new boolean[10001];
dp[0] = true;
for (int i = 0; i < n; i++) {
bz[i] = scan.nextInt();
dp[bz[i]] = true;
}
if (n == 1 && bz[0] != 1) {
System.out.println("INF");
} else if (n == 1 && bz[0] == 1) {
System.out.println(0);
} else {
int tmp = bz[0];
for (int i = 1; i < n; i++) {
tmp = gcd(tmp, bz[i]);
}
// 最大公约数为 1 那么都可以凑出来
if (tmp == 1) {
flag = 1;
}
// 如果最大公约数不是 1,说明所有能够凑成的数都是某个数的倍数,INF
if (flag == 0) {
System.out.println("INF");
} else {
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = 1; j <= 10000; j++) {
if (dp[j]) {
continue;
}
if (bz[i] <= j) {
dp[j] = dp[j - bz[i]];
}
}
}
for (int i = 1; i <= 10000; i++) {
if (dp[i]) {
continue;
}
ans++;
}
System.out.println(ans);
}
}
}
}
1070、括号配对(中等)
POJ 2955 Brackets(简单)
先看一道与它类似的题目:POJ 2955 Brackets
意思是给你一个字符串,问你其中最长正则括号子序列的长度。
转自:https://www.cnblogs.com/fxh0707/p/14655368.html
其实和求最长回文的过程差不多,因为[()],这种情况,也就是枚举到的左右括号匹配时,就可以由f[i+1][j-1] + 2转换得来,这和求最长回文串是一样的,关键是左右括号不匹配时,该怎么转换,例如()(),并不是回文的形式,而是两个有效括号的拼接,如果还用上面的转移方程,结果=2,显然是错误的,答案应该是4。 该怎么办呢?我们漏掉了其它的匹配方式
枚举每一个可能拆分点,()(),可以拆分成(|)(),()|(),()(|),这三种情况对应了三种不同的区间拆分,解决了上述匹配方式不够全面的问题。f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j])
import java.util.*;
import java.io.*;
public class Main {
static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws IOException {
char[] str = reader.readLine().trim().toCharArray();
int len = str.length;
int[][] dp = new int[len + 1][len + 1];
for (int i = len; i >= 1; i--) { // 注意转换方程跟最长回文一样,所以需要逆序遍历
for (int j = i + 1; j <= len; j++) {
// 先判断左右括号是否匹配
if ((str[i - 1] == '(' && str[j - 1] == ')') || (str[i - 1] == '[' && str[j - 1] == ']')) {
if (j - i == 1) dp[i][j] = 2; // 跟最长回文一样,特判一下,避免下标交叉的情况
else dp[i][j] = dp[i + 1][j - 1] + 2;
}
// 然后按点拆分,以保证枚举完所有的情况
for (int k = i; k < j; k++) {
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k + 1][j]); // 因为是k + 1所以k只用枚举到j - 1
}
}
}
System.out.println(dp[1][len]);
}
}
回到本题,本题需要你添加最少的字符数使得给出的BE成为GBE,也就是括号能够被匹配。这题问的是使所有括号完美匹配需要添加的最小括号数量。
要使添加的括号尽量少,我们需要使原来的括号序列尽可能多得匹配,先求最大匹配数量,那么还剩下一些没有匹配的括号,我们就需要依次加上一个括号使它们得到匹配。
状态方程的样子还是没变,只是其表达变了。如果枚举的当前左右括号匹配成功,例如:([]),那么至少需要添加的字符个数 = f[i + 1][j - 1]。但是如果是:()(),又该怎么办?同样的,括号问题我们都需要考虑这种连接的情况,解决这种问题的方法就是按不同节点进行拆分!f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j])。
import java.util.*;
import java.io.*;
public class Main {
static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws IOException {
char[] str = reader.readLine().trim().toCharArray();
int len = str.length;
int[][] dp = new int[len + 1][len + 1];
// 同样逆序遍历
for (int i = len; i >= 1; i--) {
for (int j = i; j <= len; j++) {
if (i == j) dp[i][j] = 1; // 如果只有一个括号,无论如何都要添加一个字符
else {
// 找最小值,初始化为最大值
dp[i][j] = Integer.MAX_VALUE;
// 如果左右括号能够匹配,就取f[i + 1][j - 1]
if ((str[i - 1] == '(' && str[j - 1] == ')') || (str[i - 1] == '[' && str[j - 1] == ']')) {
if (j - i == 1) {
// 特判两个括号的情况
dp[i][j] = 0;
} else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 不管能否匹配,都需要按拆分点进行区间拆分
for (int k = i; k < j; k++) {
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k + 1][j]);
}
}
}
}
System.out.println(dp[1][len]);
}
}
1078、旅游规划 (中等)
好样的!题都没看懂!
垒骰子(中等)
假设从上往下依次放骰子,最上面的骰子可以有6个面朝上,每个面朝上的基础上,侧面还有四个面可以旋转,所以对一个骰子而言,有4种可能。确定上一个骰子后,还需要考虑下一个骰子,考虑冲突面,也是可能有6个面朝上,但冲突面不能考虑,确定一个面后,侧面还有四个面4种可能,可以先给出递归代码(超时):
import java.util.*;
import java.io.*;
public class Main {
static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
static int[] to = new int[7];
static boolean[][] conflict = new boolean[7][7] ;
static int mod = 1000000007;
public static void main(String[] args) throws IOException {
String[] input = reader.readLine().trim().split(" ");
int n = Integer.parseInt(input[0]);
int m = Integer.parseInt(input[1]);
// n个骰子,m个冲突
to[1] = 4;
to[2] = 5;
to[3] = 6;
to[4] = 1;
to[5] = 2;
to[3] = 6;
// 记录骰子的对面
while (m-- > 0) {
input = reader.readLine().trim().split(" ");
int a = Integer.parseInt(input[0]);
int b = Integer.parseInt(input[1]);
conflict[a][b] = true;
conflict[b][a] = true;
}
// 对于最上面的一个骰子,可以六个面朝上
long ans = 0;
for (int i = 1; i <= 6; i++) {
ans += 4 * f(n - 1, i); // 侧面有四个面可以旋转
}
System.out.println(ans);
}
static long f(int cnt, int up) {
// 剩余骰子数,上面是什么面
if (cnt == 0) {
// 没有骰子就return 1
return 1;
}
long ans = 0;
// 遍历下一个骰子的上面是什么面
for (int i = 1; i <= 6; i++) {
if (conflict[to[up]][i]) {
continue; // 冲突了
}
ans += 4 * f(cnt - 1, i); // 侧面有四个面可以旋转
}
return ans;
}
}
我们知道了递归函数形式后,就可以试着推出动态规划的状态转移方程,dp[i][j] 代表第i个面朝上,j个骰子的方案数。所以dp[1][1]…dp[6][1] = 4,对于一个骰子而言,不管哪一面朝上都有4种方案(因为侧面可以旋转)。我们要求的答案ans = dp[1][n] + dp[2][n] + … + dp[6][n]。
dp[1][2] =4 * (dp[1][1] + dp[2][1] + … dp[6][1]),前提是两个骰子相接的面不冲突。
import java.util.Scanner;
public class Main {
static int n, m;
static int[] opp = new int[10];
static boolean[][] conflict = new boolean[10][10];
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
n = scan.nextInt();
m = scan.nextInt();
// 记录骰子的对面
opp[1] = 4;
opp[2] = 5;
opp[3] = 6;
opp[4] = 1;
opp[5] = 2;
opp[6] = 3;
// 记录冲突面
int a, b;
for (int i = 0; i < m; i++) {
a = scan.nextInt();
b = scan.nextInt();
// 它们冲突了,注意
conflict[a][b] = true;
conflict[b][a] = true;
}
long ans = 0;
// ans = dp[1][n] + dp[2][n] + dp[3][n] + ... + dp[6][n]
// dp[1][1] = 4
// dp[2][1] = 4
// ...
// dp[6][1] = 4
// dp[i][j]:i朝上的面,j第几个骰子(从下往上)
// dp[1][2] = (dp[1][1] + dp[2][1] + ... dp[6][1]) * 4
long[][] dp = new long[7][n + 1];
// 初始化,只有一个骰子,每个面朝上都有4种可能(因为侧面可以扭动)
for (int i = 1; i <= 6; i++) {
dp[i][1] = 4;
}
// i遍历骰子个数
for (int i = 2; i <= n; i++) {
// j遍历骰子的面
for (int j = 1; j <= 6; j++) {
// 遍历上一个骰子是否与当前骰子的面冲突
for (int k = 1; k <= 6; k++) {
if (conflict[k][opp[j]]) {
continue;
}
dp[j][i] = (dp[j][i] + dp[k][i - 1]) % 1000000007;
}
dp[j][i] = (dp[j][i] * 4) % 1000000007;
if (i == n) {
ans = (ans + dp[j][i]) % 1000000007;
}
}
}
System.out.println(ans);
}
}