在C#中以最小步数重新排序元素集合

时间:2017-06-09 13:49:44

标签: c# arrays algorithm subsequence

我有一个元素列表(即PowerPoint幻灯片),我需要在尽可能少的步骤中重新排序。

每个幻灯片都有一个整数唯一键(即SlideID),我可以非常快速地生成所需的键顺序,但实际上移动幻灯片(执行移动)相对较慢,因为PowerPoint更新了谁 - 知道什么时候它被调用,因此我尝试执行最少量的移动命令。

所以我所拥有的是原始和所需顺序中的键列表,例如:

int[] original = { 201, 203, 208, 117, 89 };
int[] desired = { 208, 117, 89, 203, 201 };

环顾互联网,我得出结论,找到Longest Common Subsequence并将其他所有内容移动到所需位置就可以满足我的需求,因此我实施了T[] FindLCS<T>(T[] first, T[] second)方法借用和调整{{3 }}

为了重新排序幻灯片,我提供了一个非常有限的API,我只能按slide.MoveTo(int toPos)订购。 (除此之外,我可以随时通过它的索引找到幻灯片的ID,反之亦然。)

我无法实现剩下的部分,即生成我可以执行的实际动作列表,因为移动幻灯片x会将所有幻灯片索引转移到中间,我对如何解释这个问题感到困惑。

有人可以帮我制作一个(int sourceIndex, int targetIndex)(int id, int targetIndex)元组的列表,我可以简单地迭代一下吗?

2 个答案:

答案 0 :(得分:3)

这是一个贪心算法,根据距所需位置的距离选择要移动的元素:

static void Main(string[] args)
{
    int[] original = { 201, 203, 208, 117, 89 };
    int[] desired = { 208, 117, 89, 203, 201 };
    List<int> seq = new List<int>();
    int seqLen = original.Length;

    //  find initial ordering
    foreach(int io in original)
    {
        int pos = -1;
        for (int i = 0; i < desired.Length; i++)
        {
            if (desired[i] == io)
            {
                pos = i;
                break;
            }
        }
        seq.Add(pos);
    }

    showSequence(seq, "initial");
    //  sort by moving the entry which is off by the largest distance
    bool changed;
    do
    {
        changed = false;

        int worstPos = 0;
        int worstDiff = (0 - seq[0]) * (0 - seq[0]);

        for (int pos = 1; pos < seqLen; pos++)
        {
            int diff = (pos - seq[pos]) * (pos - seq[pos]);
            if (diff > worstDiff)
            {
                worstPos = pos;
                worstDiff = diff;
            }
        }

        if (worstDiff > 0)
        {
            //  move worst entry to desired position
            int item = seq[worstPos];
            seq.Remove(item);
            seq.Insert(item, item);
            changed = true;
            showSequence(seq, $"changed {item} from index {worstPos} to index {item}");
        }
    }
    while (changed);

    Console.WriteLine("ciao!");
}

private static void showSequence(List<int> seq, string msg)
{
    string s = "";

    foreach(int i in seq)
    {
        s = s + " " + i;
    }

    Console.WriteLine($"{msg}: {s}");
}

只要正确放置所有项目,算法就会停止。

  

请注意,该算法不一定适用于所有序列。

以下是24个项目的示例:

initial:  14 0 15 22 6 8 20 21 18 17 9 7 19 1 23 12 11 5 2 16 13 3 4 10
1: changed 22 from index 3 to index 22:  14 0 15 6 8 20 21 18 17 9 7 19 1 23 12 11 5 2 16 13 3 4 22 10
2: changed 3 from index 20 to index 3:  14 0 15 3 6 8 20 21 18 17 9 7 19 1 23 12 11 5 2 16 13 4 22 10
3: changed 4 from index 21 to index 4:  14 0 15 3 4 6 8 20 21 18 17 9 7 19 1 23 12 11 5 2 16 13 22 10
4: changed 2 from index 19 to index 2:  14 0 2 15 3 4 6 8 20 21 18 17 9 7 19 1 23 12 11 5 16 13 22 10
5: changed 14 from index 0 to index 14:  0 2 15 3 4 6 8 20 21 18 17 9 7 19 14 1 23 12 11 5 16 13 22 10
6: changed 1 from index 15 to index 1:  0 1 2 15 3 4 6 8 20 21 18 17 9 7 19 14 23 12 11 5 16 13 22 10
7: changed 5 from index 19 to index 5:  0 1 2 15 3 5 4 6 8 20 21 18 17 9 7 19 14 23 12 11 16 13 22 10
8: changed 10 from index 23 to index 10:  0 1 2 15 3 5 4 6 8 20 10 21 18 17 9 7 19 14 23 12 11 16 13 22
9: changed 15 from index 3 to index 15:  0 1 2 3 5 4 6 8 20 10 21 18 17 9 7 15 19 14 23 12 11 16 13 22
10: changed 20 from index 8 to index 20:  0 1 2 3 5 4 6 8 10 21 18 17 9 7 15 19 14 23 12 11 20 16 13 22
11: changed 21 from index 9 to index 21:  0 1 2 3 5 4 6 8 10 18 17 9 7 15 19 14 23 12 11 20 16 21 13 22
12: changed 18 from index 9 to index 18:  0 1 2 3 5 4 6 8 10 17 9 7 15 19 14 23 12 11 18 20 16 21 13 22
13: changed 13 from index 22 to index 13:  0 1 2 3 5 4 6 8 10 17 9 7 15 13 19 14 23 12 11 18 20 16 21 22
14: changed 17 from index 9 to index 17:  0 1 2 3 5 4 6 8 10 9 7 15 13 19 14 23 12 17 11 18 20 16 21 22
15: changed 23 from index 15 to index 23:  0 1 2 3 5 4 6 8 10 9 7 15 13 19 14 12 17 11 18 20 16 21 22 23
16: changed 19 from index 13 to index 19:  0 1 2 3 5 4 6 8 10 9 7 15 13 14 12 17 11 18 20 19 16 21 22 23
17: changed 11 from index 16 to index 11:  0 1 2 3 5 4 6 8 10 9 7 11 15 13 14 12 17 18 20 19 16 21 22 23
18: changed 16 from index 20 to index 16:  0 1 2 3 5 4 6 8 10 9 7 11 15 13 14 12 16 17 18 20 19 21 22 23
19: changed 7 from index 10 to index 7:  0 1 2 3 5 4 6 7 8 10 9 11 15 13 14 12 16 17 18 20 19 21 22 23
20: changed 15 from index 12 to index 15:  0 1 2 3 5 4 6 7 8 10 9 11 13 14 12 15 16 17 18 20 19 21 22 23
21: changed 12 from index 14 to index 12:  0 1 2 3 5 4 6 7 8 10 9 11 12 13 14 15 16 17 18 20 19 21 22 23
22: changed 5 from index 4 to index 5:  0 1 2 3 4 5 6 7 8 10 9 11 12 13 14 15 16 17 18 20 19 21 22 23
23: changed 10 from index 9 to index 10:  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 20 19 21 22 23
24: changed 20 from index 19 to index 20:  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

通过24个步骤订购24件物品是微不足道的:选择第1,第2,第3 ...... 24日。

解释here的方法找到了必要的21个步骤。贪婪的方法似乎做得很好。但是,对于反向序列,它需要太多的交换。

<强>更新

这是面向周期的方法,受geeksforgeeks和相关StackOverflow post的启发:

struct ValuePosition<T> : IComparable<ValuePosition<T>> where T : IComparable
{
    public T value;
    public int position;

    public int CompareTo(ValuePosition<T> other)
    {
        return value.CompareTo(other.value);
    }
}

static void sortWithMinimumNumberOfSwaps<T>(T[] arr) where T : IComparable
{
    int n = arr.Length;

    // Create an array of <Value, Position> pairs
    ValuePosition<T>[] arrValuePosition = new ValuePosition<T>[n];
    for (int i = 0; i < n; i++)
    {
        arrValuePosition[i].value = arr[i];
        arrValuePosition[i].position = i;
    }

    // Sort array values to get desired positions
    Array.Sort(arrValuePosition);

    // Keep track of visited elements (all initially unvisited)
    bool[] visited = new bool[n];

    //  Members of a cycle are registered here
    int[] cycle = new int[n];

    int swapCount = 0;

    // Traverse array elements
    for (int i = 0; i < n; i++)
    {
        // already swapped and corrected or
        // already present at correct pos
        if (visited[i] || arrValuePosition[i].position == i)
            continue;

        // loop trough cycle and collect comprised items
        int cycleIdx = 0;
        int j = i;
        while (!visited[j])
        {
            visited[j] = true;
            cycle[cycleIdx++] = j;

            // move to next node
            j = arrValuePosition[j].position;
        }

        //  perform resulting swaps
        while (--cycleIdx > 0)
        {
            string s = $"{++swapCount}: {arr[cycle[cycleIdx]]}[{cycle[cycleIdx]}]"
                     + $"<--> {arr[cycle[cycleIdx-1]]}[{cycle[cycleIdx-1]}]";
            T tmp = arr[cycle[cycleIdx]];

            arr[cycle[cycleIdx]] = arr[cycle[cycleIdx - 1]];
            arr[cycle[cycleIdx - 1]] = tmp;

            foreach(T t in arr)
            {
                s = s + " " + t;
            }
            Console.WriteLine(s);
        }
    }
}

答案 1 :(得分:2)

问了问题已经一年多了,但是我自己需要一个答案,而且我有一段时间想出一种可以产生最佳结果的算法。如果有人也需要它,我会解释。但是您必须自己做一些腿部工作。

编辑:我使用Longest Increasing Subsequence algorithm from wikipedia代替了最长的 Common 子序列。直到后来我才看到。我认为我的算法可以适应使用L.C.S.如果您愿意,但可能同时有优点和缺点。


为了更清楚地说明为什么最长的递增子序列可以导致最佳解决方案,我想参考this answer上的另一个堆栈交换问题:

  

不变的是,每一步只能使最长的子序列中的数字最多增加1。

     

如果初始数组在其最长的递增子序列中具有k个值,则至少需要nk次移动才能对其进行排序。这表明必须进行nk次移动。

问题是,就像您说的那样,当您四处移动物品时,许多其他物品也会移动,而这些物品的位置也变得未知。


了解这一点,让我们退后一会。您提供了以下数组:

int[] original = { 201, 203, 208, 117, 89 };
int[] desired = { 208, 117, 89, 203, 201 };

为了能够获得最长的递增子序列并最终得到有用的东西,我们必须以某种方式对项目编号,以便最终以一个长的递增序列结尾:

original2 = { 4, 3, 0, 1, 2 }; // Replace every number by the index of that number in the "desired" array.
desired2 = { 0, 1, 2, 3, 4 }; // Increasing sequence / indexes.

现在很容易看到L.I.S.是[0,1,2],必须移动的项目是[4,3]。 Axel的答案为我们提供了一种获取original2的算法:

// find initial ordering
foreach(int io in original)
{
    int pos = -1;
    for (int i = 0; i < desired.Length; i++)
    {
        if (desired[i] == io)
        {
            pos = i;
            break;
        }
    }
    original2.Add(pos);
}

让我们以另一种方式展示数组:

enter image description here

The代表null item。红色(变成紫色)的数字必须移动。在这种情况下,必须将3和4从∅和0之间的某个位置移动到2和末尾之间的某个位置。根据项目的移动顺序,它们可能会相对于非移动编号插入不同的位置。但是我们可以肯定地知道,在整个重新排序过程中,任何时候一个项目都会在哪个固定号码之间结束。因此,将固定号码用作锚或信标是有用的。这些锚点可用于确定项目在被删除和插入时的绝对位置。

为了跟踪相对于锚点的移动项目,我将使用“ bucket”一词。这只是我给它起的名字。每个存储桶都有一个锚点,一个已插入到存储桶中的项目列表以及在某个时候将从存储桶中删除的项目列表。

class Bucket {
    int anchor; // The non-moving item in front of the bucket
    int[] inserted;
    int[] toBeRemoved;
}

存储桶的布局与普通数组同义。由于所有要删除的项目在重新排序的末尾都消失了,因此尝试在它们之间的某个位置插入新项目毫无意义。最终将没有任何余地。这仅在计算索引时很困难。比较简单的方法是,将所有新项目插入要删除的项目之前。

下面是存储桶的图形表示。了解每个项目如何与original2数组保持在同一位置。

enter image description here

让我们移动一个项目。对于此算法,按照什么顺序移动它们并不重要。对于我自己的用例,我希望它们按照最终移动的顺序移动。可以创建需要执行的动作列表并对该列表进行排序,或者如果您不关心顺序,也可以,您就可以遍历存储桶及其中的toBeRemoved项。我将在图纸中执行后一个。

enter image description here

要计算要删除的商品的绝对索引:

  1. 计算包含该项目的存储区之前的每个存储区的大小。减去一个以排除第一个存储桶中的空项目。

    numBeforeBucket = buckets.TakeWhile(b => b != sourceBucket)
                             .Sum(b => 1 + b.inserted.Length + b.toBeRemoved.Length) - 1
    
  2. 计算当前存储区中 中当前项目之前的项目数。

    numBeforeInBucket = 1 + sourceBucket.inserted.Length + indexOfItemWithinTheToBeRemovedArray
    
  3. 将这些值加起来。

    sourceIndex = numBeforeBucket + numBeforeInBucket
    

现在从桶中取出物品:

// You probably know the value already from one of the loop variables,
// but if you don't:
var item = sourceBucket.toBeRemoved[indexOfItemWithinTheToBeRemovedArray];
sourceBucket.toBeRemoved.Remove(item);

注意:如果PowerPoint中的slide.MoveTo(int toPos)方法占据了目标位置,就好像该项目尚未被移走,则您需要等待从存储桶中移出该项目,直到计算出目标位置为止

要计算插入项目的绝对索引:

  1. 计算将要插入项目的存储桶之前的每个存储桶的大小。减去一个以排除第一个存储桶中的空项目。

    numBeforeBucket = buckets.TakeWhile(b => b != targetBucket)
                             .Sum(b => 1 + b.inserted.Length + b.toBeRemoved.Length) - 1
    
  2. 计算目标存储桶中新项目之前的项目数。

    // Determine where to insert the item. Everything in "inserted" is
    // already sorted so just get the index of the first item with a
    // larger value. The way that .Insert(index, value) works is that
    // the item will be inserted before the item currently occupying
    // that index, pushing the occupying item to the right.
    var i = 0;
    for(; i < targetBucket.inserted.Length; i++) {
        var current = targetBucket.inserted[i];
        if(current > item) { // Item is the same variable from when we removed it.
            break;
        }
    }
    var indexOfItemWithinInsertedArray = i; // For clarity.
    
    numBeforeInBucket = 1 + indexOfItemWithinInsertedArray
    
  3. 将这些值加在一起,然后减去一个。

    targetIndex = numBeforeBucket + numBeforeInBucket
    

将项目插入存储桶:

targetBucket.inserted.Insert(indexOfItemWithinInsertedArray, item);

enter image description here

现在重复所有必须移动的项目。

enter image description here enter image description here

我尝试使用有效的C#。我已经有一段时间没有做C#了,我的概念证明是用Go编写的,它执行循环和数组操作有些不同。您可能需要将某些.Length更改为某些.Count()。如果事情不起作用,我建议您查找一次错误。

如果您需要更多的解释或范例,请问。