LeetCode题解之动态规划

最长子序列

已知一个序列 {S1, S2,…,Sn},取出若干数组成新的序列 {Si1, Si2,…, Sim},其中 i1、i2 … im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 子序列 。如果在子序列中,当下标 ix > iy 时,Six > Siy,称子序列为原序列的一个 递增子序列

最长递增子序列

定义一个数组 dp 存储最长递增子序列的长度,dp[n] 表示以 Sn 结尾的序列的最长递增子序列长度。对于一个递增子序列 {Si1, Si2,…,Sim},如果 im < n 并且 Sim < Sn,此时 {Si1, Si2,…, Sim, Sn} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中,长度最长的那个递增子序列就是要找的,在长度最长的递增子序列上加上 Sn 就构成了以 Sn 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | Si < Sn && i < n} 。

因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {Sn} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1,即:对于一个长度为 N 的序列,最长递增子序列并不一定会以 SN 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,max{ dp[i] | 1 <= i <= N} 即为所求。

最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4 解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

public static int lengthOfLIS(int[] nums) {
        if(nums==null||nums.length==0){
            return 0;
        }
        int n=nums.length;
        //保存以坐标i上的数结尾的最长递增子序列长度  所以需要跟nums[i]进行比较
        int []dp=new int[n];
        for(int i=0;i<n;i++){
            int maxLen=1;//每次遍历重置最大长度
            for(int j=0;j<i;j++){
                if(nums[j]<nums[i]){
                    maxLen=Math.max(maxLen,dp[j]+1);//符合要求
                }
            }
            dp[i]=maxLen;
        }
        int maxL=0;
        for(int num:dp){//因为不能保证以最后一个数字结尾的最长子字符串长度最大 所以要遍历找最大
            maxL=Math.max(maxL,num);
        }
        return maxL;
    }

降低时间复杂度
定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素。对于一个元素 x,

  • 如果它大于 tails 数组所有的值,那么把它添加到 tails 后面,表示最长递增子序列长度加 1;
  • 如果 tails[i-1] < x <= tails[i],那么更新 tails[i] = x。
    例如对于数组[4,3,5,6]:
tails      len      num
[]         0        4
[4]        1        3
[3]        1        6
[3,6]      2        5
[3,5]      2        null
public int lengthOfLIS(int[] nums) {
    int n = nums.length;
    int[] tails = new int[n];
    int len = 0;
    for (int num : nums) {
        int index = binarySearch(tails, len, num);
        tails[index] = num;
        if (index == len) {
            len++;
        }
    }
    return len;
}
private int binarySearch(int[] tails, int len, int key) {
    int l = 0, h = len;
    while (l < h) {
        int mid = l + (h - l) / 2;
        if (tails[mid] == key) {
            return mid;
        } else if (tails[mid] > key) {
            h = mid;
        } else {
            l = mid + 1;
        }
    }
    return l;
}

一组整数对能够构成的最长链

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。给定一个对数集合,找出能够形成的最长数对链的长度。

输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]
public static int findLongestChain(int[][] pairs) {
        if(pairs==null||pairs.length==0){
            return 0;
        }
        Arrays.sort(pairs, new Comparator<int[]>() {//根据二维数组的第一个元素进行排序
            @Override
            public int compare(int[] o1, int[] o2) {
                return o1[0]-o2[0];
            }
        });
        int []dp=new int[pairs.length];
        for(int i=0;i<pairs.length;i++){
            int maxLen=1;
            for(int j=0;j<i;j++){
                if(pairs[j][1]<pairs[i][0]){
                    maxLen=Math.max(maxLen,dp[j]+1);
                }
            }
            dp[i]=maxLen;
        }
        int maxL=0;
        for(int num:dp){
            maxL=Math.max(maxL,num);
        }
        return maxL;
    }

最长摆动子序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个为[1,17,10,13,10,16,8]

输入: [1,7,4,9,2,5]
输出: 6 

分析:
数组中的任何元素都对应下面三种可能状态中的一种:
上升的位置,意味着 nums[i] > nums[i - 1]
下降的位置,意味着 nums[i] < nums[i - 1]
相同的位置,意味着 nums[i] == nums[i - 1]
更新的过程如下:

  • 如果 nums[i] > nums[i-1],意味着这里在i处摆动上升,使用前一个位置下降的最大长度。所以 up[i] =down[i-1] + 1, down[i] 与 down[i−1] 保持相同
  • 如果 nums[i] < nums[i-1],意味着这里在摆动下降,使用前一个位置上升的最大长度。所以 down[i] = up[i-1] + 1,up[i]与 up[i−1] 保持不变
  • 如果 nums[i] == nums[i-1],意味着这个元素不会改变任何东西因为它没有摆动。所以 down[i] 与 up[i]与down[i−1] 和 up[i−1] 都分别保持不变。
  • 最后,将 up[length-1]和 down[length−1] 中的较大值作为问题的答案

设置up 和down等于1是因为默认第一个元素是up或down点 假如它的后一个元素比他大 那么截止到该元素的up必然等于1+1=2 down同理

public class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length < 2)
            return nums.length;
        int[] up = new int[nums.length];
        int[] down = new int[nums.length];
        up[0] = down[0] = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                up[i] = down[i - 1] + 1;
                down[i] = down[i - 1];
            } else if (nums[i] < nums[i - 1]) {
                down[i] = up[i - 1] + 1;
                up[i] = up[i - 1];
            } else {
                down[i] = down[i - 1];
                up[i] = up[i - 1];
            }
        }
        return Math.max(down[nums.length - 1], up[nums.length - 1]);
    }
}

空间优化的动态规划
public static int wiggleMaxLength(int[] nums) {
        if(nums==null||nums.length==0){
            return 0;
        }
        int n=nums.length;
        if(n<=2){
            return n;
        }
        int  up=1,down=1;
        for(int i=1;i<n;i++){
            if(nums[i-1]<nums[i]){
                up=down+1;
            }else{
                down=up+1;
            }
        }
        return Math.max(up,down);
    }

最长公共子序列

在这里插入图片描述
在这里插入图片描述
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。若这两个字符串没有公共子序列,则返回 0。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3
public int longestCommonSubsequence(String text1, String text2) {
        int len1=text1.length();
        int len2=text2.length();
        int [][]dp=new int[len1+1][len2+1];
        for(int i=1;i<=len1;i++){
            for(int j=1;j<=len2;j++){
                if(text1.charAt(i-1)==text2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
                }
            }
        }
        return dp[len1][len2];
    }

0-1背包问题

之所以叫做0-1背包问题,是由于对于每个物体而言只有选择和不选择两种情况。
题目
有N件物品和一个容量为V的背包。第i件物品的费用是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。

基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][j]表示前i件物品恰放入一个容量为j的背包可以获得的最大价值。则其状态转移方程便是:

f[i][j]=max(f[i−1][j],f[i−1][j−w[i]]+v[i])

优化问题:

for (int i = 1; i <= n; i++)
    for (int j = V; j >= 0; j--)
        f[j] = max(f[j], f[j - w[i]] + v[i]);
        或
​for (int i = 1; i <= n; i++)
   for (int j = V; j >= w[i]; j--)
       f[j] = max(f[j], f[j - w[i]] + v[i]);

初始化问题:

  • 如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1…V]均设为−∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
  • 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0…V]全部设为0。

题目:
划分数组为和相等的两部分

Input: [1, 5, 11, 5]
Output: true
Explanation: The array can be partitioned as [1, 5, 5] and [11].
将数组划分为相同的部分 等价于 像一个背包中放数,保证所放的数和是所有数之和的一半
首先获取这个限制性的和,如果是奇数,那么题目无法满足
其次定义一个数组  dp[i]表示原数组是否可以取出若干个数字,其和为i
进行数组遍历,需要注意数组中的数只有小于目标时才有可能被添加进来
同时如果 dp[i - num]true的话,说明现在已经可以组成 i-num 这个数字了,再加上num,就可以组成数字i了,那么dp[j]就一定为true。
如果之前dp[j]已经为true了,当然还要保持true,所以还要‘或’上自身
最后返回dp[target] 判断能否组成这个数字即可
public static boolean canPartition(int[] nums){
        int sum = computeArraySum(nums);
        if(sum%2!=0){
            return false;
        }
        int target=sum/2;
        boolean [] dp=new boolean[target+1];
        dp[0]=true;
        for(int num:nums){
            for(int i=target;i>=1;i--){
                if(i>=num){
                    dp[i]=dp[i]||dp[i-num];
                }
            }
        }
        return dp[target];
    }
    private static int computeArraySum(int [] nums){
        int sum=0;
        for (int num : nums) {
            sum=sum+num;
        }
        return sum;
    }

目标和问题

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释: 
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3

分析:
该问题可以转换为 Subset Sum 问题,从而使用 0-1 背包的方法来求解。可以将这组数看成两部分,P 和 N,其中 P 使用正号,N 使用负号,有以下推导:

                  sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
                       2 * sum(P) = target + sum(nums)

因此只要找到一个子集,令它们都取正号,并且和等于 (target + sum(nums))/2,就证明存在解。
dp[i]表示在数组中选取正负数,最后之和等于i的排列方式个数

public static int findTargetSumWays(int[] nums, int S){
        int sum=computeArraySum(nums);
        if(sum<S||(S+sum)%2!=0){
            return 0;
        }
        int target=(sum+S)/2;
        int []dp=new int[target+1];
        dp[0]=1;
        for(int num:nums){
            for(int i=target;i>=0;i--){
                if(i>=num){
                    dp[i]=dp[i]+dp[i-num];
                }
            }
        }
        return dp[target];
    }
    private static int computeArraySum(int [] nums){
        int sum=0;
        for (int num : nums) {
            sum=sum+num;
        }
        return sum;
    }

一和零

现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

Input: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
Output: 4
Explanation: There are totally 4 strings can be formed by the using of 5 0s and 3 1s, which are "10","0001","1","0"

分析:
这是一个多维费用的 0-1 背包问题,有两个背包大小,0 的数量和 1 的数量。
dp[i][j]是使用i个0 和j个i能组成的字符串数量的最大值

public static int findMaxForm(String[] strs, int m, int n){
        if(strs==null||strs.length==0){
            return 0;
        }
        int [][]dp=new int[m+1][n+1];
        for(String str:strs){//逐步遍历字符串数组
            int ones=0,zeros=0;
            char []chars=str.toCharArray();
            //先获取资源数 也就是0和1的个数
            for (char aChar : chars) {
                if(aChar=='0'){
                    zeros++;
                }else{
                    ones++;
                }
            }
            for(int i=m;i>=0;i--){
                for(int j=n;j>=0;j--){
                    if(i>=zeros&&j>=ones){
                        dp[i][j]=Math.max(dp[i][j],dp[i-zeros][j-ones]+1);
                    }
                }
            }
        }
        return dp[m][n];
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值