公共子序列问题系列
leetcode 1143 最长公共子序列
题目:
输入两个字符串s1和s2,找出最长公共子序列,返回这个子序列的长度。
函数签名如下:
int longestCommonSubsequence(String s1, String s2);
子序列的问题一般都是两个指针移动求解。如下,我们设置两个指针i与j
那么假设数组A长度为m,数组B长度为n,那么我们会有dp[i][j],{0<=i<m,0<=j<n 。}表示A[i->m]以及B[j->n]之间的最长子序列的长度。
那么就是填表环节。我们要填一个mxn的表。我们移动的一个元素是数组的每一个节点,那么我们就使用节点来考虑这个问题。
现在我们考虑节点A [i] 与 B[j]。
(1)如果这两个值是相等的话。
两个节点都相等了。那么肯定在子序列中吧,指针可以都往右边移动了,然后加个1.
dp[i][j] = dp[i+1][j+1] + 1
(2)如果这俩节点不相等呢?
那就有可能存在一个是最长公共子序列中,又或者两个都不是。(改用excel画图好了,大概就这么个意思)
那么dp[i]j[]就可能是
dp[i+1][j] 和 dp[i][j+1] 以及dp[i+1][j+1]中最大的那个。
好的,那么我们就可以写代码了。
首先我们填写的表dp是一个mXn的表格,那么我们做出如下的定义
先定义一个dp方法,dp方法的值是memo
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] c1 = text1.toCharArray();
char[] c2 = text2.toCharArray();
//定义方法的值memo,也就是备忘录,备忘录初始化值为-1
int[][] memo = new int[c1.length][c2.length];
for (int[] row : memo){
Arrays.fill(row,-1);
}
return dp(c1,0,c2,0,memo);
}
public int dp(char[] c1,int i,char[] c2,int j,int[][] memo){
//管你三七二十一,dp函数就是要返回memo的值,返回去就对了,沉迷于细节你就死了。
if(memo[i][j]!=-1){
return memo[i][j];
}
return memo[i][j];
}
}
把我们上面的推论放上去,其实这里可以不用转换为char数组,字符串完成够用。只是为了思路好理解,放了而已。而且memo其实是可以不用放在dp里面的,只是个人执念而已。。
int[][] memo ;
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
memo = new int[text1.length()][txte2.length()];
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
char[] c1 = text1.toCharArray();
char[] c2 = text2.toCharArray();
int[][] memo = new int[c1.length][c2.length];
for (int[] row : memo){
Arrays.fill(row,-1);
}
return dp(c1,0,c2,0,memo);
}
public int dp(char[] c1,int i,char[] c2,int j,int[][] memo){
//终止条件为i或者j跑到空的地方
if(i==c1.length | j==c2.length){
return 0;
}
if(memo[i][j]!=-1){
return memo[i][j];
}
//dp的判断
if(c1[i] == c2[j]){
memo[i][j] = dp(c1,i+1,c2,j+1,memo)+1;
}else{
memo[i][j] = Math.max(dp(c1,i+1,c2,j,memo) , dp(c1,i,c2,j+1,memo));
memo[i][j] = Math.max(memo[i][j] , dp(c1,i+1,c2,j+1,memo));
}
return memo[i][j];
}
}
leetcode712 最小 ASCII 删除和
这道题很明显还是求两个字符串最长的公共子序列。
这时候我们的dp函数变了。dp[m][n] = 最小的ascii。
不管三七二十一,先写上去。dp的值=memo,那么,定义memo和dp函数,在dp函数中返回memo
class Solution {
public int minimumDeleteSum(String s1, String s2) {
//定义memo
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
int[][] memo = new int[c1.length][c2.length];
for (int[] row : memo){
Arrays.fill(row,-1);
}
return dp(c1,0,c2,0,memo);
}
public int dp(char[] c1,int i,char[] c2,int j,int[][] memo){
//管你三七二十一返回memo
if(memo[i][j]!=-1){
return memo[i][j];
}
}
}
然后思考dp函数
这时候我们的每个单元是char[i] 和char[j]来比较
(1)相等的时候
很好,这时候不用删除,那么dp[i][j] = dp[i+1][j+1]
(2)不相等的时候
dp[i][j] = {
a.删除c1[i]的时候 c1[i]+dp[i+1][j] 这时候c2[j]
b.删除c2[j]的时候 c2[j] + dp[i][j+1]
c.都删的时候,dp[i+1][j+1]+c1[i]+c2[j]
那么base case确定一下
当i = c1.length的时候,也就是A是空串,那么B都得删光
同理B是空串也就是j=c2.lenngth的时候,A都得删光。那么代码如下
class Solution {
public int minimumDeleteSum(String s1, String s2) {
char[] c1 = s1.toCharArray();
char[] c2 = s2.toCharArray();
int[][] memo = new int[c1.length][c2.length];
for (int[] row : memo){
Arrays.fill(row,-1);
}
return dp(c1,0,c2,0,memo);
}
public int dp(char[] c1,int i,char[] c2,int j,int[][] memo){
//终止条件为i或者j跑到空的地方,另一个字符串的都要删掉
int res = 0;
if(i==c1.length){ //删光c2
for(int k=j;k<c2.length;k++){
res += c2[k];
}
return res;
}
if(j==c2.length){ //删光c1
for(int k=i;k<c1.length;k++){
res += c1[k];
}
return res;
}
if(memo[i][j]!=-1){
return memo[i][j];
}
//dp的判断
if(c1[i] == c2[j]){
//相等不用删除
memo[i][j] = dp(c1,i+1,c2,j+1,memo);
}else{
//这里要求最小值,不相等的时候加上删除的部分
memo[i][j] = Math.min(dp(c1,i+1,c2,j,memo)+c1[i] , dp(c1,i,c2,j+1,memo)+c2[j]);
memo[i][j] = Math.min(memo[i][j] , dp(c1,i+1,c2,j+1,memo)+c1[i]+c2[j]);
}
return memo[i][j];
}
}
leetcode 583 两个字符串的删除操作
题目:
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
int minDistance(String s1, String s2);
解法:
那么说白了就还是求公共子序列。那么我们使用word1.length() + word2.length() - 2*ength(公共子序列)即可。那么复用一下leetcode 1143的代码
class Solution {
public int minDistance(String word1, String word2) {
char[] c1 = word1.toCharArray();
char[] c2 = word2.toCharArray();
int[][] memo = new int[c1.length][c2.length];
for (int[] row : memo){
Arrays.fill(row,-1);
}
//-----其实就改了这行
return word1.length() + word2.length() - dp(c1,0,c2,0,memo)*2;
}
public int dp(char[] c1,int i,char[] c2,int j,int[][] memo){
if(i==c1.length | j==c2.length){
return 0;
}
if(memo[i][j]!=-1){
return memo[i][j];
}
if(c1[i] == c2[j]){
memo[i][j] = dp(c1,i+1,c2,j+1,memo)+1;
}else{
memo[i][j] = Math.max(dp(c1,i+1,c2,j,memo) , dp(c1,i,c2,j+1,memo));
memo[i][j] = Math.max(memo[i][j] , dp(c1,i+1,c2,j+1,memo));
}
return memo[i][j];
}
}
leetcode 300 最长递增子序列
题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
解法:
首先不管三七二十一,要填表吧?这一个数组那么一个就填一行,那么就是一个一维数组。
定义dp[i] = 以i开头的数组nums最长递增子序列的长度
然后dp函数的值存到memo中。
那么
class Solution {
int[] memo;
public int lengthOfLIS(int[] nums) {
//定义memo
memo = new int[nums.length];
Arrays.fill(memo,-1);
return dp(nums,0);
}
//dp函数返回memo
public int dp(int[] nums,int i){
if(memo!=-1){
return memo[i];
}
return memo[i];
}
}
定义完之后dp[i]要开始设计了,粒度是啥?数组的一个节点。
我是nums[i],现在轮询到我这儿了,我该做点啥呢?由于我们的dp定义是从i开始,那么我们和后面的比较
我先比较我面那个数字nums[i+1]呗,如果我比它大,那么dp[i+1]=dp[i]+1,不然的话dp[i] = dp[i+1]。。等会怎么感觉好像不对。。?因为被比较的两个值并不一定会在子序列中!
那么我们dp[i]重新定义一下(==),以nums[i]为结尾的最长递增子序列。这样我们可以比较了.上面那个函数重新定义一下。。
class Solution {
int[] memo;
int max = Integer.MIN_VALUE;//求最大,放最小。
public int lengthOfLIS(int[] nums) {
memo = new int[nums.length];
Arrays.fill(memo,1);
dp(nums,nums.length-1);
for(int num:memo){
System.out.print(" "+num);// 错误:1 1 1 2 2 3 1 4
} // 1 1 1 2 2 3 4 4
for(int n:memo){
max = max>n?max:n;
}
return max;
}
public int dp(int[] nums,int i){
if(i==0){
memo[i]=1;
}
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
memo[i] = Math.max(dp(nums,j) + 1,memo[i]);
}
}
return memo[i];
}
}
===============
输入
[1,3,6,7,9,4,10,5,6]
输出
5
预期结果
6
stdout
1 2 1 1 1 3 1 4 5
==============
class Solution {
int[] memo;
int max = Integer.MIN_VALUE;//求最大,放最小。
public int lengthOfLIS(int[] nums) {
memo = new int[nums.length];
Arrays.fill(memo,1);
dp(nums,nums.length-1);
for(int num:memo){
System.out.print(" "+num);// 错误:1 1 1 2 2 3 1 4
} // 正确:1 1 1 2 2 3 4 4
for(int n:memo){
max = max>n?max:n;
}
return max;
}
public int dp(int[] nums,int i){
if(i==0){
memo[i]=1;
}
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
memo[i] = Math.max(dp(nums,j) + 1,memo[i]);
}
}
return memo[i];
}
}
=================
输入
[1,3,6,7,9,4,10,5,6]
输出
6
预期结果
6
stdout
1 2 3 4 5 3 6 4 5
=================
============
终于正确的一个示范
============
class Solution {
int[] memo;
int max = Integer.MIN_VALUE;//求最大,放最小。
public int lengthOfLIS(int[] nums) {
memo = new int[nums.length];
Arrays.fill(memo,-1);
for(int i=0;i<nums.length;i++){
dp(nums,i);
}
for(int num:memo){
System.out.print(" "+num);
}
for(int n:memo){
max = max>n?max:n;
}
return max;
}
public int dp(int[] nums,int i){
if(i==0){
memo[i] = 1;
}
if(memo[i]!=-1){
return memo[i];
}
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
memo[i] = Math.max(dp(nums,j) + 1,memo[i]);
}
}
if(memo[i] ==-1){
memo[i] =1;
}
return memo[i];
}
}
好的,粒度不变我们还是一个数组的一个元素,我要干什么?我就和前面那个比较吧
(1)如果我大于nums[i-1]
dp[i] = dp[i-1] +1 我比我前面那个大嘛,那么以它结尾的递增子序列可以再加上我。
(2)不然的话
dp[i] = 1 以我结尾的就我自己好了
那么最长的应该是把dp数组遍历一遍,取个最大值 .这样有没有问题?有。
比如说
4肯定比5小,可是4可以和1在一块。谁告诉你dp[3] =1?。正确的思路把前面的数组中的比nums[i]的下标的每个dp都遍历一遍,咱门接上去取最大值就完事儿了
以下错误示范。
class Solution {
int[] memo;
int max = Integer.MIN_VALUE;//求最大,放最小。
public int lengthOfLIS(int[] nums) {
memo = new int[nums.length];
Arrays.fill(memo,1);
dp(nums,nums.length-1);
for(int num:memo){
System.out.print(" "+num);// 错误:1 1 1 2 2 3 1 4
} // 正确:1 1 1 2 2 3 4 4
for(int n:memo){
max = max>n?max:n;
}
return max;
}
public int dp(int[] nums,int i){
if(i==0){
memo[i]=1;
}
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
memo[i] = Math.max(dp(nums,j) + 1,memo[i]);
}
}
return memo[i];
}
}