LeetCode:Distinct Subsequences

本文探讨了如何通过遍历和字典序算法解决不同子串匹配问题,并介绍了一种优化算法,使其复杂度降至O(m*n),同时分析了算法执行过程及优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Given a string S and a string T, count the number of distinct subsequences of T in S.

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).

Here is an example:
S = "rabbbit"T = "rabbit"

Return 3.

这里要说明一下,题目给的描述并不准确。看了Discussion之后才明白,题意是问有多少种S的不同子串subS,使得subS与T相同。这里子串的定义是:在S中删除任意个元素,剩下的元素按照原先在S中的顺序组合成的字符串。

My Solution:

1. 最自然的方法就是遍历。首先找到S与T的长度之差diff,然后在S中删除diff个字符,比较剩余的子串是否和T相等。遍历所有的删除方法,则得结果。实现“删除”的方法是定义一个bool型数组isDeleted,长度与S相同。里面有diff个元素为true,表示该次将删除S里对应位置的元素。遍历的方法为对isDeleted的元素进行全排列,这里采用了字典序算法。

最开始的代码是这样的:

int CombinationNum(int n, int a)
{
    int res = 1;
    for(int i = 0; i < a; ++i)
    {
        res *= n--;
    }
    while(a > 1)
    {
        res /= a--;
    }
    return res;
}

int Compare(const char *s, const char *t, const bool *isDeleted)
{
    while(*t)
    {
        if(*isDeleted)
        {
            ++s;
			++isDeleted;
        }
        else if(*s != *t)
        {
            return -1;
        }
		else
		{
			++s;
			++t;
			++isDeleted;
		}
    }
    return 0;
}

bool* NextFormat(bool *isDeleted, const int len)
{
	int index0 = 0;
    int index1 = 0;
    // find the first false element after any true element
    bool flag = false;  // find any true element?
    while(index1 < len)
    {
        if(false == flag && true == isDeleted[index1])
        {
			index0 = index1;
            flag = true;
        }
        else if(true == flag && false == isDeleted[index1])
        {
            break;
        }
		isDeleted[index1] = false;
        ++index1;
    }
    if(index1 == len)
    {
        return NULL;
    }
    // exchange the index1 and index0 element
    isDeleted[index1] = true;
	index1 -= index0 + 1;
	for(index0 = 0; index0 < index1; ++index0)
	{
		isDeleted[index0] = true;
	}
	// change order from 0 to index1 - 1

    return isDeleted;
}

int numDistinct(char* s, char* t) {
    int slen = strlen(s);
    int tlen = strlen(t);
    int diff = slen - tlen;
    //bool isDeleted[diff] = {false};
    bool *isDeleted = (bool*)malloc(slen * sizeof(bool));
	memset(isDeleted, 0, slen * sizeof(bool));
    for(int i = 0;i < diff; ++i)
    {
        isDeleted[i] = true;
    }
    int maxNum = CombinationNum(slen, diff);
    int validNum = 0;
    for(int i = 0; i < maxNum; ++i)
    {
        if(0 == Compare(s, t, isDeleted))
        {
            ++validNum;
        }
        NextFormat(isDeleted, slen);
    }
    return validNum;
}


需要说明的是,我写的函数NextForm()在字典序算法的基础上,对元素类型为bool的情况进行了改进,有一点小trick在里面,可不必在意。

我定义了一个函数CombinationNum(),用来求排列组合的个数C(slen, diff)。然而当测试数据为S="anacondastreetracecar",T="contra"时,计算C(slen,diff)的返回值为0,调试发现结果溢出。遂删除函数CombinationNum(),依靠NextForm的返回值来判定循环次数。

更改后的代码如下:

int Compare(const char *s, const char *t, const bool *isDeleted)
{
    while(*t)
    {
        if(*isDeleted)
        {
            ++s;
			++isDeleted;
        }
        else if(*s != *t)
        {
            return -1;
        }
		else
		{
			++s;
			++t;
			++isDeleted;
		}
    }
    return 0;
}

bool* NextFormat(bool *isDeleted, const int len)
{
	int index0 = 0;
    int index1 = 0;
    // find the first false element after any true element
    bool flag = false;  // find any true element?
    while(index1 < len)
    {
        if(false == flag && true == isDeleted[index1])
        {
			index0 = index1;
            flag = true;
        }
        else if(true == flag && false == isDeleted[index1])
        {
            break;
        }
		isDeleted[index1] = false;
        ++index1;
    }
    if(index1 == len)
    {
        return NULL;
    }
    // exchange the index1 and index0 element
    isDeleted[index1] = true;
	index1 -= index0 + 1;
	for(index0 = 0; index0 < index1; ++index0)
	{
		isDeleted[index0] = true;
	}
	// change order from 0 to index1 - 1

    return isDeleted;
}

int numDistinct(char* s, char* t) {
    int slen = strlen(s);
    int tlen = strlen(t);
    int diff = slen - tlen;
    //bool isDeleted[diff] = {false};
    bool *isDeleted = (bool*)malloc(slen * sizeof(bool));
	memset(isDeleted, 0, slen * sizeof(bool));
    for(int i = 0;i < diff; ++i)
    {
        isDeleted[i] = true;
    }
    int validNum = 0;
    do
    {
        if(0 == Compare(s, t, isDeleted))
        {
            ++validNum;
        }
    } while(NextFormat(isDeleted, slen));
    return validNum;
}

这次代码的逻辑写对了。然而当提交之后,提示Time Limit Exceeded。带入测试数据S="dbaaadcddccdddcadacbadbadbabbbcad",T="dadcccbaab"后在VS2010上跑了一次,发现结果是正确的,但确实时间有点长(Debug模式下10s,Release模式下4s)。

这个算法的复杂度主要在求解全排列这里。

待改进。

one of others' solutions:
找到一个碉堡了的代码,O(m*n)的复杂度就解决了问题:

int numDistinct(string s, string t) {
    int tLen = t.size();
    vector<int> prefixVec(tLen+1,0);
    prefixVec[0] = 1;
	for (int i = 0; i < s.length(); ++i)
	{
		char c = s[i];
        for (int j = tLen-1;j >= 0;--j)
		{
            if (t[j] == c)
                prefixVec[j+1] += prefixVec[j];
		}
	}
    return prefixVec.back();
}
不得不说,数学的力量是伟大的!

代码执行过程自己走了两遍,有了点感觉。下面尝试着分析一下。

输入字符串S和T(参数中为小写,这里为了清晰,采用大写S和T),长度分别为sLen和tLen。建立一个vector,长度为tLen+1。vecotr中下标为0的元素恒置为1,下标为1~tLen的元素,分别对应T下标为的0~tLen-1的元素。vector中下标为j的元素的意义为numDistinct(s, t.substring(0, j))的返回值——即取T中的前j个元素组成一个子串Tj,在s中按原先顺序删除若干元素后组成的,与Tj相等的字串数量。

按照最简单的办法,用几组测试数据来说明。

当s = "aabb", t = "a"时

prefixVec = {1, 0}

第一层循环是对s中的字符遍历,这里共有四次。每次循环中,对t中的字符也进行遍历,若s中当前字符与t中当前字符相等,则执行某个奇怪的复制操作。

第一次外层循环,c = 'a'。而t只有一个元素,所以内层循环只有一次,t[j]取‘a'。此时c == t[j]成立,故prefixVec[1] += preficVec[0]。显然加完之后prefixVec = {1, 1}。这里大家显然就要问了,为啥prefixVec的第0个元素是1呀?如果仔细观察的话会发现,只有当j=0的时候,+=右面的操作数才会用到prefixVec[0]。也就是说,只有当s的某个字符匹配到了t的首字符,prefixVec[1]才会执行+=1的操作,此时的意义就是,在s中选取子串的时候,可以以s[i]为子串头,这样恰好可以匹配t的第0个元素。

相应的,在第二次外层循环的时候,又执行了一遍prefixVec[1]+=prefixVec[0]。而第三、第四次外层循环,因为s[i]都是b,所以没有和t[j]匹配,所以什么也没有执行。

循环完毕,此时prefixVec = {1, 2}。prefixVec的第1个元素(prefixVec[1])所表示的意义就是,若要匹配t的前1个元素构成的子串(这里t的长度为1,所以这个“子串”也就是原来的t),在s中有两种子串的取法。


所以,当t的长度为1的时候,大家应该明白这个算法是怎么回事了。

本来想用数学归纳法去证,tLen=k-1时成立,故tLen=k时也成立。可发现程序并不是先处理完前k-1个元素,再处理第k个元素的。所以用数学归纳法只能对sLen进行归纳。但如果能把tLen从1推广到n,那么sLen其实是不需要推广的(在证明的时候sLen就已经是变量了)。

下面举例,当s="aabb", t="ab"的时候。

由于有了上面第一次的执行过程举例,这里就不再详述了,只说一下具体的每层循环的操作。

第一次外层循环,c = 'a'。当j='a'的时候符合t[j]==c的条件,执行prefixVec[1]+=prefixVec[0],此时prefixVec={1, 1, 0}。

第二次外层循环,同样也只执行了一次prefixVec[1]+=prefixVec[0],此时prefixVec={1, 2, 0}。

这时prefixVec前两个元素的值和上一个例子的运行结果是相同的。其中,prefixVec[0]的值是给定的,prefixVec[1]的值的意义是,若在s(的前两个元素,注意这时只循环了两次)中找不连续子串,匹配t的前1个元素,有2中匹配的子串取法。

第三次外层循环,c='b',当j=1时,t[j]==c成立,prefixVec[2]+=prefixVec[1],这条语句的意义是,在之前的s[0]~s[1]中,匹配到t[0]可以有prefixVec[1]这么多种匹配的方法,现在s中的s[2]可以匹配t[1]了。那么之前的prefixVec[1]种匹配都可以各延长一个元素,s中的不连续子串可以延长一个s[2],之前匹配的t[0]现在可以延长到t[0]~t[1],所以perfixVec[2]可以加上之前的prefixVec[1],表示在s[0]~s[2]中,又多了prefixVec[1]种不连续子串,可以匹配到t[0]~t[1]。此时,prefixVec={1, 2, 2}

同理,第四次外层循环中,有prefixVec[2]+=prefixVec[1],至此,prefixVec={1, 2, 4}。

故,该算法圆满地实现了题设的要求。

需要强调的是,对t的遍历是从后向前遍历,是因为这样每次在+=操作时,右边的操作数总是上一次外层循环的结果。若对t从前向后遍历,则有可能+=右边的操作数prefixVec[j]在上一次内层循环的时候已经加过了,假设加上的数是k,就表示s[i]和t[j-1]可以有k种不同的匹配方式。可s[i]一旦和t[j-1]匹配了,那么就不能喝t[j]匹配了。所以再执行prefixVec[j+1]+=prefixVec[j]的时候就会得到错误的结果。

对s的遍历是从前向后遍历,是因为在s中取不连续子串时,s中元素的顺序保持不变。所以,从前到后的遍历顺序保证了只有当s的前i-1个元素匹配了t,才可以计算s的第i个元素是否可以匹配t的某个元素。这样就保证了找到的所有匹配中,s的不连续子串都是保持原有顺序的。


解释了这么多,其实如果自己拿例子来在纸上画两遍基本上就清楚了。

然而,让我自己设计这个算法,我绝壁是设计不出来的。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值