【问题标题】:Counting the adjacent swaps required to convert one permutation into another计算将一种排列转换为另一种排列所需的相邻交换
【发布时间】:2011-10-17 17:49:39
【问题描述】:

我们得到了两个小写拉丁字母序列。 它们的长度相同,并且具有相同数量的给定类型 字母数(第一个与第二个具有相同数量的 t,因此 在)。我们需要找到最小的交换次数(“交换”是指改变 两个相邻的字母的顺序) 将第一个序列转换为第二个序列。我们 可以安全地假设每两个序列都可以转换 进入彼此。我们可以用蛮力做到这一点,但序列太长了。

输入:
序列的长度(至少 2 个,最多 999999)和 然后是两个序列。

输出:
一个整数,表示所需的交换次数 序列变得相同。

示例:
{5, aaaaa, aaaaa} 应该输出 {0},
{4, abcd, acdb} 应该输出 {2}。

我首先想到的是冒泡排序。我们可以简单地对每个交换计数的序列进行冒泡排序。问题是:a)它是 O(n^2) 最坏的情况 b)我不相信它会给我每种情况下的最小数字......即使优化的冒泡排序似乎也没有奏效。我们可以实现可以解决海龟问题的鸡尾酒排序——但它会给我最好的性能吗?或者也许有更简单/更快的东西?

这个问题也可以表述为:当唯一允许的操作是转置时,我们如何确定两个字符串之间的编辑距离?

【问题讨论】:

  • 不是真的,教授今天给了我们这个,在我们开始工作之前,铃声响了。这不是我们的作业,但我觉得它很有趣,并想找出解决它的方法。
  • 不,不是。在那里,您可以交换任何两个单元格 - 在这里,只有相邻的。
  • 啊——没错,我错过了那个细节
  • 如果只能交换相邻的单元格,那么冒泡排序可能是最佳方法。令人惊讶的是,它在很久以前就被证明在这些情况下是最佳的。关于我能看到的唯一改进的可能性是 Shakersort (你可以交替方向的冒泡排序)。不过,我也不确定这会做得更好——我认为它真的只有减少比较次数的机会,而不是交换次数。

标签: algorithm


【解决方案1】:

关于将一个排列转换为另一个排列所需的最小交换次数(不一定是相邻的),您应该使用的度量标准是 Cayley 距离,它本质上是排列的大小 - 循环数。

计算排列中的循环数是一个非常微不足道的问题。一个简单的例子。假设排列 521634。

如果你检查第一个位置,你有 5 个,在第 5 个你有 3 个,在第 3 个你有 1 个,关闭第一个循环。 2 位于第 2 位,因此它自己形成一个循环,4 和 6 组成最后一个循环(4 位于第 6 位,6 位于第 4 位)。如果要在恒等排列中转换此排列(交换次数最少),则需要独立地重新排序每个循环。交换的总数是排列的长度 (6) 减去循环数 (3)。

给定任意两个排列,它们之间的距离等于第一个的composition 与第二个的倒数与恒等式之间的距离(计算如上所述)。因此,您唯一需要做的就是组合第一个排列和第二个排列的倒数,并计算结果中的循环数。所有这些操作都是 O(n),因此您可以在线性时间内获得最少的交换次数。

【讨论】:

    【解决方案2】:

    这里有一个简单有效的解决方案:

    Q[ s2[i] ] = the positions character s2[i] is on in s2。让P[i] = on what position is the character corresponding to s1[i] in the second string.

    构建 Q 和 P:

    for ( int i = 0; i < s1.size(); ++i )
        Q[ s2[i] ].push_back(i); // basically, Q is a vector [0 .. 25] of lists
    
    temp[0 .. 25] = {0}
    for ( int i = 0; i < s1.size(); ++i )
        P[i + 1] = 1 + Q[ s1[i] ][ temp[ s1[i] ]++ ];
    

    示例:

        1234
    s1: abcd
    s2: acdb
    Q: Q[a = 0] = {0}, Q[b = 1] = {3}, Q[c = 2] = {1}, Q[d = 3] = {2}
    P: P[1] = 1, P[2] = 4 (because the b in s1 is on position 4 in s2), P[3] = 2
       P[4] = 3
    

    P2 反转(4 24 3),所以这就是答案。

    这个解决方案是O(n log n),因为构建PQ 可以在O(n) 中完成,合并排序可以在O(n log n) 中计算反转。

    【讨论】:

    • @Positive Int:也许这张图有帮助:i.imgur.com/T80Q5.png。对于重复的字母,在第一个字符串中的第 7 个 'A' 和第二个字符串中的第 7 个 'A' 之间画一条线,等等。然后,只计算交叉点(反转)。
    • @Positive Int - 1. 确切地说,用26 零初始化一个temp 数组。 2. Q[i] 是一个列表 - 我们在第一个 for 循环中构建它。 x.push_back() 在列表 x 的末尾添加一个元素。 3.geeksforgeeks.org/archives/3968
    • @Positive Int - 这是不对的。它应该是vector&lt;char&gt; Q[26],并使用Q[ s2[i] - 'a' ].push_back(i)Q 是一个列表(向量)数组,而不是您声明的向量。
    • @Positive Int - 是的,P 是一个 int 数组。您应该使用Q[s1[1] - 'a'][ temp[ s1[1] - 'a' ] ],因为s1 是一个字符数组,而字符有一个ASCII 码,对于小写字母,它从~90 开始。所以如果s1[1] 是'a',你将访问Q[90something],这不是你想要的。
    • @Positive Int - 很多事情,我真的不能说。我的电子邮件在我的个人资料中 - 给我发一封电子邮件,我会将我的实施发送给您。
    【解决方案3】:

    您正在寻找的可能与“Kendall tau 距离”相同,这是一致减去不一致对的(标准化)差异。见Wikipedia,这里声称相当于冒泡排序距离。

    在 R 中,函数不仅可用于计算 tau,例如

    cor( X, method="kendall", use="pairwise" ) ,
    

    但也用于测试差异的显着性,例如

    cor.test( x1, x2, method="kendall" ) ,
    

    他们甚至能够正确考虑关系。

    See here for more.

    【讨论】:

      【解决方案4】:

      Kendall tau 距离”算法是这种情况下的精确解,其中必须找到 相邻元素的交换次数

      示例。

      eyssaasse(基本字符串
      海上系统

      基本字符串为每个元素提供索引:e=0, y=1, s=2 , s=3, a=4, a=5, s=6, s=7, e=8;

      有些元素是重复的,所以:
      1) 创建一个 dictionary,其中元素是键,值是索引列表:

      idx = {'e'=>[0, 8], 'y'=>[1], 's'=>[2, 3, 6, 7], 'a'=>[4, 5]}

      2) 使用 idx 字典中的元素索引创建第二个字符串的索引映射:

      seasysaes -> 204316587(循环“seasysaes”并从列表中弹出 idx 中每个键的下一个索引)

      3) 创建此映射的所有配对组合的列表,204316587: 20 24 23 21 26 25 28 27 04 03 01 06 ... 65 68 67 58 57 87;
      循环遍历这些对,计算第一个数字大于第二个数字的那些。
      这个计数是字符串之间相邻交换的寻求数量

      Python 脚本:

      from itertools import combinations, cycle
      
      word = 'eyssaasse' # base string
      cmpr = 'seasysaes' # a string to find number of swaps from the base string
      swaps = 0
      
      # 1)
      chars = {c: [] for c in word}
      [chars[c].append(i) for i, c in enumerate(word)]
      for k in chars.keys():
          chars[k] = cycle(chars[k])
      
      # 2)
      idxs = [next(chars[c]) for c in cmpr]
      
      # 3)
      for cmb in combinations(idxs, 2):
          if cmb[0] > cmb[1]:
              swaps += 1
      
      print(swaps)
      

      “eyssaasse”和“seasysaes”之间的交换次数为 7。
      'reviver' 和 'vrerev' 是 8。

      【讨论】:

        【解决方案5】:

        我编写了一个类Permutation,除其他外,它可以返回将给定排列转换为标识所需的许多转置。这是通过创建轨道(循环)并计算它们的长度来完成的。术语取自 Kostrikin A., I., “Introduction to Linear Algebra I”

        包括:

        #include <iostream>
        #include <vector>
        #include <set>
        #include <algorithm>
        #include <iterator>
        

        类排列:

        class Permutation {
        public:
            struct ei_element {    /* element of the orbit*/
                int e; /* identity index */
                int i; /* actual permutation index */
            };
            typedef std::vector<ei_element> Orbit; /* a cycle */
        
            Permutation( std::vector<int> const& i_vector);
            /* permute i element, vector is 0 indexed */
            int pi( int i) const { return iv[ i - 1]; }
            int i( int k) const { return pi( k); } /* i_k = pi(k) */
            int q() const { /* TODO: return rank = q such that pi^q = e */ return 0; }
            int n() const { return n_; }
            /* return the sequence 1, 2, ..., n */
            std::vector<int> const& Omega() const { return ev; }
            /* return vector of cycles */
            std::vector<Orbit> const& orbits() const { return orbits_; }
            int l( int k) const { return orbits_[ k].size(); } /* length of k-th cycle */
            int transpositionsCount() const;  /* return sum of all transpositions */
            void make_orbits();
        
            private:
            struct Increment {
                int current;
                Increment(int start) : current(start) {}
                int operator() () {
                    return current++;
                }
            };
            int n_;
            std::vector<int> iv; /* actual permutation */
            std::vector<int> ev; /* identity permutation */
            std::vector<Orbit> orbits_;
        };
        

        定义:

        Permutation::Permutation( std::vector<int> const& i_vector) : 
                                                              n_( i_vector.size()), 
                                                              iv( i_vector), ev( n_) {
                if ( n_) { /* fill identity vector 1, 2, ..., n */
                    Increment g ( 1);
                    std::generate( ev.begin(), ev.end(), g);
                }
        }
        
        /* create orbits (cycles) */
        void Permutation::make_orbits() {
            std::set<int> to_visit( ev.begin(), ev.end()); // identity elements to visit
            while ( !to_visit.empty()) {
                /* new cycle */
                Orbit orbit;
                int first_to_visit_e = *to_visit.begin();
                to_visit.erase( first_to_visit_e);
                int k = first_to_visit_e; // element in identity vector
                /* first orbit element */
                ei_element element;
                element.e = first_to_visit_e;
                element.i = i( first_to_visit_e);
                orbit.push_back( element);
                /* traverse permutation until cycle is closed */
                while ( pi( k) != first_to_visit_e && !to_visit.empty()) {
                    k = pi( k);
                    ei_element element;
                    element.e = k;
                    element.i = pi( k);
                    orbit.push_back( element);
                    to_visit.erase( k);
                }
                orbits_.push_back( orbit);
            }
        }
        

        和:

        /* return sum of all transpositions */
        int Permutation::transpositionsCount() const {
            int count = 0;
            int k = 0;
            while ( k < orbits_.size()) {
                count += l( k++) - 1; /* sum += l_k - 1 */ 
            }
            return count;
        }
        

        用法:

        /*
         * 
         */
        int main(int argc, char** argv) {
                               //1, 2, 3, 4, 5, 6, 7, 8       identity (e)
            int permutation[] = {2, 3, 4, 5, 1, 7, 6, 8}; //  actual (i)
            std::vector<int> vp( permutation, permutation + 8);
        
            Permutation p( vp);
            p.make_orbits();
            int k = p.orbits().size();
            std::cout << "Number of cycles:" << k << std::endl;
        
            for ( int i = 0; i < k; ++i) {
                std::vector<Permutation::ei_element> v = p.orbits()[ i];
                for ( int j = 0; j < v.size(); ++j) {
                    std::cout << v[ j].e << "," << v[ j].i << " | ";
                }
                std::cout << std::endl;
            }
        
            std::cout << "Steps needed to create identity permutation: " 
                                                        << p.transpositionsCount();
            return 0;
        }
        

        输出:

        循环次数:3

        1,2 | 2,3 | 3,4 | 4,5 | 5,1 |

        6,7 | 7,6 |

        8,8 |

        创建恒等排列所需的步骤:5

        运行成功(总时间:82 毫秒)


        coliru

        【讨论】:

        • 很好的实现。然而,这比这个问题回答了更多stackoverflow.com/questions/22899401/…。为什么不投票支持重新开放并将其发布到那里?
        • @theswine 我已经投票决定重新开放,我也会提供更多的理论报道
        【解决方案6】:

        通过在 O(n) 中反转目标排列,在 O(n) 中组合排列,然后找到从那里到一个身份置换。 鉴于:

        int P1[] = {0, 1, 2, 3}; // abcd
        int P2[] = {0, 2, 3, 1}; // acdb
        
        // we can follow a simple algebraic modification
        // (see http://en.wikipedia.org/wiki/Permutation#Product_and_inverse):
        // P1 * P = P2                   | premultiply P1^-1 *
        // P1^-1 * P1 * P = P1^-1 * P2
        // I * P = P1^-1 * P2
        // P = P1^-1 * P2
        // where P is a permutation that makes P1 into P2.
        // also, the number of steps from P to identity equals
        // the number of steps from P1 to P2.
        
        int P1_inv[4];
        for(int i = 0; i < 4; ++ i)
            P1_inv[P1[i]] = i;
        // invert the first permutation O(n)
        
        int P[4];
        for(int i = 0; i < 4; ++ i)
            P[i] = P2[P1_inv[i]];
        // chain the permutations
        
        int num_steps = NumSteps(P, 4); // will return 2
        // now we just need to count the steps
        

        为了计算步数,可以设计一个简单的算法,例如:

        int NumSteps(int *P, int n)
        {
            int count = 0;
            for(int i = 0; i < n; ++ i) {
                for(; P[i] != i; ++ count) // could be permuted multiple times
                    swap(P[P[i]], P[i]); // look where the number at hand should be
            }
            // count number of permutations
        
            return count;
        }
        

        这总是将一个项目交换到它应该在身份排列中的位置,因此在每一步它都会撤消并计算一次交换。现在,只要它返回的交换次数确实是最小的,算法的运行时间就会受到它的限制并保证完成(而不是陷入无限循环)。它将在O(m) 交换或O(m + n) 循环迭代中运行,其中m 是交换数(count 返回),n 是序列中的项目数(4)。请注意,m &lt; n 始终为真。因此,这应该优于O(n log n) 解决方案,因为上限是交换的O(n - 1) 或循环迭代的O(n + n - 1),实际上两者都是O(n)(在后一种情况下省略了常数因子2)。

        该算法仅适用于有效排列,它将无限循环用于具有重复值的序列,并对具有除[0, n) 以外的值的序列进行越界数组访问(和崩溃)。可以在here 找到完整的测试用例(使用 Visual Studio 2008 构建,算法本身应该是相当可移植的)。它生成长度为 1 到 32 的所有可能排列,并检查使用广度优先搜索 (BFS) 生成的解决方案,似乎适用于长度为 1 到 12 的所有排列,然后它变得相当慢,但我认为它会继续工作.

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2020-09-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-01-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多