zoukankan      html  css  js  c++  java
  • 记录结果再利用的"动态规划"

    2018-09-24 15:01:37

    动态规划(DP: Dynamic Programming)是算法设计方法之一,在程序设计竞赛中经常被选作题材。在此,我们考察一些经典的DP问题,来看看DP究竟是何种类型的算法。

    一、01背包问题

    问题描述:

    有n个重量和价值分别为wi,vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。

    限制条件:

    1<=n<=100

    1<=wi,vi<=100

    1<=W<=10000

    问题求解:

    这是被称为背包问题的一个著名问题。这个问题要如何求解比较好呢?不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索试试看。这个想法的代码如下。

        // 暴力搜索
        int naiveSearch(int i, int restW) {
            if (i == n) return 0; // 已经没有物品了
            if (w[i] <= restW)
                return Math.max(naiveSearch(i + 1, restW), naiveSearch(i + 1, restW - w[i]) + v[i]);
            else return naiveSearch(i + 1, restW);
        }
    

    只不过,这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2^n)的时间,当n比较大的时候就没有办法进行求解了。所以需要怎么办呢?为了优化算法,我们可以发现,在递归调用的过程中,有许多状态被重复计算了,因此,如果我们把第一计算得到的值记录下来,那么第二次就不需要进行不必要的计算了。

        // 暴力搜索 + 记忆化存储
        int naiveSearchPolish(int i, int restW) {
            if (i == n) return dp[i][restW] = 0;
            if (dp[i][restW] >= 0) return dp[i][restW];
            if (w[i] <= restW)
                return dp[i][restW] = Math.max(naiveSearch(i + 1, restW), naiveSearch(i + 1, restW - w[i]) + v[i]);
            else return dp[i][restW] = naiveSearch(i + 1, restW);
        }
    

    这个微小的改进能降低多少时间复杂度呢?对于同样的参数,只会在第一次调用到时需要执行递归部分,第二次之后就可以直接返回。参数组合总共nW种,而函数内只有两次递归,所以只需要O(nW)的复杂度就可以解决这个问题。只需要略微改良,可解的问题规模就可以大幅提高。这种方法一般称为记忆化搜索。

    使用记忆化数组自底向上递推的方法称为动态规划,下面我们就来看一下递推式。

    dp[i + 1][j] : 从0到i总共i + 1个物品中选出总重量不超过j的物品的总价值最大值。

    初始值dp[0][j] = 0。

    dp[i + 1][j] = dp[i][j]    if w[i] > j

                    = max(dp[i][j], dp[i][j - w[i]] + v[i])    others

        // dp
        int dpSolve() {
            int[][] dp = new int[n + 1][W + 1];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j <= W; j++) {
                    if (w[i] > j) dp[i + 1][j] = dp[i][j];
                    else dp[i + 1][j] = Math.max(dp[i][j], dp[i][j - w[i]] + v[i]);
                }
            }
            return dp[n][W];
        }
    

    二、最长公共子序列问题

    问题描述:

    给定两个字符串s,t。求出这两个字符串最长的公共子序列的长度。

    限制条件:

    1<=s.length(),t.length()<=1000

    问题求解:

    经典的动态规划问题,即LCS。定义递推式如下:

    dp[i][j] : s中前i个字符和t中前j个字符的最长公共子序列长度。

    初始值:dp[0][j] = 0, dp[i][0] = 0

    递推式:dp[i + 1][j + 1] = dp[i][j] + 1  if s[i] == t[j]

                  dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1])  others

        public int LCS(String s, String t) {
            if (s.length() == 0 || t.length() == 0) return 0;
            int len1 = s.length();
            int len2 = t.length();
            int[][] dp = new int[len1 + 1][len2 + 1];
            for (int i = 0; i < len1; i++) {
                for (int j = 0; j < len2; j++) {
                    if (s.charAt(i) == t.charAt(j)) dp[i + 1][j + 1] = dp[i][j] + 1;
                    else dp[i + 1][j + 1] = Math.max(dp[i + 1][j], dp[i][j + 1]);
                }
            }
            return dp[len1][len2];
        }
    

    三、完全背包问题

    问题描述:

    有n种重量和价值分别为wi,vi的物品。从这些物品中挑选总重量不超过W的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。

    限制条件:

    1 <= n <= 100

    1 <= wi, vi <= 100

    1 <= W <= 10000

    问题求解:

    经典的动态规划问题,首先我们可以先定义一下相关递推公式的含义。

    dp[i + 1][j] : 挑选前i件物品在背包容量为j的情况下能够达到的总和最大值

    dp[0][j] = 0

    dp[i + 1][j] = max ( dp[i][j - k * w[i]] + k * v[i]) k = 0,1,...,j / w[i]

    显然,这种递推式子需要三重循环进行求解,那么其时间复杂度就是O(n * W ^ 2)。

    一般来说这种递推式都是可以进行简化的,这里介绍一下简化的思路,具体来说就是,建立dp[i + 1][j] 和 dp[i + 1][j - w[i]]之间的联系,显然的,这两者的递推公式很多项都是重合的,因此,我们可以使用dp[i + 1][j - w[i]]来对dp[i + 1][j]进行表示。

    进行优化之后的递推公式为:

    dp[0][j] = 0

    dp[i + 1][j] = max (dp[i][j], d[i + 1][j - w[i]] + v[i])

    这样的话,三层的循环就可以降到二重,因此时间复杂度依然是O(nW)。

        int completeKnapsack(int[] w, int[] v, int W) {
            int n = w.length;
            int[][] dp = new int[n + 1][W + 1];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j <= W; j++) {
                    if (w[i] > j) dp[i + 1][j] = dp[i][j];
                    else dp[i + 1][j] = Math.max(dp[i][j], dp[i + 1][j - w[i]] + v[i]);
                }
            }
            return dp[n][W];
        }
    

    四、01背包问题之2

    问题描述:

    有n个重量和价值分别为wi,vi的物品。从这些物品中挑选总重量不超过W的物品,求所有挑选方案中价值总和的最大值。

    限制条件:

    1 <= n <= 100

    1 <= wi <= 10^7

    1 <= vi <= 100

    1 <= W <= 10 ^ 9

    问题求解:

    乍一看,似乎没有什么不同,但是限制条件其实是有了变化,如果依然使用最初的01背包的模板,那么本题是会TLE的,但是在看vi的值都是非常小的,因此这里我们可以变换一下递推公式的含义。

    dp[i + 1][j] : 挑选前i件物品获得价值j的最小重量

    dp[0][0] = 0

    dp[0][j] = INF

    dp[i + 1][j] = min (dp[i][j], dp[i][j - v[i]] + w[i])

        int extendDp(int[] w, int[] v, int W) {
            int n = w.length;
            int[][] dp = new int[n + 1][100 * 100 + 1];
            for (int i = 1; i < dp[0].length; i++) dp[0][i] = 10000; // 不要使用MAX_VALUE,会爆掉
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < dp[0].length; j++) {
                    if (v[i] > j) dp[i + 1][j] = dp[i][j];
                    else dp[i + 1][j] = Math.min(dp[i][j], dp[i][j - v[i]] + w[i]);
                }
            }
            int res = 0;
            for (int i = 0; i < dp[0].length; i++) if (dp[n][i] <= W) res = i;
            return res;
        }
    

    五、多重部分和问题

    问题描述:

    有n种不同大小的数字ai,每种各mi个。判断是否可以从这些数字中选出若干个使得他们的和恰好为K。

    限制条件:

    1 <= n <= 100

    1 <= ai, mi <= 100000

    1 <= K <= 100000

    问题求解:

    显然的,本题和完全背包有点类似的,唯一的区别就是不再是无限个数的数字可以获得,而是加上了限制,但是递推式还是差不多的嘛。

    dp[i + 1][j] : 取i + 1个数字(也就是取前i个数字)求和得到j的真假。

    dp[0][0] = true

    dp[0][j] = false

    dp[i + 1][j] = dp[i][j] | dp[i][j - a[i]] | ... | dp[i][j - k * a[i]   0 <= k <= min(mi, j / ai)

    这种解法可以看作比较朴素的动态规划的解法,事实上,这里的时间复杂度为O(nKm),理论上是会超时的。

    一般用DP求取bool结果的话,会有不少的浪费,同样的时间复杂度往往能够获得更多的信息。

    在这个问题中,我们不光能够求出能否得到目标的数字,同时还可以把得到时ai剩余的个数给计算出来,这样就可以减少时间复杂度。

    dp[i + 1][j] : 用前i中数字相加和得到j时第i种数字最多能够剩余多少(不能得到j的情况下为 - 1)

    dp[i + 1][j] = mi       if dp[i][j] >= 0

                -1       if j < ai  |  dp[i + 1][j - ai] <= 0

             dp[i + 1][j - ai] - 1        others

        boolean multiSum(int[] a, int[] m, int K) {
            int n = a.length;
            int[] dp = new int[K + 1];
            Arrays.fill(dp, -1);
            dp[0] = 0;
            for (int i = 0; i < n; i++) {
                for (int j = 0; j <= K; j++) {
                    if (dp[j] >= 0) dp[j] = m[i];
                    else if (j < a[i] || dp[j - a[i]] <= 0) dp[j] = -1;
                    else dp[j] = dp[j - a[i]] -1;
                }
            }
            return dp[K] >= 0;
        }
    

    六、最长上升子序列问题

    问题描述:

    有一个长为n的数列,请求出这个序列中最长上升的子序列的长度。上升子序列是指对于任意i < j都满足ai < aj的子序列。

    限制条件:

    1 <= n <= 1000

    0 <= ai <= 1000000

    问题求解:

    经典的动态规划问题,LIS。

    朴素的O(n^2)的解法应该是非常容易想到的,这里就不做讲解了。LIS的最优解法的时间复杂度是O(nlogn)。

    dp[i] : 长度为i + 1的上升子序列中末尾元素的最小值,不存在的话为INF。

    每次寻找i的lowerBound作为插入点,最后寻找INF的lowerBound即可。

        public int lengthOfLIS(int[] nums) {
            int[] dp = new int[nums.length];
            Arrays.fill(dp, Integer.MAX_VALUE);
            for (int i : nums) {
                int idx = lowerBound(dp, i);
                dp[idx] = i;
            }
            return lowerBound(dp, Integer.MAX_VALUE);
        }
    
        private int lowerBound(int[] nums, int k) {
            int lb = -1;
            int ub = nums.length;
            while (ub - lb > 1) {
                int mid = lb + (ub - lb) / 2;
                if (nums[mid] >= k) ub = mid;
                else lb = mid;
            }
            return ub;
        }
    

    七、划分数

    问题描述:

    有n个无区别的物品,将他们划分成不超过m组,求出划分方法数模M的余数。

    限制条件:

    1 <= m <= n < =1000

    2 <= M <= 10000

    问题求解:

    dp[i][j] : j的i划分的总数

    显然i > j的时候,dp[i][j] = dp[i - 1][j]

    i < j的时候,dp[i][j] = dp[i][j - i] + dp[i - 1][j],这个公式的含义是,如果划分结果为i组,如果全部非0,那么就等于dp[i][j - i]的划分数个数,如果有为0的情况,那么就可以使用dp[i - 1][j]来计算。

        int partitionNums(int n, int m) {
            int[][] dp = new int[m + 1][n + 1];
            dp[0][0] = 1;
            for (int i = 1; i <= m; i++) {
                for (int j = 0; j <= n; j++) {
                    if (i > j) dp[i][j] = dp[i - 1][j];
                    else dp[i][j] = dp[i - 1][j] + dp[i][j - i];
                }
            }
            return dp[m][n];
        }
    

    八、多重集组合数

    问题描述:

    有n种物品,第i中物品有ai个。不同种类的物品可以相互区分但相同种类的无法区分。从这些物品中取出m个话,有多少种取法?求出方案数模M的余数。

    限制条件:

    1 <= n <= 1000

    1 <= m <= 1000

    1 <= ai <= 1000

    2 <= M <= 10000

    问题求解:

    dp[i + 1][j] : 从前i种物品中取出j个的组合总数

    dp[i + 1][j] = sum(dp[i][j - k])   0 <= k <= min(ai, j)

    和完全背包类似,这里也可以尝试建立dp[i + 1][j]和dp[i + 1][j - 1]之间的联系,将递推公式进行化简得到:

    dp[i + 1][j] = dp[i + 1][j - 1] + dp[i][j] - dp[i][j - 1 - ai]

    这样复杂度就降到O(nm)了。

        int dpSolve(int[] a, int m, int mod) {
            int n = a.length;
            int[][] dp = new int[n + 1][m + 1];
            Arrays.fill(dp[0], 0);
            for (int i = 0; i <= n; i++) dp[i][0] = 1;
            for (int i = 0; i < n; i++) {
                for (int j = 1; j <= m; j++) {
                    // 有取余的情况下,要避免减法运算的结果为负数
                    if (j - 1 - a[i] >= 0) {
                        dp[i + 1][j] = (dp[i + 1][j - 1] + dp[i][j] - dp[i][j - 1 - a[i]] + mod) % mod;
                    }
                    else {
                        dp[i + 1][j] = (dp[i + 1][j - 1] + dp[i][j]) % mod;
                    }
                }
            }
            return dp[n][m];
        }
    
  • 相关阅读:
    (Java) LeetCode 44. Wildcard Matching —— 通配符匹配
    (Java) LeetCode 30. Substring with Concatenation of All Words —— 与所有单词相关联的字串
    (Java) LeetCode 515. Find Largest Value in Each Tree Row —— 在每个树行中找最大值
    (Java) LeetCode 433. Minimum Genetic Mutation —— 最小基因变化
    (Java) LeetCode 413. Arithmetic Slices —— 等差数列划分
    (Java) LeetCode 289. Game of Life —— 生命游戏
    (Java) LeetCode 337. House Robber III —— 打家劫舍 III
    (Java) LeetCode 213. House Robber II —— 打家劫舍 II
    (Java) LeetCode 198. House Robber —— 打家劫舍
    (Java) LeetCode 152. Maximum Product Subarray —— 乘积最大子序列
  • 原文地址:https://www.cnblogs.com/hyserendipity/p/9695557.html
Copyright © 2011-2022 走看看