最长连续三字符子串问题

注:本文中python代码均未经过运行测试,因此可能会有错误的地方。



"对于有拖延症的人,在你想起一件事情却又因为懒惰不愿意做的时候,最好还是先去做三分钟试试看。"


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------


昨天去面试一个公司,被问了几道题,结果没有回答很好,今天想了想,差不多有思路,我就写一下。


面试官给我笔和纸,先问我数组二分查找怎么写,我写了个递归的,然后他说写个非递归的,我想了好久没想出来,他说这个很简单的啊,他说控制住边界写不就可以了,我这才想起用while写,写出来了就开始下一个问题了,也就是这里要说的问题。


问题描述:给定长度为n的字符串s,求s中的最长连续子串,要求子串中只包含三个不同的字符。


我一听就兴奋了,我这两天刚好在看算法呢,这个感觉不难,想想怎么做。


他说你就说你一听到这个想起来用什么方法了,就能完成就好,不用想太复杂的。


我说最简单的就是O(n2)的了,从s[0]开始,往后面搜索,一直到不满足条件,再去搜索s[1],就这样一直搜。

#python

s = input()
temp = ""
maxlen = 0 
for i in range(len(s)):
    for j in range(i,len(s)):
        if(s[j] not in temp):
            temp = temp + s[j]
        if len(temp) > 3:
            if( j - i + 1 > best ):
                best = j - i + 1
            break # python 里面有 break?
print (best) 


然后他说还有别的吗,我说还有就是O(n*logn)的分治了,他说你说一下。


我说就是把s分成两部分s[0 :k],s[(k+1) : (n-1)],s中最长子串只能是在s[0:k] 或者  s[ k+1 : n -1 ]中 或者是 包含s[k],s[k+1]在内的子串。
求 s[0:k] , s[k+1 : n-1]可以递归求解,求包含s[k] , s[k+1] 在内的子串有点复杂,但是可以在O(n)内做到。因此时间就是O(n*logn) 。


他想了想说你这个方法不行,说让我实际做一下,他给了个十几个字母的字符串,让我实际试试。我说先分两份,然后继续分,分到最后了,得到一个只有2个字母的字符串,我不知道接下做什么,我有点心慌,他说接下来怎么做,我说我没想清楚,我说做不下去了,他说你最后分了以后得到的不是一个完整的字符串了,他说你这个方法对这个题目怎么能适用呢,我说是我想错了。


后来我回来的时候认真想了想,我的分治法是可以的,这是后话,下文再说为什么可以。


他说还有别的方法吗,我说还可以动态规划,他说怎么动态规划,我说我还没想好动态规划方程式是什么,他问动态规划问题特性是什么,我说重复的子问题,他说这个有吗,我说这个没有,他说那怎么用动态规划求呢,我说似乎是。


今天我看了看书,想了想,我的动态规划也是可行的,可惜那时候没有书在身边,没灵感,也是自己读书不认真,那时候没有想通。具体解法后文说。


然后他说你对这个问题有没有可以改进的地方,我说我想想,我说改进哪个?他说就你给出的那个方法吧,我说是O(n2)吗?他说是的。


我思考怎么改进。


我没想出来怎么会有太大的改进。我觉得这个方法的O(n2)是不可以避免的。我说能改吗,他说你看看你这个方法最耗时的操作是什么,我想了想,我找不到。


现在我想他说的那个改进应该就是改成了动态规划了,不过那个可以说是另一种思路了,不算是对这个方法的改进,不过人家那时候秒想出来动态规划,也是厉害。


我继续说。


我说您看看这么改行吗,他说怎样,我说加上剪枝的条件判断,如果剩余长度小于已知的best,就不用继续循环了,因为肯定得不到更好的。他说你这样的话复杂度不还是O(n2),他说这基本没有改进,我说是的。


我又想了想,没有想到别的改进。


我说我想不到别的改进了,他说时间差不多了,他说今天就这样吧,他说你先回去吧等我们给你回复,我说嗯。


我没有被录用,我心情不太好。


我回去的时候想这个怎么改进,想到了先对s进行一次预处理,把相同元素压缩;或者把s中元素按照出现次数排序,按照次数从多到少进行搜索或者怎样。但是都觉得不好。


然后,昨天的事情就成了昨天的事情了。




我说一下后来我想到的分治法以及动态规划法。


分治法思路:
还是上面说的那样,不过遇到了字符串长度少于3的,就可以直接求出最长的部分,然后直接返回1,2或者3的。中间s[k]向两边搜索可以向左多搜索一个,再向右多搜索一个,知道不满足条件,然后跟左右两部分的比较,取最长的。

#python 33
s = 0
s = input()
def divide( a , b ):
    global s
    if (b - a <= 3):
        return (b - a)
    k1 = (b - a) / 2 + a
    k2 = k1 + 1
    leftlen = divide(i , k1)
    rightlen = divide(k2 , j)
    midlen = getMidLen(a , b)  # 获取包含k1,k2的最长子串,现在还没想好具体方法,感觉O(b - a)的时间是可以的。
    return max(lefilen , rightlen , midlen)


divide( 0 , len(s) - 1)

        
我现在想到了获取包含中间的最长子串(该子串记为ms)长度求法了。之前忽然卡壳是因为ms向左边搜索,不止一种情况可能是最长的(即不能用贪心法获得),举例示意:......ab|cd......假设从”|“的地方划分,左边b为s[k1],右边c为s[k2],那么该子串可能是 "......abc"  .也可能是  "bcd.....",当然这里就不会是左右都能延伸的了,但是程序运行的时候没办法判断。所以我当时没有一个解题方向。
思路如下:
观察得到ms肯定包含s[k1],s[k2],所以只有三种情况:s[k1]左边有除s[k1],s[k2]外的0个字符,1个字符,2个字符(只能有这三种情况,左边的情况确定后,右边的也随之确定)。

#python33

def getMidLen(a , b):
    global s
    k1 = (b - a)/2 + a
    k2 = k1 +1
    ms = s[k1] + s[k2]
    best = 0
    for i in range(3):
        temp = 0
        while k1 >= a:
            if(s[k1] in ms):
                temp = temp + 1
            elif(len(ms) < i + 1):
                ms = ms + s[k1]
                temp = temp + 1
            else:
                break
            k1 = k1 - 1
        #左边的i个字符都搜索到了,下面是右边的
        while k2<=b :
            if(s[k2] in ms):
                temp = temp + 1
            elif (len(ms) <= 3):
                ms = ms + s[k2]
                temp = temp + 1
            else:
                break
            k2 = k2 + 1
        if temp > best:
            best = temp
    return best


上面getMidLen(a,b)的时间复杂度为O(3*(b - a)) = O(n),
所以分治长度为n的字符串的时间为 T(n) = 2*T(n/2) + O(n)。    得T(n) = O(n * logn)。(具体求法就是不断带入,这里不多说了)。



动态规划思路:
b是一个结构, 表示s中的一个(不一定是最长的)只包含三个字符的字符串。

struct _b
{
    int len;
    char cs[3] = {0};
    int locs[3] = {-1 , -1 , -1}; int loce[3] = {-1 , -1 ,-1};
};
typedef struct _b b;

设b[ i ].len = s[0 :i]中最长的子串长度,并且b[i]表示的这个子串是包含s[ i ]的。b[i].c包含三个字符,b[i].locs和 b[i].loce分别表示三个字符对应的在s[0 :i] 的最长子串中,第一次和最后一次出现的位置。   
举例示意:如果s = ”abccd“,那么b[3]的c = [ 'a' , 'b' ,'c '] , loce = [ 0 , 1 , 3] ,locs = [0 , 1 , 2]。
b的结构之所以这样设置,是因为在利用b[ i ]求b[ i  + 1 ]时会用到b[ i ]中最长子串的三个字符以及其最后一次出现的位置。
这样s最长子串就是 b[0].len , b[1].len ,....b[n-1].len 中的max值。


求b[i]时:
1 :若s[i]在b[i - 1].c中,那么  b[i].len = b[i - 1 ].len + 1 , b[i].c = b[i - 1].c 。b[i].locs = b[i-1].locs , b[i].loce = b[i-1].loce (”=“此处为赋值,实际代码中不是这样的。此处简写。),再  修改s[i]对应的那个字符的loce为i(最后出现的位置为i);
2:若s[i] 不在  b[i - 1].c 中 ,b[i]应该包含  b[i - 1].c中三个字符的后两个出现的字符,加上s[i] 。b[i].locs和b[i].loce为其各自对应的出现的位置。b[i].len = i - b[i].lens中最小的值。(b[i].len 即b[i]对应的字符串的长度,再次强调,b[i]对应的字符串是包含s[i]在内的)。
1操作时间复杂度为O(1),2操作时间复杂度为O(1)。
最重要的动态规划的方程式就是:
b[ i ].len = b [ i ] .len + 1 ,s[i]在b[i].c中;
或者是修改完b[i].c ,b[i].locs ,b[i].loce后 , b[i].len = i - min(b[i].locs)。


代码就不打了,有点复杂。但是思路应该是没问题的。只需要for循环一次算出b[0 : n-1],并且在计算过程中一直维护一个int best的值为最大的b[i].len就可以了。






就是分治的那个 从中间开始往两边搜索的函数还没想好具体什么样子,想一想明天可以的话就加上去。

-----今天我加上去了:)






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章