已知一个序列 {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 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]
更新的过程如下:
设置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背包问题,是由于对于每个物体而言只有选择和不选择两种情况。
题目
有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]);
初始化问题:
题目:
划分数组为和相等的两部分
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];
}