【算法练习】杂题混讲二:Acwing + 蓝桥杯练习(背包系列问题)

考虑到蓝桥杯或者一些DP问题,是通过背包问题转换而来,所以这一次练习全以DP背包问题为主,旨在将DP问题讲透彻、讲清楚。

一、01背包

在这里插入图片描述
经典的DP背包问题,这个不用多说。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值
		int[] vs = new int[n + 1];
		int[] ws = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			ws[i] = Integer.parseInt(input[1]);
		}
		// dp[i][j] : 前i个物品,背包容量为j,能够放下的最大价值
		int[][] dp = new int[n + 1][v + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= v; j++) {
				// 无论要不要当前物品,都有这个状态
				dp[i][j] = dp[i - 1][j];
				if (j >= vs[i]) {
					// 背包容量大于当前物品体积,那就放(之前已经考虑不放的情况)
					dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i]] + ws[i]);
				}
			}
		}
		log.write(dp[n][v] + "");
		log.flush();
	}
}

仔细观察:dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i]] + ws[i]),这个式子只会用到 i 的上下两层状态,但是使用二维dp,把所有的 i 的状态都记录了,所以我们可以使用一维数组进行压缩。只需要保存 i 的上面一层的结果即可。

for (int i = 1; i <= n; i++) {
	// 背包容量一定要逆序遍历
	for (int j = v; j >= 1; j--) {
		if (j >= vs[i]) {
			dp[j] = Math.max(dp[j], dp[j - vs[i]] + ws[i]);
		}
	}
}

注意,这里一定要搞清楚为什么背包容量要逆序遍历,因为当我们遍历到 i 的下一层时,dp还保存着上一层 i 的状态,回到二维dp,它是需要从上一层的较小背包容量转移过来,看下面代码:

for (int i = 1; i <= n; i++) {
	for (int j = 1; j <= v; j++) {
		// 无论要不要当前物品,都有这个状态
		dp[i][j] = dp[i - 1][j];
		if (j >= vs[i]) {
			// 背包容量大于当前物品体积,那就放(之前已经考虑不放的情况)
			dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i]] + ws[i]);
		}
	}
}

如果我们正序遍历背包容量,我们知道此时我们要求第 i 层情况,但是dp存的是第 i - 1层情况,如果从小到大遍历,会修改上一层的dp结果,一旦修改了后序使用dp[j - vs[i]]计算值的过程就会出错,所以正确的方式应该是“逆序”!这样后面的背包容量需要上一层小的背包容量就不会被修改。

其实很简单,思考一下,dp数组只保存上层状态,我们要利用上层状态更新本层状态,那就不能修改掉需要的上层状态。

二、完全背包

在这里插入图片描述
多了一个条件,每件物品可以一直拿,唯一需要更改的地方在于:拿了当前物品后,还可以继续拿;不拿当前物品,还是和01背包问题一样。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值
		int[] vs = new int[n + 1];
		int[] ws = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			ws[i] = Integer.parseInt(input[1]);
		}
		// dp[i][j] : i件物品,背包容量为j,能够放下的最大价值
		int[][] dp = new int[n + 1][v + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= v; j++) {
				// 注意每件物品可以无限
				dp[i][j] = dp[i - 1][j];  // 首先考虑不选本层物品的情况
				if (j >= vs[i]) {
					// 注意拿了本层物品后,还可以转移到本层
					dp[i][j] = Math.max(dp[i][j], dp[i][j - vs[i]] + ws[i]);
				}
			}
		}
		log.write(dp[n][v] + "");
		log.flush();
	}
}

同样可以进行空间优化,那该正序还是逆序?当然是逆序吗?,看二维dp,需要的是 j - vs[i]的状态,那么我们从大的v开始更新,不去碰小的v就行?

上面的想法很明显是错误的,不要忽略二维dp数组的第一个状态,dp[i][j] = Math.max(dp[i][j], dp[i][j - vs[i]] + ws[i]),它是从本层向本层转移,而不再是上一层,这一点非常关键,意思就是说,一维dp现在就只记录当前层的状态,那直接从小到大遍历就行了呀,而且必须要正序,因为当前层的后续背包容量需要当前层的前续小的背包容量的计算支撑。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值
		int[] vs = new int[n + 1];
		int[] ws = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			ws[i] = Integer.parseInt(input[1]);
		}
		// dp[j] : 背包容量为j,能够放下的最大价值
		int[] dp = new int[v + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= v; j++) {
				// 注意每件物品可以无限
				if (j >= vs[i]) {
					// 注意拿了本层物品后,还可以转移到本层
					dp[j] = Math.max(dp[j], dp[j - vs[i]] + ws[i]);
				}
			}
		}
		log.write(dp[v] + "");
		log.flush();
	}
}

三、多重背包Ⅰ

在这里插入图片描述
每个物品不是无限个了,而是有一定的数量限制,怎么办?

很简单,可以把多重背包转换为01背包,让每个背包中的物品都只能被拿一次。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值、数量
		int[] vs = new int[10001];
		int[] ws = new int[10001];
		int cnt = 1;
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			int vv = Integer.parseInt(input[0]);
			int ww = Integer.parseInt(input[1]);
			int c = Integer.parseInt(input[2]);
			for (int j = 1; j <= c; j++) {
				vs[cnt] = vv;
				ws[cnt] = ww;
				cnt++;
			}
		}
		// 直接用一维01背包dp
		int[] dp = new int[v + 1];
		for (int i = 1; i < cnt; i++) {
			for (int j = v; j >= 1; j--) {
				// 注意逆序遍历背包容量
				if (j >= vs[i]) {
					dp[j] = Math.max(dp[j], dp[j - vs[i]] + ws[i]);
				}
			}
		}
		log.write(dp[v] + "");
		log.flush();
	}
}

将多重背包转换为01背包,是一种不错的方法,但实际可以直接对动态转移方程做修改,考虑当前物品个数,我们一次可以拿0个、1个、2个,直到达到物品有的个数,当然拿0、1、2…个,能够拿的前提是,这几个物品的体积之和小于背包剩余容量。当拿0个时,就转换为了一个都不拿的状态,所以就不需要再去考虑一个都不拿的状态。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值、数量
		int[] vs = new int[n + 1];
		int[] ws = new int[n + 1];
		int[] nums = new int[n + 1];
		int cnt = 1;
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			ws[i] = Integer.parseInt(input[1]);
			nums[i] = Integer.parseInt(input[2]);
		}
		// dp[i][j]:前i件物品,背包容量为j能够装下的最大价值
		int[][] dp = new int[n + 1][v + 1];
		for (int i = 1; i <= n; i++) { // 遍历物品
			for (int j = 1; j <= v; j++) { // 遍历容量
				for (int k = 0; k <= nums[i]; k++) { // 遍历物品个数
					if (j >= vs[i] * k) {
						// 只有容量不超过现有容量才能拿
						dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i] * k] + ws[i] * k);
					}
				}
			}
		}
		log.write(dp[n][v] + "");
		log.flush();
	}
}

考虑优化dp数组,如何优化?逆序?正序?看到dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i] * k] + ws[i] * k),也就是说用一维dp数组记录上一层的状态即可,如果是记录上一层的状态,那就必须逆序遍历:

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值、数量
		int[] vs = new int[n + 1];
		int[] ws = new int[n + 1];
		int[] nums = new int[n + 1];
		int cnt = 1;
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			ws[i] = Integer.parseInt(input[1]);
			nums[i] = Integer.parseInt(input[2]);
		}
		int[] dp = new int[v + 1];
		for (int i = 1; i <= n; i++) { // 遍历物品
			for (int j = v; j >= 1; j--) { // 遍历容量,注意逆序
				for (int k = 0; k <= nums[i]; k++) { // 遍历物品个数
					if (j >= vs[i] * k) {
						// 只有容量不超过现有容量才能拿
						dp[j] = Math.max(dp[j], dp[j - vs[i] * k] + ws[i] * k);
					}
				}
			}
		}
		log.write(dp[v] + "");
		log.flush();
	}
}

四、二维费用的背包问题

在这里插入图片描述
现在对背包有:容量 + 最大承重量的限制,那就相当于多了一个状态,现在一个物品能否放入,必须得同时考虑容量 + 重量的限制。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// m背包称重能力
		int m = Integer.parseInt(input[2]);
		// 记录物品体积、重量、价值
		int[] vs = new int[n + 1];
		int[] zs = new int[n + 1];
		int[] ws = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			zs[i] = Integer.parseInt(input[1]);
			ws[i] = Integer.parseInt(input[2]);
		}
		// dp[i][j][k],前i件物品,背包容量为j,背包称重能力为k,能放下的最大价值
		int[][][] dp = new int[n + 1][v + 1][m + 1];
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= v; j++) {
				for (int k = 1; k <= m; k++) {
					// 先考虑不拿的情况
					dp[i][j][k] = dp[i - 1][j][k];
					// 在考虑拿
					if (j >= vs[i] && k >= zs[i]) {
						// 要同时满足体积、重量,才能拿
						dp[i][j][k] = Math.max(dp[i][j][k], dp[i - 1][j - vs[i]][k - zs[i]] + ws[i]);
					}
				}
			}
		}
		log.write(dp[n][v][m] + "");
		log.flush();
	}
}

同样可以进行dp数组优化:

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n件物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// m背包称重能力
		int m = Integer.parseInt(input[2]);
		// 记录物品体积、重量、价值
		int[] vs = new int[n + 1];
		int[] zs = new int[n + 1];
		int[] ws = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			vs[i] = Integer.parseInt(input[0]);
			zs[i] = Integer.parseInt(input[1]);
			ws[i] = Integer.parseInt(input[2]);
		}
		// dp[j][k],背包容量为j,背包称重能力为k,能放下的最大价值
		int[][] dp = new int[v + 1][m + 1];
		for (int i = 1; i <= n; i++) {
		    // 注意容量、承重能力都要逆序
			for (int j = v; j >= 1; j--) {
				for (int k = m; k >= 1; k--) {
					if (j >= vs[i] && k >= zs[i]) {
						// 要同时满足体积、重量,才能拿
						dp[j][k] = Math.max(dp[j][k], dp[j - vs[i]][k - zs[i]] + ws[i]);
					}
				}
			}
		}
		log.write(dp[v][m] + "");
		log.flush();
	}
}

五、分组背包问题

在这里插入图片描述
在这里插入图片描述
简单地说,就是把物品分到了不同的组,每个组有各自的物品,现在每个组中最多拿一个物品出来,问能够取得的最大价值是多少?

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n组物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值
		int[][] vs = new int[n + 1][10001];
		int[][] ws = new int[n + 1][10001];
		// 每个组的物品个数
		int[] nums = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			// 当前组的物品个数
			int s = Integer.parseInt(input[0]);
			nums[i] = s;
			for (int j = 1; j <= s; j++) {
				input = reader.readLine().trim().split(" ");
				// 第i组的第j个物品
				vs[i][j] = Integer.parseInt(input[0]);
				ws[i][j] = Integer.parseInt(input[1]);
			}
		}
		// dp[i][j] 前i个物品,背包容量为j的最大价值
		int[][] dp = new int[n + 1][v + 1];
		for (int i = 1; i <= n; i++) {  // 遍历物品个数
			for (int j = 1; j <= v; j++) {  // 遍历背包容量
				for (int k = 0; k <= nums[i]; k++) {  // 遍历当前分组中的物品,拿哪一个?
					if (j >= vs[i][k]) {
						// k=0时就不拿,注意转移方程按照01背包来,因为每个组中物品只能拿一个
						dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i][k]] + ws[i][k]);
					}
				}
			}
		}
		log.write(dp[n][v] + "");
		log.flush();
	}
}

同样可以一维优化:

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n组物品
		int n = Integer.parseInt(input[0]);
		// v背包容量
		int v = Integer.parseInt(input[1]);
		// 记录物品体积、价值
		int[][] vs = new int[n + 1][10001];
		int[][] ws = new int[n + 1][10001];
		// 每个组的物品个数
		int[] nums = new int[n + 1];
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			// 当前组的物品个数
			int s = Integer.parseInt(input[0]);
			nums[i] = s;
			for (int j = 1; j <= s; j++) {
				input = reader.readLine().trim().split(" ");
				// 第i组的第j个物品
				vs[i][j] = Integer.parseInt(input[0]);
				ws[i][j] = Integer.parseInt(input[1]);
			}
		}
		// dp[j] 背包容量为j的最大价值
		int[] dp = new int[v + 1];
		for (int i = 1; i <= n; i++) {  // 遍历物品个数
			for (int j = v; j >= 1; j--) {  // 遍历背包容量,注意dp保存上一层i的状态,所以需要逆序遍历
				for (int k = 0; k <= nums[i]; k++) {  // 遍历当前分组中的物品,拿哪一个?
					if (j >= vs[i][k]) {
						// k=0时就不拿,注意转移方程按照01背包来,因为每个组中物品只能拿一个
						dp[j] = Math.max(dp[j], dp[j - vs[i][k]] + ws[i][k]);
					}
				}
			}
		}
		log.write(dp[v] + "");
		log.flush();
	}
}

※六、有依赖的背包问题(树状DP)

在这里插入图片描述
在这里插入图片描述
其实仔细想想,对于某个根节点,我们需要遍历它的所有子节点(所有物品组合),然后还要遍历每个节点分配的体积,以及每个子节点分配的体积。

在这里插入图片描述
在这里插入图片描述
直接看代码:

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	static List<Integer>[] edges;
	static int n, v;
	static int[][] dp;
	static int[] vs;
	static int[] ws;
	static boolean[] vis;
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		// n个物品
		n = Integer.parseInt(input[0]);
		// v背包容量
		v = Integer.parseInt(input[1]);
		vs = new int[n + 1];
		ws = new int[n + 1];
		edges = new LinkedList[n + 1];
		for (int i = 0; i < n + 1; i++) {
			edges[i] = new LinkedList<>();
		}
		int root = -1;
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			int vi = Integer.parseInt(input[0]);
			int wi = Integer.parseInt(input[1]);
			int p = Integer.parseInt(input[2]);
			if (p == -1) {  // 记录根节点
				root = i;
			} else {
				edges[p].add(i);
			}
			vs[i] = vi;
			ws[i] = wi;
		}
		// dp[i][j] : 以i为根节点,背包容量为v时,能够装下的最大价值,最终答案:dp[root][v]
		dp = new int[n + 1][v + 1];
		vis = new boolean[n + 1];
		// 从根节点开始搜索
		vis[root] = true;
		dfs(root);
		log.write(dp[root][v] + "");
		log.flush();
	}
	static void dfs(int k) {
		for (int next : edges[k]) {
			if (vis[next]) continue;
			// 遍历当前节点的子节点
			vis[next] = true;
			dfs(next);
			// 考虑当前节点的子节点们能够分得的体积
			for (int i = v - vs[k]; i >= 0; i--) { // - vs[k]因为拿了子节点必须拿根节点,所以容量至少要为vs[k]
				// 由于需要从上一层子节点的状态推得,所以必须逆序遍历
				for (int j = 0; j <= i; j++) {
					// 上面循环考虑k节点的子节点们能够分得的体积总数,下面循环考虑当前一个子节点能够分得的体积数
					dp[k][i] = Math.max(dp[k][i], dp[k][i - j] + dp[next][j]);
				}
			}
		}
		// 最后把能够选上根节点的选上根节点
		for (int i = v; i >= vs[k]; i--) {
			dp[k][i] = dp[k][i - vs[k]] + ws[k];
		}
		// 不够放根节点的就只有清零(不够放根节点则其它子节点也不能放)
		for (int i = 0; i < vs[k]; i++) {
			dp[k][i] = 0;
		}
	}
}

考虑对当前节点分配一定体积,然后再考虑该节点的子节点能够分配多少体积。细心的同学应该发现了,整个dfs里面是三层循环,第一层循环是遍历的孩子节点,应该也是作为状态存储的,但是因为当前节点只需要知道它的子节点的状态即可,所以就可以用滚动数组来压缩:变成二维的,由于当前节点是由子节点转移过来,所以遍历当前节点的体积时,必须逆序!

既然都谈到了树状 DP,那就赶紧再来几道~

※七、二叉苹果树

在这里插入图片描述
dp[i][j],考虑某一个节点 i,可以为以该节点为根节点的子树保留 j 个枝条,所以答案即为:dp[1][q],即根节点保留q个枝条的结果。

考虑,我们给节点 i 保留 j 个枝条,和上一题一样的思考方式,我们再去考虑该节点的子节点能够分配多少个枝条,注意,考虑子节点分配多少枝条时,由于已经在考虑子节点了!说明根节点与子节点有连接,也就是说,最多还剩下 j - 1个枝条,同时还要思考问题,但前子节点是否需要那么多枝条?所以还得统计当前子节点的子树大小,取子树大小和枝条数的最小值来分配枝条。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	static int n, q;
	static int[][] dp;
	static LinkedList<int[]>[] edges;
	static boolean[] vis;
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		n = Integer.parseInt(input[0]);
		q = Integer.parseInt(input[1]);
		edges = new LinkedList[n + 1];
		for (int i = 0; i < n + 1; i++) {
			edges[i] = new LinkedList<>();
		}
		for (int i = 0; i < n - 1; i++) {
			input = reader.readLine().trim().split(" ");
			int u = Integer.parseInt(input[0]);
			int v = Integer.parseInt(input[1]);
			int wei = Integer.parseInt(input[2]);
			// 建边
			edges[u].add(new int[] {v, wei});
			edges[v].add(new int[] {u, wei});
		}
		vis = new boolean[n + 1];
		cnt = new int[n + 1];
		// dp[i][j] 第i个节点,分配j个枝条的最大价值
		dp = new int[n + 1][q + 1];
		vis[1] = true;
		// 从根节点开始搜素
		dfs(1);
		log.write(dp[1][q] + "");
		log.flush();
	}
	static void dfs(int u) {
		for (int[] next : edges[u]) {
			int v = next[0];
			int wei = next[1];
			if (vis[v]) continue;
			vis[v] = true;
			dfs(v);
			// 后序位置干事情
			for (int i = q; i >= 0; i--) {
				// 子节点至多分配i - 1个枝条,1个枝条用于连接
				for (int j = 0; j <= i - 1; j++) {
					// 另外一个孩子就只能分配:i - j - 1个枝条
					dp[u][i] = Math.max(dp[u][i], dp[u][i - j - 1] + dp[v][j] + wei);
				}
			}
		}
	}
}

同样,需要对根节点分配的枝条逆序遍历,原因和上面一样,dp数组保存的是子节点的状态,根节点状态需要由子节点推得,所以必须逆序。

※八、背包问题求方案数(边更新最大值、边更新方案数)

在这里插入图片描述
注意需要统计的是最大价值的方案数,在整个求解最大价值的过程中,最大价值可能是在一直更新的,所以每次我们要计算出当前可能的转移值,与之前的值比较,如果大了,那就更新最大值,方案数就是之前转移过来的方案数。而当值和最大价值一样,那就是相加取模。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		int n = Integer.parseInt(input[0]);
		int v = Integer.parseInt(input[1]);
		int mod = 1000000007;
		// 记录当前最大价值 dp[i] 背包容量为 i 时能够放下的最大价值
		int[] dp = new int[v + 1];
		// 记录当前背包容量下的方案数
		int[] cnt = new int[v + 1];
		// 什么都不选也是一种方案,最大值为0
		Arrays.fill(cnt, 1);
		for (int i = 1; i <= n; i++) {
			input = reader.readLine().trim().split(" ");
			int vi = Integer.parseInt(input[0]);
			int wi = Integer.parseInt(input[1]);
			// 一维DP,逆序遍历背包容量
			for (int j = v; j >= vi; j--) {
				int tmp = dp[j - vi] + wi;
				// 如果新值大于旧值,需要更新
				if (tmp > dp[j]) {
					dp[j] = tmp;
					cnt[j] = cnt[j - vi];
				} else if (tmp == dp[j]) {
					// 如果当前值和最大值相同,那就是说有新的方案
					cnt[j] = (cnt[j] + cnt[j - vi]) % mod;
				}
			}
		}
		log.write(cnt[v] + "");
		log.flush();
	}
}

九、合唱队形(DP)

在这里插入图片描述
要先上升一段,到达中心位置再下降一段,问最少需要删除多少人。这两段都可以看作是以第一个元素开始和以最后一个元素开始的最长递增子序列,找到正反两个方向的最长递增子序列,我们求其中的pos + neg - 1的最大值,-1是因为两端的最长递增子序列会重复计算中心元素,要减掉一次。最后的答案就是n - max(posi + negi - 1)。

import java.util.*;
import java.io.*;

public class Main {
	static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
	static BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
	public static void main(String[] args) throws IOException {
		String[] input = reader.readLine().trim().split(" ");
		int n = Integer.parseInt(input[0]);
		int[] h = new int[n + 1];
		input = reader.readLine().trim().split(" ");
		for (int i = 1; i <= n; i++) {
			h[i] = Integer.parseInt(input[i - 1]);
		}
		int[] pos = new int[n + 1];
		// 先正着求最长上升子序列
		for (int i = 1; i <= n; i++) {
			pos[i] = 1;  // 每个位置至少为1
			for (int j = 1; j <= i; j++) {
				if (h[i] > h[j]) {
					pos[i] = Math.max(pos[j] + 1, pos[i]);
				}
			}
		}
		// 再反着求最长上升子序列
		int[] neg = new int[n + 1];
		for (int i = n; i >= 1; i--) {
			neg[i] = 1;  // 每个位置至少为1
			for (int j = n; j >= i; j--) {
				if (h[i] > h[j]) {
					neg[i] = Math.max(neg[j] + 1, neg[i]);
				}
			}
		}
		int ans = 0;
		for (int i = 1; i <= n; i++) {
			ans = Math.max(ans, pos[i] + neg[i] - 1);  // -1是因为前后两个上升序列对中间元素计数两次
		}
		log.write((n - ans) + "");
		log.flush();
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@u@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值