修改排列算法以防止重复打印输出的策略

Posted

技术标签:

【中文标题】修改排列算法以防止重复打印输出的策略【英文标题】:Strategy to modify permutation algorithm to prevent duplicate printouts 【发布时间】:2012-02-29 03:15:32 【问题描述】:

我一直在审查用于练习的算法,目前正在研究一种我非常喜欢的置换算法:

void permute(char* set, int begin, int end) 
    int range = end - begin;

    if (range == 1)
        cout << set << endl;
    else 
        for(int i = 0; i < range; ++i) 
            swap(&set[begin], &set[begin+i]);
            permute(set, begin+1, end);
            swap(&set[begin], &set[begin+i]);
        
    

我实际上想将此应用到会有很多重复字符的情况,所以我需要能够修改它以防止打印重复排列。

我将如何检测我正在生成重复项?我知道我可以将它存储在散列或类似的东西中,但这不是最佳解决方案 - 我更喜欢不需要额外存储空间的解决方案。有人可以给我一个建议吗?

PS:我不想使用 STL 置换机制,也不想在某处引用另一个“唯一置换算法”。我想了解用于防止重复的机制,以便我可以将其构建到学习中,如果可能的话。

【问题讨论】:

在此处查看解决方案#2 ---> n1b-algo.blogspot.com/2009/01/string-permutations.html 【参考方案1】:

没有通用的方法来防止任意函数生成重复。当然,您总是可以过滤掉重复项,但您不希望这样做,并且有很好的理由。所以你需要一种特殊的方式来只生成非重复。

一种方法是生成按字典顺序递增的排列。然后,您可以比较“新”排列是否与上一个排列相同,然后跳过它。它变得更好:http://en.wikipedia.org/wiki/Permutations#Generation_in_lexicographic_order 给出的以增加的字典顺序生成排列的算法甚至根本不生成重复!

但是,这不是您问题的答案,因为它是一种不同的算法(尽管也基于交换)。

那么,让我们更深入地了解一下您的算法。一个关键的观察结果是:

一旦一个字符被交换到位置begin,对于permute 的所有嵌套调用,它将保持在那里。

我们将把它与以下关于排列的一般观察结合起来:

如果您置换字符串s,但仅在有相同字符的位置,s 将保持不变。事实上,所有重复排列对于某个字符 c 的出现都有不同的顺序,其中 c 出现在相同的位置。

好的,所以我们要做的就是确保每个字符的出现始终与开始时的顺序相同。代码如下,但是...我不会说 C++,所以我会使用 Python,并希望能够避免声称它是伪代码。

我们从您的原始算法开始,用“伪代码”重写:

def permute(s, begin, end):
    if end == begin + 1:
        print(s)
    else:
        for i in range(begin, end):
            s[begin], s[i] = s[i], s[begin]
            permute(s, begin + 1, end)
            s[begin], s[i] = s[i], s[begin]

还有一个帮助函数,使调用更容易:

def permutations_w_duplicates(s):
    permute(list(s), 0, len(s)) # use a list, as in Python strings are not mutable

现在我们扩展 permute 函数,记录某个字符被交换到 begin 位置的次数(即已固定),我们还记得原来的顺序每个字符的出现次数 (char_number)。我们尝试交换到begin 位置的每个字符都必须是原始顺序中的下一个更高的字符,即一个字符的修复次数定义了接下来可以修复该字符的哪个原始出现 - 我称之为@987654331 @。

def permute2(s, next_fixable, char_number, begin, end):
    if end == begin + 1:
        print(s)
    else:
        for i in range(begin, end):
            if next_fixable[s[i]] == char_number[i]: 
                next_fixable[s[i]] += 1
                char_number[begin], char_number[i] = char_number[i], char_number[begin]

                s[begin], s[i] = s[i], s[begin]
                permute2(s, next_fixable, char_number, begin + 1, end)
                s[begin], s[i] = s[i], s[begin]

                char_number[begin], char_number[i] = char_number[i], char_number[begin]
                next_fixable[s[i]] -= 1

再次,我们使用辅助函数:

def permutations_wo_duplicates(s):
    alphabet = set(s)
    next_fixable = dict.fromkeys(alphabet, 0)
    count = dict.fromkeys(alphabet, 0)
    char_number = [0] * len(s)
    for i, c in enumerate(s):
        char_number[i] = count[c]
        count[c] += 1

    permute2(list(s), next_fixable, char_number, 0, len(s))

就是这样!

差不多了。如果您愿意,可以在这里停下来用 C++ 重写,但如果您对一些测试数据感兴趣,请继续阅读。

我使用稍微不同的代码进行测试,因为我不想打印所有排列。在 Python 中,您可以将 print 替换为 yield,并将函数转换为生成器函数,其结果可以使用 for 循环进行迭代,并且仅在需要时计算排列。这是我使用的真实代码和测试:

def permute2(s, next_fixable, char_number, begin, end):
    if end == begin + 1:
        yield "".join(s) # join the characters to form a string
    else:
        for i in range(begin, end):
            if next_fixable[s[i]] == char_number[i]:
                next_fixable[s[i]] += 1
                char_number[begin], char_number[i] = char_number[i], char_number[begin]
                s[begin], s[i] = s[i], s[begin]
                for p in permute2(s, next_fixable, char_number, begin + 1, end):
                    yield p
                s[begin], s[i] = s[i], s[begin]
                char_number[begin], char_number[i] = char_number[i], char_number[begin]
                next_fixable[s[i]] -= 1

def permutations_wo_duplicates(s):
    alphabet = set(s)
    next_fixable = dict.fromkeys(alphabet, 0)
    count = dict.fromkeys(alphabet, 0)
    char_number = [0] * len(s)
    for i, c in enumerate(s):
        char_number[i] = count[c]
        count[c] += 1

    for p in permute2(list(s), next_fixable, char_number, 0, len(s)):
        yield p


s = "FOOQUUXFOO"
A = list(permutations_w_duplicates(s))
print("%s has %s permutations (counting duplicates)" % (s, len(A)))
print("permutations of these that are unique: %s" % len(set(A)))
B = list(permutations_wo_duplicates(s))
print("%s has %s unique permutations (directly computed)" % (s, len(B)))

print("The first 10 permutations       :", A[:10])
print("The first 10 unique permutations:", B[:10])

结果:

FOOQUUXFOO has 3628800 permutations (counting duplicates)
permutations of these that are unique: 37800
FOOQUUXFOO has 37800 unique permutations (directly computed)
The first 10 permutations       : ['FOOQUUXFOO', 'FOOQUUXFOO', 'FOOQUUXOFO', 'FOOQUUXOOF', 'FOOQUUXOOF', 'FOOQUUXOFO', 'FOOQUUFXOO', 'FOOQUUFXOO', 'FOOQUUFOXO', 'FOOQUUFOOX']
The first 10 unique permutations: ['FOOQUUXFOO', 'FOOQUUXOFO', 'FOOQUUXOOF', 'FOOQUUFXOO', 'FOOQUUFOXO', 'FOOQUUFOOX', 'FOOQUUOFXO', 'FOOQUUOFOX', 'FOOQUUOXFO', 'FOOQUUOXOF']

请注意,排列的计算顺序与原始算法相同,只是没有重复。注意37800 * 2! * 2! * 4! = 3628800,正如您所期望的那样。

【讨论】:

感谢您提供非常详细的答案,这是您的赏金 :) @w00te:谢谢!事实证明这比我最初预期的更有趣 :-) 是的,告诉我吧哈哈。我勇敢地尝试避免使用集合/哈希来跟踪重复项,但我找不到方法:p【参考方案2】:

如果交换两个相同的字符,您可以添加一个 if 语句来防止交换代码执行。然后是for循环

for(int i = 0; i < range; ++i) 
    if(i==0 || set[begin] != set[begin+i]) 
      swap(&set[begin], &set[begin+i]);
      permute(set, begin+1, end);
      swap(&set[begin], &set[begin+i]);
    

允许i==0 的原因是确保递归调用只发生一次,即使集合的所有字符都相同。

【讨论】:

这不起作用。该算法将在较早的时候交换一个字符,这意味着以后不一定要对其进行排序。因此,在进一步迭代中,相似的两个字符可能不一定彼此相邻。 我不明白为什么这不起作用 - 你能举个反例吗? 假设您要为“ABAB”生成不同的排列。它应该有 6 个不同的排列,而上述方法生成 11 个。 它只是防止从一个字符生成重复,不防止为整个列表生成重复。对于案例“AABB”,它肯定会失败。【参考方案3】:

选项 1

一种选择是使用堆栈上的 256 位存储来存储您在 for 循环中尝试过的字符的位掩码,并且只对新字符进行递归。

选项 2

第二种选择是使用 cmets (http://n1b-algo.blogspot.com/2009/01/string-permutations.html) 中建议的方法并将 for 循环更改为:

else 
    char last=0;
    for(int i = 0; i < range; ++i) 
        if (last==set[begin+i])
            continue;
        last = set[begin+i];
        swap(&set[begin], &set[begin+i]);
        permute(set, begin+1, end);
        swap(&set[begin], &set[begin+i]);
    

但是,要使用这种方法,您还必须在函数入口处对字符 set[begin]、set[begin+1]、...set[end-1] 进行排序。

请注意,每次调用函数时都必须进行排序。 (博文似乎没有提到这一点,否则你会为输入字符串“aabbc”生成太多结果。问题是使用swap后字符串没有保持排序。)

这仍然不是很有效。例如,对于包含 1 'a' 和 N 'b' 的字符串,这种方法最终将调用排序 N 次,整体复杂度为 N^2logN

选项 3

对于包含大量重复的长字符串,一种更有效的方法是同时维护字符串“set”和一个字典,其中包含您还可以使用的每种类型字符的数量。 for 循环将更改为对字典键的循环,因为这些将是该位置允许的唯一字符。

这将具有与输出字符串数量相等的复杂性,并且只需要非常少量的额外存储空间来保存字典。

【讨论】:

【参考方案4】:

一个简单的解决方案是将重复的字符随机更改为不存在的字符。然后在排列后,将字符变回。仅当字符有序时才接受排列。

例如如果你有“a,b,b”

您将拥有以下内容:

a b b
a b b
b a b
b a b
b b a
b b a

但是,如果我们以 a,b,b 开头并注意重复的 b,那么我们可以将第二个 b 更改为 c

现在我们有一个 b c

a b c - accept because b is before c. change c back to b to get a b b
a c b - reject because c is before b
b a c - accept as b a b
b c a - accept as b b a
c b a - reject as c comes before b.
c a b - reject as c comes before b.

【讨论】:

【参考方案5】:

只需将每个元素插入到一个集合中。它会自动删除重复项。将 set s 声明为全局变量。

set <string>s;
void permute(string a, int l, int r) 
    int i;
    if (l == r)
        s.insert(a);
    else
    
        for (i = l; i <= r; i++)
        
            swap((a[l]), (a[i]));
            permute(a, l+1, r);
            swap((a[l]), (a[i])); //backtrack
        
    

最后使用函数打印

void printr()

    set <string> ::iterator itr;
    for (itr = s.begin(); itr != s.end(); ++itr)
    
        cout << '\t' << *itr;
    
    cout << '\t' << *itr;

【讨论】:

【参考方案6】:

关键是不要将同一个字符交换两次。因此,您可以使用 unordered_set 来记住已交换的字符。

void permute(string& input, int begin, vector<string>& output) 
    if (begin == input.size())
        output.push_back(input);
    
    else     
        unordered_set<char> swapped;
        for(int i = begin; i < input.size(); i++) 
            // Do not swap a character that has been swapped
            if(swapped.find(input[i]) == swapped.end())
                swapped.insert(input[i]);
                swap(input[begin], input[i]);
                permute(input, begin+1, output);
                swap(input[begin], input[i]);
            
        
    

您可以手动查看原始代码,您会发现出现重复的情况是“与已交换的字符交换。

例如:输入 = "BAA"

索引 = 0,i = 0,输入 =“BAA”

---->索引=1,i=1,输入=“BAA”

----> index = 1, i = 2, input = "BAA" (重复)

索引 = 0,i = 1,输入 = "ABA"

---->索引=1,i=1,输入=“ABA”

---->索引=1,i=2,输入=“AAB”

索引 = 0,i = 2,输入 = "AAB"

----> index = 1, i = 1, input = "AAB" (重复)

----> index = 1, i = 2, input = "ABA" (重复)

【讨论】:

以上是关于修改排列算法以防止重复打印输出的策略的主要内容,如果未能解决你的问题,请参考以下文章

算法剑指 Offer 38. 字符串的排列 重刷

算法入门经典-第七章 例题7-2-2 可重集的排列

算法:字符串的排列

用于检测图中循环的算法的修改

一文通数据结构与算法之——回溯算法+常见题型与解题策略+Leetcode经典题

使用修改后的弗洛伊德战争打印给定节点之间的最短路径