题目分析
给出一个字符串,求出最长的没有重复字符的子串。
解题思路
记录法:首先我们必须知道怎么判断一个子串的字符是否都只出现一次可以开一个足够大的布尔数组,然后每个字符是否出现记录在对应ASCLL码值的下标位置,初始化数组都为0一旦遍历到某个字符CH,若arr[ch]==1则表示CH已经在之前出现过,那我们我们就可以断定这个子串不是题目要求的子串。
解法一:
通过枚举子串的左右下标直接枚举子串,然后对于这个子串用记录法扫一遍。教育学习语言文字一n^2个子串,“扫一遍”复杂度是O(n)时,总体时间复杂度是O( ñ^ 3),虽然难以接受,但是我们可以在此算法基础上进行改进。
解法二:
对于一个给定的子串起始点I,只我们关心它最远能延伸到哪儿。所以我们只需要枚举子串起点I,此时清空记录数组,在不断增大右边界Ĵ的同时更新记录数组,一直更新到串的右边界,或者找到某一个已经出现过的字符。那么我们就可以断定这个Ĵ是起始点我对应的右边界,更新答案即可。枚举左边界时间复杂度O( n)时,延伸右边界也是为O(n),总体时间复杂度为O(n ^ 2)。
解法三:
我们继续改进解法二,如果对于某一个起始点我我们已经找到了他的最远右边界Ĵ,那么在解法二中我们接下来需要枚举+ 1,然后又要开始重新延伸一遍寻找右边界。但是我们发现串[I … j]的相比于串第[i + 1 … j]的只少了一个字符[I],回忆我们记录数组的作用,我们完全可以擦除对于字符[I ]的记录,然后对于i + 1的开始的串,它一直到Ĵ必然是满足的(因为即使增加一个[I]字符都能满足),所以我们可以在上一次的基础上(J)往右延伸。
更一般的:
找到字符串从[0]开始的最右边界[j]时,使得子串[0 … j]的是合法的子串;
每次左边界我加一,擦除[I]的记录,右边界从我时的右边界[j]的开始延伸到[J ‘],此时的第[i + 1 … J’]便是新的合法子串;
每次更新答案。
这个算法中主要是两个指针I,J,两个指针都是从0开始最远移动到N,并且在移动的过程中是一直增加的,故时间复杂度是O(变化(ⅰ)+变化(J))= O(2 * N)= O(N)。
总结:
三解法其实的英文一种尺取法,尺取法一般的做法就是定左右两个指针,每次移动左指针后更新右指针(当然两者都必须是递增的),故这个算法的英文名也叫2指针。我们在解法三的末尾已经证明了尺取法的时间复杂度是O(n),那么什么样的题能够用尺取法优化呢?答案就是当我们稍大状态的问题成立时比他的子状态也必须成立,在此题中某如果个子串[I … j]的是合法的,那么对于任意的i<=i’<=j’<=j,都有子串[I ‘… J’]是合法的。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
bool vis[233] = {0}; int n = s.length ();
int l = 0, r = 0;
while (r < n && !vis[s[r]]) {
vis[s[r]] = true;
r++;
}
int ans = r-l;
while (l < n) {
while (r < n && !vis[s[r]]) {
vis[s[r]] = true;
r++;
}
ans = max (ans, r-l);
vis[s[l]] = 0;
l++;
}
return ans;
}
};
题目分析
给定一个起始单词和终结单词和一些单词,问能不能在每次只变一个字母的情况下,找出所有的从起始到终结单词的变化路径。
解题思路
首先对题目进行抽象,对于可以相互变化的单词建立边。
题目就变成了,给定一个无向图,求起点s到终点t的所有最短路径。
最短路径好求,只要bfs一遍就可以。比较难的部分在于求出所有的最短路径。假设我们已经求出了dis(v),代表点v到起点的最短路径长度,令L=dis(t)。
假设某一条最短路径为s->a->b->c->…->t,那么路径的长度为L,而且dis(s)=0,dis(a)=1, dis(b)=2,dis©=3…dis(t)=L。为什么呢?首先dis(t)不能<L,因为代表我们找到了一条更短路,与最短路矛盾。dis(t)也不能大于L,因为这不是最短路径。也就是说,最终答案中的每一条边(u,v)都有dis(v)=dis(u)+1。也就是说我们只要从点s开始,每一次只走dis(v)=dis(u)+1的边,一直走到点t,那么这个时候就是一条合法路径。
但是这么搞会搜到一些dis(u)=dis(t) (u!=t)的路径。我们可以从反面去想,去找点t到点s的路径,每一次只走u->v且dis(v)=dis(u)-1的边,走了L步后一定会走到点s, 因为最短路径长度为0的只有点s一个点,这样就做到了每一次搜出来的路径都是合法的。
例子:
令起点为ab,终点为cd,单词为[ad,cd,cb,cd,aa]。 首先对单词进行编号ad->0,cd->1,cb->2,cd->3,aa->4,ab->5
那么可行边是(0,1),(0,3),(0,4),(0,5),(1,2),(1,3),(2,3),(2,5),(4,5),起点为5,终点为1。 首先bfs得出dis(5)=0,dis(2)=1,dis(4)=1,dis(0)=1,dis(1)=2,dis(3)=2。
然后我们从点1出发,合法的边有(1,0),(1,2),边(1,3)不行因为dis(3)!=dis(1)-1
假设我们走了边(1,0),下一个能走的是边(0,5),其他的都不合法,这时候dis(5)=0,找到了一条路径(1,0,5)
假设我们走了边(1,2),下一个能走的是边(2,5),其他的都不合法,这个时候dis(5)=0,找到了一条路径(1,2,5)。
至此所有的路径都找出来了。
class Solution { public:
vector<vector> findLadders(string beginWord, string endWord, vector& wordList)
{
int n=wordList.size();
unordered_map<string,int> mp;
for(int i=0;i<n;i++) mp[wordList[i]]=i;int begin; if(mp.find(beginWord)==mp.end()) { wordList.push_back(beginWord); ++n; mp[wordList[n-1]]=n-1; begin=n-1; } else begin=mp[beginWord]; if(mp.find(endWord)==mp.end()) return {}; int end=mp[endWord]; vector<vector<int>> g(n); for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) if(canChange(wordList[i],wordList[j])) g[i].push_back(j),g[j].push_back(i); const int inf=n*10; vector<int> dis(n,inf); dis[begin]=0; queue<int> que; que.push(begin); while(!que.empty()) { int u=que.front();que.pop(); for(int i=0;i<g[u].size();i++) { int v=g[u][i]; if(dis[v]>dis[u]+1) //路径松弛条件,同时也避免了无限宽搜。 { dis[v]=dis[u]+1; que.push(v); } } } if(dis[end]==inf) return {}; cur.push_back(wordList[end]); genAns(end,g,dis,wordList); for(int i=0;i<ans.size();i++) reverse(ans[i].begin(),ans[i].end()); return ans; } vector<vector<string>> ans; vector<string> cur; void genAns(int u,vector<vector<int>> &g,vector<int> &dis,vector<string> &wordList) { if(dis[u]==0) { ans.push_back(cur); return; } for(int i=0;i<g[u].size();i++) { int v=g[u][i]; if(dis[v]+1==dis[u]) { cur.push_back(wordList[v]); genAns(v,g,dis,wordList); cur.pop_back(); } } }; bool canChange(string &a,string &b) { int tot=0; for(int i=0;i<a.size();i++) if(a[i]!=b[i]) ++tot; return tot==1; } };