查找具有彼此最接近的K个元素的子集

时间:2013-10-20 20:15:34

标签: python algorithm

给定一个整数大小 N 的数组,如何使用彼此最接近的元素有效地找到大小 K 的子集?

让子集(x1,x2,x3,.. xk)的接近度定义为:

enter image description here

2 <= N <= 10^5

2 <= K <= N

约束:数组可能包含重复项,无法保证排序。

对于大N,我的强力解决方案非常慢,并且它不会检查是否有超过1个解决方案:

N = input()
K = input()
assert 2 <= N <= 10**5
assert 2 <= K <= N
a = []
for i in xrange(0, N):
    a.append(input())
a.sort()

minimum = sys.maxint
startindex = 0

for i in xrange(0,N-K+1):
    last = i + K
    tmp = 0
    for j in xrange(i, last):
        for l in xrange(j+1, last):
            tmp += abs(a[j]-a[l])
            if(tmp > minimum):
                break

    if(tmp < minimum):
        minimum = tmp
        startindex = i #end index = startindex + K?

示例:

N = 7
K = 3
array = [10,100,300,200,1000,20,30]
result = [10,20,30]

N = 10
K = 4
array = [1,2,3,4,10,20,30,40,100,200]
result = [1,2,3,4]

6 个答案:

答案 0 :(得分:6)

您当前的解决方案是O(NK^2)(假设K > log N)。通过一些分析,我相信您可以将其减少到O(NK)

最接近的K大小集将由排序列表中相邻的元素组成。您基本上必须首先对数组进行排序,因此后续分析将假设每个K数字序列都已排序,这样可以简化双和。

假设数组在x[j] >= x[i]时排序j > i,我们可以重写您的接近度量标准以消除绝对值:

enter image description here

接下来,我们将您的符号重写为带有简单边界的双重求和:

enter image description here

请注意,我们可以将x[i]x[j]之间的内部距离重写为第三个总和:

enter image description here

我使用d[l]来简化前进的记法:

enter image description here

请注意,d[l]是列表中每个相邻元素之间的距离。查看固定i的内部两个求和的结构:

j=i+1         d[i]
j=i+2         d[i] + d[i+1]
j=i+3         d[i] + d[i+1] + d[i+2]
...
j=K=i+(K-i)   d[i] + d[i+1] + d[i+2] + ... + d[K-1]

注意内部两个求和的三角形结构。这允许我们根据相邻项的距离将内部两个求和重写为单个求和:

total: (K-i)*d[i] + (K-i-1)*d[i+1] + ... + 2*d[K-2] + 1*d[K-1]

将总和减少到:

enter image description here

现在我们可以看一下这个双重求和的结构:

i=1     (K-1)*d[1] + (K-2)*d[2] + (K-3)*d[3] + ... + 2*d[K-2] + d[K-1]
i=2                  (K-2)*d[2] + (K-3)*d[3] + ... + 2*d[K-2] + d[K-1]
i=3                               (K-3)*d[3] + ... + 2*d[K-2] + d[K-1]
...
i=K-2                                                2*d[K-2] + d[K-1]
i=K-1                                                           d[K-1]

再次注意三角形图案。然后总和变为:

1*(K-1)*d[1] + 2*(K-2)*d[2] + 3*(K-3)*d[3] + ... + (K-2)*2*d[K-2] 
  + (K-1)*1*d[K-1]

或者,写成一个总和:

enter image description here

这种紧凑的相邻差异单一总和是更有效算法的基础:

  1. 对数组进行排序,订购O(N log N)
  2. 计算每个相邻元素的差异,订单O(N)
  3. 迭代每个N-K差异序列并计算上述总和,订单O(NK)
  4. 请注意,第二步和第三步可以合并,但使用Python时,您的里程可能会有所不同。

    代码:

    def closeness(diff,K):
      acc = 0.0
      for (i,v) in enumerate(diff):
        acc += (i+1)*(K-(i+1))*v
      return acc
    
    def closest(a,K):
      a.sort()
      N = len(a)
      diff = [ a[i+1] - a[i] for i in xrange(N-1) ]
    
      min_ind = 0
      min_val = closeness(diff[0:K-1],K)
    
      for ind in xrange(1,N-K+1):
        cl = closeness(diff[ind:ind+K-1],K)
        if cl < min_val:
          min_ind = ind
          min_val = cl
    
      return a[min_ind:min_ind+K]
    

答案 1 :(得分:2)

如果O(N*K)已排序,则可以使用A完成此过程。如果未对A进行排序,则时间将受到排序过程的限制。

这基于2个事实(仅在订购A时相关):

  • 最近的子集将始终是后续的
  • 在计算K后续元素的接近程度时,距离之和可以计算为(K-i)*i i 1,...,K-1 K时间K的每两个后续元素的总和。
  • 当遍历排序的数组时,重新计算整个总和是多余的,我们可以删除先前两个最小元素之间距离的O(1)倍,并添加List<pair> FindClosestSubsets(int[] A, int K) { List<pair> minList = new List<pair>; int minVal = infinity; int tempSum; int N = A.length; for (int i = K - 1; i < N; i++) { tempSum = 0; for (int j = i - K + 1; j <= i; j++) tempSum += (K-i)*i * (A[i] - A[i-1]); if (tempSum < minVal) { minVal = tempSum; minList.clear(); minList.add(new pair(i-K, i); } else if (tempSum == minVal) minList.add(new pair(i-K, i); } return minList; } 次两个新的最大元素的距离。这一事实被用于通过使用前一个子集的接近度来计算{{1}}中子集的接近程度。

这是伪代码

{{1}}

此函数将返回表示最佳解决方案(每个解决方案的起始和结束索引)的索引对的列表,在您要返回所有解决方案的问题中暗示最低价值。

答案 2 :(得分:1)

尝试以下方法:

N = input()
K = input()
assert 2 <= N <= 10**5
assert 2 <= K <= N
a = some_unsorted_list
a.sort()

cur_diff = sum([abs(a[i] - a[i + 1]) for i in range(K - 1)])
min_diff = cur_diff
min_last_idx = K - 1
for last_idx in range(K,N):
    cur_diff = cur_diff - \
               abs(a[last_idx - K - 1] - a[last_idx - K] + \
               abs(a[last_idx] - a[last_idx - 1])
    if min_diff > cur_diff:
        min_diff = cur_diff
        min_last_idx = last_idx

从min_last_idx,您可以计算min_first_idx。我使用范围来保持idx的顺序。如果这是python 2.7,它将需要线性更多的RAM。这与您使用的算法相同,但效率稍高(复杂度较小的常数),因为它的总和少于总和。

答案 3 :(得分:1)

itertools救援?

from itertools import combinations

def closest_elements(iterable, K):
    N = set(iterable)
    assert(2 <= K <= len(N) <= 10**5)

    combs = lambda it, k: combinations(it, k)
    _abs = lambda it: abs(it[0] - it[1])
    d = {}
    v = 0

    for x in combs(N, K):
        for y in combs(x, 2):
            v += _abs(y)

        d[x] = v
        v = 0

    return min(d, key=d.get)

>>> a = [10,100,300,200,1000,20,30]
>>> b = [1,2,3,4,10,20,30,40,100,200]
>>> print closest_elements(a, 3); closest_elements(b, 4)
(10, 20, 30) (1, 2, 3, 4)

答案 4 :(得分:1)

排序后,我们可以肯定,如果x1,x2,... xk是解,那么x1,x2,... xk是连续的元素,对吗?

所以,

  1. 取数字之间的间隔
  2. 将这些间隔相加以得到k个数之间的间隔
  3. 选择最小的

答案 5 :(得分:0)

我的初始解决方案是查看所有K元素窗口并将每个元素乘以m并取该范围内的总和,其中m由 - (K-1)初始化并在每一步中递增2并取整个清单中的最小金额。因此对于大小为3的窗口,m是-2,范围的值将是-2 0 2.这是因为我观察到一个属性,即K窗口中的每个元素都会为总和添加一定的权重。例如,如果元素是[10 20 30],则总和是(30-10)+(30-20)+(20-10)。因此,如果我们分解表达式,我们有2 * 30 + 0 * 20 +( - 2)* 10。这可以在O(n)时间内实现,并且整个操作将在O(NK)时间内。然而事实证明,这种解决方案并不是最优的,并且存在这种算法失败的某些边缘情况。我还没弄清楚那些情况,但是如果有人能从中找出有用的东西,那么无论如何都要分享解决方案。

for(i = 0 ;i <= n - k;++i)
{
    diff = 0;
    l = -(k-1);
    for(j = i;j < i + k;++j)
    {
        diff += a[j]*l;
        if(min < diff)
            break;
        l += 2;
    }
    if(j == i + k && diff > 0)
    min = diff;
}