題:
給定一個序列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+雅黑字體好太多了,線上的就只能將就看下。
