• 算法题:求一个序列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("
    ");
        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("
    
    distinct sub sequence of "%s" for "%s" : ===============", whole_seqs[i], "124");
            DistinctSub(whole_seqs[i], "124");
        }
     
        getchar();
    }
     

    后话

     
    作为一个算法的话,这显然不能说over了。
    首先,算法的正确性,应该做作大量的测试验证。譬如,还有没有没有想到的重复解的情况?对于这个算法,我并没有十分的把握,还需要验证来发现问题并确认正确性。
    其次,在去重这一块,我觉得我的思路似乎并不直观,以及并没有和数学里的问题应对起来。而现实中很多算法其实都能转换为数学问题。所以我怀疑有更简洁和明了的思路。
    上午百度了一下,网上有一堆人问这个,想必应该是别的思路的,不过我还是先把我的思路记下来。再去学习。
     
    顺便再吐槽一下,博客园的编辑器依然烂成翔...等宽代码字体都没有,代码缩进又被吃了。有个高亮的功能聊胜于无吧...本地纯文本或markdown但是consolas+雅黑字体好太多了,线上的就只能将就看下。
     
  • 相关阅读:
    ABP 菜单 修改
    C# 过滤器
    RabbitMQ框架构建系列(三)——Net实现RabbitMQ之Producer
    RabbitMQ系列(二)RabbitMQ基础介绍
    RabbitMQ系列(一)AMPQ协议
    MVC 解读WebConfig
    MVC过滤器特性
    asp.net中使用JQueryEasyUI
    asp.net请求到响应的整个过程
    Redis的下载安装部署(Windows)
  • 原文地址:https://www.cnblogs.com/yedaoq/p/5840917.html
Copyright © 2020-2023  润新知