【问题标题】:Efficient algorithm for ordering different types of objects排序不同类型对象的高效算法
【发布时间】:2016-05-26 05:48:01
【问题描述】:

鉴于我们收集了不同类型的视频(例如类型 A、B 和 C,...),我们正在寻找一种有效的算法来将这些对象排序到播放列表中,以便我们具有最大的分散性。也就是说,如果可以避免的话,我们要确保来自 A 的两个视频不会背靠背放置。 播放列表会重复播放(结束时会重新开始。所以这方面也应该考虑)。

什么是能够以良好的分散性执行上述操作的有效算法?

输入示例:

  • 5 个 A 型对象(A1、A2、A3、A4、A5)
  • 3 个 B 类对象(B1、B2、 B3)

输出 - 非最佳

A1、B1、A2、B2、A3、B3、A4、A5

这不是最优的,因为在 A4 之后,A5 播放,然后播放列表循环返回并播放 A1。现在我们已经连续播放了 3 个类型 A 的视频。

最佳输出

A1、B1、A2、A3、B2、A4、B4、A5

这是最佳选择,因为我们只有 2 个相同类型的视频背靠背播放。

请注意,该算法应适用于不同数量的类型和视频。

【问题讨论】:

  • 你的意思是列表永远循环播放,我们想尽量减少从一组连续播放的视频数量吗?
  • 我是否理解正确,您只想最小化最大数量的连续项目:即,如果无法避免,您更喜欢连续多次出现 3 个元素而不是单次连续 4 个元素。这是正确的吗?
  • @dingalapadum 是的,我想最小化同一组中连续项目的最大数量。
  • 如果有多个解决方案,其中没有来自同一组的连续项目,如何选择一个解决方案?
  • 每个视频只有一个属性/标签?它可以像一个视频一样适合 A 和 C 类型

标签: algorithm


【解决方案1】:

这类似于我几年前遇到的一个问题:混合液体以避免分层。这个想法是,如果您将液体 A、B 和 C 混合到一个容器中,您不想将它们一个接一个地倒入容器中。相反,您想按相对比例添加一些 A、一些 B、一些 C 等。

这和在列表中均匀分布项目是一样的问题。

假设您有 30 个 A 类视频、20 个 B 类视频和 10 个 C 类视频,总共 60 个视频。那么,每隔一个视频就必须是 A。每隔三个视频就是一个 B,每隔六个视频就是一个 C。

所以 A 在 0、2、4、6、8 等处。 B 在 0、3、6、9、12 等处。而 C 的位置是 0、6、12、18 等。

显然,您有必须解决的冲突。

我这样做的方法是构建一个最小堆,其中包含视频类型及其频率,以及它的当前位置,从频率/2 开始。所以堆包含:{A,2,1},{B,3,1},{C,6,3}

要生成您的列表,请从堆中删除最低的项目并将其添加到您的列表中。然后,将其频率添加到当前位置,并将其放回堆中。所以在第一次通过之后,你已经输出了 A,你的堆现在包含:{B,3,1},{A,2,2},{C,6,3}

输出B,然后加回来,给你{A,2,2},{C,6,3},{B,3,4}

您当然还希望保留每个项目的计数,每次输出该项目时都会减少计数,并且如果计数变为 0,则不会将其添加回堆中。

大约一年前,我在我的博客中详细介绍了这一点。见Evenly distributing items in a list

就效率而言,算法复杂度为O(n log k),其中n是视频总数,k是视频类型数。

【讨论】:

  • 澄清一下,堆是按“当前位置”值排序的。为了获得 O(n log k) 复杂度,在每个步骤中,您只需更新取出并重新插入堆中的单个项目的成本(而我的方法会更新所有类型的成本)。但是根据堆如何打破关系,输入 [3,2,1] 可以给出 ABACAB 或 ABACBA,它们在包装时具有连续的 AA。
  • @PeterStock:你完全正确。事实证明,如果您将堆调整为以一种方式工作,则该算法对某些输入产生的输出不是最优的。但是如果你调整它来为那些创造最佳输出,那么它为其他人产生的输出就不是最佳的。我最终得出的解决方案(我还没有在我的博客上发布)是一种折衷方案,它在某些情况下产生最佳输出,而在其他情况下产生非常好的输出。这在很大程度上取决于您认为什么是“最佳”。
【解决方案2】:

这是我的算法,适用于任意数量的类型,而不仅仅是 2:

  • 调用类型(例如 A、B、C、...)T。
  • 调用类型为 T N(T) 的项目数。

伪代码算法:

var size = 0;
for_each (T)
  size += N(T);

var output = array(size); // Initialised to null, to mean gap (no item)
var gapsRemaining = size;

for_each (T)
{
  var itemsRemaining = N(T);
  var i = 0;
  var limit = itemsRemaining / gapsRemaining;
  while (itemsRemaining > 0)
  {
    if (itemsRemaining / (gapsRemaining - i) >= limit)
    { output[{i}th_gap] = next_item_of_type(T)
      gapsRemaining--;
      itemsRemaining--;
    }
    else
      i++;
  }
}

{i}th_gap 从零开始,如数组索引。

如果你可以在恒定时间内计算出{i}th_gap(这可以通过使用另一个计数器来完成),那么算法是线性时间,即O(size) O(size * numTypes)。

对于您的示例,它给出了输出 a b a b a a b a


编辑

再想一想:如果你只维护每种类型的计数,它不需要那么复杂。

工作 JS 代码 (http://js.do/code/96801)

var numItems = [5,3]; // for AAAAABBB
var numItems = [6,3,5]; // for AAAAAABBBCCCCC
var totalNumItems = 0;
for (i=0; i<numItems.length; i++)
    totalNumItems += numItems[i];
var limits = [];
for (i=0; i<numItems.length; i++)
    limits[i] = numItems[i] / totalNumItems;
var numGaps = totalNumItems;
var output = [];
for (i=0; i<totalNumItems; i++)
{   var bestValue = 0;
    var bestType;
    for (j=0; j<numItems.length; j++)
    {   var value = numItems[j] / numGaps - limits[j];
        if (value >= bestValue)
        {   bestValue = value;
            bestType = j;
    }   }
    output[i] = bestType;
    numItems[bestType]--;
    numGaps--;
}
for (i=0; i<totalNumItems; i++)
    document.writeln(output[i]);
document.writeln("<br>");

但正如@Jim 所说,它是 O(n * k),其中 n 是 totalNumItems,k 是 numItems.length。所以他的 O(n log k) 解具有更好的复杂度。


编辑 2

调整以更好地打破关系,因此首选更频繁的项目。 [10,1,1] 之前的代码输出是 caaabaaaaaaa,现在是 abaaaaacaaaa

http://js.do/code/96848

var numItems = [10,1,1];
var totalNumItems = 0;
for (i=0; i<numItems.length; i++)
    totalNumItems += numItems[i];
var limits = [];
for (i=0; i<numItems.length; i++)
    limits[i] = numItems[i] / totalNumItems;
var numGaps = totalNumItems;
var output = [];
for (i=0; i<totalNumItems; i++)
{   var bestValue = 0;
    var bestNumItems = 0;
    var bestType;
    for (j=0; j<numItems.length; j++)
    {   var value = numItems[j] / numGaps - limits[j];
        if (value >= bestValue && numItems[j] > bestNumItems)
        {   bestValue = value;
            bestNumItems = numItems[j];
            bestType = j;
    }   }
    output[i] = bestType;
    numItems[bestType]--;
    numGaps--;
}
for (i=0; i<totalNumItems; i++)
    document.writeln(output[i]);
document.writeln("<br>");

【讨论】:

  • 一个有趣的想法,虽然我不清楚如何在恒定时间内找到第 i 个间隙。您显示的代码看起来像是 O(total_items * number_of_types),因为您的循环中有 i++。我认为您还希望确保您开始使用最频繁的类型填充,并逐步降低到最不频繁的类型。也就是说,你给了我一些改进我目前使用的基于堆的算法的新想法。
  • 你是对的 - i++ 破坏了它:( 我正在研究恒定时间间隔的东西,维护一系列间隔索引。我会考虑它是否可以在没有的情况下工作也对频率输入进行了排序 - 如果需要对它们进行排序,那么这将使其成为 O(n log n)。
  • 除非类型的数量很大,否则排序的频率输入不是问题。它甚至可能没有必要。我的基于堆的算法(请参阅我的答案和相关博客)是 O(n log k)(n 是项目总数,k 是类型数)。我想你可以做一些类似的事情,但对差距索引使用跳过列表。这也应该是 O(n log k)。
  • 干得好。您的代码和我的代码之间的主要区别在于,我明确维护一个优先级队列,而您在每次迭代时计算优先级。算法基本一样。
  • 当我尝试解决这个问题时,我最终编写了将单例组合在一起并专门处理它们的代码。所以像[10,1,1] 这样的东西变成了[10,2],所以它均匀地分布了单例。否则,它们总是倾向于在开头、结尾或正好在中间。
【解决方案3】:

这个问题似乎并不容易,因为组合的数量很大。如果我是对的,对于NaNbNc 三种类型的视频,有(Na+Nb+Nc-1)!/Na!Nb!Nc! 的可能性。 (分子中的-1 来自这样一个事实,即相互循环排列的序列被认为是相同的。)

对组合结构没有清楚的了解,我会尝试如下:

  • 定义一个评价播放列表分散程度的评价指标。这可能是同一组视频之间(循环)距离的总和。

例如

A1, B1, A2, B2, A3, B3, A4, A5

给予

2+2+2+2+2+4+1+1 = 16

A1, B1, A2, A3, B2, A4, B4, A5

给予

2+3+1+2+2+2+3+1 = 16

(这可能是一个不充分的指标,短距离应该受到更多的惩罚。)

  • 通过在可用类型中进行选择来尝试随机序列,直到它们用尽,并对序列进行评分。经过多次随机试验,保持最好成绩。 [我会在不替换的情况下模拟绘图,以便以平衡的方式消耗不同的类型。]

对于小型N,可以进行详尽的试验。

更新

令人惊讶的是,我建议的简单指标给出了一个恒定值!

【讨论】:

  • 我发现了一个更好的指标来比较相同类型项目之间距离的标准差。但即使这样也不理想,当您有许多(五种或更多)不同类型的视频和不同的数量时,这一点就变得很明显。而且,正如您所提到的,详尽的搜索很快就会变得难以处理。
【解决方案4】:

假设您有 A1、A2、...、An 和 B1、B2、...、Bm。

如果n>m,那么至少有2个A项会一个接一个地播放(如果播放列表是循环的[一直重复所有])。

您应该首先将 A 项放在一个循环上。然后在每两个连续的 A 项目之间放置一个 B 项目。这将分隔下一个到下一个 A 项目。剩下的 B 项,如果有剩下的,就放上去。

如果要确保第一个和最后一个项目不会都是A,那么在开头放置一个A项目,如果有足够的B项目,则在最后放置一个B项目。

作为一种计算算法,给每个 A 项分配数字(以 double 类型允许它们是有理数)并将这些数字从小到大排序。然后为每个 B 项分配连续 A 项的平均值。例如:

A1=3 A2=5 A3=10

ArrayA(0)=3

ArrayA(1)=5

ArrayA(2)=10

那么假设你有 4 个 B 项。

 On Error Resume Next

 For n=0 to 3

 ArrayB(n)=(ArrayA(n)+ArrayA(n+1))/2

 Loop

这个循环将尝试调用 ArrayA(3) 并给出一个错误,我们将在接下来的错误恢复中跳过它。然后,您可以将随机数分配给未分配的 B 项。

最后,合并两个数组,对它们进行排序。你会得到排序的数字。通过这些数字,以最佳排序的方式召回项目。

【讨论】:

    【解决方案5】:

    分割最大的视频数组并在分割点插入其他元素。

    视频类型 A:(An=5)[A1, A2, A3, A4, A5]

    视频类型 B:(Bn=3)[B1, B2, B3]

     1. Choose the Video type having maximum number of instances, in this
        case A.
     2. Divide: (An=5)[A1, A2, A3, A4, A5] / 2 = 2, (An=2)[A1, A2](An=3)[A3, A4, A5]
     3. Now insert one instance of B at the point of division as per step 1, 
        i.e (An=2)[A1, A2](Bn=1)[B1](An=3)(A3, A4, A5)
     4. Now repeat step 2, 3 with (An=2)[A1, A2]  and   (An=3)[A3, A4, A5] and so forth like we do in binary search.
        Final arrangement: (An=1)[A1](Bn=1)[B2](An=1)[A2](Bn=1)[B1](An=2)[A3, A4](Bn=1)[B3](An=1)[A5]
    

    【讨论】:

      【解决方案6】:

      以下是排列一个数组以使相邻的两个数字都不相同的Java程序。

              public int[] rearrangeArray(int[] arr) {
                  int n = arr.length;
      
                  // Store frequencies of all elements
                  // of the array
                  int[] count = new int[1000]; 
                  int[] visited = new int[1000]; 
      
                  for (int i = 0; i < n; i++)
                      count[arr[i]]++;
      
                  // Insert all characters with their frequencies
                  // into a priority_queue
                  PriorityQueue<RandomKey> pq = new PriorityQueue<>(11, new KeyComparator());
      
                  // Adding high freq elements in descending order
                  for (int i = 0; i < n; i++) {
                      int val = arr[i];
      
                      if (count[val] > 0 && visited[val] != 1)
                          pq.add(new RandomKey(count[val], val));
                      visited[val] = 1;
                  }
      
                  // 'result[]' that will store resultant value
                  int[] result = new int[n];
      
                  // work as the previous visited element
                  // initial previous element will be ( '-1' and
                  // it's frequency will also be '-1' )
                  RandomKey prev = new RandomKey(-1, -1);
      
                  // Traverse queue
                  int l = 0;
                  while (pq.size() != 0) {
      
                      // pop top element from queue and add it
                      // to result
                      RandomKey k = pq.peek();
                      pq.poll();
                      result[l] = k.num;
      
                      // If frequency of previous element is less
                      // than zero that means it is useless, we
                      // need not to push it
                      if (prev.freq > 0)
                          pq.add(prev);
      
                      // make current element as the previous
                      // decrease frequency by 'one'
                      (k.freq)--;
                      prev = k;
                      l++;
                  }
      
                  return result;
              }
      
      
      
           public class RandomKey {
      
                   int freq;
                   int num;
      
                  RandomKey(int freq, int num) {
                      this.freq = freq;
                      this.num = num;
                  }
              }
      
      
      class KeyComparator implements Comparator<RandomKey> {
      
          // Overriding compare()method of Comparator
          public int compare(RandomKey k1, RandomKey k2) {
              if (k1.freq < k2.freq)
                  return 1;
              else if (k1.freq > k2.freq)
                  return -1;
              return 0;
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-10-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-21
        • 2011-09-23
        • 2011-07-02
        • 1970-01-01
        相关资源
        最近更新 更多