左神算法进阶班笔记Part1:KMP、Manacher、BFPRT、窗口滑动问题

文章目录

  • KMP算法
  • 最大回文子串
    • Manacher算法
  • TOPK问题
    • Partition
    • BFPRT算法
  • 窗口滑动问题
    • 滑动窗口最大值
    • 求最大值减去最小值小于或等于num的子数组数量

KMP算法

1.KMP详细
2.相关题目
1、【京东】给定一个字符串,要求在后面添加长度最短的字符,生成一个新的字符串,包含两个原始字符串。
【思路】将字符串最长前后缀匹配长度算出后,next数组再多求一位,即可得到一个最长前缀、最长后缀。然后第二个字符串只需要将前缀和原始字符串的后缀重合,补充完整即可。
2.判断树B是不是树A的某一子树
【思路】将两个数序列化,判断树A序列化后是否为树B序列化的子子串。
3.判断某个字符串能否由某个字符串重复若干次组成
【思路】算出的next(n-1),令k=n-1-next(n-1),若k整除n,且k < n,则s满足条件,否则不满足。详细分析

最大回文子串

【问题】求字符串中最长回文子串的长度
【暴力求解】以每个位置作为中心,向两边扩展,可以确定奇回文,但是偶回文无法这样做。
解决方法:在字符串中间及两边插入某种字符,此时可以按照这种方法进行扩展。此时无论奇回文还是偶回文都可以找到。
例如112211,此时添加任意字符在两边#1#1#2#2#1#1#此时均可以进行回文判断。

Manacher算法

补充概念:
回文直径:以一个位置为中心,扩出来整个串的长度为回文直径
回文半径:以一个位置为中心,扩出来半个串长度为回文半径
回文数组:对于字符串而言,从0位置开始,一直到最后,新建一个数组,数组中保存对应位置的回文半径。
最右回文右边界:所有回文半径中,最靠右的边界,回文右边界只要没更新,记录最早取得此处的回文中心。

Manacher在向外扩展的过程整体跟之前的算法相似,但是有加速。

【步骤】
1.回文右边界R不包含位置i时,此时暴力扩展,直到R包含i。
2.i位置在回文有边界内时,知道了回文右边界可以知道回文左边界,对称中心为c,此时关于c做i的对称点i‘。关于i’的回文边界有三种情况,依次分析:
1)若i‘的回文彻底在c为中心的回文里面,此时i的回文半径和i’的回文半径相同。
2)i位置的对称位置i’的回文半径越过了以c为中心的左边范围。(i‘扩出的范围以c为中心的回文没包住,存在一部分在回文直径外面)此时i’的回文半径是i-R。
3)正好i‘的回文半径正好跟左边L相等,此时可以知道i的回文半径大于等于i-R,然后需要判断R后面的位置,重新返回第一步。
【时间复杂度】整个算法的复杂度O(n),虽然第一步和第四步花费时间长,但是1,4都会扩展R,依次变化的过程中,R最多也就是变化到n,所以时间复杂度为O(n)。

//使用manacher算法寻找字符中最长的回文子串
int Manacher(string s) {
    string s1="@#";//首部加入哨兵,尾部有天然哨兵‘/0’
    for(char i:s){
        s1.push_back(i);
        s1.push_back('#');
    }
    int right=0,res=0,mid=0,len=s1.size();
    vector<int> dp(len);//dp[]记录最大回文半径,不包括自身
    for(int i=1;i<len;i++){
        //在以j为中心的回文串内,2*j-i与i对称,如果加上半径超出right,则限制半径为right-i
        if(right>i) 
        	dp[i]=min(dp[2*mid-i],right-i);
        while(s1[i+dp[i]+1]==s1[i-dp[i]-1])//半径还可能更大,继续增加,任何情况下都可以考虑半径是否增大
            dp[i]++;
        if(right<dp[i]+i){//如果最大右边端点比此时右边端点小,更新
            right=dp[i]+i;
            mid=i;
        }
        res=max(res,dp[i]);
    }
    return res;
}

void Test()
{
    string str = "abc1234321ab";
    cout << Manacher(str) << endl;
}

【例题】在末尾加最少字符,使字符串整体为回文串。
【思路】改写Manacher,整个字符串一旦出现回文右边界到达最后一位,回文字符串之前的内容全部倒序添加到最后,即可形成回文字符串。

左神算法进阶班笔记Part1:KMP、Manacher、BFPRT、窗口滑动问题_第1张图片

class Solution {
public:
    int countSubstrings(string s) {
        string s1="@#";//首部加入哨兵
        for(char i:s){
            s1.push_back(i);
            s1.push_back('#');
        }
        int right=0,res=s.size(),mid=0;
        vector<int> dp(s1.size());
        for(int i=1;i<s1.size();i++){//i从1开始,否则在while中会越界
            dp[i]= right>=i ? min(dp[2*mid-i],right-i) : 0;
            while(s1[i+dp[i]+1]==s1[i-dp[i]-1])//尾部有'/0'天然哨兵,所以不需要考虑边界
                dp[i]++;
            if(right<dp[i]+i){
                right=dp[i]+i;
                mid=i;
            }
            res+=dp[i]/2;
        }
        return res;
    }
};

TOPK问题

Partition

快排partition思想,时间复杂度期望O(n),最坏O(n²)。可以用荷兰国旗方法改进。

BFPRT算法

BFPRT算法跟partition的算法只有在选取划分值的情况上不同,其他全部一样。
【步骤】
1.分组(假设每五个一组,最后剩余的不到五个的一组) O(1)
2.分组之后每个小组之内排序,跨组不排序,五个数排序,总共需要划分的时间复杂度为O(N)。
3.将每个组的中位数拿出,构成新的数组,此时新数组长度为N/5(最后不到五个的可以拿上中位数,也可以拿下中位数) O(N)
4.调用BFPRT算法,此时递归过程中不再寻找k项,而是选择中间的中位数 T(N/5)
5.下面就是利用上述num,进行荷兰国旗问题排序 O(N)
6.选择走左边或者走右边
之所以要这样来选出中位数作为基准值,是因为这样分组后的左右规模就是确定了的,此时估计至少有多少个数比k更大,则确定了左右部分的最大规模。

窗口滑动问题

窗口的概念就是一个由左右边界划分的一个区域,窗口从左向右滑动,右边进数,左边出数。窗口长度可以变也可以不变,具体问题具体分析。

滑动窗口最大值

【问题】 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
【思路】使用单调递减双端队列,只需存储下标即可(也可以存储下标和元素大小)。使用双指针L、R,分别表示窗口左边界和右边界。
右边界右移:
1)遇到比队列尾部值还小的数,则该坐标直接进入队列尾部;
2)如果出现新的数值大于尾部的小数值,则将小的数弹出,直到放得下新的数值;
3)如果出现相等的数值,则将先前的数弹出,将新的数保存 。
【注意】这里弹出、加入的是数的坐标,重点是数的位置而不是数的大小,因为大小可以用arr通过位置去找。
左边界右移:
1)L移动的时候,分析当前最大值所在的位置信息是否过期,如果过期则弹出,没过期则显示当前的最大值。

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        deque<int> dq;
        int len=nums.size();
        vector<int> res;
        for(int i=0;i<len;i++){
            if(!dq.empty()&&i-dq.front()>=k)
                dq.pop_front();
            while(!dq.empty()&&nums[i]>=nums[dq.back()])
                dq.pop_back();
            dq.push_back(i);
            if(i>=k-1)
                res.push_back(nums[dq.front()]);
        }
        return res;
    }
};

求最大值减去最小值小于或等于num的子数组数量

【问题】给定数组arr和整数num,返回一段数组中最大值减去最小值小于num的数目,要求:数组长度为N,请实现时间复杂度为O(N)的解法。(子数组为连续数组)
【思路】如果中间某个数组L——R达标,则任何其中的子数组也全部达标。同理,如果一个子数组不达标,则将其任意向外扩,新得到的数组肯定不达标。属于窗口问题扩展,可以用两个双端队列实现。
【步骤】
1)L停留在0位置,如果一直达标,则将R向右扩,直到扩到某个位置X,再往后扩一个,则不达标。
2)窗口内最大值的更新结构和窗口内的最小值更新结构可以很简单获得最大和最小值,从而进行计算。
3)以0开头的子数组达标数量则计算了出来,一共有x+1长度的数组达标,他的所有子数组全部达标。
4)将L来到1位置,此时将窗口最大值和最小值更新,此时R可能仍然可以向外扩,此时右可以得到以1位置开头的所有可行的数组。
因为没有回退,一直是当前位置向后走,总的复杂度为O(N)。

public static int getNum(int[] arr,int num){
	if(arr == null || arr.length == 0){
		return 0;
	}
	LinkedList<Integer> qmin = new LinkedList<Integer>();
	LinkedList<Integer> qmax = new LinkedList<Integer>();
	int i = 0;//i为start,j为end
	int j = 0;
	int res = 0;
	//整个while循环是将R向右不断扩展
	while(i<arr.length){
		while(j<arr.length){
			while(!qmin.isEmpty() && arr[qmin.peeklast()] >= arr[j]){
				qmin.polllast();
			}
			qmin.addLast(j);
			while(!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[j]){
				qmax.pollLast();
			}
			qmax.addLast();
			if(arr[qmax.getFirst()]- arr[qmin.getFirst()] > num){
				break;
			}
			j++;
		}
		//判断是否进行下一个数开头
		if(qmin.peekFirst() == i){
			qmin.pollFirst;
		}
		if(qmax.peekFirst() == i){
			qmax.pollFirst;
		}
		//一次性获取完以i开头的所有满足的数组
		res += j-i;
		i++;
	}
	return res;

你可能感兴趣的:(数据结构和算法)