Day36-数据结构与算法-算法策略
作者:互联网
title: Day36-数据结构与算法-算法策略
date: 2021-02-01 10:44:30
tags: Liu_zimo
常用的经典数据结构
递归(Recursion)
- 递归:函数(方法)直接或间接调用自身。是一种常用的编程技巧
- 如果递归调用没有终止,将会一直消耗栈空间
- 最终导致栈内存溢出(Stack Overflow)
- 所以必需要有一个明确的结束递归的条件
- 也叫作边界条件、递归基
- 注意:使用递归不是为了求得最优解,是为了简化解决问题的思路,代码会更加简洁
- 递归求出来的很有可能不是最优解,也有可能是最优解
递归的基本思想
- 拆解问题
- 把规模大的问题变成规模较小的同类型问题
- 规模较小的问题又不断变成规模更小的问题
- 规模小到一定程度可以直接得出它的解
- 求解
- 由最小规模问题的解得出较大规模问题的解
- 由较大规模问题的解不断得出规模更大问题的解
- 最后得出原来问题的解
- 凡是可以利用上述思想解决问题的,都可以尝试使用递归
递归使用的套路
- 明确函数的功能
- 先不要去思考里面代码怎么写,首先搞清楚这个函数的干嘛用的,能完成什么功能?
- 明确原问题与子问题的关系
- 寻找f(n)与f(n -1)的关系
- 明确递归基(边界条件)
- 递归的过程中,子问题的规模在不断减小,当小到一定程度时可以直接得出它的解
- 寻找递归基,相当于是思考:问题规模小到什么程度可以直接得出解?
练习
- 斐波那契数列:1,1,2,3,5,8,13,21,34…,
- 当前数等于前两个数之和
- F(1) = 1, F(2) = 1,F(n) = F(n-1) + F(N-2) (n ≥ 3)
package com.zimo.算法.算法策略.递归;
/**
* 算法策略 - 递归:斐波那契数列
*
* @author Liu_zimo
* @version v0.1 by 2021/2/1 14:02
*/
public class Fibonacci {
public static int fib(int n){
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
}
}
- 根据递推式 T(n) = T(n - 1) + T(n - 2) + O(1),可得知时间复杂度:O(2n)
- 空间复杂度:O(n)
- 递归调用的空间复杂度 = 递归深度 * 每次调用所需的辅助空间
-
出现了特别的多的重复计算
-
这是一种“自顶向下”的调用过程
-
Fibonacci 优化1:避免重复计算
public static int fib_1(int n){
if (n <= 2) return 1;
int[] array = new int[n + 1];
array[1] = array[2] = 1;
return fib_1(n, array);
}
private static int fib_1(int n, int[] array){
if (array[n] == 0){
array[n] = fib_1(n - 1, array) + fib_1(n - 2, array);
}
return array[n];
}
时间、空间复杂度:O(n)
- Fibonacci 优化2:去除递归调用
public static int fib_2(int n){
if (n <= 2 )return 1;
int[] array = new int[n + 1];
array[2] = array[1] = 1;
for (int i = 3; i <= n; i++){
array[i] = array[i - 1] + array[i - 2];
}
return array[n];
}
时间、空间复杂度:O(n)
这是一种“自底向上”的计算过程
- Fibonacci 优化3:由于每次运算只需要用到数组中的2个元素,所以可以使用滚动数组来优化
public static int fib_3(int n){
if (n <= 2 )return 1;
int[] array = new int[2];
array[0] = array[1] = 1;
for (int i = 3; i <= n; i++){
array[i % 2] = array[(i - 1) % 2] + array[(i - 2) % 2]; // x % 2 === x & 1
}
return array[n%2];
}
时间复杂度:O(n),空间复杂度:O(1)
Fibonacci 优化4:特征方程
public static int fib_4(int n) {
double c = Math.sqrt(5);
return (int) ((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c);
}
时间复杂度、空间复杂度取决于pow 函数(至少可以低至0(logn))
- 上楼梯(跳台阶)
- 楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶,走完n阶台阶共有多少种不同的走法?
- 假设n阶台阶有 f(n) 种走法,第1步有2种走法
- 如果上1阶,那就还剩 n - 1 阶,共 f(n - 1) 种走法
- 如果上2阶,那就还剩 n - 2 阶,共 f(n - 2) 种走法
- 所以 f(n) = f(n - 1) + f(n - 2)
- 假设n阶台阶有 f(n) 种走法,第1步有2种走法
- 跟上面斐波那契数列一样,差别就是退出条件不同:f(1) = 1,f(2) = 2
- 所以优化思路跟斐波那契数列一致
- 楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶,走完n阶台阶共有多少种不同的走法?
package com.zimo.算法.算法策略.递归;
/**
* 算法策略 - 递归:上楼梯
*
* @author Liu_zimo
* @version v0.1 by 2021/2/1 15:05
*/
public class ClimbStairs {
public static int climbStairs(int n){
if (n <= 2) return n;
return climbStairs(n - 1) + climbStairs(n - 2);
}
}
- 汉诺塔(Hanoi)
- 其实分2种情况讨论即可
- 当n ==1时,直接将盘子从A移动到C
- 当n >1时,可以拆分成3大步骤
- 将n-1个盘子从A移动到B
- 将编号为n的盘子从A移动到C
- 将n-1个盘子从B移动到C
package com.zimo.算法.算法策略.递归;
/**
* 算法策略 - 递归:汉诺塔
*
* @author Liu_zimo
* @version v0.1 by 2021/2/1 15:28
*/
public class Hanoi {
/**
* 将n个碟子从p1柱子挪动到p3柱子
* @param n
* @param p1
* @param p2
* @param p3
*/
public static void hanoi(int n, String p1, String p2, String p3){
if (n == 1) {
move(n, p1,p3 );
return;
}
hanoi(n - 1, p1, p3, p2);
move(n, p1, p3);
hanoi(n - 1, p2, p1, p3);
}
private static void move(int no, String from, String to){
System.out.println("将" +no + "号盘子从" + from + "移动到" + to);
}
public static void main(String[] args) {
hanoi(4, "A", "B", "C");
}
}
时间复杂度:O(2n),空间复杂度:O(n)
递归转非递归
- 递归调用的过程中,会将每一次调用的参数、局部变量都保存在了对应的栈帧(Stack Frame)中
- 若递归调用深度较大,会占用比较多的栈空间,甚至会导致栈溢出
- 在有些时候,递归会存在大量的重复计算,性能非常差
- 这时可以考虑将递归转为非递归(递归100%可以转换成非递归)
- 万能方法:
- 自己维护一个栈,来保存参数、局部变量
- 但是空间复杂度依然没有得到优化
- 在某些时候,也可以重复使用一组相同的变量来保存每个栈帧的内容
- 这里重复使用变量 i 保存原来栈帧中的参数
- 空间复杂度从O(n)降到了O(1)
尾调用(Tail Call)
- 尾调用:一个函数的最后一个动作是调用函数
- 如果最后一个动作是调用自身,称为尾递归(Tail Recursion),是尾调用的特殊情况
- —些编译器能对尾调用进行优化,以达到节省栈空间的目的
尾调用优化(Tail Call Optimization)
- 尾调用优化也叫做尾调用消除(Tail Call Elimination)
- 如果当前栈帧上的局部变量等内容都不需要用了,当前栈帧经过适当的改变后可以直接当作被尾调用的函数的栈帧使用,然后程序可以jump 到被尾调用的函数代码
- 生成栈帧改变代码与jump的过程称作尾调用消除或尾调用优化
- 尾调用优化让位于尾位置的函数调用跟goto语句性能一样高
- 消除尾递归里的尾调用比消除—般的尾调用容易很多(栈空间一样大,不需要扩大或者缩小栈空间)
- 比如Java虚拟机(JVM)会消除尾递归里的尾调用,但不会消除一般的尾调用(因为改变不了栈帧)
- 因此尾递归优化相对比较普遍,平时的递归代码可以考虑尽量使用尾递归的形式
优化后的汇编代码(C++)
void test(int n) {
if (n<0) return;
printf( "test - %d\n", n);
test(n - 1);
}
- 案例1:n的阶乘
package com.zimo.算法.算法策略.递归;
/**
* 算法策略 - 递归:尾递归优化 - 阶乘案例
*
* @author Liu_zimo
* @version v0.1 by 2021/2/1 17:38
*/
public class Factorial {
// 优化前
public static int factorial(int n) {
if (n <= 1) return n;
return n * factorial(n - 1);
}
// 优化后
public static int factorial_1(int n) {
return factorial_1(n, 1);
}
private static int factorial_1(int n, int result) {
if (n <= 1) return result;
return factorial_1(n - 1, n * result);
}
}
- 案例2:斐波那契数列
// 尾递归优化
int fibo(int n){
return fibo(n,1,1);
}
private int fibo(int n, int first, int second) {
if (n <= 1) return first;
return fibo(n-1, first, second);
}
回溯(Back Tracking)
- 回溯可以理解为:通过选择不同的岔路口来通往目的地
- 每一步都选择一条路出发,能进则进,不能进则退回上一步(回溯),换一条路再试
- 图的深度优先搜索(DFS)就是典型的回溯应用
- 不难看出来,回溯很适合使用递归
练习
- 八皇后问题
- 在8x8格的国际象棋上摆放八个皇后,使其不能互相攻击:任意两个皇后都不能处于同一行、同一列、同一斜线上
- 请问有多少种摆法?
- 思路一:暴力解决
- 从64个格子中选出任意8个格子摆放皇后,检查每个摆法的可行性
- 一共C(8,64)种摆法(大概是4.4 * 109种摆法)
- 思路二:减小暴力程度
- 每一行只能放一个皇后,所以共有88种摆法(16777216种),检查每一种摆法的可行性
- 思路三:回溯法
- 跟四皇后问题一样采取:回溯 + 剪枝
package com.zimo.算法.算法策略.回溯;
/**
* 算法策略 - 回溯:皇后摆放问题
*
* @author Liu_zimo
* @version v0.1 by 2021/2/2 10:04
*/
public class PlaceQueens {
int[] cols; // 数组索引是行号,数组元素是列号
int ways; // 一共又多少种摆放方式
/**
* 从第row行开始摆放皇后
* @param row
*/
void place(int row){
if (row == cols.length){
this.ways++;
show(ways);
return;
}
for (int col = 0; col < cols.length; col++) {
if (isValid(row, col)){
cols[row] = col; // 在第row行第col列摆放皇后
place(row + 1);
// 回溯
}
}
}
/**
* 判断第row行 第col列是否可以摆放皇后
* @param row
* @param col
* @return
*/
boolean isValid(int row, int col){
// 全新的一行,不用考虑行冲突
for (int i = 0; i < row; i++) {
if (cols[i] == col) return false; // 这一列上是否有摆放皇后
/** 如果斜率相等,说明在同一条线上(row-i)/(col-cols[i]) == (1 or -1)
* (row-i) == (col-cols[i]) || (row-i) == -(col-cols[i])
*/
if (row - i == Math.abs(col - cols[i])) return false; // 斜线上有皇后
}
return true;
}
/**
* n皇后摆放问题
* @param n
*/
void placeQueens(int n){
if (n < 1) return;
cols = new int[n];
place(0);
System.out.println(n + "皇后一共有" + ways + "种摆法");
}
void show(int way){
System.out.println("--- 方法" + way + "---");
String sb = "";
for (int i = 0; i < cols.length; i++) {
sb += "***";
}
sb+="**";
System.out.println(sb);
for (int row = 0; row < cols.length; row++) {
System.out.print("*");
for (int col = 0; col < cols.length; col++) {
if (cols[row] == col){
System.out.print(" 0 ");
}else {
System.out.print(" # ");
}
}
System.out.println("*");
}
System.out.println(sb);
}
// 8皇后摆放问题:回溯 + 剪枝
void eightQueensPlace(){
placeQueens(8);
}
public static void main(String[] args) {
new PlaceQueens().eightQueensPlace();
}
}
- 优化版
package com.zimo.算法.算法策略.回溯;
/**
* 算法策略 - 回溯:皇后摆放问题 优化版
*
* @author Liu_zimo
* @version v0.1 by 2021/2/2 11:47
*/
public class Queens {
boolean[] cols; // 标记这一列是否有皇后了
boolean[] leftTop; // 左上角 - 右下角 对角线上是否有斜线
boolean[] rightTop; // 右上角 - 左下角 对角线上是否有斜线
int[] queens; // 数组索引是行号,数组元素是列号
int ways; // 一共又多少种摆放方式
void place(int row) {
if (row == cols.length) {
this.ways++;
this.show(ways);
return;
}
for (int col = 0; col < cols.length; col++) {
if (cols[col]) continue; // 这一列有皇后
// 左上角 - 右下角 的对角线索引:row - col + (n-1)
int leftIndex = row - col + (cols.length - 1);
if (leftTop[leftIndex]) continue; // 左斜线有皇后了
// 右上角 - 左下角 的对角线索引:row + col
int rightIndex = row + col;
if (rightTop[rightIndex]) continue; // 右斜线有皇后了
this.cols[col] = true; // 在第row行第col列摆放皇后
this.leftTop[leftIndex] = true;
this.rightTop[rightIndex] = true;
this.queens[row] = col;
place(row + 1);
// 开始回溯 如果条件不成立,清楚标志
this.cols[col] = false;
this.leftTop[leftIndex] = false;
this.rightTop[rightIndex] = false;
}
}
void placeQueens(int n) {
if (n < 1) return;
this.cols = new boolean[n];
this.leftTop = new boolean[(n << 1) - 1];
this.rightTop = new boolean[leftTop.length];
this.queens = new int[n];
place(0);
System.out.println(n + "皇后一共有" + ways + "种摆法");
}
void show(int way) {
System.out.println("--- 方法" + way + "---");
String sb = "";
for (int i = 0; i < cols.length; i++) {
sb += "***";
}
sb += "**";
System.out.println(sb);
for (int row = 0; row < cols.length; row++) {
System.out.print("*");
for (int col = 0; col < cols.length; col++) {
if (queens[row] == col) {
System.out.print(" Q ");
} else {
System.out.print(" # ");
}
}
System.out.println("*");
}
System.out.println(sb);
}
public static void main(String[] args) {
new Queens().placeQueens(8);
}
}
- 针对8皇后进行空间复杂度优化
package com.zimo.算法.算法策略.回溯;
/**
* 算法策略 - 回溯:8皇后空间复杂度 优化版
*
* @author Liu_zimo
* @version v0.1 by 2021/2/2 11:47
*/
public class EightPlaceQueens {
int[] queens; // 数组索引是行号,数组元素是列号
byte cols;
short leftTop;
short rightTop;
int ways; // 一共又多少种摆放方式
void place(int row) {
if (row == 8) {
this.ways++;
this.show(ways);
return;
}
for (int col = 0; col < 8; col++) {
int cv = 1 << col;
if ((cols & cv) != 0) continue; // 这一列有皇后
// 左上角 - 右下角 的对角线索引:row - col + (n-1)
int leftIndex = row - col + 7;
int lv = 1 << leftIndex;
if ((leftTop & lv) != 0) continue; // 左斜线有皇后了
// 右上角 - 左下角 的对角线索引:row + col
int rightIndex = row + col;
int rv = 1 << rightIndex;
if ((rightTop & rv) != 0) continue; // 右斜线有皇后了
this.cols |= cv; // 在第row行第col列摆放皇后
this.leftTop |= lv;
this.rightTop |= rv;
this.queens[row] = col;
place(row + 1);
// 开始回溯 如果条件不成立,清楚标志
this.cols &= ~cv;
this.leftTop &= ~lv;
this.rightTop &= ~rv;
}
}
void placeEightQueens() {
this.queens = new int[8];
place(0);
System.out.println(8 + "皇后一共有" + ways + "种摆法");
}
void show(int way) {
System.out.println("--- 方法" + way + "---");
String sb = "";
for (int i = 0; i < 8; i++) {
sb += "***";
}
sb += "**";
System.out.println(sb);
for (int row = 0; row < 8; row++) {
System.out.print("*");
for (int col = 0; col < 8; col++) {
if (queens[row] == col) {
System.out.print(" Q ");
} else {
System.out.print(" # ");
}
}
System.out.println("*");
}
System.out.println(sb);
}
public static void main(String[] args) {
new EightPlaceQueens().placeEightQueens();
}
}
贪心(Greedy)
- 贪心策略,也称为贪婪策略
- 每一步都采取当前状态下最优的选择(局部最优解),从而希望推导出全局最优解
- 贪心的应用
- 哈夫曼树
- 最小生成树算法: Prim、Kruskal
- 最短路径算法:Dijkstra
注意事项
- 贪心策略并不一定能得到全局最优解
- 因为一般没有测试所有可能的解,容易过早做决定,所以没法达到最佳解
- 贪图眼前局部的利益最大化,看不到长远未来,走一步看一步
- 优点:简单、高效、不需要穷举所有可能,通常作为其他算法的辅助算法来使用
- 缺点:鼠目寸光,不从整体上考虑其他可能,每次采取局部最优解,不会再回溯,因此很少情况会得到最优解
练习
- 最优装载问题(加勒比海盗)
- 在北美洲东南部,有一片神秘的海域,是海盗最活跃的加勒比海
- 有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值
- 海盗船的载重量为W,每件古董的重量为wi,海盗们该如何把尽可能多数量的古董装上海盗船?
- 比如W为30,wi分别为3、5、4、10、7、14、2、11
- 贪心策略:每一次都优先选择重量最小的古董
- 选择重量为2的古董,剩重量28
- 选择重量为3的古董,剩重量25
- 选择重量为4的古董,剩重量21
- 选择重量为5的古董,剩重量16
- 选择重量为7的古董,剩重量9
- 最多装载5个古董
- 在北美洲东南部,有一片神秘的海域,是海盗最活跃的加勒比海
package com.zimo.算法.算法策略.贪心;
import java.util.Arrays;
/**
* 算法策略 - 贪心:最优装载问题(加勒比海盗问题)
*
* @author Liu_zimo
* @version v0.1 by 2021/2/2 15:00
*/
public class Pirate {
private int capacity; // 总容量
private int weight; // 装载的容量
private int count; // 装载的数量
public Pirate(int capacity) {
this.capacity = capacity;
}
public void loder(int[] weights){
for (int i = 0; i < weights.length; i++) {
int newWeight = this.weight + weights[i];
if (capacity >= newWeight){
this.weight = newWeight;
this.count++;
}
}
System.out.println("一共选了" + count + "件古董,当前重量为:" + this.weight);
}
public static void main(String[] args) {
int[] weights = {3, 5, 4, 10, 7, 14, 2, 11};
Arrays.sort(weights);
new Pirate(30).loder(weights);
}
}
- 零钱兑换
- 假设有25分、10分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
- 贪心策略:每一次都优先选择面值最大的硬币
- 选择25分的硬币,剩16分
- 选择10分的硬币,剩6分
- 选择5分的硬币,剩1分
- 选择1分的硬币
- 最终的解是共4枚硬币,25分、10分、5分、1分硬币各一枚
- 假设修改一笔面值为:25分、20分、5分、1分的硬币,得到是:[25,5,5,5,1]
- 实际上最优解是:[20,20,1]
package com.zimo.算法.算法策略.贪心;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 算法策略 - 贪心:找零钱
*
* @author Liu_zimo
* @version v0.1 by 2021/2/2 15:21
*/
public class CoinChange {
public static final Integer[] faces = {25, 10, 5, 1};
private int money;
private final List<Integer> coins = new ArrayList<>();
public CoinChange(int money) {
this.money = money;
}
public void change() {
Arrays.sort(faces, (Integer f1, Integer f2)->f2 - f1);
int idx = 0;
while (idx < faces.length){
while (money >= faces[idx]){
this.money -= faces[idx];
this.coins.add(faces[idx]);
}
idx++;
}
System.out.println(this.coins.toString());
}
public void change1() {
Arrays.sort(faces, (Integer f1, Integer f2)->f2 - f1);
int i = 0;
while (i < faces.length){
if (money < faces[i]){
i++;
continue;
}
this.coins.add(faces[i]);
this.money -= faces[i];
}
System.out.println(this.coins.toString());
}
public static void main(String[] args) {
new CoinChange(41).change();
}
}
- 0 - 1背包
- 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
- 在保证总重量不超过W的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
- 注意:每个物品只有1件,也就是每个物品只能选择0件或者1件,因此称为0-1背包问题
- 如果采取贪心策略,有3个方案
- 价值主导:优先选择价值最高的物品放进背包
- 重量主导:优先选择重量最轻的物品放进背包
- 价值密度主导:优先选择价值密度最高的物品放进背包(价值密度 = 价值 ÷ 重量)
- 假设背包最大承重150,7个物品如表格所示
- 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
重量 | 35 | 30 | 60 | 50 | 40 | 10 | 25 |
价值 | 10 | 40 | 30 | 50 | 35 | 40 | 30 |
价值密度 | 0.29 | 1.33 | 0.5 | 1.0 | 0.88 | 4.0 | 1.2 |
package com.zimo.算法.算法策略.贪心;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
/**
* 算法策略 - 贪心:0-1背包问题
*
* @author Liu_zimo
* @version v0.1 by 2021/2/2 16:46
*/
public class Knapsack {
private static Item[] items; // 所有物品
private int capacity;
private int weight;
private int value;
private List<Item> result = new ArrayList<>(); // 选择后的物品
public Knapsack(int capacity, Item[] items) {
this.capacity = capacity;
this.items = items;
}
public void getKnapsack(String title, Comparator<Item> cmp){
Arrays.sort(this.items, cmp);
for (int i = 0; i < items.length && this.weight < capacity; i++) {
int newWeight = this.weight + items[i].weight;
if(newWeight <= capacity){
this.weight = newWeight;
this.value += items[i].value;
this.result.add(items[i]);
}
}
System.out.println("----" + title + "----");
System.out.println("总价值:" + this.value + ",总重量:" + this.weight + ",物品" + this.result.toString());
}
public static void main(String[] args) {
Item[] items = new Item[]{
new Item(35,10), new Item(30,40),
new Item(60,30), new Item(50,50),
new Item(40,35), new Item(10,40),
new Item(25,30)
};
Comparator<Item> valueCmp = (Item i1, Item i2) -> i2.value - i1.value;
new Knapsack(150, items).getKnapsack("价格主导", valueCmp);
Comparator<Item> weightCmp = (Item i1, Item i2) -> i1.weight - i2.weight;
new Knapsack(150, items).getKnapsack("重量主导", weightCmp);
Comparator<Item> valueDensityCmp = (Item i1, Item i2) -> Double.compare(i2.valueDensity, i1.valueDensity);
new Knapsack(150, items).getKnapsack("性价比主导", valueDensityCmp);
}
static class Item{
public int weight;
public int value;
public double valueDensity;
public Item(int weight, int value) {
this.weight = weight;
this.value = value;
this.valueDensity = value * 1.0 / weight;
}
@Override
public String toString() {
return "item{" + "weight=" + weight + ", value=" + value + ", valueDensity=" + valueDensity + '}';
}
}
}
分治(Divide And Conquer)
- 分治,也就是分而治之。它的一般步骤是
- 将原问题分解成若干个规模较小的子问题(子问题和原问题的结构一样,只是规模不一样)
- 子问题又不断分解成规模更小的子问题,直到不能再分解(直到可以轻易计算出子问题的解)
- 利用子问题的解推导出原问题的解
- 因此,分治策略非常适合用递归
- 需要注意的是:子问题之间是相互独立的
- 分治的应用
- 快速排序
- 归并排序
- Karatsuba算法(大数乘法)
主定理(Master Theorem)
- 分治策略通常遵守一种通用模式
- 解决规模为n的问题,分解成a个规模为(n/b)的子问题,然后在O(nd)时间内将子问题的解合并起来
- 算法运行时间为:T(n) = aT(n/b) + O(nd),a > 0,b > 1,d ≥ 0
- d > logba,T(n) = O(nd)
- d = logba,T(n) = O(ndlogn)
- d < logba,T(n) = O(nlogba)
- 比如归并排序的运行时间是:T(n) = 2T(n/2) + O(n),a = 2,b = 2,d = 1,所以T(n)= O(nlogn)
练习
- 最大连续子序列和
- 给定一个长度为n的整数序列,求它的最大连续子序列和
- 比如-2、1、-3、4、-1、2、1、-5、4的最大连续子序列和是4+(-1) + 2 + 1 = 6
- 这道题也属于最大切片问题(最大区段,Greatest Slice)
- 概念区分
- 子串、子数组、子区间必须是连续的,子序列是可以不连续的
- 解法1 - 暴力破解
- 穷举出所有可能的连续子序列,并计算出它们的和,最后取它们中的最大值
- 空间复杂度:O(1),时间复杂度:O(n3),优化后的时间复杂度为:O(n2)
- 解法2 - 分治
- 将序列均匀地分割成2个子序列
- [begin,end) = [begin,mid) + [mid,end),mid = (begin + end) >> 1
- 假设问题的解是 S[i,j),那么问题的解有3种可能
- [i,j) 存在于 [begin,mid) 中
- [i,j) 存在于 [mid,end) 中
- [i,j) 一部分存在于 [begin,mid) 中,另一部分存在于 [mid,end) 中
- [i,j) = [i,mid) + [mid,j)
- S[i,mid) = max {S[k,mid)},begin ≤ k < mid
- 空间复杂度:O(logn),时间复杂度:O(nlogn)
- 跟归并排序、快速排序一样T(n) = 2T(n/2) + O(n)
- 将序列均匀地分割成2个子序列
package com.zimo.算法.算法策略.分治;
/**
* 算法策略 - 分治:最大连续子序列和
*
* @author Liu_zimo
* @version v0.1 by 2021/2/3 16:42
*/
public class MaxSubArray {
public static final int[] array = {-2,1,-3,4,-1,2,1,-5,4};
// 暴力破解法
static int maxSubArray(){
if (array.length == 0) return 0;
int max = Integer.MIN_VALUE;
for (int begin = 0; begin < array.length; begin++) {
for (int end = 0; end < array.length; end++) {
int sum = 0;
for (int i = begin; i <= end; i++) {
sum += array[i];
}
max = Math.max(sum,max);
}
}
return max;
}
// 暴力破解法 - 优化
static int maxSubArray_1(){
if (array.length == 0) return 0;
int max = Integer.MIN_VALUE;
for (int begin = 0; begin < array.length; begin++) {
int sum = 0;
for (int end = begin; end < array.length; end++) {
sum += array[end];
max = Math.max(max,sum);
}
}
return max;
}
// 分治法
static int maxSubArray_2(){
if (array.length == 0) return 0;
return maxSubArray_3(0, array.length);
}
// 求解[begin, end)中最大连续子序列的和
private static int maxSubArray_3(int begin, int end) {
if (end - begin < 2) return array[begin];
int mid = (begin + end) >> 1;
/**** 如果连续最大子序列在中间的情况 begin ****/
int leftMidMax = Integer.MIN_VALUE;
int leftMidSum = 0;
for (int i = mid - 1; i >= begin; i--) {
leftMidSum += array[i];
leftMidMax =Math.max(leftMidMax, leftMidSum);
}
int rightMidMax = Integer.MIN_VALUE;
int rightMidSum = 0;
for (int i = mid; i < end; i++) {
rightMidSum += array[i];
rightMidMax =Math.max(rightMidMax, rightMidSum);
}
int midMax = leftMidMax + rightMidMax;
/**** 如果连续最大子序列在中间的情况 end ****/
int leftMax = maxSubArray_3(begin, mid); // 左边最大的
int rightMax = maxSubArray_3(mid, end); // 右边最大的
int lr_Max = Math.max(leftMax, rightMax);
return Math.max(midMax, lr_Max);
}
public static void main(String[] args) {
System.out.println(maxSubArray_2());
}
}
- 大数乘法
- 2个超大的数(比如2个100位的数),如何进行乘法?
- 按照小学时学习的乘法运算,在进行n位数之间的相乘时,需要大约进行n2次个位数的相乘
- 比如计算36x 54
- 2个超大的数(比如2个100位的数),如何进行乘法?
- 1960年 Anatolii Alexeevitch Karatsuba提出了Karatsuba算法,提高了大数乘法的效率
动态规划(Dynamic Programming)
- 动态规划,简称DP
- 是求解最优化问题的一种常用策略
- 通常的使用套路(—步一步优化)
- 暴力递归(自顶向下,出现了重叠子问题)
- 记忆化搜索(自顶向下)
- 递推(自底向上)
动态规划常规步骤
- 动态规划中的“动态”可以理解为是“会变化的状态”
- 定义状态(状态是原问题、子问题的解)
比如定义dp(i)的含义 - 设置初始状态(边界)
比如设置dp(0)的值 - 确定状态转移方程
比如确定dp(i)和dp(i - 1)的关系
相关概念
- 将复杂的原问题拆解成若干个简单的子问题
- 每个子问题仅仅解决1次,并保存它们的解
- 最后推导出原问题的解
- 可以用动态规划来解决的问题,通常具备2个特点
- 最优子结构(最优化原理)︰通过求解子问题的最优解,可以获得原问题的最优解
- 无后效性
- 某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关)
- 在推导后面阶段的状态时,只关心前面阶段的具体状态值,不关心这个状态是怎么一步步推导出来的
练习
- 找零钱
- 假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
- 此前用贪心策略得到的并非是最优解(贪心得到的解是5枚硬币)
- 假设dp(n)是coudaon分需要的最少硬币个数
- 如果第1次选择了25分的硬币,那么dp(n) = dp(n - 25) + 1
- 如果第1次选择了20分的硬币,那么dp(n) = dp(n - 20) + 1
- 如果第1次选择了5分的硬币,那么dp(n) = dp(n - 5) + 1
- 如果第1次选择了1分的硬币,那么dp(n) = dp(n - 1) + 1
- 所以dp(n) = min{dp(n - 25),dp(n - 20),dp(n - 5),dp(n - 1)} + 1
- 假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
package com.zimo.算法.算法策略.动态规划;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 算法策略 - 动态规划:找零钱
*
* @author Liu_zimo
* @version v0.1 by 2021/2/4 14:48
*/
public class CoinChange {
public static final Integer[] faces = {25, 20, 5, 1};
private final List<Integer> coins = new ArrayList<>();
public CoinChange() {
}
// 暴力递归 (自顶向下的调用,出现了重叠子问题)
int coins(int money) {
if (money < 0) return Integer.MAX_VALUE;
List<Integer> list = Arrays.asList(this.faces);
if (list.contains(money)) return 1;
int min1 = Math.min(coins(money - 25), coins(money - 20));
int min2 = Math.min(coins(money - 5), coins(money - 1));
return Math.min(min1, min2) + 1;
}
// 记忆化搜索 (自顶向下的调用)
int coins_1(int money) {
if (money < 0) return -1;
int[] dp = new int[money + 1];
Arrays.sort(this.faces);
for (Integer face : faces) {
if (money < face) break;
dp[face] = 1;
}
return coins_1(money, dp);
}
private int coins_1(int money, int[] dp) {
if (money < 1) return Integer.MAX_VALUE;
if (dp[money] == 0) {
int min1 = Math.min(coins_1(money - 25, dp), coins_1(money - 20, dp));
int min2 = Math.min(coins_1(money - 5, dp), coins_1(money - 1, dp));
dp[money] = Math.min(min1, min2) + 1;
}
return dp[money];
}
// 递推 (自底向上的调用)
int coins_2(int money) {
if (money < 0) return Integer.MAX_VALUE;
int[] dp = new int[money + 1];
// faceList[i]是凑够i分时最后的那枚硬币的面值
int[] faceList = new int[dp.length];
for (int i = 1; i <= money; i++) {
// dp[i] = min{dp[i - 25],dp[i - 20],dp[i - 5],dp[i - 1]} + 1;
int min = Integer.MAX_VALUE;
if (i >= 1 && dp[i - 1] < min) {
min = dp[i - 1];
faceList[i] = 1;
}
if (i >= 5 && dp[i - 5] < min) {
min = dp[i - 5];
faceList[i] = 5;
}
if (i >= 20 && dp[i - 20] < min) {
min = dp[i - 20];
faceList[i] = 20;
}
if (i >= 25 && dp[i - 25] < min) {
min = dp[i - 25];
faceList[i] = 25;
}
dp[i] = min + 1;
}
selectCoins(faceList, money);
return dp[money];
}
private void selectCoins(int[] faceList, int money) {
while (money > 0) {
this.coins.add(faceList[money]);
money -= faceList[money];
}
}
// 递推优化版 - 通用版
int coins_3(int money) {
if (money < 1 || this.faces == null || this.faces.length == 0) return -1;
int[] dp = new int[money + 1];
for (int i = 1; i <= money; i++) {
int min = Integer.MAX_VALUE;
for (Integer face : this.faces) {
if (i < face) continue;
if (dp[i - face] < 0 || dp[i - face] >= min) continue;
min = dp[i - face];
}
if (min == Integer.MAX_VALUE) {
dp[i] = -1;
} else {
dp[i] = min + 1;
}
}
return dp[money];
}
}
- 时间复杂度、空间复杂度:O(n)
- 最大连续子序列和
- 给定一个长度为n的整数序列,求它的最大连续子序列和
- 比如-2、1、-3、4、-1、2、1、-5、4的最大连续子序列和是4+(-1) + 2 + 1 = 6
- 状态定义
- 假设 dp(i) 是以 nums[i] 结尾的最大连续子序列和(nums是整个序列)
- 以nums[0] -2 结尾的最大连续子序列是 -2,所以dp(0) = -2
- 以nums[1] 1结尾的最大连续子序列是1,所以dp(1) = 1
- 以nums[2] -3结尾的最大连续子序列是1、-3,所以dp(2) = dp(1)+(-3) = -2
- 以nums[3] 4结尾的最大连续子序列是4,所以dp(3) = 4
- 以nums[4] -1结尾的最大连续子序列是4、-1,所以dp(4) = dp(3) + (-1) = 3
- 以nums[5] 2结尾的最大连续子序列是4、-1、2,所以dp(5) = dp(4) + 2 = 5
- 以nums[6] 1结尾的最大连续子序列是4、-1、2、1,所以dp(6) = dp(5) + 1 = 6
- 以nums[7] -5结尾的最大连续子序列是4、-1、2、1、-5,所以dp(7) = dp(6) + (-5) = 1
- 以nums[8] 4结尾的最大连续子序列是4、-1、2、1、-5、4,所以dp(8) = dp(7) + 4 = 5
- 假设 dp(i) 是以 nums[i] 结尾的最大连续子序列和(nums是整个序列)
- 状态转移方程
- 如果dp(i - 1) ≤ 0,那么dp(i) = nums[i]
- 如果dp(i - 1) > 0,那么dp(i) = dp(i - 1) + nums[i]
- 初始状态
- dp(0)的值是nums[0]
- 最终的解
- 最大连续子序列和是所有dp(i)中的最大值 max{dp(i)},i ∈ [0,nums.length)
- 给定一个长度为n的整数序列,求它的最大连续子序列和
package com.zimo.算法.算法策略.动态规划;
/**
* 算法策略 - 动态规划:最大连续子序列
*
* @author Liu_zimo
* @version v0.1 by 2021/2/4 18:23
*/
public class MaxSubArray {
public static final int[] array = {-2,1,-3,4,-1,2,1,-5,4};
static int maxSubArray(){
if (array == null || array.length == 0) return 0;
int[] dp = new int[array.length];
dp[0] = array[0];
int max = dp[0];
for (int i = 1; i < dp.length; i++) {
if (dp[i - 1] <= 0){
dp[i] = array[i];
}else {
dp[i] = dp[i - 1] + array[i];
}
max = Math.max(dp[i],max);
}
return max;
}
// 空间优化 O(1)
static int maxSubArray_1(){
if (array == null || array.length == 0) return 0;
int dp = array[0];
int max = dp;
for (int i = 1; i < array.length; i++) {
if (dp <= 0){
dp = array[i];
}else {
dp = dp + array[i];
}
max = Math.max(dp,max);
}
return max;
}
public static void main(String[] args) {
System.out.println(MaxSubArray.maxSubArray_1());
}
}
- 时间复杂度、空间复杂度:O(n),优化后空间复杂度O(1)
- 最长上升子序列(LIS)
- 最长上升子序列(最长递增子序列,Longest Increasing Subsequence,LIS)
- 给定一个无序的整数序列,求出它最长上升子序列的长度(要求严格上升)
- 比如[10,2,2,5,1,7,101,18]的最长上升子序列是[2,5,7,101]、[2,5,7,18],长度是4
- 动态规划 - 状态定义
- dp(i) 是以 nums[i] 结尾的最长上升子序列的长度,初始值为1,i ∈ [0,nums.length)
- 以 nums[0] 10结尾的最长上升子序列是10,所以 dp(O) = 1
- 以 nums[1] 2结尾的最长上升子序列是2,所以 dp(1) = 1
- 以 nums[2] 2结尾的最长上升子序列是2,所以 dp(2) = 1
- 以 nums[3] 5结尾的最长上升子序列是2、5,所以 dp(3) = dp(1) + 1 = dp(2) + 1 = 2
- 以 nums[4] 1结尾的最长上升子序列是1,所以 dp(4) = 1
- 以 nums[5] 7结尾的最长上升子序列是2、5、7,所以 dp(5) = dp(3) + 1 = 3
- 以 nums[6] 101结尾的最长上升子序列是2、5、7、101,所以 dp(6) = dp(5) + 1 = 4
- 以 nums[7] 18结尾的最长上升子序列是2、5、7、18,所以 dp(7) = dp(5) + 1= 4
- 最长上升子序列的长度是所有dp(i)中的最大值max{dp(i)},i ∈ [0,nums.length)
- dp(i) 是以 nums[i] 结尾的最长上升子序列的长度,初始值为1,i ∈ [0,nums.length)
- 动态规划 - 状态转移方程
- 遍历 j 属于 [0,i)
- 当 nums[i] > nums[j]
nums[i] 可以接在 nums[j] 后面,形成一个比 dp(j) 更长的上升子序列,长度为 dp(j) + 1
dp(i) = max{dp(i),dp(j) + 1} - 当nums[i] ≤ nums[j]
nums[i] 不能接在 nums[j] 后面,跳过此次遍历(continue)
- 当 nums[i] > nums[j]
- 状态的初始值
- dp(0)=1
- 所有的dp(i)默认都初始化为1
- 遍历 j 属于 [0,i)
package com.zimo.算法.算法策略.动态规划;
/**
* 算法策略 - 动态规划:最长上升子序列(LIS)
*
* @author Liu_zimo
* @version v0.1 by 2021/2/5 15:04
*/
public class LIS {
public static final int[] array = {10,2,2,5,1,7,101,18};
static int lengthOfLIS(){
if (array == null || array.length == 0) return 0;
int[] dp = new int[array.length];
int max = dp[0] = 1;
for (int i = 1; i < dp.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (array[i] <= array[j]) continue;
dp[i] = Math.max(dp[i],dp[j]+1);
}
max = Math.max(dp[i], max);
}
return max;
}
public static void main(String[] args) {
System.out.println(LIS.lengthOfLIS());
}
}
- 空间复杂度:O(n),时间复杂度O(n2)
- 3.1 - 二分搜索法
- 思路:
- 把每个数字看做是一张扑克牌,从左到右按顺序处理每一个扑克牌
- 将它压在(从左边数过来)第一个牌顶≥它的牌堆上面
- 如果找不到牌顶≥它的牌堆,就在最右边新建一个牌堆,将它放入这个新牌堆中
- 【10 2 2 1】
- 【5】
- 【7】
- 【101 18】
- 当处理完所有牌,最终牌堆的数量就是最长上升子序列的长度
- 把每个数字看做是一张扑克牌,从左到右按顺序处理每一个扑克牌
- 假设数组是array,也就是最初的牌数组
- top[i]是第i个牌维的牌页,len是牌堆的数量,初始值为0
- 遍历每一张牌num
- 利用二分搜索找出num最终要放入的牌堆位置index
- num 作为第index个牌堆的牌顶,top[index] = num
- 如果index等于len,相当于新建一个牌堆,牌堆数量 +1,也就是 len++
- 思路:
package com.zimo.算法.算法策略.动态规划;
/**
* 算法策略 - 动态规划:最长上升子序列(LIS)
*
* @author Liu_zimo
* @version v0.1 by 2021/2/5 15:04
*/
public class LIS {
public static final int[] array = {10,2,2,5,1,7,101,18};
// 非动态规划 O(n²)
static int lengthOfLIS_1(){
if (array == null || array.length == 0) return 0;
// 牌堆的数量
int len = 0;
// 牌顶数组
int[] top = new int[array.length];
// 遍历所有的牌
for (int num : array) {
int i = 0;
while (i < len){
// 找到一个 ≥ num的牌顶
if (top[i] >= num){
top[i] = num;
break;
}
// 牌顶 < num
i++;
}
if (i == len) { // 新建一个排堆
top[i] = num;
len++;
}
}
return len;
}
// 非动态规划 - 二分搜索优化 O(nlogn)
static int lengthOfLIS_Binary(){
if (array == null || array.length == 0) return 0;
// 牌堆的数量
int len = 0;
// 牌顶数组
int[] top = new int[array.length];
// 遍历所有的牌
for (int num : array) {
int begin = 0;
int end = len;
while (begin < end){
int mid = (begin + end) >> 1;
if (num <= top[mid]){
end = mid;
}else {
begin = mid + 1;
}
}
// 覆盖牌顶
top[begin] = num;
// 检查是否要新建一个牌堆
if (begin == len) len++;
}
return len;
}
public static void main(String[] args) {
System.out.println(LIS.lengthOfLIS_Binary());
}
}
- 最长公共子序列(LCS)
- 最长公共子序列(Longest Common Subsequence,LCS)
- 求两个序列的最长公共子序列长度
- [1,3,5,9,10] 和 [1,4,9,10] 的最长公共子序列是[1,9,10],长度为3
- ABCBDAB和BDCABA的最长公共子序列长度是4,可能是
ABCBDAB 和 BDCABA > BDAB
ABCBDAB 和 BDCABA > BDAB
ABCBDAB 和 BDCABA > BCAB
ABCBDAB 和 BDCABA > BCBA
- 求两个序列的最长公共子序列长度
- 思路
- 假设2个序列分别是nums1、nums2
- i ∈ [1,nums1.length]
- j ∈ [1,nums2.length]
- 假设dp(i,j)是【nums1 前 i 个元素】与【nums2 前 j 个元素】的最长公共子序列长度
- dp(i,0)、dp(0,j) 初始值均为0
- 如果nums1[i - 1] = nums2[j - 1],那么 dp(i,j) = dp(i - 1,j - 1) + 1
- 如果nums1[i - 1] ≠ nums2[j - 1],那么 dp(i,j) = max{dp(i - 1,j),dp(i,j-1)}
- 假设2个序列分别是nums1、nums2
- 最长公共子序列(Longest Common Subsequence,LCS)
package com.zimo.算法.算法策略.动态规划;
/**
* 算法策略 - 动态规划:最长公共子序列(LCS)
*
* @author Liu_zimo
* @version v0.1 by 2021/2/5 16:11
*/
public class LCS {
static final int[] num1 = {1,3,5,9,10};
static final int[] num2 = {1,4,9,10};
static int lcs(){
if (num1 == null || num1.length == 0) return 0;
if (num2 == null || num2.length == 0) return 0;
return lcs(num1.length, num2.length);
}
// 求num1前i个元素和num2前j个元素的最长公共子序列长度
static int lcs(int i, int j){
if (i == 0 || j == 0) return 0;
if (num1[i - 1] == num2[j - 1]){
return lcs(i-1, j-1) + 1;
}
return Math.max( lcs(i-1,j), lcs(i, j-1));
}
// 优化版:非递归实现 二维数组[n][m]
static int lcs_1(){
if (num1 == null || num1.length == 0) return 0;
if (num2 == null || num2.length == 0) return 0;
int[][] dp = new int[num1.length + 1][num2.length + 1];
for (int i = 1; i <= num1.length; i++) {
for (int j = 1; j <= num2.length; j++) {
if (num1[i - 1] == num2[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]);
}
}
}
return dp[num1.length][num2.length];
}
// 优化版1:非递归实现 二维数组[2][m]
static int lcs_2(){
if (num1 == null || num1.length == 0) return 0;
if (num2 == null || num2.length == 0) return 0;
int[][] dp = new int[2][num2.length + 1]; // 优化空间
for (int i = 1; i <= num1.length; i++) {
int row = i & 1; // % 2 == & 1
int prevRow = (i - 1) & 1;
for (int j = 1; j <= num2.length; j++) {
if (num1[i - 1] == num2[j - 1]){
dp[row][j] = dp[prevRow][j-1] + 1;
}else {
dp[row][j] = Math.max(dp[prevRow][j],dp[row][j-1]);
}
}
}
return dp[num1.length & 1][num2.length];
}
// 优化版2:非递归实现 一维数组[m]
static int lcs_3(){
if (num1 == null || num1.length == 0) return 0;
if (num2 == null || num2.length == 0) return 0;
int[] dp = new int[num2.length + 1]; // 优化空间
for (int i = 1; i <= num1.length; i++) {
int cur = 0;
for (int j = 1; j <= num2.length; j++) {
int leftTop = cur;
cur = dp[j];
if (num1[i - 1] == num2[j - 1]){
dp[j] = leftTop + 1;
}else {
dp[j] = Math.max(dp[j],dp[j-1]);
}
}
}
return dp[num2.length];
}
public static void main(String[] args) {
System.out.println(lcs_3());
}
}
- 空间复杂度:O(k),k = min{n,m},n、m是2个序列的长度,优化后O(n*m)
- 时间复杂度O(n2),当n == m时,优化后O(n*m)
- 最长公共子串
- 最长公共子串(Longest Common Substring)
- 子串是连续的子序列
- 求两个字符串的最长公共子串长度
- ABCBA和BABCA的最长公共子串是ABC,长度为3
- 思路
- 假设2个字符串分别是str1、str2
- i ∈ [1,str1.length]
- j ∈ [1,str2.length]
- 假设 dp(i,j) 是以 str1[i - 1]、str2[j - 1] 结尾的最长公共子串长度
- dp(i,0)、dp(0,j)初始值均为0
- 如果str1[i - 1] = str2[j - 1],那么dp(i,j) = dp(i - 1,j - 1) + 1
- 如果str1[i - 1] ≠ str2[j - 1],那么dp(i,j) = 0
- 最长公共子串的长度是所有dp(i,j)中的最大值max{dp(i,j)}
- 假设2个字符串分别是str1、str2
- 最长公共子串(Longest Common Substring)
package com.zimo.算法.算法策略.动态规划;
/**
* 算法策略 - 动态规划:最长公共字串
*
* @author Liu_zimo
* @version v0.1 by 2021/2/23 15:03
*/
public class LCSubstring {
public static final String str1 = "ABCBA";
public static final String str2 = "BABCA";
static int lcsubstring() {
if (str1 == null || str2 == null) return 0;
char[] ch1 = str1.toCharArray();
if (ch1.length == 0) return 0;
char[] ch2 = str2.toCharArray();
if (ch2.length == 0) return 0;
int[][] dp = new int[ch1.length + 1][ch2.length + 1];
int max = 0;
for (int i = 1; i <= ch1.length; i++) {
for (int j = 1; j <= ch2.length; j++) {
if (ch1[i - 1] != ch2[j - 1]) continue;
dp[i][j] = dp[i - 1][j - 1] + 1;
max = Math.max(dp[i][j], max);
}
}
return max;
}
// 优化版
static int lcsubstring_1() {
if (str1 == null || str2 == null) return 0;
char[] ch1 = str1.toCharArray();
if (ch1.length == 0) return 0;
char[] ch2 = str2.toCharArray();
if (ch2.length == 0) return 0;
char[] rows = ch1, cols= ch2;
if (ch1.length < ch2.length){
cols = ch1;
rows = ch2;
}
int[] dp = new int[cols.length + 1];
int max = 0;
for (int row = 1; row <= rows.length; row++) {
int cur = 0;
for (int col = 1; col <= cols.length; col++) {
int leftTop = cur;
cur = dp[col];
if (ch1[row - 1] != ch2[col - 1]) {
dp[col] = 0;
}else {
dp[col] = leftTop + 1;
max = Math.max(dp[col], max);
}
}
}
return max;
}
public static void main(String[] args) {
System.out.println(lcsubstring_1());
}
}
- 时间复杂度:O(n*m)
- 空间复杂度:O(n*m),优化版 O(k),k = min{m,n}
- 0 - 1背包问题
- 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
- 在保证总重量不超过W的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
- 注意:每个物品只有1件,也就是每个物品只能选择0件或者1件,因此称为0-1背包问题
- 思路:
- 假设values是价值数组,weights是重量数组
- 编号为k的物品,价值是values[k],重量是weights[k],k ∈ [0,n)
- 假设dp(i,j)是最大承重为j、有前i件物品可选时的最大总价值,i∈ [0,n],j ∈ [0,W]
- dp(i,0)、dp(0,j)初始值均为0
- 如果j < weights[i - 1],那么dp(i,j) = dp(i - 1,j)
- 如果j ≥ weights[i - 1],那么dp(i,j) = max{dp(i - 1,j),dp(i - 1,j - weights[i - 1]) + values[i - 1]}
- 假设values是价值数组,weights是重量数组
- 思路1:非递归 - 一维数组实现
- dp(i,j)都是由dp(i - 1,k)推导出来的,也就是说,第i行的数据是由它的上一行第i - 1行推导出来的
- 因此,可以使用一维数组来优化
- 另外,由于k ≤ j,所以 j 的遍历应该由大到小,否则导致数据错乱
- dp(i,j)都是由dp(i - 1,k)推导出来的,也就是说,第i行的数据是由它的上一行第i - 1行推导出来的
- 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
package com.zimo.算法.算法策略.动态规划;
/**
* 算法策略 - 动态规划:0 - 1背包问题
*
* @author Liu_zimo
* @version v0.1 by 2021/2/23 17:15
*/
public class Knapsack {
private static final int[] values = {6,3,5,4,6};
private static final int[] weights = {2,2,6,5,4};
private static int capacity = 10;
static int maxValue(){
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[][] dp = new int[values.length + 1][capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = 1; j <= capacity; j++) {
if (j < weights[i - 1]){
dp[i][j] = dp[i - 1][j];
}else {
dp[i][j] = Math.max(dp[i - 1][j], values[i - 1] + dp[i - 1][j - weights[i - 1]] );
}
}
}
return dp[values.length][capacity];
}
// 优化版
static int maxValue_1(){
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= 1; j--) { // j >= 1 可以优化为:j >= weights[i - 1] if条件可以删掉
if (j < weights[i - 1]){
continue;
}
dp[j] = Math.max(dp[j], values[i - 1] + dp[j - weights[i - 1]] );
}
}
return dp[capacity];
}
public static void main(String[] args) {
System.out.println(maxValue_1());
}
}
- 0 - 1背包:恰好装满
- 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
- 在保证总重量恰好等于W的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
- 注意:每个物品只有1件,也就是每个物品只能选择0件或者1件,因此称为0-1背包问题
- dp(i,j)初始状态调整
- dp(i,0) = 0,总重量恰好为0,最大总价值必然也为0
- dp(0,j) = -∞(负无穷),j ≥ 1,负数在这里代表无法恰好装满
- 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
package com.zimo.算法.算法策略.动态规划;
import java.lang.reflect.Array;
/**
* 算法策略 - 动态规划:0 - 1背包问题
*
* @author Liu_zimo
* @version v0.1 by 2021/2/23 17:15
*/
public class Knapsack {
private static final int[] values = {6,3,5,4,6};
private static final int[] weights = {2,2,6,5,4};
private static int capacity = 10;
/**
* 背包恰好装满版本
* @return -1:无法凑到capacity这个容量
*/
static int maxValueExactly(){
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= capacity; i++) {
dp[i] = Integer.MIN_VALUE;
}
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i - 1]; j--) {
dp[j] = Math.max(dp[j], values[i - 1] + dp[j - weights[i - 1]] );
}
}
return dp[capacity] < 0 ? -1 : dp[capacity];
}
public static void main(String[] args) {
System.out.println(maxValueExactly());
}
}
标签:return,Day36,length,int,算法,static,数据结构,public,dp 来源: https://blog.csdn.net/qq_38205875/article/details/114031373