子序列问题
-
子序列问题几乎都可以通过dfs进行处理;如果该问题具有无后效性,那么将可以通过动态规划处理,理由如下:
- 子序列问题即对于串(或数组)的元素进行选择,无论是否具有后效性,其本质都可以通过暴力穷举所有选择,所以基于穷举的、自底向上的求解,所以其必然可解该类问题;
- 如果子序列问题具有无后效性,那么对于该问题即可使用记忆化矩阵进行搜索,解空间将由n!变为n^x(x一般为2)
- 对于可记忆化搜索的递归显然可以转化为迭代的动态规划;
- 这里有俩个要点:
- 子序列具有翻转对称性:所以f(x,y) = f(x+1,y+1) === f(x,y) = f(x-1,y-1)
- dp的初值是起始条件而dfs初值是终止条件
- 这里有俩个要点:
-
子序列问题和0-1背包问题几乎一样,所谓的子序列问题可能是:
- 给定一个数组(逻辑数组、数值数组)
- 每个数组的元素只有两种状态,被选入结果集和不选
- 必须在当前状态下就可以确定是否选择进入结果集
- 显著特征
- 可选集是原数组的一部分
- 选择后对可选集有显著影响(即判断条件)
逻辑数组子序列问题
例题
-
逻辑数组:逻辑数组由于和数组数组的最大区别在于其结果集需要初始化(每次选择逻辑数组的一部分结果集都将确定一部分)
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。 --完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。 -- 显然:小于n的平方数是确定的即:1*1~k*k ;其中k是小于根号n的最大正整数;所以其本身是一个逻辑数组; 结果集就是:逻辑数组遍历到的当前元素到n --
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 -- 你可以认为每种硬币的数量是无限的。 -- 显然:同理 --
数值数组子序列问题
-
数值数组:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 --子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 --本题可以通过贪心实现(每次选跳跃最小的)
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。 --连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。 --本题直接遍历即可
-
这类问题共同特点除了符合dp三要素,共同特点是:所有元素只有两种可能:选或者不选,而且选择后对结果集的影响具有显著特征(类似0-1背包):
- 279:选择后影响的唯一特征是
dp[价值]=dp[价值-选择的数]+1
- 300:
dp[价值]=dp[选择的数]+1
- 674:
dp[价值]=dp[前一个]+1
- 279:选择后影响的唯一特征是
for(遍历数组) {
未选择前的初始化
for(遍历可选结果集) {
if(符合选择条件) {
dp[i] = 修改
}
}
}
//279
//一般dp方法
int[] dp = new int[n+1];
for(int i=1;i<dp.length;i++) {
dp[i] = Integer.MAX_VALUE-1;
for(int j=1;j*j<=i;j++) {
dp[i] = Math.min(dp[i],dp[i-j*j]);
}
dp[i]++;
}
return dp[n];
//逻辑数组dp
int[] dp = new int[n+1];
for(int i=1;i<=n;i++) {
dp[i] = Integer.MAX_VALUE-1;
}
for(int i=1,k=1;k<=n;k=i*i) {
for(int j=k;j<=n;j++) {
dp[j] = Math.min(dp[j],dp[j-k]+1);
}
i++;
}
return dp[n];
// Offer II 103(一般dp就省略)
//逻辑数组dp
int[] dp = new int[amount+1];
for(int i=1;i<dp.length;i++) {
dp[i] = Integer.MAX_VALUE-1;
}
for(int i=0;i<nums.length;i++) {
for(int j=nums[i];j<=amount;j++) {
dp[j] = Math.min(dp[j],dp[j-nums[i]]+1);
}
}
return dp[amount]==Integer.MAX_VALUE-1?-1:dp[amount];
//300
int[] dp = new int[nums.length];
int res = 0;
for(int i=0;i<dp.length;i++) {
dp[i] = 0;
for(int j=1;j<=i;j++) {
if(nums[i]>nums[i-j]) {
dp[i] = Math.max(dp[i],dp[i-j]);
}
}
dp[i]++;
res = Math.max(dp[i],res);
}
return res;
//674
int[] dp = new int[nums.length];
int res = 0;
for(int i=0;i<dp.length;i++) {
dp[i] = 0;
//for(int j=i;j<=i;j++) {
if(i>0&&nums[i]>nums[i-1]) {
dp[i] = Math.min(dp[i],dp[i-j*j]);
}
//}
dp[i]++;
res = Math.max(dp[i],res);
}
return res;
字符串子序列问题
双向删除
- 双向:两个字符串
- 删除:可以删除两者中任意一个的任意字符串
特征
- 这种问题和单串子序列问题的区别最大的区别在于:可选集
- 单串子序列问题中,可选集完全取决前面显著条件的影响,换句话说就是:可选集在前面已经直接被确定;或者说遍历到x的时候,已经可以确定是否选择x
- 双向选择:
- 关键在于双选选择,在本轮选择中,不能直接决定后面的选择;;体现必须考虑下一阶段不可选条件下的情况","也就是说在当前选择不可选的时候,必须进行处理以确定最佳值(由于可选集不是数组一部分)
- 解决思路:反向选择使可选集是两数组组合的一部分(变为前面子序列问题):
A[i]->B[j]反向选择为:B[j]->A[i]
;问题变为两个单向选择问题
- 解决思路:反向选择使可选集是两数组组合的一部分(变为前面子序列问题):
- 单串子序列问题,不可选不需要处理,本身已经由前面直接确定(体现在可选集本身就是确定的);
- 关键在于双选选择,在本轮选择中,不能直接决定后面的选择;;体现必须考虑下一阶段不可选条件下的情况","也就是说在当前选择不可选的时候,必须进行处理以确定最佳值(由于可选集不是数组一部分)
例题
-
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足: 1.nums1[i] == nums2[j] 2.且绘制的直线不与任何其他连线(非水平线)相交。 3.端点也不能相交:每个数字只能属于一条连线。 --以这种方法绘制线条,并返回可以绘制的最大连线数。 -- f(x,y)定义为:可选数组为x、y下的最大连线 --
-
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。 --子数组:截取 1<=i<=j<=length中,i~j部分 -- f(x,y)定义为: 可选数组在x、y的最长公共长度 --
-
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 -- 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 --例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 --两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。 -- f(x,y)定义为:可选数组为x、y下的最大公共子序列 --
解题
for(遍历子串1) {
for(遍历子串2) {
if(子串1选择部分==子串2选择部分) {
选择操作
} else {
处理部分
}
}
}
//1035. 不相交的线
int[][] dp = new int[2][nums2.length+1];
for(int i=0;i<nums1.length;i++) {
for(int j=1;j<=nums2.length;j++) {
if(nums1[i]==nums2[j-1]) {
dp[1][j] = dp[0][j-1]+1;
} else {
dp[1][j] = Math.max(dp[0][j],dp[1][j-1]);
}
}
int[] swap = dp[0];
dp[0] = dp[1];
dp[1] = swap;
}
return dp[0][nums2.length];
//1143. 最长公共子序列
char[] nums1 = text1.toCharArray();
char[] nums2 = text2.toCharArray();
int[][] dp = new int[2][nums2.length+1];
for(int i=0;i<nums1.length;i++) {
for(int j=1;j<=nums2.length;j++) {
if(nums1[i]==nums2[j-1]) {
dp[1][j] = dp[0][j-1]+1;
} else {
dp[1][j] = Math.max(dp[0][j],dp[1][j-1]);
}
}
int[] swap = dp[0];
dp[0] = dp[1];
dp[1] = swap;
}
return dp[0][nums2.length];
//718
int[] dp = new int[nums2.length+1];
int res = 0;
for(int i=0;i<nums1.length;i++) {
for(int j=nums2.length;j>0;j--) {
if(nums1[i]==nums2[j-1]) {
dp[j] = dp[j-1]+1;
res = Math.max(res,dp[j]);
} else {
dp[j] = 0;
}
}
}
return res;