[LeetCode] 395. Longest Substring with At Least K Repeating Characters

原题链接: https://leetcode.com/problems/longest-substring-with-at-least-k-repeating-characters/

1. 题目介绍

Find the length of the longest substring T of a given string (consists of lowercase letters only) such that every character in T appears no less than k times.

给出一个字符串,该字符串由小写字母组成,寻找其中的最长子串 T 。 T的要求是: T 中的每一字符出现次数都不少于 k 。输出 T 的长度。

Example 1:

Input:
s = "aaabb", k = 3

Output:
3

The longest substring is "aaa", as 'a' is repeated 3 times.

Example 2:

Input:
s = "ababbc", k = 2

Output:
5

The longest substring is "ababb", 
as 'a' is repeated 2 times and 'b' is repeated 3 times.

2. 解题思路

2.1 动态规划 - 利用int整数的数位

注: 本方法的思路参考了 https://www.cnblogs.com/grandyang/p/5852352.html

2.1.1 特殊技巧:用int类型的位做判断

这道题可以采用动态规划的方法解决。一般的动态规划题,需要使用一个一维或者二维的数组来存储数据。然后在更新的时候使用前一步的数组。但是这道题不同,可以将数组简化为使用一个int整数的数位。
具体的使用方法如下:

在本题中,需要快速判断一个子串里面的字母是否满足最少出现k次的要求。字母只有26种,因此可以使用一个数组或者hashmap来标记26个字母,但是每一次判断都要遍历一遍,比较麻烦,而采用一个int整数就可以很好地解决这个问题。

字母只有26个,而整型int有32位,足够用了。我们规定低位的26位中,每一位代表一个字母,如果该位为1,那么表示该字母不够k次,如果该位为0,那么代表这个字母出现够了k次。当这个int的整数等于0的时候,就意味着所有的字母都出现够了k次。

2.1.2 遍历方式

我们需要找出所有可能的子串,所以需要i 、j 两个变量,代表着从 i 开始,到 j 结束的子串。。还需要定义一个int类型的mask,mask的低26位中,每一位代表一个字母。

每分析一个子串时,我们都要记录26个字母在这个子串中出现的次数。如果某个字母没有达到 k 个,就要将mask中对应位置1,如果某个字母达到了 k 个,就要将mask对应字母置0。

这里还有一个减少无用循环的小技巧:每次找到一个符合要求的子串[ i ,j ],我们都要把 j 的值记录一下,max_idx = j 。当以第 i 个字符开头的所有子串分析完后,就可以直接 i = max_idx+1,节省时间。

比如字符串“ aaabbbcdefff ”, k = 3, 从第0个字符a开始的子串的中,满足要求的最长子串的 j 为 5。这时我们就不必要从下一个a再分析了,直接把 i 放到c的位置就可以了。

实现代码

class Solution {
    public int longestSubstring(String s, int k) {
        int length = s.length();
        if(length ==0) {
        	return 0;
        }
        
        int ans = 0;
        char [] aa = s.toCharArray();//为了提高速度,把字符串转化为字符数组

        for(int i = 0;i <= length-k ;  ) {
        	int letter[] = new int [26];//存放每个字母出现的次数,共26位,代表26个元素
        	int mask = 0;//int整数共有32位,其中26位用于判断某个字母的次数是否达到k次
        	int max_idx = i;
        	
        	for(int j = i ;j<length;j++) {
        		
        		int whichletter = aa[j] - 'a';
        		letter[whichletter] ++;
        		
        		//如果该字母没有达到k个,就要将mask对应字母置1
        		if(letter[whichletter] < k) {
        			mask |= (1 << whichletter);
        			//<<左移操作符:左移动whichletter位,末尾补0
        		}
        		//如果该字母达到了k个,就要将mask对应字母置0
        		else {
        			mask &= ( ~(1 << whichletter) );
        		}
        		
        		//如果mask等于0,意味着所有的字母的次数都大于等于k
        		//那么就需要更新最长子串的长度了
        		if( mask == 0) {
        			ans = Math.max(ans, j-i+1);
        			max_idx = j;
        		}
        	}
        	
        	i = max_idx + 1;
        }
        return ans;
    }
}

2.2 递归

在LeetCode官网上可以看到其他人提交的代码,看到这些大神的代码,真是佩服他们。其中速度最快的一个代码是用递归做的。我按照他的思路,重新仿写了一遍,具体代码如下,注释里简单介绍一下该思路。

实现代码

class Solution {
    public int longestSubstring(String s, int k) {
        return recursion(s.toCharArray() , 0 , s.length() , k );
    }
	
	public int recursion(char str[],int left,int right,int k) {
		//这里的left是子串第一个字符的标号
		//right是子串最后一个字符的标号再加1
		
		//如果子串长度小于k,则不可能满足条件
		if(right - left < k) {
			return 0;
		}
		
		int letter[] = new int[26];
		for(int i = left; i < right ; i++) {
			letter[ str[i] - 'a']++;
		}
		
		for(int i = left;i<right;i++) {
			if(letter[ str[i] - 'a'] <k) {
				int j = i+1;
				//不停推进,直到次数大于等于k的字母
				while(j < right && letter[str[j]-'a'] < k ) j++;
				
				//次数小于k次的字母会把字符串分成两个部分
				return Math.max(recursion(str,left,i,k), recursion(str,j,right,k));
			}
		}
		//最后如果所有字母的出现次数都大于等于k次,那么就返回子串的长度
		return right - left;
	}
}

3. 参考资料

https://www.cnblogs.com/grandyang/p/5852352.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值