DP问题主要就是想出状态转移方程,这里用y总的DP分析方法
DP分析流程
核心思想是从集合的角度来分析问题,主要三部分
一、状态表示–集合
主要是分析题意,判断该集合由几个条件决定,也就是分析 f 数组的维度
二、状态表示–属性
考虑最终结果的集合是哪一种属性得来的,通常为Min,Max,Count
三、状态计算
把目标集合根据最后一个条件划分成子集合,分析每个子集合的表达式,最后根据属性将所有子集合汇总的到总集合的表达式即状态转移方程
Acwing 01背包
题意:N件物品(只取一次),每件物品体积v价值w,背包体积V,求解在背包容量内的最大价值
状态表示–集合
f ( i , j ),考虑前 i 个物品,总体积不超过 j 的最大价值
状态表示–属性
Max
状态计算
对于第 i 件物品来说,集合划分为选和不选两种,即
(1)不选第 i 件 :
f ( i , j ) = f ( i - 1, j )
(2)选第 i 件:
f ( i , j ) = f ( i - 1, j - v [ i ] ) + w [ i ]
因此状态转移方程为
在这里插入代码片
由于数组下标存在一个 j - v [ i ] ,避免数组越界需要进行判断
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 1010;
public static void main(String[] args) throws IOException {
int f[][] = new int[N][N];
int v[] = new int[N];
int w[] = new int[N];
int T = tab.nextInt();
int V = tab.nextInt();
for (int i = 1; i <= T; i++) {
v[i] = tab.nextInt();
w[i] = tab.nextInt();
}
for (int i = 1; i <= T; i++) {
for (int j = 1; j <= V; j++) {
if (j >= v[i])
f[i][j] = Math.max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
else
f[i][j] = f[i - 1][j];
}
}
System.out.println(f[T][V]);
}
}
Acwing 完全背包
题意:N件物品(不限量),每件物品体积v价值w,背包体积V,求解在背包容量内的最大价值
朴素无简化
状态表示–集合
f ( i , j ),考虑前 i 个物品,总体积不超过 j 的最大价值
状态表示–属性
Max
状态计算
对第 i 件物品来说,可以取到的最大数量 k 为 j / v[ i ]
因此取第 i 件物品数量分为0~k件,即集合分为k + 1个子集合
(0)第 i 件选0个:
f ( i , j ) = f ( i - 1, j - 0 * v [ i ] ) + 0 * w[ i ]
(1)第 i 件选1个:
f ( i , j ) = f ( i - 1, j - 1 * v [ i ] ) + 1 * w[ i ]
(2)第 i 件选2个:
f ( i , j ) = f ( i - 1, j - 2 * v [ i ] ) + 2 * w[ i ]
…
(k)第 i 件选k个:
f ( i , j ) = f ( i - 1, j - k * v [ i ] ) + k * w[ i ]
使用一个循环来遍历出 k 的值,因此状态转移方程为
for (int k = 0; k * v[i]<= j; k++) {
f[i][j] = Math.max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
在判断表达式中尽可能用乘法来表示除法避免出错
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 1010;
static int f[][] = new int[N][N];
public static void main(String[] args) throws IOException {
int v[] = new int[N];
int w[] = new int[N];
int n = tab.nextInt();
int m = tab.nextInt();
for (int i = 1; i <= n; i++) {
v[i] = tab.nextInt();
w[i] = tab.nextInt();
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
for (int k = 0; k * v[i] <= j; k++) {
f[i][j] = Math.max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
}
System.out.println(f[n][m]);
}
}
Acwing 石子合并(区间DP)
题意:n堆石子,每堆含有一个质量,每次合并相邻的两堆耗费的体力是两堆的质量和,求解最终合并成一堆的最小体力
区间DP模板题,区间DP常规套路是先枚举长度,再枚举左端点,即可得右端点,之后再枚举一个区间内的分界点通过状态转移方程计算即可
看到一半的思路是直接二维枚举左右端点,但结果不对,错在比如计算2 ~ 5区间时会用到3 ~ 5的区间值,如果二维枚举端点的方式计算到这里时,3 ~ 5的值还没有计算,因此要通过枚举长度、左端点这两维来计算
状态表示–集合
f ( i , j ),区间 i ~ j 的最小体力耗费
状态表示–属性
Min
状态计算
对 i ~ j 区间来说,将分界点 k 从 i 枚举至 j - 1
则每个子集合的值为分界点两端集合的和值加上此次合并需要的体力x,即
f ( i , j ) = f ( i , k ) + f ( k + 1 , j ) + x
因此状态转移方程为
for(int k = i; k < j; k++) {
f[i][j] = Math.min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i-1]);
}
使用前缀和 s [ ] 来进行区间求值
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 1010;
static int f[][] = new int[N][N];
static int s[] = new int[N]; // 前缀和求区间值
public static void main(String[] args) throws IOException {
int n = tab.nextInt();
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + tab.nextInt();
}
// 枚举长度
for(int len = 2; len <= n; len ++) {
// 枚举左端点
for(int i = 1; i + len - 1 <= n ;i++) {
// 右端点
int j = i + len - 1;
// 初始化
f[i][j] = (int)1e8;
// 枚举分界点
for(int k = i; k < j; k++) {
f[i][j] = Math.min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i-1]);
}
}
}
System.out.println(f[1][n]);
}
}
SDUT OJ 最长公共子序列问题
题意:给出两个字符串s1 s2,找出他们的最长公共子序列
主要讲求一个灵性分析
状态表示–集合
f ( i , j ),s1从1 ~ i 与s2从1 ~ j 的最长公共子序列集合中长度最大值
状态表示–属性
Max
状态计算
每次对最后一个字母即s1[ i ]和s2[ j ]来进行判断
(1)如果它们相同,则这个字母一定在最长序列内
f ( i , j ) = f ( i - 1 , j - 1 ) + 1
(2)如果它们不同,定义s1[ i ]或s2[ j ]其中一个一定不在最长序列内
f ( i , j ) = f ( i , j - 1 )
f ( i , j ) = f ( i - 1 , j )
(3)若s1[ i ]和s2[ j ]一定都不在最长序列内
f ( i , j ) = f ( i - 1 , j - 1 )
这种情况已经被(2)包含,因此不必考虑
最终状态转移方程为
f[i][j] = Math.max(f[i-1][j], f[i][j-1]);
if(a.charAt(i) == b.charAt(j)) {
f[i][j] = Math.max(f[i][j], f[i-1][j-1]+1);
}
代码如下
import java.io.*;
import java.util.*;
public class Main {
static Scanner tab = new Scanner(System.in);
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
static int N = 1010;
static int f[][] = new int[N][N];
public static void main(String[] args) throws IOException {
String a = 'a'+tab.next();
String b = 'a'+tab.next();
for(int i = 1; i<a.length(); i++) {
for(int j = 1; j < b.length(); j++) {
f[i][j] = Math.max(f[i-1][j], f[i][j-1]);
if(a.charAt(i) == b.charAt(j)) {
f[i][j] = Math.max(f[i][j], f[i-1][j-1]+1);
}
}
}
System.out.println(f[a.length()-1][b.length()-1]);
}
}
最后看到一句我怎么才能发现这是一个DP问题真是unbengable
DP说白了还是玄学😅