背包问题
01背包问题
有 N 件物品和一个容量为 V的背包,每件物品有各自的价值且只能被选择一次,要求在有限的背包容量下,装入的物品总价值最大。
「0-1 背包」是较为简单的动态规划问题,也是其余背包问题的基础。
动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第 i个物品的做出决策,「0-1」正好代表不选与选两种决定。
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
代码:
//朴素做法
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[] v = new int[N], w = new int[N];
//考虑前 i个物品,背包容量 j下的最大价值
static int[][] dp = new int[N][N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1; i <= n; i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
dp[i][j] = dp[i-1][j];//左边集合
if(j >= v[i]){
dp[i][j] = Math.max(dp[i][j], dp[i-1][j-v[i]] + w[i]);
}
}
}
System.out.print(dp[n][m]);
}
}
//空间压缩优化
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[] v = new int[N], w = new int[N];
//降维使用因为每次使用的是上一层的数据,所有我们不需要保存所有的数据,只需要上一层的数据。
//可以理解dp[][]仍为2维,但是第一维长度为1.只有一层。保存上一层。
static int[] dp = new int[N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1; i <= n; i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
//逆序是为了保存更新本层时,使用的是上一层的原始数据
for(int j = m; j >= v[i]; j--){
dp[j] = Math.max(dp[j], dp[j-v[i]] + w[i]);
}
}
System.out.print(dp[m]);
}
}
完全背包问题
思路同01背包问题。区别在于01背包对于每种物品只有选或不选,这也即「01」的由来。多重背包则对于每种物品可以多次选择。


f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包
f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
代码:
//暴力 O(n3)
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[] v = new int[N], w = new int[N];
static int[][] dp = new int[N][N]; //前i个物品,背包容量j下的最优解(最大价值)
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1; i <= n; i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m ; j++){
int amount = j / v[i]; // j体积时物品最多能选的次数
for(int k = 0; k <= amount; k++) // 枚举选择第i个物品的个数
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
System.out.print(dp[n][m]);
}
}
//朴素 O(n2)
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[] v = new int[N], w = new int[N];
static int[][] dp = new int[N][N]; //前i个物品,背包容量j下的最优解(最大价值)
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1; i <= n; i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m ; j++){
dp[i][j] = dp[i - 1][j];
if(j >= v[i]){
dp[i][j] = Math.max(dp[i][j], dp[i][j - v[i]] + w[i]);
}
}
}
System.out.print(dp[n][m]);
}
}
//空间优化
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[] v = new int[N], w = new int[N];
static int[] dp = new int[N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1; i <= n; i++){
v[i] = in.nextInt();
w[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
for(int j = v[i]; j <= m ; j++){//从小到大枚举
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
System.out.print(dp[m]);
}
}
多重背包问题

在完全背包中,通过两个状态转移方程:
$$
\begin{aligned}
f[i,j]&=max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,f[i-1,j-3v]+3w,……)\
f[i,j-v]&=max(\qquad \qquad, f[i-1,j-v],f[i-1,j-2v]+w,f[i-1,j-3v]+2w,……)
\end{aligned}
$$
可以得到 f[i][j] = max(f[i - 1][j], f[i][j - v] + w)。
在多重背包通过两个状态转移方程
$$
\begin{aligned}
f[i,j]&=max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,……,f[i-1,j-Sv]+Sw)\
f[i,j-v]&=max(\qquad \qquad, f[i-1,j-v],f[i-1,j-2v]+w,……,f[i-1,j-Sv]+(S-1)w,f[i-1,j-(S+1)v]+Sw)
\end{aligned}
$$
不能得到上述结论,因为多重背包方程比完全背包方程多出了一项,所以不能用完全背包的优化方式优化多重背包
那为什么完全背包不会有最后一项?
完全背包由于对每种物品没有选择个数的限制,所以只要体积够用就可以一直选,没有最后一项
我们可以考虑使用二进制的方法优化多重背包问题
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
代码:
//暴力做法
import java.util.*;
public class Main{
static int N = 110, n, m;
static int[] v = new int[N], w = new int[N], s = new int[N];
static int[][] dp = new int[N][N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1 ; i <= n ; i ++ ){
v[i] = in.nextInt();
w[i] = in.nextInt();
s[i] = in.nextInt();
}
for(int i = 1 ; i <= n ; i ++ )
for(int j = 0 ; j <= m ; j ++ )
for(int k = 0 ; k <= s[i] && k * v[i] <= j; k ++ )
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
System.out.println(dp[n][m]);
}
}
//二进制优化
import java.util.*;
public class Main{
static int N = 12010, M = 2010, n, m;
static int[] v = new int[N], w = new int[N];
static int[] dp = new int[M];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
int cnt = 0;
for(int i = 1; i <= n; i++){
int a = in.nextInt();
int b = in.nextInt();
int s = in.nextInt();
int k = 1;
while(k < s){
cnt++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k *= 2;
}
if(s > 0){
cnt++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt;
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j --){
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
System.out.print(dp[m]);
}
}
分组背包问题
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
代码:
import java.util.*;
public class Main{
static int N = 110, n, m;
//v为体积,w为价值,s代表第i组物品的个数
static int[][] v = new int[N][N], w = new int[N][N];
static int[] s = new int[N];
//只从前i组物品中选,当前体积小于等于j的最大值
static int[] dp = new int[N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
for(int i = 1; i <= n; i++){
s[i] = in.nextInt();
for(int j = 1; j <= s[i]; j++){
v[i][j] = in.nextInt();
w[i][j] = in.nextInt();
}
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= 0; j--){
for(int k = 1; k <= s[i]; k++){
if(v[i][k] <= j){
dp[j] = Math.max(dp[j], dp[j - v[i][k]] + w[i][k]);
}
}
}
}
System.out.print(dp[m]);
}
}
线性DP
数字三角形

输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
代码:
import java.util.*;
public class Main{
static int N = 510, n;
static int[][] w = new int[N][N], dp = new int[N][N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
w[i][j] = in.nextInt();
}
}
for(int i = 1; i <= n; i++){
dp[n][i] = w[n][i];
}
for(int i = n - 1; i > 0; i--){
for(int j = 1; j <= i; j++){
dp[i][j] = Math.max(dp[i + 1][j], dp[i + 1][j + 1]) + w[i][j];
}
}
System.out.print(dp[1][1]);
}
}
最长上升子序列

输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
代码:
import java.util.*;
public class Main{
static int N = 1010, n;
static int[] a = new int[N], dp = new int[N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i = 1; i <= n; i++){
a[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
dp[i] = 1;
for(int j = 1; j < i; j++){
if(a[j] < a[i]){
dp[i] =Math.max(dp[i], dp[j] + 1);
}
}
}
int res = 0;
for(int i = 1; i <= n; i++){
res = Math.max(res, dp[i]);
}
System.out.println(res);
}
}
//输出路径
import java.util.*;
public class Main{
static int N = 1010, n;
//g[i]表示以a[i]结尾的最长子序列的前一个字母的下标
static int[] a = new int[N], dp = new int[N], g = new int[N], path = new int[N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i = 1; i <= n; i++){
a[i] = in.nextInt();
}
for(int i = 1; i <= n; i++){
dp[i] = 1;
//等于0表示只有一个数
g[i] = 0;
for(int j = 1; j < i; j++){
if(a[j] < a[i]){
if(dp[i] < dp[j] + 1){
dp[i] = dp[j] + 1;
g[i] = j;
}
}
}
}
int len = 0;
int k = 0;
for(int i = 1; i <= n; i++){
if(len < dp[i]){
len = dp[i];
k = i;
}
}
System.out.println(len);
int index = 1;
while(g[k] != 0){
path[index] = a[k];
k = g[k];
index++;
}
path[index] = a[k];
for(int i = index; i > 1; i--){
System.out.print(path[i] + "->");
}
System.out.print(path[1]);
}
}
最长公共子序列

输入样例:
4 5
acbd
abedc
输出样例:
3
代码:
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[][] dp = new int[N][N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
m = in.nextInt();
in.nextLine();
String a = in.nextLine();
String b = in.nextLine();
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(a.charAt(i - 1) == b.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
System.out.println(dp[n][m]);
}
}
最短编辑距离
输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例:
4
代码:
import java.util.*;
public class Main{
static int N = 1010, n, m;
static int[][] dp = new int[N][N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
String a = in.next();
m = in.nextInt();
String b = in.next();
for(int i = 1; i <= n; i++){
dp[i][0] = i;
}
for(int j = 1; j <= m; j++){
dp[0][j] = j;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(a.charAt(i - 1) == b.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1];
}else{
dp[i][j] = dp[i - 1][j - 1] + 1;
dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
}
}
}
System.out.println(dp[n][m]);
}
}
区间DP

思路:
输入样例:
4
1 3 5 2
输出样例:
22
代码:
import java.util.*;
public class Main{
static int N = 310, n;
static int[] a = new int[N], s = new int[N];//前缀和
static int[][] dp =new int[N][N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i = 1; i <= n; i++){
a[i] = in.nextInt();
s[i] = s[i-1] + a[i];
}
//所有的区间dp问题枚举时,
//第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;
//第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)
for(int len = 2; len <= n; len++){
for(int i = 1; i + len - 1 <= n; i++){
int l = i, r = i + len - 1;
dp[l][r] = (int)1e9;
for(int k = l; k < r; k++){
dp[l][r] = Math.min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
}
}
}
System.out.println(dp[1][n]);
}
}
计数类DP
思路:
输入样例:
5
输出样例:
7
代码:
import java.util.*;
//完全背包解法
public class Main{
static int N = 1010, mod = (int)1e9 + 7, n;
//dp[i][j]表示只从1~i中选,且总和等于j的方案数
static int[] dp = new int[N];
public static void main(String[] args){
Scanner in = new Scanner(System.in);
n = in.nextInt();
dp[0] = 1;
for(int i = 1; i <= n; i++){
for(int j = i; j <= n; j++){
//dp[i][j] = dp[i - 1][j] + dp[i][j - i]
dp[j] = (dp[j] + dp[j - i]) % mod;
}
}
System.out.println(dp[n]);
}
}
数位统计DP
状态压缩DP
思路:
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18
代码:
import java.util.Arrays;
import java.util.Scanner;
/**
* @author liuzhangjie
* @date 2023/05/11
*/
public class Main {
static int N = 20, M = 1 << N, n;
static int[][] f = new int[M][N], w = new int[N][N];
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++){
w[i][j] = in.nextInt();
}
}
for(int i = 0 ; i < 1 << n ; i ++ )
Arrays.fill(f[i],0x3f3f3f); //除了第一个点,其他点初始化成正无穷
f[1][0] = 0;
//枚举所有状态
for (int i = 0; i < 1 << n; i++) {
//枚举所有终点
for (int j = 0; j < n; j++) {
//表示能够走到j,才能进行下一步
if(((i >> j) & 1) == 1){
//j是终点,k是倒数第二个点
for(int k = 0; k < n; k++){
//首先减掉最后一个点剩下的路径中要能够走到k才能够进行状态计算
if(((i >> k) & 1) == 1){
//0 - k - j的途径 f[state - (1 << j)][k] + w[k][j]
f[i][j] = Math.min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
}
}
}
}
}
System.out.println(f[(1 << n) - 1][n - 1]);
}
}
树形DP

思路:
输入样例:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出样例:
5
代码:
import java.util.Arrays;
import java.util.Scanner;
/**
* @author liuzhangjie
* @date 2023/05/10
*/
public class Main {
static int N = 6010, n, idx;
static int[] e = new int[N], ne = new int[N], h = new int[N];
static int[] w = new int[N];
static boolean[] st = new boolean[N];//st[i]表示节点i是否有父节点
static int[][] f = new int[N][2];//f[i][0]表示不选i节点的情况下,以i为根节点的子数的最大值,f[i][1]表示选i节点
static void add(int a, int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++ ;
}
static void dfs(int u){
f[u][1] = w[u];
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
dfs(j);
f[u][0] += Math.max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
n = in.nextInt();
for(int i = 1; i <= n; i++){
w[i] = in.nextInt();
}
Arrays.fill(h, -1);
for(int i = 0; i < n - 1; i++){
int a = in.nextInt();
int b = in.nextInt();
add(b, a);
st[a] = true;
}
int root = 1;
while(st[root]){
root++;
}
dfs(root);
System.out.println(Math.max(f[root][0], f[root][1]));
}
}