更快的计算活动呼叫的算法

时间:2012-05-23 18:52:51

标签: java algorithm optimization

我们正在为呼叫中心实施密度报告。结果必须显示为每天一行显示当天同时有效呼叫的最大数量的表格。

我们正在构建UI背后的lib。合同指定我们接收当天的调用次数和两个整数数组,一个包含开始时间,另一个包含每次调用的结束时间,例如:

在某一天,只收到两个电话:一个从20到30,另一个从10到20.同时拨打的最大号码是1.

另一方面,另一天,还收到两个电话,一个从10到45,另一个从15到40,然后最大同时电话数是2.

Web服务合同是

public static int GetMaxDensity(int N, int[] X, int[] Y)

数据看起来像这样(假设当天收到3个电话)。第一个从10到25,第二个从12到30,第三个从20到23。

N = 3, 
X = {10, 12, 20}
Y = {25, 30, 23}

返回必须是:3。

我已经实施了这个解决方案:

public static int GetMaxDensity(int N, int[] X, int[] Y) 
{
  int result = 0;
  for (int i = 0; i < N; i++) 
  {
      int count = 0, t = X[i];
      for (int j = 0; j < N; j++) 
      {
        if (X[j] <= t && t < Y[j])
        count++;
      }
      result = Math.max(count, result);
   }
   return result;
}

当呼叫次数达到1000次(周末)时效果很好,但在工作日内,数量非常大,计算时间也很长(> 5分钟)。我现在的原因可能是我的解决方案是使用两个嵌套循环,但我没有很多复杂算法的经验所以我的问题是:

鉴于我只需要最大数量的同时呼叫(不是时间和呼叫者),如果有的话,这可能是更快的方式来执行此计算。

7 个答案:

答案 0 :(得分:5)

随着N的增长,你的时间会迅速增长(N * N)。一个简单的解决方案(如果你的时间是午夜过了几分钟)将创建一个1440个整数的数组,其中包含当天每分钟的通话数变化。然后,您只需从0循环一次到N-1,并且对于每个元素,通过在呼叫开始时递增值并在结束时递减来调整该时间点的呼叫计数增量的计数。之后,只需查看计数即可获得最大值。对于较大的N值,这应该快得多。

由于1440是常数(对于最后一步),并且输入不需要排序,因此这应该具有线性时间复杂度。该算法的运行时间不受平均呼叫长度的影响。

public static int GetMaxDensity(int N, int[] X, int[] Y) {
    int rangeStart = Integer.MAX_VALUE;
    int rangeEnd = Integer.MIN_VALUE;
    for(int i=0; i<N; i++) {
        if (X[i] < rangeStart) rangeStart = X[i];
        if (Y[i] > rangeEnd) rangeEnd = Y[i];
    } 
    int rangeSize = rangeEnd - rangeStart + 1;
    int[] histogram = new int[rangeSize];
    for (int t = 0; t < rangeSize; t++) histogram[t] = 0;
    for (int i = 0; i < N; i++) {
        histogram[X[i]-rangeStart]++;
        histogram[Y[i]-rangeStart]--;
    }
    int maxCount = 0;
    int count = 0;
    for (int t = 0; t < rangeSize; t++) {
        count += histogram[t];
        if (count > maxCount) maxCount = count;
    }
    return maxCount;        
}

对于比较,当N = 50,000并且随机呼叫长度在1到40分钟之间时,问题中的算法使用29,043毫秒,并且该算法使用8毫秒。我在c#中运行这些测试,但它们应该与Java产生的相当。

答案 1 :(得分:2)

请允许我提出一个不同的算法。鉴于每天最多24 * 60 = 1440分钟,为什么不制作直方图阵列来计算每分钟的同时呼叫数。

public static int GetMaxDensity(int N, int[] X, int[] Y) 
{
  int[] h = new int[1440];
  // loop through all calls
  for (int i=0; i<N ; i++){
    addIt(X[i], Y[i], h);
  }

  // find max
  int m = 0;
  for(int i =0 ; i<1440; i++){
    if (h[i]>m)
      m = h[i];
  }
  return m;
}

// counting for one call
public static void addIt(int x, int y, int[] h){
  for ( int i=x;i<y;i++){
    h[i]++;
  }
}

复杂度为O(m * n),其中m是呼叫的平均长度。由于呼叫次数可能远远超过1000,所以运气好的话,这种算法在实践中会更快。

答案 2 :(得分:1)

你的算法非常慢,因为它会测试所有可能的情况,即O(n ^ 2)。

假设您收到呼叫时已经订购了,这里有一个O(n)算法: [编辑:第二个数组应该排序]

    int max;
    int i=0,j=0,count=0;
    while(i<n && j<n){
        if(x[i]<y[j]){ //new call received
            count++;
            max = count>max? count:max;
            i++;
        }else if(x[i]==x[j]){ //receive new call at the same time of end call
            i++;
            j++;
        }else { //call ended
            count--;
            j++;
        }
    }
    return max;
  }

[注意:这段代码很可能会导致数组索引超出范围错误,但应该足以证明这个想法,以便您可以实现其余的]

如果调用未排序,则算法为O(n lg n):

array_of_calldata a = x union y
a.sort();
foreach(calldata d in a){
    if (d is new call) count++;
    else count--;
}
return max_value_of_count;

答案 3 :(得分:1)

按开始时间对所有通话进行排序。遍历列表并保留“活动呼叫”列表,按结束时间排序。看起来应该类似于:

public class DensityReport {

  static int count;

  static class Call {
    public Call(int x, int y) {
      double f = 0.1/(++count);
      start = x + f;
      end = y + f;
    }
    double start;
    double end;
  }

  public static int getMaxDensity(int n, int[] x, int[] y) {
    // Calls sorted by start time
    TreeSet<Call> calls = new TreeSet<Call>(new Comparator<Call>() {
      public int compare(Call c1, Call c2) {
        return c1.start < c2.start ? -1 : c1.start > c2.start ? 1 : 0;
      }
    });

    // Add all calls to the sorted set.
    for (int i = 0; i < n; i++) {
      calls.add(new Call(x[i], y[i]));
    }

    int max = 0;
    // Active calls sorted by end time
    TreeSet<Call> activeCalls = new TreeSet<Call>(new Comparator<Call>() {
      public int compare(Call c1, Call c2) {
        return c1.end < c2.end ? -1 : c1.end > c2.end ? 1 : 0;
      }
    });

    for (Call call: calls) {
      // Remove all calls that end before the current call starts.
      while(activeCalls.size() > 0 && activeCalls.first().end < call.start) {
        activeCalls.pollFirst();
      }
      activeCalls.add(call);
      if (activeCalls.size() > max) {
        max = activeCalls.size();
      }
    }
    return max;
  }
}

运行时应为O(n log n)

P.S。:如果我们可以假设调用是按照开始时间排序的,那么应该可以简化这一点。

答案 4 :(得分:0)

使用两个列表,将X [i] Y [i]对添加到这些列表中。第一个列表按呼叫开始时间排序,第二个列表按结束时间排序。迭代列表只能踩到最低时间列表。

class Call {
    int start;
    int end;
}

Call callsSortedOnStart[];
Call callsSortedOnEnd[];

int indexStart = 0;  // Position in the array
int indexEnd = 0;

int nCalls = 0;      // Current density of calls
int maxCalls = 0;    // Maximum density of calls

while(indexStart < callsSortedOnStart.length && indexEnd < callsSortedOnEnd.length) {
    while(callsSortedOnStart[indexStart].start <= callsSortedOnEnd[indexEnd].end) {
        indexStart++;
        nCalls++;
    }
    maxCalls = max(maxCalls, nCalls);

    while(callsSortedOnStart[indexStart].start > callsSortedOnEnd[indexEnd].end) {
        indexEnd++;
        nCalls--;
    }
}

答案 5 :(得分:0)

制作一系列通话活动。呼叫事件只是一个具有时间字段和起始字段的结构,其值为+1或-1,用于呼叫开始和呼叫结束。按时间字段对此数组进行排序(如果时间相等,则使用第二个字段,在开始事件之前结束事件)。 Initialize CurrentCalls = 0.迭代数组,将StartEnd字段添加到CurrentCalls。阵列扫描期间CurrentCalls的最大值是您所需要的。

答案 6 :(得分:0)

按开始时间对您的持续时间进行排序。这样,当内循环中的开始时间超出外循环提供的范围时,您可以break内循环。