经典动态规划子串与子序列问题二

3最长重复子串

上一篇说了两个子序列问题,这片开始说子串问题,要注意到子串和子序列的区别。
看到这个问题可能第一时间想到暴力搜索,确实暴力可以解决一切问题。但是今天说一下利用后缀数组求解的问题,至于什么是后缀数组在《编程珠玑》上又很好地解释。这个结构是一个字符指针数组,记录目标字符串的所有后缀的起始地址,例如banana这个单词的后缀数组为:
suff[0]:banana
suff[1]:anana
suff[2]:nana
suff[3]:ana
suff[4]: na
suff[5]: a
如果某个子串在目标字符串中出现两次,那么它必将出现在两个不同的后缀中,因此对后缀数组进行排序,以寻找相同的后缀,然后扫描数组,比较相邻的元素便可以找出最长的重复子串。
代码如下:

class Solution {
public:
	static bool comp(string& s1, string& s2)
	{
		int m = s1.size();
		int n = s2.size();
		int l = 0, r = 0;
		while (l < m && r < n)
		{
			if (s1[l] < s2[r])
				return true;
			else if (s1[l] == s2[r])
			{
				l++;
				r++;
			}
			else
				return false;
		}
		return true ? false : m < n ; m >= n;
	}
	string MaxLenstring(string& s)
	{
		int m = s.size();
		if (m == 0)
			return "";
		vector<string> suff;
		for (int i = 0; i < m; i++)
			suff.push_back(s.substr(i));
		sort(suff.begin(), suff.end(), comp);
		int res = 0, index = 0;
		for (int i = 0; i < m - 1; i++)
		{
			int _Same = Same(suff[i], suff[i + 1]);
			if (_Same > res)
			{
				res = _Same;
				index = i;
			}
		}
		return suff[index];
	}
private:
	int Same(string& s1, string& s2)
	{
		int res = 0;
		int m = s1.size();
		int n = s2.size();
		int l = 0, r = 0;
		while (l < m && r < n)
		{
			if (s1[l++] == s2[r++])
				res++;
			else
				break;
		}
		return res;
	}
};

4.最长公共子串(LCS)

这个LCS跟前面说的最长公共子序列的LCS不一样,不过也算是LCS的一个变体,在LCS中,子序列是不必要求连续的,而子串则是“连续”的。即:

题:给定两个字符串X,Y,求二者最长的公共子串,例如X=[aaaba],Y=[abaa]。二者的最长公共子串为[aba],长度为3。

方法1:
这种方法是基本方法俗称暴力。因为是子串问题,所以非常容易想到,也比较好写再次就不在贴出代码。

方法2:
既然最长公共子串是最长公共子序列的变体,那么最长公共子串是不是也可以用动态规划来求解呢?
我们还是像之前一样“从后向前”考虑是否能分解这个问题,在最大子数组和中,我们也说过,对于数组问题,可以考虑“如何将arr[0,…i]的问题转为求解arr[0,…i-1]的问题”,类似最长公共子序列的分析,这里,我们使用dp[i][j]表示 以x[i]和y[j]结尾的最长公共子串的长度,因为要求子串连续,所以对于X[i]与Y[j]来讲,它们要么与之前的公共子串构成新的公共子串;要么就是不构成公共子串。故状态转移方程
X[i] == Y[j],dp[i][j] = dp[i-1][j-1] + 1
X[i] != Y[j],dp[i][j] = 0
对于初始化,i == 0或者j == 0,如果X[i] == Y[j],dp[i][j] = 1;否则dp[i][j] = 0

int dp[30][30];
 
void LCS_dp(char * X, int xlen, char * Y, int ylen)
{
    maxlen = maxindex = 0;
    for(int i = 0; i < xlen; ++i)
    {
        for(int j = 0; j < ylen; ++j)
        {
            if(X[i] == Y[j])
            {
                if(i && j)
                {
                    dp[i][j] = dp[i-1][j-1] + 1;
                }
                if(i == 0 || j == 0)
                {
                    dp[i][j] = 1;
                }
                if(dp[i][j] > maxlen)
                {
                    maxlen = dp[i][j];
                    maxindex = i + 1 - maxlen;
                }
            }
        }
    }
    outputLCS(X);
}

方法3:
前面提过后缀数组的基本定义,与子串有关,可以尝试这方面思路。由于后缀数组最典型的是寻找一个字符串的重复子串,所以,对于两个字符串,我们可以将其连接到一起,如果某一个子串s是它们的公共子串,则s一定会在连接后字符串后缀数组中出现两次,这样就将最长公共子串转成最长重复子串的问题了,这里的后缀数组我们使用基本的实现方式。

值得一提的是,在找到两个重复子串时,不一定就是X与Y的公共子串,也可能是X或Y的自身重复子串,故在连接时候我们在X后面插入一个特殊字符‘#’,即连接后为X#Y。这样一来,只有找到的两个重复子串恰好有一个在#的前面,这两个重复子串才是X与Y的公共子串。

char * suff[100];
 
int pstrcmp(const void *p, const void *q)
{
    return strcmp(*(char**)p,*(char**)q);
}
 
int comlen_suff(char * p, char * q)
{
    int len = 0;
    while(*p && *q && *p++ == *q++)
    {
        ++len;
        if(*p == '#' || *q == '#')
        {
            return len;
        }
    }
    return 0;
}
 
void LCS_suffix(char * X, int xlen, char * Y, int ylen)
{
    int suf_index = maxlen = maxindex = 0;
 
    int len_suff = xlen + ylen + 1;
    char * arr = new char [len_suff + 1];  /* 将X和Y连接到一起 */
    strcpy(arr,X);
    arr[xlen] = '#';
    strcpy(arr + xlen + 1, Y);
 
    for(int i = 0; i < len_suff; ++i)  /* 初始化后缀数组 */
    {
        suff[i] = & arr[i];
    }
 
    qsort(suff, len_suff, sizeof(char *), pstrcmp);
 
    for(int i = 0; i < len_suff-1; ++i)
    {
        int len = comlen_suff(suff[i],suff[i+1]);
        if(len > maxlen)
        {
            maxlen = len;
            suf_index = i;
        }
    }
    outputLCS(suff[suf_index]);
}

5最长不重复子串

题:从一个字符串中找到一个连续子串,该子串中任何两个字符不能相同,求子串的最大长度并输出一条最长不重复子串。

目前这个题目我还只会使用hash的方法来求解,但是这道题可以利用DP+hash来做到最好的时间复杂度,我会在以后更新此方法,再次我先写上利用hash的写法

#include<unordered_set>

class Solution {
public:
	string LNRS(string& s)
	{
		int m = s.size();
		if (m == 0)
			return "";
		unordered_set<char> set;
		int res = 0;
		int start = 0, end = 0;
		for (int i = 0; i < m; i++)
		{
			int sum = 0;
			for (int j = i; j < m; j++)
			{
				auto it = set.insert(s[j]);
				if (it.second == false)
					break;
				else
					sum++;
				if (sum > res)
				{
					start = i;
					end = j;
				}
			}
		}
		return s.substr(start, (end - start + 1));
	}
};

6最长回文子串

题:给出一个只由小写英文字符a,b,c…y,z组成的字符串S,求S中最长回文串的长度。回文就是正反读都是一样的字符串,如aba, abba等。
暴力法在这里就不讲解了,应为答题的复杂度高切容易理解,在这里我要说一种时间复杂度O(n)的非常规解法,可以说开阔视野的一种解题思路,来让我们看看。

首先解决回文串有一个非常麻烦的问题就是奇偶性的问题,那么有一个非常巧妙的设计可以将所有字符串转换成奇数:在每一个字符的两侧插入一个特殊字符。比如abba就变成了#a#b#b#a#,两个连续的数相加必然是一个奇数,道理大家都懂,但是没有考虑过,哈哈哈。好了,目标字符串已经发生了转换,我来具体解释这个O(n)算法过程。
下面以字符串12212321为例,经过上一步,变成了 S[] = “$ #1#2#2#1#2#3#2#1#”
为什么我在字符串的头添加了‘$’,这个问题是因为防止溢出,在代码里你一看就懂了。

然后用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i],也就是把该回文串“对折”以后的长度),比如S和P的对应关系:

S: # 1 # 2 # 2 # 1 # 2 # 3 # 2 # 1 #
P: 1 2 1 2 5 2 1 4 1 2 1 6 1 2 1 2 1
(p.s. 可以看出,P[i]-1正好是原字符串中回文串的总长度)

那么怎么计算P[i]呢?该算法增加两个辅助变量(其实一个就够了,两个更清晰)id和mx,其中 id 为已知的 {右边界最大} 的回文子串的中心,mx则为id+P[id],也就是这个子串的右边界。

然后可以的到一个非常有意思的结论,如果mx > i,那么P[i] >= MIN(P[2 * id - i], mx - i)。
这是一个非常神奇的公式,一图胜千言,直接上图(盗的图)。


那么我们其实可以分两种情况考虑,上图中j是i关于id对称的点,为什么我们要求这个j点,很明显,id是我们上一次求得回文串,也就是在id - p[id] +1 到 id + p[id] - 1之间是对称的,因为他是回文串。那么如果下一个所求的i在这个范围内,那么我们可以利用之前求得信息很快求出以i为中心的回文串长度。因此:
if (p[j] < mx - i) :那么我们可以直接得出p[i] = p[j]。
else (p[j] >= mx - i)
在这里插入图片描述
当 P[j] >= mx - i 的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] >= mx - i。至于mx之后的部分是否对称,就只能老老实实去匹配了。

对于 mx <= i 的情况,无法对 P[i]做更多的假设,只能P[i] = 1,然后再去匹配了。

int p[1000], mx = 0, id = 0;
memset(p, 0, sizeof(p));
for (i = 1; s[i] != '\0'; i++) {
    p[i] = mx > i ? min(p[2*id-i], mx-i) : 1;
    while (s[i + p[i]] == s[i - p[i]]) p[i]++;
    if (i + p[i] > mx) {
        mx = i + p[i];
        id = i;
    }
}

你可能要问了这个while循环难道不算是O(N^2)吗,但是仔细观察代码,你会发现这个while的循环其实不和这个子串成比例增加,且他是一个常亮计算,因此改代码最终的时间复杂度依然是O(n)。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值