算法题:求一个序列S中所有包含T的子序列(distinct sub sequence)

题:

给定一个序列S以及它的一个子序列T,求S的所有包含T的子序列。例:

S = [1, 2, 3, 2, 4]

T = [1, 2, 4]

则S的所有包含T的子序列为:

[1, 2, 3, 2, 4]

[1, 2, 3, 4]

[1, 2, 2, 4]

[1, 2, 4]

解:

首先可以拆解为两个问题:

1. 求S的所有子序列;其中又涉及到去重的问题。

2. 求S的所有子序列中包含T的子序列。

暂时先不考虑去重,看看问题1怎么解:

一、求S的子序列

单纯求一个序列的所有子序列的话,就是求序列的所有组合。一般的思路为:S中每个元素有输出和不输出两种状态,解集为所有元素是否输出的状态组合。由于两个元素有两种状态,所以解集的大小就是2的n次方(n为S的长度),也就是一个长度为n的二进制序列的所有可能值。所以求所有子序列的代码:

void PrintDistinctSubByFlags(char* seq, int seq_len, bool* seq_flags)
{
    printf("\r\n");
    char buf[] = "  ";
    for (int i = 0; i < seq_len; ++i)
    {
        if (seq_flags[i])
        {
            buf[0] = seq[i];
            printf(buf);
        }
    }
}

void DistinctSubInner(char* seq, int seq_len, bool* seq_flags, int seq_flags_idx)
{
    if (seq_flags_idx >= seq_len)
    {
        PrintDistinctSubByFlags(seq, seq_len, seq_flags);
        return;
    }

    seq_flags[seq_flags_idx] = false;
    DistinctSubInner(seq, seq_len, seq_flags, seq_flags_idx + 1);
    seq_flags[seq_flags_idx] = true;
    DistinctSubInner(seq, seq_len, seq_flags, seq_flags_idx + 1);
}

void DistinctSub(char* whole_seq)
{
    if(!whole_seq || !*whole_seq)
    {
        return;
    }

    bool* seq_flags = new bool[strlen(whole_seq) + 1];
    DistinctSubInner(whole_seq, strlen(whole_seq), seq_flags, 0);
    delete seq_flags;
}

可以换一个方式来写,不使用位数组标记,而是从序列的首元素开始处理,分输出和不输出两种情况,再递归处理首元素之外的子序列,这样可以直接生成输出序列(只包含要输出的元素):

void DistinctSubInner(char* whole_seq, char* sub_seq, int sub_seq_len)
{
    if (!*whole_seq)
    {
        PrintDistinctSub(sub_seq_len);
        return;
    }

    sub_seq[sub_seq_len] = *whole_seq;
    DistinctSubInner(whole_seq + 1, sub_seq, sub_seq_len + 1); // output head of S
    DistinctSubInner(whole_seq + 1, sub_seq, sub_seq_len); // not ouput head of S
}

void DistinctSub(char* whole_seq)
{
    if(!whole_seq || !*whole_seq)
    {
        return;
    }

    sub_seq = new char[strlen(whole_seq)];
    DistinctSubInner(whole_seq, sub_seq, 0);
    delete sub_seq;
}

在这个基础上,可以再加入子序列的匹配逻辑:

二、求S中所有包含T的子序列

void DistinctSubInner(char* whole_seq, char* min_seq, char* sub_seq, int sub_seq_len)
{
    if (!*whole_seq)
    {
        if(!*min_seq)
        {
             PrintDistinctSub(sub_seq, sub_seq_len);
        }
        else
        {
             // unmatch sub sequence
        }
        return;
    }

    sub_seq[sub_seq_len] = *whole_seq;

    if (*whole_seq == *min_seq)
    {
         // 1. output head of S and match head of T
         DistinctSubInner(whole_seq + 1, min_seq + 1, sub_seq, sub_seq_len + 1);
    }

    // 2. output head of S but do not match head of T
    DistinctSubInner(whole_seq + 1, min_seq, sub_seq, sub_seq_len + 1);

    // 3. do not ouput head of S
    DistinctSubInner(whole_seq + 1, min_seq, sub_seq, sub_seq_len);
}

void DistinctSub(char* whole_seq, char* min_seq)
{
    if(!whole_seq || !*whole_seq || !min_seq || !*min_seq)
    {
         return;
    }

    char* sub_seq = new char[strlen(whole_seq) + 1];
    DistinctSubInner(whole_seq, min_seq, sub_seq, 0);
    delete sub_seq;
}

这里S的首元素是否输出和是否和T的首元素进行匹配一共有三种组合:

1. S的首元素输出,但是不和T的首元素匹配(不相同无法匹配或故意不匹配);

2. S的首元素输出,且S的首元素与T的首元素相同,进行匹配;

3. S的首元素不输出。

如果S的首元素不输出的话,自然就不能和T进行匹配,所以没有第4种可能。

那么,这里有一个问题,如果S的首元素和T的首元素相同时,为什么要分匹配和不匹配两种情况呢?原因在于如果S中包含两个相同的元素能够进行匹配的话,这么做可以使T中对应元素能够匹配到S中的不同位置,从而形成。

接下来再考虑重复元素的问题。

三、去重

我看到的重复问题有两类:

1. 如果S中有多个位置能够匹配T中某一个元素的话,是否需要匹配不同位置?

2. 当已生成的输出序列中有连续两个相同的元素时,会形成重复解。

分开来看。

3.1 是否需要匹配不同位置?

这其实也是两个子问题:

a. 匹配不同位置会不会造成重复的解?

以S=[1,2,3,2,4], T=[1,2,4]为例。T中第2个元素可以匹配到S的第2和第4个位置。

显然,在S中两个可选匹配位置(第2和第4)之间的区域(此例中第3个元素)不输出的情况下,匹配到这两个位置的结果集是相同的,所以匹配到不同位置会有重复解。

b. 只匹配单一位置的话会不会造成漏解?

如果只匹配S中的第一个可选位置的话,那么输出解的组合可以更改为:

1. 输出S的首元素,并且如果S的首元素与T的首元素相同,就匹配;

2. 不输出S的首元素。

这样问题就转换成了:这样一个逻辑形成的解空间,能否够覆盖将T中元素匹配到其它位置形成的解空间?为了解答这个问题,我们需要再回到用位数组来表示解的表达方式。如果S中某个元素被匹配的话,那么它必定是要输出的,解空间类似:

{x,x,x,x,1,x,x...} // x表示状态未定0|1,0表示不输出,1表示输出;

以上面的例子来说:

如果T中第2个元素匹配S中的第2个元素,解空间为: {1,1,x,x,1} & {1,0,x,1,1} = {1,x,x,x,1}

如果T中第2个元素匹配S中的第4个元素,解空间为: {1,x,x,1,1},显然是{1,x,x,x,1}的子集。

所以,结论是只需要S中的任何一个位置即可。

这样程序就变成了:

void DistinctSubInner(char* whole_seq, int sub_seq_len)
{
    if (!*whole_seq)
    {
        if(!*min_seq)
        {
             PrintDistinctSub(sub_seq_len);
        }
        return;
    }

    sub_seq[sub_seq_len] = *whole_seq;

    if (*whole_seq == *min_seq)
    {
        DistinctSubInner(whole_seq + 1, min_seq + 1, sub_seq_len + 1);
    }
    else
    {
        DistinctSubInner(whole_seq + 1, min_seq, sub_seq_len + 1);
    }

    DistinctSubInner(whole_seq + 1, sub_seq_len);
}

3.2 如何解决连续相同元素造成的重复解?

以[1, 1]例,它的子序列为:

[1, 1]

[1]    只输出第一个元素

[1]    只输出第二个元素

解决此问题的一个简单的做法是,如果序列中有连续相同的元素,则在第一个元素输出的情况下,忽略第二个元素不输出的解。

void DistinctSubInner(char* whole_seq, int sub_seq_len)
{
    if (!*whole_seq)
    {
        if(!*min_seq)
        {
             PrintDistinctSub(sub_seq_len);
        }
        return;
    }

    sub_seq[sub_seq_len] = *whole_seq;

    if (*whole_seq == *min_seq)
    {
        DistinctSubInner(whole_seq + 1, min_seq + 1, sub_seq_len + 1);
    }
    else
    {
        DistinctSubInner(whole_seq + 1, min_seq, sub_seq_len + 1);
    }

    if(sub_seq[sub_seq_len] != sub_seq[sub_seq_len - 1])
    {
        DistinctSubInner(whole_seq + 1, min_seq, sub_seq_len);
    }
}

需要注意的是,不能在S上作这个判断,因为形如[1,2,1]的序列,虽然两个1不连续,但在2不输出的情况下,同样会形成重复解。所以只能在生成的结果序列上作处理。

事实上,S中可能不只是包含两个相同的元素,还可能包含两个相同的子序列,例如[1,2,1,2],上面的逻辑似乎并不能防止形成重复的[1,2]解。但是,很有趣的是,上面代码就是不会导致形成重复的子序列解。为什么?请跟我一样看下测试结果的输出,大概就能看出原委了,这个已经不知道怎么表达了...

所以,暂时的解就是这样。补上测试代码:

int main(int argc, _TCHAR* argv[])
{
    char* whole_seqs[] = {
    "1",
    "124",
    "1234",
    "1124",
    "1224",
    "1244",
    "12324",
    "135635624",
    "1323245",
    "13523524",};

    for (int i = 0; i < sizeof(whole_seqs) / sizeof(char*); ++i)
    {
        printf("\r\n\r\ndistinct sub sequence of \"%s\" for \"%s\" : ===============", whole_seqs[i], "124");
        DistinctSub(whole_seqs[i], "124");
    }

    getchar();
}
 

后话

作为一个算法的话,这显然不能说over了。

首先,算法的正确性,应该做作大量的测试验证。譬如,还有没有没有想到的重复解的情况?对于这个算法,我并没有十分的把握,还需要验证来发现问题并确认正确性。

其次,在去重这一块,我觉得我的思路似乎并不直观,以及并没有和数学里的问题应对起来。而现实中很多算法其实都能转换为数学问题。所以我怀疑有更简洁和明了的思路。

上午百度了一下,网上有一堆人问这个,想必应该是别的思路的,不过我还是先把我的思路记下来。再去学习。

顺便再吐槽一下,博客园的编辑器依然烂成翔...等宽代码字体都没有,代码缩进又被吃了。有个高亮的功能聊胜于无吧...本地纯文本或markdown但是consolas+雅黑字体好太多了,线上的就只能将就看下。

时间: 2024-10-19 11:42:54

算法题:求一个序列S中所有包含T的子序列(distinct sub sequence)的相关文章

求一个序列中两个只出现一次的数

当然了,O(1)空间复杂度是必须的... 先看一个简单版: 求出一个序列中一个只出现一次的数 COJ 1217 奇数个的那个数 http://122.207.68.93/OnlineJudge/problem.php?id=1217 我们知道任意两个相同的数 异或结果为0  任何数与0异或结果是其本身  异或运算满足交换律 亦即:a^a=0     a^0=a      (a^b)^(a^b)=(a^a)^(b^b)=0^0=0 这样我们就得到了一个用异或运算的解法 1 #include<std

每天一道算法题:数字二进制形式中1的个数

题目:请实现一个函数,属于一个整数,输出该数二进制表示中1的个数,例如把9表示成二进制是1001,有2位为1.因此如果输入9,该函数输出2. 可能的死循环陷阱 看完题目,相信大家很快就能想到一个解题思路:先判断整数二进制表示中最右边的一位是否为1,接着把输入的整数右移一位,此时原来处于从右边起的第二位被移动至最右边了,再判断是不是1,这样每次移动一位,直到这个整数变成0,即能够得到整数二进制表示形式中1的个数,而现在问题变为如何判断数字的最后一位为1,其实这个也很简单,只需要将数字与1做与运算,

Excel-判断一个文本字符串中是否包含数字! 判断一个文本字符串是否是纯汉字!

0.判断一个文本字符串中是否包含数字!/判断一个文本字符串是否是纯汉字! 公式=IF(LENB(A1)=2*LEN(A1),”都是汉字“,“含有非汉字字符”) 解释函数: LEN(A1)#返回文本字符串中的字符个数:  ##双字字符*1*双字节字符个数+单字节字符*1*单字节字符个<=>计算字符个数: LENB(A1)#返回文本字符串中的字符个数.与双字节字符集(DBCS)一起使用.##双字节字符*2*双字节字符个数+单字节字符*1*单字节字符个数<=>计算字节个数: 字符:分为双

c#生成一个某文本中不包含的随机字符串

//生成一个某文本中不包含的随机字符串 private static string GetRandomStr(string allStr) { int number; string resStr; do { resStr = string.Empty; Random random = new Random(); for (int i = 0; i < 9; i++) { number = random.Next(); number %= 36; if (number < 10) { numbe

求一个序列中的主元素

问题(2013 统考408真题):已知一个整数序列A = (a0,a1,...,an-1), 其中0≤ai≤n (0≤i<n).若存在ap1=ap2...=apm=x且m>n/2 (0≤pk≤n,1≤k≤m),则称x为A的主元素.例如,A=(0,5,5,3,5,7,5,5),则5为主元素,又如A=(0, 5, 5,3, 5, 1, 5,7),则A中没有主元素.假设A中的n个元素保存在一个一维数组中, 请设计一个尽可能高效的算法,找出A的主元素.若存在主元素,则输出该元素:否则输出-1. 解答:

求一个序列的所有组合

在C++ STL标准模板库中已经有线程的思想,这样就是介绍STL中的思想. 其实也可以使用递归的方法解决,后续问题,STL中的方法也解决了有重复字符的问题. 思路: 借助了字典序的方法,首先将序列按照升序进行排序(当然也可以使用降序排列,都是一样的道理),将这个序列作为一个字典序的输入序列,从这个字典序如何变换出下一个字典序列呢?对这个序列从后往前进行搜索,找到一对相邻的升序元素,元素的位置分别为i和j(i<j).如果能找到这样的一对元素,说明所有的组合还存在,如果没有找到这样的一对元素,说明所

hdu 1394 求一个序列的最小逆序数 单点增 区间求和

题目的意思就好比给出一个序列 如:0 3 4 1 2 设逆序数初始n = 0: 由于0后面没有比它小的,n = 0 3后面有1,2 n = 2 4后面有1,2,n = 2+2 = 4: 所以该序列逆序数为 4 或者这样想 先输0 前面没有比它大的 n = 03也没有 4也没有1前面 3 4 比它大 n += 22前面 3 4 比它大 n += 2n = 4 其根据题意移动产生的序列有 3 4 1 2 0 逆序数:8 4 1 2 0 3 逆序数:6 1 2 0 3 4 逆序数:2 2 0 3 4

设计一个算法,求非空二叉树中指定的第k层(k&gt;1)的叶子节点的个数

思想:采用基于层序遍历的方法.用level扫描各层节点,若某一层的节点出队后,rear指向该层中最右节点,则将rear赋值给last(对于第一层,last=1).在出队时,若front=last,表示这一层处理完毕,让层号level增1,并置last为下一层最右节点.那么如何求一层的最右节点呢?这是因为第一层只有一个节点,它就是最右节点.对于其他层,上一层最右节点最后进队的孩子一定是该层的最右节点. 例如,对于如图所示的二叉树,求k=3的叶子节点个数的过程如下:level=1;A进队时rear=

求一个非负整数数组中不相邻元素之和的最大值

该题来自leetcode,原题如下: You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connecte