基本概念
核心思想
为了解决一个“大”问题,将“大”问题分解成两个“小”问题
举个例子:一次可以走一个台阶或者两个台阶,问走到第n个台阶时,一共有多少种走法?要走到第n级台阶,分成两种情况,一种是从n-1级台阶走一步过来,一种是从n-2级台阶走两步过来
状态
形如dp[i][j] = val的取值,其中i,
j为下标,用于描述、确定状态所需的变量,val为状态值
状态转移
状态与状态之间的转移关系,转移方向决定了迭代或递归方向
常见特征:重叠子问题,最优子结构
重叠子问题:
斐波那契数为例,用递归计算fib(5),分解为图示的子问题,其中fib(3)计算了2次,其实只算1次就够了
最优子结构
两种编码方法
自顶向下与记忆化
先考虑大问题,再缩小到小问题,递归很直接地体现了这种思路。为避免递归时重复计算子问题,可以在子问题得到解决时,就保存结果,再次需要这个结果时,直接返回保存的结果就行了。这种存储已经解决的子问题的结果的技术称为“记忆化(Memoization)”。
以斐波那契数为例,记忆化代码如下:
1 2 3 4 5 6 7
| int memoize[N]; int fib (int n){ if (n == 1 || n == 2) return 1; if(memoize[n] != 0) return memoize[n]; memoize[n]= fib (n - 1) + fib (n - 2); return memoize[n]; }
|
自下而上与制表递推
先解决子问题,再递推到大问题。通常通过填写表格来完成,编码时用若干for循环语句填表。根据表中的结果,逐步计算出大问题的解决方案。
用制表法计算斐波那契数,维护一个一维表dp[],记录自下而上的计算结果,更大的数是前面两个数的和。
1 2 3 4 5 6 7
| const int N = 255; int dp[N]; int fib (int n){ dp[1] = dp[2] =1; for (int i=3;i<=n;i++) dp[i] = dp[i-1] +dp[i-2]; return dp[n]; }
|
分析步骤
确定状态
一般为“到第i个为止,方案数/最小代价/最大价值
确定状态转移方程
根据状态转移方向决定迭代还是递归
确定最终状态
题干
分析
状态:dp[i][j]表示第i行,第j列的和的最大值
状态迁移方程:方向从底向上,取当前位置的下一层的左边或右边的最大值与当前位置求和,作为当前位置dp值
最终状态:dp[1][1]
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import java.util.Scanner;
public class Main { static int MAXSIZE = 105; static int[][] dp = new int[MAXSIZE][MAXSIZE]; public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); for(int i = 1; i <= n; i++) { for(int j = 1; j <= i; j++) { dp[i][j] = scan.nextInt(); } } for(int i = n - 1; i >= 1; i--) { for(int j = 1; j <= i; j++) { dp[i][j] += Math.max(dp[i + 1][j], dp[i + 1][j + 1]); } } System.out.println(dp[1][1]); scan.close(); } }
|
题干
分析
确定状态:dp[i]表示到第i级台阶有多少种方案
确定状态转移方程:从后往前,dp[i] = dp[i - 1] + dp[i - 2]
确定最终状态:dp[N]
注意,题目中如果告诉你结果比较大,java一般需要开long
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import java.util.Scanner;
public class Main { static int p = (int)1e9 + 7; static int MAXSIZE = (int)10e5 + 5; static boolean[] broken = new boolean[MAXSIZE]; static long[] dp = new long[MAXSIZE]; public static void main(String[] args) { Scanner scan = new Scanner(System.in); int N = scan.nextInt(); int M = scan.nextInt(); for(int i = 0; i < M; i++) { int worse = scan.nextInt(); broken[worse] = true; } broken[0] = false; dp[0] = 1; if(broken[1] == false) dp[1] = 1; for(int i = 2; i <= N; i++) { if(broken[i] == false) { dp[i] = (dp[i - 1] + dp[i - 2]) % p; } }
System.out.println(dp[N]); scan.close(); } }
|
题干
分析
确定状态
dp[i]:最后一个桶放在位置i的方案总数
状态转移方程
因为dp[i]表示最后一个桶在位置i所具有的方案总数,而要求最近的相邻两个桶之间要相隔k个桶,所以dp[i]等于从最后一个桶在位置1的方案(dp[1])到最后一个桶在位置i
- k - 1的方案数(dp[i - k - 1])的和,即\(dp[j]
= \sum_{i = 1}^{j - k - 1}dp[i]\)
最终状态
题目要求的是总方案数,由dp数列定义可知不是dp[N],所以要对dp数组求前缀和,prefix[N]才是答案
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import java.util.Scanner;
public class Main { static int p = (int)1e9 + 7; static int N = (int)1e6 + 5; static long[] dp = new long[N]; static long[] prefix = new long[N]; public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); int k = scan.nextInt(); dp[0] = prefix[0] = 1; for(int i = 1; i <= n; i++) { if(i - k - 1 < 1) dp[i] = 1; else dp[i] = prefix[i - k - 1]; prefix[i] = (prefix[i - 1] + dp[i]) % p; } System.out.println(prefix[n]); scan.close(); } }
|