LeteCode刷题|动态规划(二分法、贪心、双指针)|#392判断子序列

LeteCode刷题|动态规划(二分法、贪心、双指针)|#392判断子序列

1、题目

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1:
s = “abc”, t = “ahbgdc”

返回 true.

示例 2:
s = “axc”, t = “ahbgdc”

返回 false.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/is-subsequence

2、所用算法介绍

1)、介绍

所用算法详解见以下链接
各查找算法Java实现
Java常用算法

  1. 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解
    的处理算法
  2. 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这
    些子问题的解得到原问题的解。
  3. 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子
    阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
  4. 动态规划可以通过填表的方式来逐步推进,得到最优解.

2)、适用情况

能采用动态规划求解的问题的一般要具有3个性质:

(1)最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

3)、基本步骤

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

​ 图1 动态规划决策过程示意图

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。

(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

(1)分析最优解的性质,并刻画其结构特征。

(2)递归的定义最优解。

(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

(4)根据计算最优值时得到的信息,构造问题的最优解

3、动态规划法代码实现

class Solution {
    public boolean isSubsequence(String s, String t) {
         //将两个字符串都转为字符数组
        char[] chars01 = s.toCharArray();
        char[] chars02 = t.toCharArray();
        
        int n = chars01.length;
        int m = chars02.length;
        
        if (n == 0 ) return true;
        if (m == 0 ) return false;

        int temp = 0;
        int[][] dp = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = temp; j < m; j++) {
                if (chars01[i] != chars02[j]) continue;
                for (int k = j; k < m; k++) {
                    if ( i - 1 < 0){
                        dp[0][k] = 1;
                    }
                    else if (dp[i-1][j]==1){
                        dp[i][k] = 1;
                    }
                }
                temp = j + 1;
                break;
            }

        }
        return dp[n-1][m-1] == 1;
    }
}

4、其他算法实现

1)二分法

class Solution {
     List<Integer>[] tl = new ArrayList[26];
    public boolean isSubsequence(String s, String t) {
        for(int i = 0; i < 26; i++) {
            tl[i] = new ArrayList<Integer>();
        }
        for(int i = 0; i < t.length(); i++) {
            tl[t.charAt(i) - 'a'].add(i);
        }
        return check(s);
    }
    public boolean check(String s) {
        int c = -1;
        for(int i = 0; i < s.length(); i++) {
            c = bs(tl[s.charAt(i) - 'a'], c);
            if(c == -1) {
                return false;
            }
        }
        return true;
    }
    public int bs(List<Integer> tl, int c) {
        int head = 0, tail = tl.size() - 1, mid = (head + tail) / 2;
        if(tail == -1 || tl.get(tail) <= c) {
            return -1;
        }
        while(head < tail) {
            if(tl.get(mid) > c) {
                tail = mid;
            } else if(tl.get(mid) < c) {
                head = mid;
                if(tl.get(mid + 1) > c) {
                    return tl.get(mid + 1);
                }
            } else {
                return tl.get(mid + 1);
            }
            mid = (head + tail) / 2;
        }
        return tl.get(tail);
    }
}

2)贪心算法

从s中依次取出一个字符,到t中去查找,记录出现的位置;
s中取出下一个字符,从上次出现位置的下一个开始查找,
直到s中的字符全部扫描完成

class Solution {
   public boolean isSubsequence(String s, String t) {
    if(s==null) return true;
    if(t==null) return false;
    //将两个字符串都转为字符数组
    char[] chars01 = s.toCharArray();
    char[] chars02 = t.toCharArray();

    int index=-1;
    //从t查找的索引初始值为0,search函数返回更新index值
    //因为后面需要+1,所以这里index取-1
    //每次从上次查询返回索引值的下一个位置开始
    for(int i=0;i<chars01.length;i++){
      char target=chars01[i];

      if(index==-2){
        return false;
      }else{
        index=search(chars02,index+1,target);
      }
    }
    if(index==-2){//这里加if是为了特殊情况,s="a",t="b",search方法之后返回-2,上一个else中index=-2之后,for循环直接结束了,没来得及判断if(index==-2)
      return false;
    }else {
      return true;
    }
  }

  //因为是无序数组,所以采用线性查找算法 index是查找的起点
  public int search(char[] arr,int index,char target){
    for(int i=index;i<arr.length;i++){
      if(arr[i]==target){
        return i;
      }
    }
    return -2;//如果循环结束还没有返回索引值,说明没有返回-1
  }
}

可以继续改进:

遍历扫描chars02的时候,不必遍历到最后。如当我们搜索chars01=“abcde”中的c时,就可以只搜寻到chars02的倒数第三个数,只要我们在每次search之后并返回值不是-2的时候记录此时chars01已被搜寻到的字符串个数k,并将k传入到search方法中,然后search方法中for循环的中值条件就可以改为i<arr.length-(chars.length-k)

3)双指针法

  • 可以知道的是,当扫描s中的第k个字符时,假如它在t字符串中的第i位和第j位都出现过(i < j),那么我们从左到右扫描到第i位时,就认为已经找到了s中第k个字符。因为i后面有更多的备选字符可以用来找s中的剩余字符。也就是说,我们在t中找字符时,是严格不回溯的。这个问题可以使用双指针解决。

  • 初始化指针i,j为0,分别指向s和j的第0个字符,在t中找到s[i]字符后,i++试图找下一个字符。

  • 若最后i到达s末尾,则说明找到了该字符串

这个算法比上一种算法时间要长,因为,即使s中间已经有字符不在t存在了,依然没有停止遍历寻找。因为他是通过判断最终能够匹配的个数是否等于s长度。

class Solution {
    public boolean isSubsequence(String s, String t) {
        int i=0;
        int j=0;

        char[] chars01 = s.toCharArray();
        char[] chars02 = t.toCharArray();

        while(i<chars01.length&&j<chars02.length){
            if(chars01[i]==chars02[j]){
                //如果相等则两个指针都向后移
                i++;
                j++;
            }else{
                //如果不相等,则移动chars02的指针,直到找到或遍历完还没有找到
                j++;
            }
        }
          //确认chars01最终有没有遍历到结尾
           return i==chars01.length;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值