最长重复子串和最长不重复子串求解

布满荆棘的人生 2022-07-13 02:21 343阅读 0赞

1最长重复子串

1.1问题描述

首先这是一个单字符串问题。子字符串R 在字符串L 中至少出现两次,则称R 是L 的重复子串。重复子串又分为可重叠重复子串和不可重叠重复子串。

1.2基本方法

枚举子串,让子串和子串进行比较。直接看代码:

C代码 收藏代码

  1. /* 最长重复子串 Longest Repeat Substring */
  2. int maxlen; /* 记录最长重复子串长度 */
  3. int maxindex; /* 记录最长重复子串的起始位置 */
  4. void outputLRS(char * arr); /* 输出LRS */
  5. /* 最长重复子串 基本算法 */
  6. int comlen(char * p, char * q)
  7. {
  8. int len = 0;
  9. while(*p && *q && *p++ == *q++)
  10. {
  11. ++len;
  12. }
  13. return len;
  14. }
  15. void LRS_base(char * arr, int size)
  16. {
  17. for(int i = 0; i < size; ++i)
  18. {
  19. for(int j = i+1; j < size; ++j)
  20. {
  21. int len = comlen(&arr[i],&arr[j]);
  22. if(len > maxlen)
  23. {
  24. maxlen = len;
  25. maxindex = i;
  26. }
  27. }
  28. }
  29. outputLRS(arr);
  30. }

    ╝①

优化思路

一般的优化方法就是在充分利用已有的结果,最长重复子串的长度增加一定是建立在之前已经找到的重复子串之上的,充分利用已找到的重复子串的位置和长度是优化的一个重点,此外还有就是未不是重复子串的,以后就不会被包含在重复子串内,如”ab”只有一个,则重复子串就不能包含”ab”(允许重叠的重复子串例外)。

1.2KMP算法求解

对KMP算法还不是很了解的,可以查看我的另一篇博文(不懂猛点),在KMP算法的关键就是求解next数组,针对next[j]=k,可以得到P[0,1,…,k-1]=P[j-k,j-k+1,…,j-1]。看到P[0,1,…,k-1]=P[j-k,j-k+1,…,j-1]应该会眼前一亮,大脑顿时清醒些,这不就是重复子串吗!由此求解最长重复子串就转化为求解KMP算法next数组中的最大值(即max{next[j]=k}。

现在就是求解next数组的问题了,还是直接查看代码:

C代码 收藏代码

  1. int getNext(char *str,int *next)
  2. {
  3. int len=strlen(str);
  4. int index=0;
  5. int k=-1;
  6. next[0]=k;
  7. int max=0;
  8. //kmp算法求next值,取得最大的字串
  9. while (index<len)
  10. {
  11. if (k==-1 || str[index]==str[k])
  12. {
  13. k++;
  14. index++;
  15. next[index]=k;
  16. if (k>max)//求得其中重复最大的字串的个数,也就是与最前面串的重复数
  17. {
  18. max=k;
  19. }
  20. }
  21. else
  22. k=next[k];
  23. }
  24. return max;
  25. }
  26. int main()
  27. {
  28. char str[50];//输入字符串
  29. cin>>str;
  30. int max=0;//最大的字串
  31. int nextMax;//接受getNext函数中返回的最大值
  32. int index;
  33. int maxIndex;//保存最大的字串起始位置
  34. int len=strlen(str);
  35. //将一个字符串从开始一直减少到只剩一个字符,通过这个取得最小字串
  36. for (index=0;index<len-1;index++)
  37. {
  38. int *next=new int[len-index];//取得next在这没用
  39. nextMax=getNext(str+index,next);//每次从str+index开始
  40. if (nextMax>max)
  41. {
  42. max=nextMax;
  43. maxIndex=index;
  44. }
  45. }
  46. //输出最长字串
  47. cout<<”最长字串: “;
  48. for (index=0;index<max;index++)
  49. {
  50. cout<<str[index+maxIndex];
  51. }
  52. cout<<endl;
  53. return 0;
  54. }

╝②

1.3后缀数组求解

后缀数组在我的另外一篇博文有介绍,还没有概念的可以移步查看点击。后缀数组就是保留字符串所有位置到字符串末尾的子字符串,a[i]就是第i位置到末尾的子串。有了后缀数组,对后缀数组进行排序,然后进行求后缀数组相邻元素的最大前缀就是最大重复子串。

Cpp代码 收藏代码

  1. #include
  2. using namespace std;
  3. const int MAXN = 1000;
  4. int mycmp(const void* p1, const void* p2)
  5. {
  6. return strcmp(*(char* const*)p1, *(char* const*)p2);
  7. }
  8. int getLen(char* p, char* q)
  9. {
  10. int ret = 0;
  11. while (*p && *p++ == *q++)
  12. {
  13. ++ret;
  14. }
  15. return ret;
  16. }
  17. char* foo(char result[], char s[])
  18. {
  19. int len = strlen(s);
  20. char** suffix = new char*[len];
  21. for (int i = 0; i < len; ++i)
  22. {
  23. suffix[i] = s + i;
  24. }
  25. qsort(suffix, len, sizeof (char*), mycmp);
  26. //for (int i = 0; i < len; ++i)
  27. //{
  28. // cout << suffix[i] << endl;
  29. //}
  30. int maxlen = 0, maxi = 0, maxj = 0, temp = 0;
  31. for (int i = 0; i < len - 1; ++i)
  32. {
  33. temp = getLen(suffix[i], suffix[i + 1]);
  34. if (temp > maxlen)
  35. {
  36. maxlen = temp;
  37. maxi = i;
  38. maxj = i + 1;
  39. }
  40. }
  41. //cout << maxlen << endl;
  42. //cout << suffix[maxi] << endl;
  43. //cout << suffix[maxj] << endl;
  44. //printf(“%.*s\n”, maxlen, suffix[maxi]);
  45. for (int i = 0; i < maxlen; ++i)
  46. {
  47. result[i] = suffix[maxi][i];
  48. }
  49. result[maxlen] = ‘\0’;
  50. return result;
  51. }
  52. int main()
  53. {
  54. char s[MAXN];
  55. char result[MAXN];
  56. while (cin >> s)
  57. {
  58. cout << foo(result, s) << endl;
  59. }
  60. return 0;
  61. }

╝③

§2最长不重复子串

2.1问题描述

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

下面介绍四种方法逐步优化,时间复杂度从O(n²)到O(n)。

2.2基本算法(使用hash)

要求子串中的字符不能重复,判重问题首先想到的就是hash,寻找满足要求的子串,最直接的方法就是遍历每个字符起始的子串,辅助hash,寻求最长的不重复子串,由于要遍历每个子串故复杂度为O(n^2),n为字符串的长度,辅助的空间为常数hash[256]。

C代码 收藏代码

  1. /* 最长不重复子串 设串不超过30
  2. * 我们记为 LNRS
  3. */
  4. int maxlen;
  5. int maxindex;
  6. void output(char * arr);
  7. /* LNRS 基本算法 hash */
  8. char visit[256];
  9. void LNRS_hash(char * arr, int size)
  10. {
  11. int i, j;
  12. for(i = 0; i < size; ++i)
  13. {
  14. memset(visit,0,sizeof visit);
  15. visit[arr[i]] = 1;
  16. for(j = i+1; j < size; ++j)
  17. {
  18. if(visit[arr[j]] == 0)
  19. {
  20. visit[arr[j]] = 1;
  21. }else
  22. {
  23. if(j-i > maxlen)
  24. {
  25. maxlen = j - i;
  26. maxindex = i;
  27. }
  28. break;
  29. }
  30. }
  31. if((j == size) && (j-i > maxlen))
  32. {
  33. maxlen = j - i;
  34. maxindex = i;
  35. }
  36. }
  37. output(arr);
  38. }

2.3动态规划求解

动态规划思想就是用于处理有重叠问题的求解,最大不重复子串一定是两个相同字符夹着的一段字符串加上这个字符,如abcac这里的最大不重复子串是a夹的一段。

当一个最长子串结束时(即遇到重复的字符),新的子串的长度是与第一个重复的字符的下标有关的,如果该下标在上一个最长子串起始位置之前,则dp[i] = dp[i-1] + 1,即上一个最长子串的起始位置也是当前最长子串的起始位置;如果该下标在上一个最长子串起始位置之后,则新的子串是从该下标之后开始的。简短几句话可能讲不是很明白,不过好在有程序可以帮助理解,惯例下面附上程序:

C代码 收藏代码

  1. /* LNRS dp */
  2. int dp[30];
  3. void LNRS_dp(char * arr, int size)
  4. {
  5. int i, j;
  6. int last_start = 0; // 上一次最长子串的起始位置
  7. maxlen = maxindex = 0;
  8. dp[0] = 1;
  9. for(i = 1; i < size; ++i)
  10. {
  11. for(j = i-1; j >= last_start; —j) // 遍历到上一次最长子串起始位置
  12. {
  13. if(arr[j] == arr[i])
  14. {
  15. dp[i] = i - j;
  16. last_start = j+1; // 更新last_start
  17. break;
  18. }else if(j == last_start) // 无重复
  19. {
  20. dp[i] = dp[i-1] + 1;
  21. }
  22. }
  23. if(dp[i] > maxlen)
  24. {
  25. maxlen = dp[i];
  26. maxindex = i + 1 - maxlen;
  27. }
  28. }
  29. output(arr);
  30. }

2.4动态规划+Hash求解

上面动态规划求解时间复杂度还是O(n²),主要是还是进行“回头”查找了重复元素位置,其实,上面并不是真正的动态规划方法,因为上面的求解过程没有记录有用的结果,所以可以通过记录之前出现的下标来改进算法,这样就不用每次都回去查找重复元素位置,这其实才是真正的动态规划方法,只是记录结果是用的Hash,这样的时间复杂度就是O(n)。

C代码 收藏代码

  1. /* LNRS dp + hash 记录下标 */
  2. void LNRS_dp_hash(char * arr, int size)
  3. {
  4. memset(visit, -1, sizeof visit);
  5. memset(dp, 0, sizeof dp);
  6. maxlen = maxindex = 0;
  7. dp[0] = 1;
  8. visit[arr[0]] = 0;
  9. int last_start = 0;
  10. for(int i = 1; i < size; ++i)
  11. {
  12. if(visit[arr[i]] == -1)
  13. {
  14. dp[i] = dp[i-1] + 1;
  15. visit[arr[i]] = i; /* 记录字符下标 */
  16. }else
  17. {
  18. if(last_start <= visit[arr[i]])
  19. {
  20. dp[i] = i - visit[arr[i]];
  21. last_start = visit[arr[i]] + 1;
  22. visit[arr[i]] = i; /* 更新最近重复位置 */
  23. }else
  24. {
  25. dp[i] = dp[i-1] + 1;
  26. }
  27. }
  28. if(dp[i] > maxlen)
  29. {
  30. maxlen = dp[i];
  31. maxindex = i + 1 - maxlen;
  32. }
  33. }
  34. output(arr);
  35. }

进一步优化

上面的程序因为辅助的空间多了,是不是还能优化,仔细看DP最优子问题解的更新方程:

dp[i] = dp[i-1] + 1;

dp[i-1]不就是更新dp[i]当前的最优解么?这与最大子数组和问题的优化几乎同出一辙,我们不需要O(n)的辅助空间去存储子问题的最优解,而只需O(1)的空间就可以了,至此,我们找到了时间复杂度O(N),辅助空间为O(1)(一个额外变量与256大小的散列表)的算法,代码如下:

注意:当前最长子串的构成是与上一次最长子串相关的,故要记录上一次最长子串的起始位置!

C代码 收藏代码

  1. /* LNRS dp + hash 优化 */
  2. void LNRS_dp_hash_impro(char * arr, int size)
  3. {
  4. memset(visit, -1, sizeof visit);
  5. maxlen = maxindex = 0;
  6. visit[arr[0]] = 0;
  7. int curlen = 1;
  8. int last_start = 0;
  9. for(int i = 1; i < size; ++i)
  10. {
  11. if(visit[arr[i]] == -1)
  12. {
  13. ++curlen;
  14. visit[arr[i]] = i; /* 记录字符下标 */
  15. }else
  16. {
  17. if(last_start <= visit[arr[i]])
  18. {
  19. curlen = i - visit[arr[i]];
  20. last_start = visit[arr[i]] + 1;
  21. visit[arr[i]] = i; /* 更新最近重复位置 */
  22. }else
  23. {
  24. ++curlen;
  25. }
  26. }
  27. if(curlen > maxlen)
  28. {
  29. maxlen = curlen;
  30. maxindex = i + 1 - maxlen;
  31. }
  32. }
  33. output(arr);
  34. }

最后在给出测试用例

C代码 收藏代码

  1. /* 输出LNRS */
  2. void output(char * arr)
  3. {
  4. if(maxlen == 0)
  5. {
  6. printf(“NO LNRS\n”);
  7. }
  8. printf(“The len of LNRS is %d\n”,maxlen);
  9. int i = maxindex;
  10. while(maxlen—)
  11. {
  12. printf(“%c”,arr[i++]);
  13. }
  14. printf(“\n”);
  15. }
  16. void main()
  17. {
  18. char arr[] = “abcaacdeabacdefg”;
  19. /* LNRS 基本算法 */
  20. LNRS_hash(arr,strlen(arr));
  21. /* LNRS dp */
  22. LNRS_dp(arr,strlen(arr));
  23. /* LNRS dp + hash 记录下标 */
  24. LNRS_dp_hash(arr,strlen(arr));
  25. /* LNRS dp + hash 优化方案 */
  26. LNRS_dp_hash_impro(arr,strlen(arr));
  27. }

    ╝④

§3 小结

这篇文章把字符串最长重复子串和最长不重复子串的求解方法,能有了一定的认识和理解,基本可以掌握这些方法。

发表评论

表情:
评论列表 (有 0 条评论,343人围观)

还没有评论,来说两句吧...

相关阅读

    相关 重复

    给定一个字符串,找到最长的子串,要求该子串中没有重复的字符。 例如: 字符串”abcabcbb”的不含重复字符的最长子串为“abc”,长度为 3。 而“bbbbbb”的不

    相关 重复

    思路:使用后缀数组解决 分析: 1、由于要求最长公共子序列,则需要找到字符串的所有子串,即通过产生字符串的后缀数组实现。 2、由于要求最长的重复子串,则需要对所有子串进行

    相关 重复

    1. 前言 最近看了LeetCode上的一道题,想了很久才摸明白其中的逻辑,网站上有很多答题者提交了自己的代码,实现方式是多种多样,也包含很多种程序语言。因此,我这里记录