題目描述:
最長不重復子串(Longest No Repeat String,LNRS)就是從一個字符串中找到一個連續子串,該子串中任何兩個字符都不能相同,且該子串的長度是最大的。
分析:
解法一:動態規划
動態規划就是用來解決這種最優化問題,關於字符串的很多有趣的問題如最長公共自序列,最長上升子序列等都可以用動態規划來解,這道題我的第一想法也是動態規划。
動態規划的核心在於尋找最優子結構,對於一個字符,如果他與他前面的最長不重復子串都沒有相同的字符,那么他也可以加入這個子串中,構成一個新的子串。即對於字符數組a[],dp[i]表示以a[i]為結尾的最長不重復子串長度,dp[0] = 1,最后取dp中的最大值即可。代碼如下:

1 #include <cstdio> 2 #include <cstdlib> 3 #include <string.h> 4 5 using namespace std; 6 char in[10001]; 7 8 int dp[10001]; 9 10 int LNRS_dp(char *in) 11 { 12 int i, j; 13 int len = strlen(in); 14 int last = 0; // 上一次最長子串的起始位置 15 int maxlen,maxindex; 16 maxlen = maxindex = 0; 17 18 dp[0] = 1; 19 for(i = 1; i < len; ++i) 20 { 21 for(j = i-1; j >= last; --j) // 遍歷到上一次最長子串起始位置 22 { 23 if(in[j] == in[i]) 24 { 25 dp[i] = i - j; 26 last = j+1; // 更新last_start 27 break; 28 }else if(j == last) // 無重復 29 { 30 dp[i] = dp[i-1] + 1;//長度+1 31 } 32 } 33 if(dp[i] > maxlen) 34 { 35 maxlen = dp[i]; 36 } 37 } 38 return maxlen; 39 } 40 41 int main() 42 { 43 freopen("1530.in","r",stdin); 44 freopen("1530.out","w",stdout); 45 46 while(scanf("%s",in)!=EOF) 47 { 48 printf("%d\n",LNRS_dp(in)); 49 } 50 return 0; 51 }
顯然這個方案是O(n2)的復雜度,且對字符種類沒有限制。
解法二:Hash
很多情況下,在描述一個字符串時,都是英文字母組成的字符,因此可能出現的元素是有限的(26個),因此就有了第二種想法,Hash。
這種想法在於使用一個大小為26的數組visited[]記錄每個字符是否出現,然后枚舉最長不重復子串的起點。代碼如下:

1 void LNRS_hash(char* s) 2 { 3 char visited[26]; 4 int i, j; 5 int maxlen = 0; 6 int maxIndex; 7 int len = strlen(s); 8 9 memset(visited, -1, 26); 10 for (i = 0; i < len; i++) 11 { 12 visited[s[i] - 'a'] = 1; 13 for(j = i+1 ; j < len; j++) 14 { 15 if(visited[s[j]-'a'] == -1) //該字符沒有出現 16 { 17 visited[s[j]-'a'] = 1; 18 if(j - i+1> maxlen) 19 { 20 maxlen = j - i+1; 21 maxIndex = i;//最長不重復子串的起點 22 } 23 } 24 else 25 break; 26 } 27 memset(visited, -1, 26); 28 } 29 printf("%d\n", maxlen); 30 }
這也是O(n2)的復雜度。僅適用於字符種類明確的情形。
解法三:動態規划的改進
有了第二種Hash的思想,我們是不是也可以考慮對第一種動態規划的方法進行針對性改造呢?
回顧之前動態規划的解法,枚舉每一個以a[i]為結尾的最長不重復子串,將a[i]與之前的子串元素一一比較,判斷是否出現過。很顯然,在字符種類有限的情況下(如只有字母組成的字符串),這種判重可以用Hash來實現。代碼如下:

1 int LNRS_dp(char *in) 2 { 3 int i, j; 4 int len = strlen(in); 5 int last = 0; // 上一次最長子串的起始位置 6 int maxlen; 7 char visited[26]; 8 memset(visited, -1, sizeof(visited)); 9 dp[0] = 1; 10 visited[in[0]-'a'] = 0; 11 maxlen = 1; 12 for(i = 1; i < len; ++i) 13 { 14 if(visited[in[i]-'a'] == -1 || visited[in[i]-'a']<last)//未訪問過,或者是之前訪問的 15 { 16 dp[i] = dp[i-1] + 1; 17 visited[in[i]-'a'] = i; 18 } 19 else 20 { 21 dp[i] = i - visited[in[i]-'a']; 22 last = visited[in[i]-'a']+1; 23 visited[in[i]-'a'] = i; 24 } 25 if(dp[i] > maxlen) 26 { 27 maxlen = dp[i]; 28 } 29 } 30 return maxlen; 31 }
PS:這是一個OJ的題,http://ac.jobdu.com/problem.php?pid=1530,前兩種方法可以過,第三種方法過不了,求大神指導下,第三種方法哪里沒有考慮到,多謝!
有網友給出了一個更簡潔的解法,思想與解法三是類似的,代碼如下:

1 #include <cstdio> 2 #include <cstring> 3 4 int main() 5 { 6 char s[10001]; 7 while (scanf("%s", s) != EOF) 8 { 9 int p[128]; 10 for (char c = 'a'; c <= 'z'; ++c) 11 { 12 p[c] = -1; 13 } 14 15 int n = strlen(s), right = -1, answer(0); 16 for (int i = 0; i < n; ++i) 17 { 18 if (p[s[i]] > right) 19 { 20 right = p[s[i]]; 21 } 22 p[s[i]] = i; 23 24 if (i - right > answer) 25 { 26 answer = i - right; 27 } 28 } 29 30 printf("%d\n", answer); 31 } 32 33 return 0; 34 }