标签(空格分隔): hihocoder
经典的01背包问题。主要考察动态规划,自己太渣,动态规划认识的肤浅,下面整理了知乎大神一些关于动态规划的解释,有兴趣的朋友也可以直接上知乎看看。
什么是动态规划?动态规划的意义是什么?
王勐:
动态规划的本质不在于是递归或是递推,也不需要纠结是不是内存换时间。
理解动态规划并不需要数学公式介入,首先需要明白哪些问题不是动态规划可以解决的,才能明白为什么需要动态规划。
同时也需要厘清递推贪心搜索和动归之间有什么关系。
动态规划是对于某一类问题的解决方法,重点在于如何“鉴定”某一类问题是动态规划可解而不是纠结解决方法是递归还是递推
计算机本质是一个状态机,内存里存储所有数据构成了当前的状态,CPU只能利用当前的状态计算出下一个状态。
当你企图使用计算机解决一个问题时,其实就是在思考如何将这个问题表达成状态(用哪些变量存储哪些数据)以及如何在状态中转移(怎样根据一些变量计算出另一些变量)。所以所谓空间复杂度就是为了支持你的计算所必需存储的状态最多有多少,所谓时间复杂度就是从初始状态到达最终状态中间需要多少步。
所谓阶段是指随着问题的解决,在同一时刻可能会得到不同状态的集合。
假设问题有n个阶段,每个阶段都有多个状态,不同阶段的状态数不必相同,一个阶段的一个状态可以得到下个阶段的所有状态中的几个。那我们要计算出最终阶段的状态数自然要经历之前每个阶段的某些状态。
贪心算法:下一步最优是从当前最优得到的,计算方式是递推。
迷宫的例子:需要知道之前走过的所有位置。之前的路线会影响到下一步的选择,这叫有后效性。
关键点:后效性
最长上升子序列的递推公式:
每个阶段的最优状态可以从某个阶段的某个或某些状态直接得到,这个性质叫做最优子结构;而不管之前这个状态如何得到的,这个性质叫做无后效性。
另:其实动态规划中的最优状态的说法容易产生误导,以为只需要计算最优状态就好,LIS问题确实如此,转移时只用到了每个阶段“选”的状态。但实际上有的问题往往需要对每个阶段的所有状态都算出一个最优值,然后根据这些最优值再来找最优状态。比如背包问题就需要对前i个包(阶段)容量为j时(状态)计算出最大价值。然后在最后一个阶段中的所有状态种找到最优值。
熊大大:
动态规划中递推式的求解方法不是动态规划的本质。
0. 动态规划的本质,是对问题状态的定义和状态转移方程的定义。维基百科:
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.
动态规划是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。如何拆分问题,才是动态规划的核心。而拆分问题,靠的就是状态的定义和状态转移方程的定义。
1. 什么是状态的定义?
一个动态规划的教学题:
给定一个数列,长度为N,求这个数列的最长上升(递增)子序列(LIS)的长度。以1 7 2 8 3 4为例。
这个数列的最长递增子数列是 1 2 3 4,长度为4;
次长的长度为3, 包括 1 7 8; 1 2 3 等。
要解决这个问题,我们首先定义这个问题和这个问题的子问题。
我们来重新定义这个问题:
给定一个数列,长度为N。设 Fk 为:以数列中第k项结尾的最长递增子序列的长度。求 F1...FN 中的最大值。
显然,这个问题和原问题等价。而对于 Fk 来讲, F1...Fk−1 都是 Fk 的子问题:因为以第 k 项结尾的最长递增子序列(下称LIS),包含着以第1..k-1中某项结尾的LIS。
上述的新问题 Fk 也可以叫做状态,定义中的“ Fk ”为数列中第k项结尾的LIS的长度”,就叫做对状态的定义。之所以把 Fk 做“状态”而不是“问题”,一是因为避免跟原问题中“问题”混淆,二是因为这个新问题是数学化定义的。
对状态的定义只有一种吗?不是。
我们可以定义二维的,以完全不同的视角定义这个问题:
给定一个数列,长度为N,设 Fi,k 为:
在前i项中,长度为k的最长递增子序列中,最后一位的最小值。其中 1≤k≤N 。
若在前i项中,不存在长度为k的最长递增子序列,则 Fi,k 为正无穷。求最大的x,使得 FN,x 不为正无穷。
这个新定义与原问题也等价。
2.什么是状态转移方程? #好奇怪,2后过个空格就神奇了…
上述状态定义好之后,状态和状态之间的关系式,就叫做状态转移方程。
比如,定义LIS问题,我们的第一种定义:
设 Fk 为:以数列中第k项结尾的最长递增子序列的长度。
设A为题中数列,状态转移方程为:
F1=1 (根据状态定义导出边界情况)
Fk=max(Fi+1|Ak>Ai,i∈(1..k−1))(k>1)
用文字解释一下是:
以第k项结尾的LIS的长度是:保证第i项比第k项小的情况下,以第i项结尾的LIS长度加一的最大值,取遍i的所有值(i小于k)。
第二种定义:
设 Fi,k 为:在数列前i项中,长度为k的递增子序列中,最后一位的最小值。
设A为题中数列,状态转移方程为:
若 Ai>Fi−1,k−1 ,则 Fi,k=min(Ai,Fi−1,k) ,否则: Fi,k=Fi−1,k
(边界情况需要分类讨论较多,在此不列出,需要根据状态定义导出边界情况。)
状态转移方程,就是定义了问题和子问题之间的关系,就是带有条件的递推式。
3.动态规划迷思
递归超时版
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sin = new Scanner(System.in);
int N = sin.nextInt();
int M = sin.nextInt();
sin.nextLine();
ArrayList<Integer> values = new ArrayList<Integer>();
ArrayList<Integer> needs = new ArrayList<Integer>();
int tmp = N;
while (tmp-- > 0) { // sin.hasNext()
needs.add(sin.nextInt());
values.add(sin.nextInt());
}
System.out.println(recursion01(M, N, values, needs));
}
private static int recursion01(int tot, int pos, ArrayList<Integer> values, ArrayList<Integer> needs) {
if (pos == 0 || tot == 0) return 0;
if (tot > needs.get(pos-1)) {
return Math.max(recursion01(tot, pos - 1, values, needs),
values.get(pos-1) + recursion01(tot - needs.get(pos-1), pos - 1, values, needs));
} else {
return recursion01(tot, pos - 1, values, needs);
}
}
}
动态规划版,这个方法比较耗内存
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sin = new Scanner(System.in);
int N = sin.nextInt();
int M = sin.nextInt();
sin.nextLine();
ArrayList<Integer> values = new ArrayList<Integer>();
ArrayList<Integer> needs = new ArrayList<Integer>();
int tmp = N;
while (tmp-- > 0) { // sin.hasNext()
needs.add(sin.nextInt());
values.add(sin.nextInt());
}
int[][] state = new int[M+1][N+1];
for (int i = 0; i < N+1; i++) {
state[0][i] = 0;
}
for (int i = 0; i < M + 1; i++) {
state[i][0] = 0;
}
for (int i = 1; i < N + 1; i++) {
for (int j = 1; j < M + 1; j++) {
if (j < needs.get(i-1)) {
state[j][i] = state[j][i-1];
} else {
state[j][i] = Math.max(state[j][i-1],
values.get(i-1) + state[j-needs.get(i-1)][i-1]);
}
}
}
System.out.println(state[M][N]);
}
}