算法題:求一個序列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怎麼解:

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;
}

 

 
在這個基礎上,能夠再加入子序列的匹配邏輯:

2、求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中的不一樣位置,從而造成。
 
接下來再考慮重複元素的問題。
 

3、去重

我看到的重複問題有兩類:
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+雅黑字體好太多了,線上的就只能將就看下。
相關文章
相關標籤/搜索