560. 和为 K 的子数组(前缀和计数map)

官方题解:https://leetcode.cn/problems/subarray-sum-equals-k/solution/he-wei-kde-zi-shu-zu-by-leetcode-solution/

假设 left 到 right 下标的子数组和为 k
nums[left...right] = k
preSum[right] - preSum[left] = k
preSum[left] = preSum[right] - k
所以每次到 right 的时候,找到等于 preSum[right] - k 的 preSum[left] 有多少个
用一个 map 来记录,前缀和的 count (见官方题解动画)
key: 前缀和的值
value: 前缀和为这个值的个数

注意!!!:

  • map 要放入一个初始值 {0,1}
  • 一定要先 getPreSumCount ,再把加上当前节点的 PreSum 放进 map +1 (因为当前节点的和不属于当前节点的前缀和)
 

 

class Solution {
    public int subarraySum(int[] nums, int k) {
        Map<Integer, Integer> leftPreSum2CntMap = new HashMap();
        
        leftPreSum2CntMap.put(0, 1);
        int rightPreSum = 0;
        // 总的满足条件的子数组个数的计数,最后的结果
        int count = 0;
        for (int right=0;right<nums.length;right++) {
            rightPreSum += nums[right];
            // [...i]
            // left 到 right 的子数组和为 k
            // nums[left...right] = k
            // preSum[right] - preSum[left] = k
            // preSum[left] = preSum[right] - k
            // 所以每次到 right 的时候,找到等于 preSum[right] - k 的 preSum[left] 有多少个
            Integer leftPreSumCnt = leftPreSum2CntMap.get(rightPreSum - k);
            if (leftPreSumCnt != null) {
                count += leftPreSumCnt;
            }

            // 将这次的和放到 map 里
            Integer rightPreSumCnt = leftPreSum2CntMap.get(rightPreSum);
            if (rightPreSumCnt == null) {
                rightPreSumCnt = 0;
            }
            leftPreSum2CntMap.put(rightPreSum, ++rightPreSumCnt);
        }
        return count;
    }
}

 

53. 最大子数组和(动态规划)

dp[i] 代表表示以 nums[i] 结尾 的 连续 子数组的最大和。 
要么把 nums[i] 附加到前一个连续最大和的后面,要么从 nums[i] 重新开始   
  • dp[i]=max{nums[i],dp[i1]+nums[i]} 
  • dp[0]= nums[0]
class Solution {
    public int maxSubArray(int[] nums) {
        int result = nums[0];
        int dp[] = new int[nums.length];
        dp[0] =  nums[0];
        for(int i=1;i<nums.length;i++){
           //递推公式
           dp[i] = Math.max(dp[i-1]+nums[i],nums[i]);
           //result用来记dp数组里的最大数值
           if(dp[i]>result)
                result = dp[i];
        }
        return result;
    }
}

 

128. 最长连续序列(全放set里 num-1 不存在则一直往后)

两种最朴素的解法之一:

  1. 先排序,从前往后找最长连续上升序列即可。该思路简单有效,但是复杂度已经至少有 O(nlogn)。实现起来也比较简单,在此不讨论该解法。
  2. 遍历数组中的每个元素num,然后以num为起点,每次+1向后遍历num+1,num+2,num+3...,判断这些元素是否存在于数组中。假设找到的最大的连续存在的元素为num+x,那么这个连续序列的长度即为x+1。最后将每个num所开始序列长度取个最大值即可。

解题思路1:哈希集合
方法 2 不用想,是肯定超时的。它的最坏时间复杂度已经达到了 O(n^3)
我们需要优化代码。优化的点主要有两个:

  1. 判断num+1,num+2,num+3...是否在数组中。上面的代码是用直接遍历的方式去查找的,时间复杂度为 O(n) 。我们可以改为哈希表查找,时间复杂度为 O(1)
  2. 遍历数组中每个元素num。逐一遍历每个元素会产生很多冗余工作,实际上我们无需一次针对每个元素num去判断num+1,num+2,num+3...是否在数组中。如果num-1已经在数组中的话,那么num-1肯定会进行相应的+1遍历,然后遍历到num,而且从num-1开始的+1遍历必定比从num开始的+1遍历得到的序列长度更长。因此,我们便可将在一个连续序列中的元素进行删减,让其只在最小的元素才开始+1遍历。比如,现有元素[1,2,4,3,5],当2,3,4,5发现均有比自己小1的元素存在,那么它们就不会开始+1遍历,而1是连续序列中最小的元素,没有比自己小1的元素存在,所以会开始+1遍历。通过上述方式便可将时间复杂度优化至O(n)。

解法描述:

1、先排序再找最长递增子序列

最长递增子序列在已排序 且 没有重复元素的情况下可用 dp 解决

  • dp[0] = 1
  • if(nums[i] == nums[i-1]+1) dp[i] = dp[i-1]+1
  • if(nums[i] != nums[i-1]+1) dp[i] = 1
  • res = Max(res, dp[i])

但是对于有重复元素的情况,比如 [1,2,0,1] (排序后是 [0,1,1,2])预期结果是 3 不是 2,两个 1 不会中断连续的定义

所以如果想要用 dp 来做的话,要先去重,去重后再排序,然后再 dp

2、哈希表(O(n))

  • 先将所有元素都加入到 HashSet 
  • 遍历 HashSet,当前数字记为 num:
    • 如果 Set 中不存在 num-1,那么往后找 num+1,num+2,num+3.... 直到没有,那么结果就是 每次 在这段连续最长里面 找最大的那个

参考题解: https://leetcode.cn/problems/longest-consecutive-sequence/solution/xiao-bai-lang-ha-xi-ji-he-ha-xi-biao-don-j5a2/

class Solution {

    //哈希表实现。哈希表的 插入、删除、查找的时间复杂度近似为 O(1)
    public int longestConsecutive2(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        Set<Integer> set = new HashSet();
        // 每个加到哈希表里(去重)
        for (int i=0;i<nums.length;i++) {
            set.add(nums[i]);
        }

        int ans = 1;
        for (int num:set) {
            int cur = num;
            // 如果 num-1 存在,那么 num 就不用搜索了,搜索 num-1 的时候一直往后 +1 搜索会把 num 包括进去
            // 当 num-1 不存在,才开始往后搜索
            if (!set.contains(num-1)) {
                // 只有当num-1不存在时,才开始向后遍历num+1,num+2,num+3......
                while (set.contains(cur+1)) {
                    cur++;
                }
                // [num, cur] 是连续的区间
                ans = Math.max(ans, cur-num+1);
            }
        }
        return ans;
    }


    public int longestConsecutive(int[] nums) {
        if (nums.length == 0) {
            return 0;
        }
        int res = 1;
        Set<Integer> set = new HashSet();
        // 每个加到哈希表里(去重)
        for (int i=0;i<nums.length;i++) {
            set.add(nums[i]);
        }
        // 去重之后长度会缩短
        int newLen = 0;
        int[] newNums = new int[set.size()];
        for (int num : set) {
            newNums[newLen++] = num;
        }
        // 去重之后再排序
        Arrays.sort(newNums);


        // 去重&排序之后用dp
        // 这样复杂度取决于排序的复杂度
        // dp[i] 是 0~i 的最长递增子序列
        // dp[i] = dp[i-1] + 1 if(nums[i]==nums[i-1]+1)
        // dp[i] = 1 if(nums[i]!=nums[i-1]+1)
        // 结果为 max(dp[])
        int[] dp = new int[newLen];
        dp[0] = 1;
        for (int i=1;i<dp.length;i++) {
            if (newNums[i] == newNums[i-1]+1) {
                dp[i] = dp[i-1] + 1;
            }
            else {
                dp[i] = 1;
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

 

300. 最长递增子序列

dp[i] 表示 nums[0:i] 的最长递增子序列

枚举所有 <i 的 j,即在 nums[i] 之前的所有递增子序列

如果 nums[i]>nums[j],dp[j] 是 [0:j] 的最长,又加上 nums[i] 这一个比 nums[j] 大的

dp[i] = Math.max(dp[i], dp[j] + 1);
class Solution {
    public int lengthOfLIS(int[] nums) {
        int res=1;
        // dp[i] nums[0:i] 的最长递增子序列
        int[] dp = new int[nums.length];
        for (int i=0;i<dp.length;i++) {
            // 最短默认1
            dp[i] = 1;
            for (int j=0;j<i;j++) {
                if (nums[i]>nums[j]) {
                    // dp[j] 是 [0:j] 的最长,又加上nums[i]这一个比nums[j]大的
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

 

 

438. 找到字符串中所有字母异位词(滑动窗口map差异计数)

维持一个 map ,如果这个 map 的每项值都为 0,说明

  • key : p 中的字母
  • value:当前窗口与 p 中字母数量的差距

map 初始化:遍历 p ,统计 p 中每个字母的数量,放到 map 中

左右指针:l = 0; r = 0

滑动窗口初始化:从 0 到 < p.length 只移动右指针 r,每进来 s 的一个字符,判断其如果在 map 中,说明这个字符与 p 中这个字符数量的差距-1,将其计数 -1

在这之后 l r 同步移动:

lc 表示 s 中 l 下标的字符,rc 表示 s 中 r 下标的字符

  • 旧 l 要出去了,如果 map 里包含 lc,说明这个曾经抵消掉一个 p 中的该字符数量,但现在要出去了,不能抵消了,map 中的差异计数 +1
  • 新 r 要进来了,如果 map 里包含 rc,说明现在这个新来的字符可以抵消掉一个 p 中的该字符数量,map 中的差异计数-1
  • 每次遍历 map,如果 map 中所有的值为 0 ,说明窗口里现在的字串与 p 中的字符计数没有差异,将 l 加到结果中
class Solution {
    public List<Integer> findAnagrams(String s, String p) {
        List<Integer> res = new ArrayList();
        if (s.length() < p.length()) {
            return res;
        }
        Map<Character, Integer> map = new HashMap();
        for(int i=0;i<p.length();i++) {
            map.put(p.charAt(i), map.getOrDefault(p.charAt(i), 0)+1);
        }
        int l=0;
        int r=0;
        // 滑动窗口初始化
        for (int i=0;i<p.length();i++) {
            char si = s.charAt(i);
            if (map.containsKey(si)) {
                map.put(si, map.get(si)-1);
            }
            r++;
        }
        if (mapAllZero(map)) {
            res.add(0);
        }
        while(l<r && r<s.length()) {
            char lc = s.charAt(l);
            // 左边的元素出去,map 中如果存在的话,则其数量 +1
            if (map.containsKey(lc)) {
                map.put(lc, map.get(lc)+1);
            }
            // 右边的元素进来,map 中如果存在的话,则其数量 -1
            char rc = s.charAt(r);
            if (map.containsKey(rc)) {
                map.put(rc, map.get(rc)-1);
            }
            if (mapAllZero(map)) {
                res.add(l+1);
            }
            l++;
            r++;
        }
        return res;
    }

    private boolean mapAllZero(Map<Character, Integer> map) {
        for (Map.Entry<Character, Integer> entry : map.entrySet()) {
            if (entry.getValue() != 0) {
                return false;
            }
        }
        return true;
    }
}

 

76. 最小覆盖子串(滑动窗口 map 差异计数)

 和前面的 438.找到字符串中所有字母异位词 解法一样

用滑动窗口,维持当前窗口和字串 t 中的字符数差值

不同的是,前面那个题要求子串连续,滑动窗口的大小是固定的

这个题要求覆盖即可,滑动窗口的大小是可变的,因此每个循环

  • 如果当前窗口的字符个数 可以覆盖子串的,那么 l 右移,看可不可以使窗口进一步缩短
  • 如果当前窗口的字符个数 不可以覆盖子串的,那么 r 右移,看新加进来的字符可不可使当前窗口可以覆盖

需要注意的是退出循环的条件,并不是 r>s.length()

例如:

s = "ADOBECODEBANC", t = "ABC" 答案是 BANC
当 r 指向最后一个的时候,l 还指向的是 2,也就是说当前窗口是 OBECODEBANC, 可以覆盖
但是此时循环还要继续,l 可以继续右移,使窗口进一步缩短,到最小覆盖子串 BANC

而对于另一种情况,也就是说 r 到了末尾,但是现在的窗口不能覆盖子串那么 l 无论怎么左移,剩下的也都不能覆盖子串了

因此,退出循环的条件就是 r 到了末尾,而此时的窗口不能覆盖子串

其它注意事项:

  • map 初始化:遍历 t,统计每个字符的个数,放入 map
  • 判断当前窗口可以覆盖子串的方法:看 map 里所有的 value 是不是 <=0(>0 不能覆盖,=0 个数相等刚好覆盖,<0 覆盖了之后还有多的)
class Solution {
    public String minWindow(String s, String t) {
        String res;
        int resStartIndex=0;
        int resEndIndex=0;
        int resShortestLen = Integer.MAX_VALUE;
        Map<Character, Integer> curWinMap = new HashMap();
        // 初始化 map, 放入 t 串每个字符的计数
        for (int i=0;i<t.length();i++) {
            curWinMap.put(t.charAt(i), curWinMap.getOrDefault(t.charAt(i), 0) + 1);
        }
        int l = 0;
        int r = 0;
        while (true) {
            boolean winContainsAllt = winContainsAllt(curWinMap);
            if ((r-l) < resShortestLen && winContainsAllt) {
                resStartIndex = l;
                resEndIndex = r;
                resShortestLen = r-l;
            }

            if (winContainsAllt) {
                char lc = s.charAt(l);
                // 如果已经包含全部了。那么左边的出去,看能不能继续缩短一些
                if (curWinMap.containsKey(lc)) {
                    // 左边的出去了一个可以 cover t 串的,差距计数 +1
                    curWinMap.put(lc, curWinMap.get(lc) + 1);
                }
                l++;
            }
            else {
                // 如果 r 到末尾了,当前窗口可以覆盖,循环还是要继续往下走,看 l 能不能右移使窗口更缩短
                // 如果 r 到末尾了,当前窗口的还不能覆盖,那 l 继续往右走后面更都不能覆盖了,所以可以退出循环了
                if (r == s.length()) {
                    break;
                }
                // 这个要在前面那个 break 的后面,防止数组越界
                char rc = s.charAt(r);
                // 没有包含全部,右边的继续进来新的,看新来的能不能覆盖
                if (curWinMap.containsKey(rc)) {
                    // 右边新进来的这个可以 cover t 串,差距计数 -1
                    curWinMap.put(rc, curWinMap.get(rc) - 1);
                }
                r++;
            }
        }
        return s.subSequence(resStartIndex, resEndIndex).toString();
    }

    private boolean winContainsAllt(Map<Character, Integer> curWinMap) {
        for (Map.Entry<Character, Integer> entry : curWinMap.entrySet()) {
            // 如果不够 cover 掉 t 里面的字符
            // >0 不能 cover; =0 完全 cover ,字符计数相等; <0 完全 cover,并且还有多的
            if (entry.getValue() > 0) {
                return false;
            }
        }
        // 全部 cover 的情况是每一项都 < 0
        return true;
    }
}

 

 

3. 无重复字符的最长子串(滑动窗口 map 记录字符上次出现的位置)

 维持一个滑动窗口,使窗口内始终维持没有重复字符,计算此窗口的最大值

怎么实现滑动窗口内没有重复字符呢?

用一个 map ,记录每个字符上一次出现的位置

每次移动 r ,让右边新字符加进来,根据 map 获得这个新字符上次出现的位置

如果在当前窗口内(即>左指针l),说明在当前窗口内

因此要移动左指针到 上次出现的位置 + 1,把这个重复字符排出当前窗口内

class Solution {

    public int lengthOfLongestSubstring(String s) {
        if (s == null || "".equals(s)) {
            return 0;
        }
        int res = Integer.MIN_VALUE;
        int l = 0;
        int r = 0;
        // 记录字符上一次出现的位置。
        Map<Character, Integer> char2LastIndexMap = new HashMap<>();
        while (r<s.length()) {
            char rc = s.charAt(r);
            Integer rcLastIndex = char2LastIndexMap.get(rc);
            
            // 对于右边新加入的字符,要维持滑动窗口内无重复的
            // 就要看它上次出现的位置是不是在窗口内,也就是说它上次出现的位置 > l左指针
            // 如果在窗口内,就将 l 移动到它上次出现的位置 +1
            if (rcLastIndex != null && rcLastIndex >= l) {
                l = rcLastIndex + 1;
            }
            // 为什么要 + 1,应为这里已经把 r 当成新加进来的了
            if (r-l+1 > res) {
                res = r-l+1;
            }
            char2LastIndexMap.put(rc ,r);
            r++;
        }
        return res;
    }
}

 

1143. 最长公共子序列 (动态规划,dp[i][j] s1[0:i]和s2[0:j]最长)

dp[i][j] 表示 text1(0:i) 和 text2(0:j) 的最长公共子序列的长度

用 dp 的 0 行 0 列表示字符串为空的场景,即 i=0 或 j=0 的 0 行 0 列,dp 值为0

因此 dp 对应的下标比字符串的下标多1

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // dp[i][j] 表示 text1(0...i) 与 text2(0...j) 的最长公共子序列
        // dp[0] 用来表示空字符串的情况了,所以要 length+1
        int[][] dp=new int[text1.length()+1][text2.length()+1];
        // 要由左上角得来,所以从左上到右下
        // text1[i] == text2 dp[i][j] = dp[i-1][j-1]+1
        // text1[i] != text2 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
        int maxLen = 0;
        for (int i=1;i<text1.length();i++) {
            dp[i][0] = 0;
        }
        for (int j=1;j<text2.length();j++) {
            dp[0][j] = 0;
        }
        for (int i=1;i<text1.length()+1;i++) {
            // dp[0] 用来表示空字符串的情况了,所以要 length+1
            // dp[1] 才是 text.charAt(0),所以要 -1 这里
            char c1=text1.charAt(i-1);
            for (int j=1;j<text2.length()+1;j++) {
                char c2=text2.charAt(j-1);
                if (c1 == c2) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    if (dp[i][j] > maxLen) {
                        maxLen = dp[i][j];
                    }
                }
                else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return maxLen;
    }


}

 

最长公共子串 (动态规划+Map存起始下标)

和最长公共子序列类似

dp[i][j] 表示 text1(0:i) 和 text2(0:j) 的最长公共子串的长度

用 dp 的 0 行 0 列表示字符串为空的场景,即 i=0 或 j=0 的 0 行 0 列,dp 值为0

只是状态转移方程,在比较的当前字符不等时,不一样

  • dp[i][j] = dp[i-1][j-1]+1, text1(i-1)==text2(i-1)
  • dp[i][j] = 0, text1(i-1) != text2(i-1)
要求出结果子串,已知 dp[i][j] i j 相当于这个长度的字符串的结束下标
而且最后的结果不论在 text1 还是 text2 都是一样的,所以
用一个 map 保存 dp 当前最长公共子串的在 text1 的起始下标
map<int, int> 
  • key为 dp[i][j] 的i,即text1的子串结束下标
  • value为text1(0:i)的最长公共子串起始下标
如果走到 text1(i-1)==text2(i-1) 判断分支,那么 dp[i][j] = dp[i-1][j-1]+1
i 处的起始下标从 i-1 处得来
import java.util.HashMap;
import java.util.Map;

public class TestLongestCommonSubStr {

    public static void main(String[] args) {
        String res = longestCommonSubsequence("rghello123wohellord66665rld", "vfhello123abc4hellord66665");
        System.out.println(res);
    }

    public static String longestCommonSubsequence(String text1, String text2) {
        // dp[i][j] 表示 text1(0...i) 与 text2(0...j) 的最长公共子序列
        // dp[0] 用来表示空字符串的情况了,所以要 length+1
        int[][] dp=new int[text1.length()+1][text2.length()+1];
        // 要由左上角得来,所以从左上到右下
        // text1[i] == text2 dp[i][j] = dp[i-1][j-1]+1
        // text1[i] != text2 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
        // key为dp[i][j]的i,即text1的子串结束下标,value为text1(0:i)的最长公共子串起始下标
        Map<Integer, Integer> map = new HashMap<>();
        int maxLen = 0;
        int[] maxLenIndex = new int[2];
        for (int i=1;i<text1.length();i++) {
            dp[i][0] = 0;
        }
        for (int j=1;j<text2.length();j++) {
            dp[0][j] = 0;
        }
        for (int i=1;i<text1.length()+1;i++) {
            // dp[0] 用来表示空字符串的情况了,所以要 length+1
            // dp[1] 才是 text.charAt(0),所以要 -1 这里
            char c1=text1.charAt(i-1);
            for (int j=1;j<text2.length()+1;j++) {
                char c2=text2.charAt(j-1);
                if (c1 == c2) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    // 起始下标从 i-1 来
                    int istart = put(map, i-1, i);
                    if (dp[i][j] > maxLen) {
                        // 最长公共子串的长度
                        maxLen = dp[i][j];
                        // 最长公共子串在 text1 的起始下标
                        maxLenIndex[0] = istart;
                        // 最长公共子串在 text1 的结束下标
                        maxLenIndex[1] = i;
                    }
                }
                else {
                    // 子序列
                    //dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                    // 子串
                    dp[i][j] = 0;
                }
            }
        }
        return text1.subSequence(maxLenIndex[0], maxLenIndex[1]).toString();
    }

    private static int put(Map<Integer, Integer> map, int istartFrom, int i) {
        // istartFrom 起始下标从 istartFrom 来
        Integer istart = map.get(istartFrom);
        if (istart == null) {
            // 当 i=1 时会走到这里
            map.put(i, i);
            return i;
        }
        else {
            // 放入 结束下标-起始下标
            map.put(i, istart);
            return istart;
        }
    }
}

 

 

131. 分割回文串 (dfs 所有组合)

有点像上面的组合

  • 起始位置是上一层的结束位置 +1
  • 判断是 回文串 之后,再往下 dfs
  • 每一层是 起始位置~结束位置 遍历
  • 当起始位置==s.length() 时添加结果,退出循环

由于每个节点都要判断是否是回文串,所以可以先对整个字符串求出 dp :

boolean dp[i][j] 表示字符串从 i 到 j 是否是回文串:

  • if i==j dp[i][j]=true
  • else if s.charAt(i) != s.charAt(j) dp[i][j]=false;
  • else if j-i==1(如"aa") dp[i][j]=true
  • else dp[i][j]=dp[i+1][j-1]

dp[i][j]=dp[i+1][j-1] 由于需要从左下角的元素推出现在的元素

所以遍历应该是从左下到右上——从下到上&从左到右

即从下到上: i 从 n-1 到 0

从左到右(又j>i):j 从 i 到 n

class Solution {

    List<List<String>> res = null;

    Solution() {
        res = new ArrayList();
    }

    public List<List<String>> partition(String s) {
        boolean[][] dp = getDP(s);
        dfs(s, dp, new LinkedList(), 0);
        return res;
    }

    private void dfs(String s, boolean[][] dp, LinkedList<String> curRes, int startIndex) {
        // startIndex 越界,添加结果集
        if (startIndex==s.length()) {
            res.add(new ArrayList(curRes));
        }
        for (int j0=startIndex;j0<s.length();j0++) {
            //substring 方法i,j不包含j,而其它地方包括dp认为的i,j都是包括j的。
            // 所以这里 substring 的时候要 j0+1
            String substr = s.substring(startIndex, j0+1);
            // 是回文串才往下
            // 和八皇后一样,这里不能在前面循环外面 return,而是符合条件才往下。因外外面没有 j0?
            if (dp[startIndex][j0]) {
                curRes.addLast(substr);
                // 如 aabcb, startIndex是0,j0现在等于1,那么已经分出来了是回文的aa
                // 那么后面就要继续分bcb,即startIndex是j+1,里面循环j+1到length分bcb
                dfs(s, dp, curRes, j0+1);
                curRes.removeLast();
            }
        }
    }


    private boolean[][] getDP(String s) {
        int n=s.length();
        boolean[][] dp = new boolean[n][n];
        // dp[i][j] s的i到j是否是回文
        // dp[i][j] = dp[i+1][j-1];
        // 需要通过左下角的结果推出来,所以遍历顺序应该是从左下到右上
        // i从下到上
        for (int i=n-1;i>=0;i--) {
            // 由于是从i到j,所以j永远是大于i的,所以j从i开始往上增
            // j从左到右
            for (int j=i;j<n;j++) {
                if (i==j) {
                    dp[i][j] = true;
                }
                else if (s.charAt(i) != s.charAt(j)) {
                    dp[i][j] = false;
                }
                // 只有两个元素时,而且此时已经通过了上面的s.charAt(i) != s.charAt(j)
                // 举例,如 aa
                else if (j-i==1) {
                    dp[i][j] = true;
                }
               else {
                    // 需要通过左下角的结果推到出来
                    // 所以 dp 遍历的顺序应该是从左下到右上
                    dp[i][j] = dp[i+1][j-1];
                }
            }
        }
        return dp;
    }
}

 

 

 

763. 划分字母区间 (贪心,用结束下标更新当前片段结束下标)

先遍历字符串,得到 字符-该字符最后一次出现的位置 的map

用一个 curStart 和 curEnd 来记录当前片段的起始下标和结束下标,初始值都为0

然后再次遍历字符串,如果 新字符的endIndex > curEnd,超出了当前片段,则扩充当前片段结束边界:curEnd = 字符endIndex

如果 i==curEnd ,经过前面的扩充,与一些字符的不扩充,终于走到了片段的结束

则将当前片段的长度 curEnd-curStart+1 添加到结果数组中,同时下一个片段要开始了:curStart=i+1 curEnd=i+1

class Solution {
    public List<Integer> partitionLabels(String s) {
        // 得到 字符-该字符最后一次出现位置 的map
        Map<Character, Integer> char2EndMap = new HashMap();
        for (int i=0;i<s.length();i++) {
            char c = s.charAt(i);
            char2EndMap.put(c, i);
        }

        List<Integer> res = new ArrayList();
        // 当前片段起始位置
        int curStart = 0;
        // 当前片段结束位置
        int curEnd = 0;
        for (int i=0;i<s.length();i++) {
            char c = s.charAt(i);
            Integer cEndIndex = char2EndMap.get(c);
            // 这个片段内出现的新字符的endIndex大于当前片段的end,则更新
            if (cEndIndex>curEnd) {
                curEnd = cEndIndex;
            }
            // 当前片段走到了最后一个字符
            if (i == curEnd) {
                // 当前片段已经结束,将当前片段的长度添加到结果中
                res.add(curEnd - curStart + 1);
                // 现在下个片段开始了
                curStart = i+1;
                curEnd = i+1;
            }
        }
        return res;
    }
}

 

 

392. 判断子序列(双指针)

为短串中的每一个字符,依次在长串中寻找。i=0 j=0

如果相等,两个都移动;如果不相等,只长串中的那个移动,去为短串当前字符继续寻找相等的

class Solution {
    public boolean isSubsequence(String s, String t) {
        int i=0;
        int j=0;
        while (i<s.length() && j<t.length()) {
            // 如果相等,都移动一步
            if (s.charAt(i) == t.charAt(j)) {
                i++;
                j++;
            }
            // 如果不相等,长的那个移动一步去为"子序列"寻找下一个数字
            else {
                j++;
            }
        }
        // 如果 "子序列" 走完了,说明是子序列
        return i==s.length();
    }
}