编程语言
首页 > 编程语言> > Day36-数据结构与算法-算法策略

Day36-数据结构与算法-算法策略

作者:互联网


title: Day36-数据结构与算法-算法策略
date: 2021-02-01 10:44:30
tags: Liu_zimo


常用的经典数据结构


递归(Recursion)

递归的基本思想

递归使用的套路

  1. 明确函数的功能
    • 先不要去思考里面代码怎么写,首先搞清楚这个函数的干嘛用的,能完成什么功能?
  2. 明确原问题与子问题的关系
    • 寻找f(n)与f(n -1)的关系
  3. 明确递归基(边界条件)
    • 递归的过程中,子问题的规模在不断减小,当小到一定程度时可以直接得出它的解
    • 寻找递归基,相当于是思考:问题规模小到什么程度可以直接得出解?

练习

  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);
    }
}

Fibonacci调用过程

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)

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)
这是一种“自底向上”的计算过程

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))

  1. 上楼梯(跳台阶)
    • 楼梯有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)
    • 跟上面斐波那契数列一样,差别就是退出条件不同:f(1) = 1,f(2) = 2
    • 所以优化思路跟斐波那契数列一致
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);
    }
}
  1. 汉诺塔(Hanoi)
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)

递归转非递归

尾调用(Tail Call)
尾调用优化(Tail Call Optimization)
优化后的汇编代码(C++)
void test(int n) {
    if (n<0) return;
    printf( "test - %d\n", n);
    test(n - 1);
}

尾调用优化后的汇编代码

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);
    }
}
// 尾递归优化
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)

练习

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);
    }
}
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)

注意事项

练习

  1. 最优装载问题(加勒比海盗)
    • 在北美洲东南部,有一片神秘的海域,是海盗最活跃的加勒比海
      • 有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值
      • 海盗船的载重量为W,每件古董的重量为wi,海盗们该如何把尽可能多数量的古董装上海盗船?
      • 比如W为30,wi分别为3、5、4、10、7、14、2、11
    • 贪心策略:每一次都优先选择重量最小的古董
      1. 选择重量为2的古董,剩重量28
      2. 选择重量为3的古董,剩重量25
      3. 选择重量为4的古董,剩重量21
      4. 选择重量为5的古董,剩重量16
      5. 选择重量为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);
    }
}
  1. 零钱兑换
    • 假设有25分、10分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?
    • 贪心策略:每一次都优先选择面值最大的硬币
      1. 选择25分的硬币,剩16分
      2. 选择10分的硬币,剩6分
      3. 选择5分的硬币,剩1分
      4. 选择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();
    }
}
  1. 0 - 1背包
    • 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
      • 在保证总重量不超过W的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
      • 注意:每个物品只有1件,也就是每个物品只能选择0件或者1件,因此称为0-1背包问题
    • 如果采取贪心策略,有3个方案
      1. 价值主导:优先选择价值最高的物品放进背包
      2. 重量主导:优先选择重量最轻的物品放进背包
      3. 价值密度主导:优先选择价值密度最高的物品放进背包(价值密度 = 价值 ÷ 重量)
    • 假设背包最大承重150,7个物品如表格所示
编号1234567
重量35306050401025
价值10403050354030
价值密度0.291.330.51.00.884.01.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)

主定理(Master Theorem)

分治原理

练习

  1. 最大连续子序列和
    • 给定一个长度为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种可能
        1. [i,j) 存在于 [begin,mid) 中
        2. [i,j) 存在于 [mid,end) 中
        3. [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)
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());
    }
}
  1. 大数乘法
    • 2个超大的数(比如2个100位的数),如何进行乘法?
      • 按照小学时学习的乘法运算,在进行n位数之间的相乘时,需要大约进行n2次个位数的相乘
      • 比如计算36x 54

分治_大数乘法

分治_大数乘法_优化

动态规划(Dynamic Programming)

动态规划常规步骤

  1. 定义状态(状态是原问题、子问题的解)
    比如定义dp(i)的含义
  2. 设置初始状态(边界)
    比如设置dp(0)的值
  3. 确定状态转移方程
    比如确定dp(i)和dp(i - 1)的关系

相关概念

  1. 将复杂的原问题拆解成若干个简单的子问题
  2. 每个子问题仅仅解决1次,并保存它们的解
  3. 最后推导出原问题的解

后效性

练习

  1. 找零钱
    • 假设有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
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];
    }
}
  1. 最大连续子序列和
    • 给定一个长度为n的整数序列,求它的最大连续子序列和
      • 比如-2、1、-3、4、-1、2、1、-5、4的最大连续子序列和是4+(-1) + 2 + 1 = 6
    • 状态定义
      • 假设 dp(i) 是以 nums[i] 结尾的最大连续子序列和(nums是整个序列)
        1. 以nums[0] -2 结尾的最大连续子序列是 -2,所以dp(0) = -2
        2. 以nums[1] 1结尾的最大连续子序列是1,所以dp(1) = 1
        3. 以nums[2] -3结尾的最大连续子序列是1、-3,所以dp(2) = dp(1)+(-3) = -2
        4. 以nums[3] 4结尾的最大连续子序列是4,所以dp(3) = 4
        5. 以nums[4] -1结尾的最大连续子序列是4、-1,所以dp(4) = dp(3) + (-1) = 3
        6. 以nums[5] 2结尾的最大连续子序列是4、-1、2,所以dp(5) = dp(4) + 2 = 5
        7. 以nums[6] 1结尾的最大连续子序列是4、-1、2、1,所以dp(6) = dp(5) + 1 = 6
        8. 以nums[7] -5结尾的最大连续子序列是4、-1、2、1、-5,所以dp(7) = dp(6) + (-5) = 1
        9. 以nums[8] 4结尾的最大连续子序列是4、-1、2、1、-5、4,所以dp(8) = dp(7) + 4 = 5
    • 状态转移方程
      • 如果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)
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());
    }
}
  1. 最长上升子序列(LIS)
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());
    }
}
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());
    }
}
  1. 最长公共子序列(LCS)
    • 最长公共子序列(Longest Common Subsequence,LCS)
      • 求两个序列的最长公共子序列长度
        1. [1,3,5,910] 和 [1,4,910] 的最长公共子序列是[1,9,10],长度为3
        2. ABCBDAB和BDCABA的最长公共子序列长度是4,可能是
          ABCBDABBDCABA > BDAB
          ABCBDABBDCABA > 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)}
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());
    }
}
  1. 最长公共子串
    • 最长公共子串(Longest Common Substring)
      • 子串是连续的子序列
    • 求两个字符串的最长公共子串长度
      • ABCBA和BABCA的最长公共子串是ABC,长度为3
    • 思路
      • 假设2个字符串分别是str1str2
        • i ∈ [1,str1.length]
        • j ∈ [1,str2.length]
      • 假设 dp(ij) 是以 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)}
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());
    }
}
  1. 0 - 1背包问题
    • 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
      • 在保证总重量不超过W的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
      • 注意:每个物品只有1件,也就是每个物品只能选择0件或者1件,因此称为0-1背包问题
    • 思路:
      • 假设values是价值数组,weights是重量数组
        • 编号为k的物品,价值是values[k],重量是weights[k],k ∈ [0,n)
      • 假设dp(ij)是最大承重为j、有前i件物品可选时的最大总价值,i∈ [0,n],j ∈ [0,W]
        • dp(i,0)、dp(0,j)初始值均为0
        • 如果j < weights[i - 1],那么dp(ij) = dp(i - 1,j)
        • 如果j ≥ weights[i - 1],那么dp(ij) = max{dp(i - 1,j),dp(i - 1,j - weights[i - 1]) + values[i - 1]}
    • 思路1:非递归 - 一维数组实现
      • dp(ij)都是由dp(i - 1,k)推导出来的,也就是说,第i行的数据是由它的上一行第i - 1行推导出来的
        • 因此,可以使用一维数组来优化
        • 另外,由于k ≤ j,所以 j 的遍历应该由大到小,否则导致数据错乱
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());
    }
}
  1. 0 - 1背包:恰好装满
    • 有n件物品和一个最大承重为W的背包,每件物品的重量是wi、价值是vi
      • 在保证总重量恰好等于W的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
      • 注意:每个物品只有1件,也就是每个物品只能选择0件或者1件,因此称为0-1背包问题
    • dp(ij)初始状态调整
    • dp(i,0) = 0,总重量恰好为0,最大总价值必然也为0
    • dp(0,j) = -∞(负无穷),j ≥ 1,负数在这里代表无法恰好装满
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