记录结果再利用的"动态规划"

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

猜你喜欢

转载自www.cnblogs.com/TIMHY/p/9695557.html