动态规划常见题型总结

递归和动态规划都是将原问题拆成多个子问题然后求解,他们之间最本质的区别是,动态规划保存了子问题的解,避免重复计算。
  • 动态规划一般可分为4类:
    • 线性动规
    • 区域动规
    • 树形动规
    • 背包动规

动态规划的状态定义和状态转移方程

1.斐波那契数列

1)递归
public static int fib1(int n) {
        if (n == 0) {
            return 0;
        }

        if (n == 1) {
            return 1;
        }
        return fib1(n-1) + fib1(n-2);
    }
2) 自底向上(记忆化搜索)
int[] mem = null;
public  int fib2(int n) {
    mem = new int[n+1];
    Arrays.fill(mem, -1);
    fib122(n);

    return mem[n];
}
int fib122(int n){
    if (n==0)
        return 0;

    if (n==1)
        return 1;

    if (mem[n] == -1){
        mem[n] = fib122(n-1) + fib122(n-2);
    }
    return mem[n];
}
3)动态规划
int fib3(int n) {
    int[] mem = new int[n + 1];
        mem[0] = 0;
        mem[1] = 1;

    for (int i = 2; i <= n; i++) {
        mem[i] = mem[i - 1] + mem[i - 2];
    }
    return mem[n];
}


1272
1
0
267914296
267914296
267914296

2.背包问题(NPC)

背包问题(Knapsack problem)是一种组合优化的NP完全(NP-Complete,NPC)问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高

背包问题有以下几种分类:

  • 01背包问题
  • 完全背包问题
  • 多重背包问题
1)01背包问题

在最优解中,每个物品只有两种可能的情况,即在背包中或者不在背包中(背包中的该物品数为0或1),因此称为0-1背包问题

如果采用暴力穷举的方式,每件物品都存在装入和不装入两种情况,所以总的时间复杂度是O(2^N),这是不可接受的。而使用动态规划可以将复杂度降至O(NW)。

dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W

不装入第i件物品,即dp[i−1][j];
装入第i件物品(前提是能装下),即dp[i−1][j−w[i]] + v[i]。
即状态转移方程为

dp[i][j] = max(dp[i−1][j], dp[i−1][j−w[i]]+v[i]) // j >= w[i]

i.记忆化搜索
  • 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
  • 空间复杂度: O(n * C)
//记忆搜索
static int[][] mem = null;

static int npc1(int[] w, int[] v, int C) {
    int n = w.length;


    mem = new int[n][C + 1];
    for (int i = 0; i < n; i++) {
        Arrays.fill(mem[i], -1);
    }
    //前n个限重 C的最大价值
    return bestValue(w, v, n - 1, C);
}

static int bestValue(int[] w, int[] v, int i, int C) {

    if (i < 0 || C <= 0) {
        return 0;
    }

    if (mem[i][C] != -1) {
        return mem[i][C];
    }
    // 如果装不下
    int res = 0;
    res = bestValue(w, v, i - 1, C);
    if (C >= w[i]) {
        res = Math.max(res, (bestValue(w, v, i - 1, C - w[i]) + v[i]));
    }

    return mem[i][C] = res;
}

public static void main(String[] args) {
    int[] w = {4, 5, 6, 7, 8};
    int[] v = {10, 9, 5, 4, 3};
    int maxV = npc1(w, v, 10);

    System.out.println("count=" + count);
    System.out.println(maxV);
}

结果如下

count=23
19
ii.动态规划
  • 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
  • 空间复杂度: O(n * C)
int npc2(int[] w, int[] v, int C) {
    int n = w.length;
    int[][] mem = new int[n][C + 1];

    for (int k = 0; k <= C; k++) {
        mem[0][k] = (k >= w[0]) ? v[0] : 0;
    }

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= C; j++) {
            mem[i][j] = mem[i - 1][j];
            if (j >= w[i]) {
                mem[i][j] = Math.max(mem[i][j], mem[i - 1][j - w[i]] + v[i]);
            }
        }
    }

    return mem[n - 1][C];
}
iii.动态规划优化一

优化思路:第i行元素只依赖于第i-1行元素,理论上,只需要保持两行元素即可

/// 动态规划改进: 滚动数组
/// 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
/// 空间复杂度: O(C), 实际使用了2*C的额外空间

int npc2(int[] w, int[] v, int C) {
    int n = w.length;
    int[][] mem = new int[n][C + 1];

    for (int k = 0; k <= C; k++) {
        mem[0][k] = (k >= w[0]) ? v[0] : 0;
    }

    for (int i = 1; i < n; i++) {
        for (int j = 0; j <= C; j++) {
            mem[i % 2][j] = mem[(i - 1) % 2][j];
            if (j >= w[i]) {
                mem[i % 2][j] = Math.max(mem[i % 2][j], mem[(i - 1) % 2][j - w[i]] + v[i]);
            }
        }
    }
    return mem[(n - 1) % 2][C];
}

iii.动态规划优化二
/// 动态规划改进
/// 时间复杂度: O(n * C) 其中n为物品个数; C为背包容积
/// 空间复杂度: O(C), 只使用了C的额外空间

int npc3(int[] w, int[] v, int C) {
    int n = w.length;
    int[] mem = new int[C + 1];

    for (int k = 0; k <= C; k++) {
        mem[k] = (k >= w[0]) ? v[0] : 0;
    }

    for (int i = 1; i < n; i++) {
        for (int j = C; j >= w[i]; j--) {
            mem[j] = Math.max(mem[j], mem[j - w[i]] + v[i]);
        }
    }

    return mem[C];
}

3.背包问题更多变种

  • 多重背包问题:每个物品不不⽌止1个,有num(i)个
  • 完全背包问题:每个物品可以⽆无限使⽤用
  • 多维费⽤用背包问题:要考虑物品的体积和重量量两个维度?
  • 物品间加⼊入更更多约束:物品间可以互相排斥;也可以互相依赖

4.最长上升子序列

Longest Increasing Subsequence (LIS)

【Leetcode 300】最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

i.你算法的时间复杂度应该为 O(n2)
ii.进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
int lengthOfLis2(int[] nums) {

    int[] mem = new int[nums.length];
    Arrays.fill(mem, 1);

    for (int i = 1; i < nums.length; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                mem[i] = Math.max(mem[i], mem[j] + 1);
            }
        }
    }

    int len = 1;
    for (int i = 0; i < mem.length; i++) {
        len = Math.max(len, mem[i]);
    }

    return len;
}
iii.这里思考一个问题:在上面的代码中只求解出了上升子序列的长度,那么如何求出具体的上升子序列呢?
List<List<Integer>> listOfLis2(int[] nums) {
    List<List<Integer>> resList = new ArrayList<>();
    int[] mem = new int[nums.length];
    Arrays.fill(mem, 1);

    for (int i = 1; i < nums.length; i++) {
        for (int j = 0; j < i; j++) {
            count2++;
            if (nums[j] < nums[i]) {
                mem[i] = Math.max(mem[i], mem[j] + 1);
            }
        }
    }

    //最长的递增序列的长度
    int len = 1;
    for (int i = 0; i < mem.length; i++) {
        len = Math.max(len, mem[i]);
    }

    //递增序列index列表
    List<Integer> upList = new ArrayList<>();
    for (int i = 0; i < mem.length; i++) {
        if (mem[i] == len) {
            upList.add(i);
        }
    }

    for (Integer up : upList) {
        List<Integer> re = new ArrayList<>();
        int maxLen = mem[up];
        for (int i = up; i >= 0; i--) {
            if (maxLen - mem[i] == 1 || maxLen - mem[i] == 0) {
                re.add(nums[i]);
                maxLen--;
            }
        }
        Collections.reverse(re);
        resList.add(re);
    }

    return resList;
}

5. 最长公共子序列

Longest Common Sequence (LCS):给出两个字符串S1和S2,求这两个字符串的最长公共子序列的长度

LCS( m , n ) S1[0…m] 和 S2[0…n] 的最长公共子序列的长度

S1[m] == S2[n] :

LCS(m,n) = 1 + LCS(m-1,n-1)

S1[m] != S2[n] :

LCS(m,n) = max( LCS(m-1,n) , LCS(m,n-1) )

static int LCS(String s1, String s2) {
    if (s1.length() == 0 || s2.length() == 0) {
        return 0;
    }
    return maxLen(s1, s2, s1.length() - 1, s2.length() - 1);
}

static int maxLen(String s1, String s2, int m, int n) {

    if (m == 0 || n == 0) {
        return 0;
    }

    int res = 0;
    if (s1.charAt(m) == s2.charAt(n)) {
        res = maxLen(s1, s2, m - 1, n - 1) + 1;
    } else {
        res = Math.max(maxLen(s1, s2, m - 1, n), maxLen(s1, s2, m, n - 1));
    }

    return res;
}

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值