算法系列之动态规划
动态规划(Dynamic Programming,简称DP)是一种用于解决复杂问题的算法设计技术。它通过将问题分解为更小的子问题,并存储这些子问题的解来避免重复计算,从而提高算法的效率。本文将介绍动态规划的基本概念、适用场景、复杂度分析,并通过Java代码实现经典的动态规划问题。
动态规划介绍
动态规划的核心思想是将一个复杂的问题分解为若干个相互重叠的子问题,通过解决这些子问题来构建原问题的解。动态规划通常适用于具有以下两个性质的问题:
-
最优子结构:问题的最优解包含其子问题的最优解。
-
重叠子问题:在求解过程中,相同的子问题会被多次计算。
动态规划算法的实现方式:
- 自底向上(Bottom-Up):通过迭代的方式从最小的子问题开始,逐步构建更大的子问题的解,直到解决原问题。
复杂度分析
动态规划的时间复杂度和空间复杂度取决于问题的规模和子问题的数量。假设问题的规模为n,子问题的数量为m,则:
-
时间复杂度:通常为O(m * n),其中m是子问题的数量,n是每个子问题的计算复杂度。
-
空间复杂度:通常为O(m),用于存储子问题的解。
Java实现斐波那契数列
斐波那契数列:斐波那契数列的定义是从0和1开始,后续的每一项都是前两项的和,即0、1、1、2、3、5、8、13、21、34……这个数列在数学、自然界以及日常生活中都有着广泛的应用和意义。
我们通过动态规划和递归两种方式实现输入一个正整数 n ,输出斐波那契数列的第 n 项。
/**
* 斐波那契数列
*
* 斐波那契数列:斐波那契数列的定义是从0和1开始,后续的每一项都是前两项的和,即0、1、1、2、3、5、8、13、21、34……这个数列在数学、自然界以及日常生活中都有着广泛的应用和意义。
*
* 输入一个正整数 n ,输出斐波那契数列的第 n 项。
*/
public class FibonacciSequence {
/**
*动态规划解法
* @param n
* @return
*/
public static int fibonacciDP(int n) {
// 边界条件
if (n <= 1) {
return n;
}
//定义初始发f(n-2)的值
int prev2 = 0;
//定义初始发f(n-1)的值
int prev1 = 1;
//定义初始发f(n)的值
int current = 0;
// 循环计算斐波那契数列的第 n 项(f(n) = f(n-1)+f(n-2))
for (int i = 2; i <= n; i++) {
current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return current;
}
/**
*递归解法
* @param n
* @return
*/
public static int fibonacciRec(int n) {
// 边界条件
if (n <= 1) {
return n;
}
//递归求解
return fibonacciRec(n-1) +fibonacciRec(n-2);
}
public static void main(String[] args) {
System.out.println(fibonacciDP(10));
System.out.println(fibonacciRec(10));
}
}
⼯作安排问题
- 问题描述:
⼩明每周上班都会拿到⾃⼰的⼯作清单,⼯作清单内包含 n 项⼯作,每项⼯作都有对应的耗时时⻓(单位h)和报酬,⼯作的总报酬为所有已完成⼯作的报酬之和。那么请你帮⼩明安排⼀下⼯作,保证⼩明在指定的⼯作时间内⼯作收⼊最⼤化。
- 输入描述:
输⼊的第⼀⾏为两个正整数 T , n 。 T 代表⼯作时⻓(单位h, 0 < T < 100000 ) , n 代表⼯作数量( 1 < n < 3000 )
接下来是 n ⾏,每⾏包含两个整数 t , w 。 t 代表该项⼯作消耗的时⻓(单位h, t > 0 ) , w 代表该项⼯作的报酬。
- 输出描述
输出⼩明指定⼯作时⻓内⼯作可获得的最⼤报酬。
- 示例
示例1
输入:
40 3
20 10
20 20
20 5
输出:
30
示例2
输入:
100 3
50 10
20 30
50 20
输出:
50
- 代码实现
public class WorkArrangement {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 输⼊最⼤⼯作时间max_time和⼯作数量N
int max_time = scanner.nextInt();
// ⼯作数量N
int N = scanner.nextInt();
//工作时常数组
int[] times = new int[N];
//工资数组
int[] wages = new int[N];
// 输⼊N份⼯作的时间和报酬
for (int i = 0; i < N; i++) {
times[i] = scanner.nextInt();
wages[i] = scanner.nextInt();
}
Map<Integer,Integer> dpMap = new HashMap<>();
//初始化工作时长和工资
dpMap.put(0,0);
for(int i = 0; i< N; i++){
// 复制当前的dpMap
Map<Integer, Integer> currentDP = new HashMap<>(dpMap);
//当前工作的时长和工资
int time = times[i];
int wage = wages[i];
// 遍历当前currentDP哈希表中,所有的时间和报酬
for(Map.Entry<Integer, Integer> entry: currentDP.entrySet()){
int preTime = entry.getKey();
int preWage = entry.getValue();
// 当前的结束时间
int endTime = preTime + time;
// 当前结束时间对应的工资
int endWage = preWage + wage;
// 如果结束时间不超过最大时间,则更新dpMap
if(endTime <= max_time){
dpMap.put(endTime, Math.max(dpMap.getOrDefault(endTime,0),endWage));
}
}
}
// 输出dpMap哈希表中的最⼤值
int maxWage = 0;
for (int wage : dpMap.values()) {
maxWage = Math.max(maxWage, wage);
}
System.out.println(maxWage);
}
}
总结
动态规划是一种强大的算法设计技术,适用于解决具有最优子结构和重叠子问题性质的问题。通过合理地分解问题和存储子问题的解,动态规划可以显著提高算法的效率。本文通过斐波那契数列和工作安排问题展示了动态规划的基本思想和Java实现。希望本文能帮助你更好地理解和应用动态规划算法。