计算相交圆盘数量的算法

67

给定一个由N个整数组成的数组A,我们在二维平面上绘制N个圆盘,其中第i个圆盘的中心为(0,i),半径为A[i]。 如果第k个圆盘和第j个圆盘有至少一个公共点,则称第k个圆盘和第j个圆盘相交。

编写一个函数

int number_of_disc_intersections(int[] A);

给定一个描述N个圆盘的数组A,如上所述,返回相交圆盘对的数量。例如,给定N=6

A[0] = 1
A[1] = 5
A[2] = 2
A[3] = 1
A[4] = 4
A[5] = 0

有11对相交的圆盘:

0th and 1st
0th and 2nd
0th and 4th
1st and 2nd
1st and 3rd
1st and 4th
1st and 5th
2nd and 3rd
2nd and 4th
3rd and 4th
4th and 5th

因此该函数应返回11。 如果相交对数超过10,000,000, 该函数应返回-1。该函数可以假设N不超过10,000,000。


1
算法必须高效吗?有具体的大O要求吗?我想到了一个O(n**2)的解决方案。 - vz0
2
@vz0 N 不超过 10,000,000 - Nikita Rybak
@vz0 是的,需要高效,因为它会影响总分。 - user590076
5
任何想要尝试这个挑战的人,可以参加Codility的演示挑战之一:圆盘交叉数量。他们在这篇博客文章中还有其他几个挑战。 - Defragged
4
第0和第1个圆相交的方式是什么?“公共点”是否意味着有任何重叠(比如一个圆在另一个圆内)或者只是它们的线条接触? - dlp
44个回答

90

O(N)时间复杂度和O(N)内存解决方案。

private static int Intersections(int[] a)
{
    int result = 0;
    int[] dps = new int[a.length];
    int[] dpe = new int[a.length];

    for (int i = 0, t = a.length - 1; i < a.length; i++)
    {
        int s = i > a[i]? i - a[i]: 0;
        int e = t - i > a[i]? i + a[i]: t;
        dps[s]++;
        dpe[e]++;
    }

    int t = 0;
    for (int i = 0; i < a.length; i++)
    {
        if (dps[i] > 0)
        {
            result += t * dps[i];
            result += dps[i] * (dps[i] - 1) / 2;
            if (10000000 < result) return -1;
            t += dps[i];
        }
        t -= dpe[i];
    }

    return result;
}

1
你的解决方案非常快速。你能解释一下吗?我修改了你的代码,以便它获得了100%(见下文)。 - ady
9
这很简单。首先,我们计算所有圆盘的起点和终点。然后遍历所有线并检查当前点内的圆盘数量。如果在当前点开始了一些圆盘,则交点计数会增加:已经激活的圆盘乘以当前点中开始的圆盘数(result += t * dps[i]),以及已开始的圆盘的相交计数(result += dps[i] * (dps[i] - 1) / 2)。例如,如果在某个点启动了5个圆盘,则其相交次数将增加(1+2+3+4+5个交点,或者使用求和公式5*(5-1)/2)。 - Толя
12
多么出色的解决方案啊!这应该是正确(更正确 :))的答案。只需对Tolia所说的内容进行更详细的解释。基本上,我们正在跟踪每个位置的当前“活动”磁盘。这是t的值。每当新的磁盘在位置i处开始时,它就会与该位置的所有现有磁盘相交。这就是为什么我们有result += t * dps[i]。我们还需要添加所有刚在位置i开始的磁盘之间的交点数,即排除已经存在的任何交点(result += dps[i] * (dps[i] - 1) / 2)。 - Abdul
2
在 Codility 上,此答案得分 87% 的正确性和 75% 的性能。 - cheshy
1
@cheshy 因为Codility还提到:“如果相交的对数超过10,000,000,则函数应返回-1”。 - wonder.mice
显示剩余6条评论

65

所以您想找到区间[i-A[i],i+A[i]]的交点数。

维护一个排序数组(称为X),其中包含i-A[i](还有一些额外空间,其中具有值i+A[i])。

现在遍历数组X,从最左侧区间开始(即最小的i-A[i])。

对于当前区间,进行二进制搜索以查看区间右端点(即i+A[i])将放置在哪里(称为排名)。 现在您知道它与左侧所有元素相交。

增加计数器和排名,并减去当前位置(假设为单索引),因为我们不希望计算区间和自身的交点两次。

O(nlogn)时间,O(n)空间。


2
@Moron- 这个能在O(n lg n)的时间内运行吗?难道不可能有O(n^2)的交点吗? - templatetypedef
@template: 不,我们最多只会将计数器增加O(n)次。 - Aryabhatta
3
我明白了,因为你不需要返回实际的交叉点,只需要数量。 - templatetypedef
为了保持O(nlogn)的时间复杂度并维护一个有序数组,需要使用像HeapSort或MergeSort这样复杂的排序算法。在仅30分钟内完成这样的任务(codility的要求)真的不现实,除非可以直接从标准库中调用这样的排序算法。 - jscoot
4
我不同意。在许多算法中,排序是基本步骤之一。你最好准备好一个排序函数。事实上,快速排序(quicksort)可能已经足够了。 - Aryabhatta
在重复的区间端点的情况下,建议的解决方案可能会返回错误的结果。例如,2个区间(1,3)和(3,5)。请注意,有两种排序方式:1 3(第一个区间的元素) 3(第二个区间的元素),5;或者1 3(第二个区间的元素) 3(第一个区间的元素),5;在第一种情况下,区间1的2个端点之间没有元素;而在第二种情况下,有1个元素。 - auxdx

16

在Codility上测试Python得分为100/100,时间复杂度为O(nlogn),空间复杂度为O(n)。

这里是@noisyboiler对@Aryabhatta方法的Python实现,包含注释和示例。完全归功于原始作者,任何错误/措辞不当都是我的错。

from bisect import bisect_right

def number_of_disc_intersections(A):
    pairs = 0

    # create an array of tuples, each containing the start and end indices of a disk
    # some indices may be less than 0 or greater than len(A), this is fine!
    # sort the array by the first entry of each tuple: the disk start indices
    intervals = sorted( [(i-A[i], i+A[i]) for i in range(len(A))] )

    # create an array of starting indices using tuples in intervals
    starts = [i[0] for i in intervals]

    # for each disk in order of the *starting* position of the disk, not the centre
    for i in range(len(starts)):

        # find the end position of that disk from the array of tuples
        disk_end = intervals[i][1]

        # find the index of the rightmost value less than or equal to the interval-end
        # this finds the number of disks that have started before disk i ends
        count = bisect_right(starts, disk_end )

        # subtract current position to exclude previous matches
        # this bit seemed 'magic' to me, so I think of it like this...
        # for disk i, i disks that start to the left have already been dealt with
        # subtract i from count to prevent double counting
        # subtract one more to prevent counting the disk itsself
        count -= (i+1)
        pairs += count
        if pairs > 10000000:
            return -1
    return pairs

实例:给定 [3, 0, 1, 6],磁盘半径如下所示:

disk0  -------         start= -3, end= 3
disk1      .           start=  1, end= 1
disk2      ---         start=  1, end= 3
disk3  -------------   start= -3, end= 9
index  3210123456789   (digits left of zero are -ve)

intervals = [(-3, 3), (-3, 9), (1, 1), (1,3)]
starts    = [-3, -3, 1, 1]

the loop order will be: disk0, disk3, disk1, disk2

0th loop: 
    by the end of disk0, 4 disks have started 
    one of which is disk0 itself 
    none of which could have already been counted
    so add 3
1st loop: 
    by the end of disk3, 4 disks have started 
    one of which is disk3 itself
    one of which has already started to the left so is either counted OR would not overlap
    so add 2
2nd loop: 
    by the end of disk1, 4 disks have started 
    one of which is disk1 itself
    two of which have already started to the left so are either counted OR would not overlap
    so add 1
3rd loop: 
    by the end of disk2, 4 disks have started
    one of which is disk2 itself
    two of which have already started to the left so are either counted OR would not overlap
    so add 0

pairs = 6
to check: these are (0,1), (0,2), (0,2), (1,2), (1,3), (2,3),    

为什么这个能够工作?我尝试在 PHP 中重新实现它,虽然结果正确率达到了100%,但性能却是0%。 - CMCDragonkai
要获得0%的性能分数,您必须在Codility运行的所有“性能”测试中全部失败。也许您可以尝试打印出这些测试用例的输入集,并检查您的PHP实现为什么会失败?这个Python方法确实(通过了所有这些测试)[https://codility.com/demo/results/demo58ZEHU-GNZ/]。 - GnomeDePlume
我认为我知道它为什么变慢了。PHP中没有bisect_right,也没有二分查找。所以我只是通过迭代值来找到右边之前有多少个左边缘。Bisect使用二分查找对吧? - CMCDragonkai
听起来这就是你的问题——你的搜索过程过于复杂,导致你的解决方案的时间复杂度为O(n^2)。是的,bisect_right是二分查找。二分查找的时间复杂度为O(log(n)),再加上外层循环的O(n),总时间复杂度为O(n log(n))。 - GnomeDePlume
是的,我为 PHP 实现了自己的 bisect_right 和 bisect_left,它们工作正常。 - CMCDragonkai

14

我将Falk Hüffner的想法改编成了C++,并对范围进行了修改。与上面所写的相反,没有必要超出数组的范围(无论其中的值有多大)。在Codility上,这段代码获得了100%的分数。感谢Falk提供的绝妙思路!

int number_of_disc_intersections ( const vector<int> &A ) {
    int sum=0;
    vector<int> start(A.size(),0);
    vector<int> end(A.size(),0);
    for (unsigned int i=0;i<A.size();i++){
        if ((int)i<A[i]) start[0]++;
        else        start[i-A[i]]++;
        if (i+A[i]>=A.size())   end[A.size()-1]++;
        else                    end[i+A[i]]++;
    }
    int active=0;
    for (unsigned int i=0;i<A.size();i++){
        sum+=active*start[i]+(start[i]*(start[i]-1))/2;
        if (sum>10000000) return -1;
        active+=start[i]-end[i];
    }
    return sum;
}

1
可以减少(可见)条件语句,使代码更易读。start[Max(0,i-a[i])]++; end[Min(a.Size()-1,i+a[i])]++; - ctrl-alt-delor
注释代码和/或一般策略的解释会提高这个答案的质量。 - Richard
为什么使用 if ((int)i<A[i]) start[0]++; 而不是 if ((int)i-A[i]<=0) start[0]++; - user3424033

12

这甚至可以在线性时间内完成[编辑:这不是线性时间,请参见评论]。事实上,如果你忽略每个点都恰好有一个间隔的事实,只将其视为间隔的起始点和终止点集合,那么这将变得更容易。然后你可以从左边扫描它(为简单起见,以下是Python代码):

from collections import defaultdict

a = [1, 5, 2, 1, 4, 0]

start = defaultdict(int)
stop = defaultdict(int)

for i in range(len(a)):
    start[i - a[i]] += 1
    stop[i + a[i]] += 1

active = 0
intersections = 0
for i in range(-len(a), len(a)):
    intersections += active * start[i] + (start[i] * (start[i] - 1)) / 2    
    active += start[i]
    active -= stop[i]

print intersections

3
这不太对。你的迭代需要从min(start)到max(stop)才能正确,这在输入大小上可能是指数级的。相反,你可以对start和stop进行排序,并稀疏地遍历它们,但这是O(n lg n)的。 - Keith Randall
你能解释一下为什么在计算交叉点时需要 + (start[i] * (start[i] - 1)) / 2 这部分吗? - Bunny Rabbit
您的解决方案在以下测试用例中失败:a = [1, 55, 2, 1, 4, 0] - Толя
扫描线算法可以基于Falk的思想用来解决这个问题。 - Jingguo Yao
可能我漏掉了什么,但是看起来算法似乎取决于半径的大小(数组“a”中的数字将强制创建适当的附加数组以进行开始和停止)。问题要求依赖于输入数组的长度。我错过了什么吗??? - Mike.R
显示剩余2条评论

10
这是一个时间复杂度为O(N),空间复杂度为O(N)的算法,在数组上需要运行3次,不需要排序,已验证得分100%:
您感兴趣的是圆盘对。每一对涉及一个盘的一侧和另一个盘的另一侧。因此,如果我们处理每个盘的一侧,就不会有重复的对了。让我们称这些侧为右侧和左侧(我在考虑时旋转了空间)。
重叠可能是由于右侧直接与中心重叠的另一个盘(因此对等于半径,并且要注意数组长度)或由于在最右边缘存在左侧的数量而导致的。
所以我们创建一个包含每个点左侧数量的数组,然后简单地求和。
C代码:
int solution(int A[], int N) {
    int C[N];
    int a, S=0, t=0;

    // Mark left and middle of disks
    for (int i=0; i<N; i++) {
        C[i] = -1;
        a = A[i];
        if (a>=i) {
            C[0]++;
        } else {
            C[i-a]++;
        }
    }
    // Sum of left side of disks at location
    for (int i=0; i<N; i++) {
        t += C[i];
        C[i] = t;
    }
    // Count pairs, right side only:
    // 1. overlaps based on disk size
    // 2. overlaps based on disks but not centers
    for (int i=0; i<N; i++) {
        a = A[i];
        S += ((a<N-i) ? a: N-i-1);
        if (i != N-1) {
          S += C[((a<N-i) ? i+a: N-1)];
        }
        if (S>10000000) return -1;
    }
    return S;
}

它按照描述工作,但是我就是无法理解! =/ 请问还有其他人能提供更好的解释吗? - Bill_BsB
1
C[i] = -1; 这一行是使得该算法正常工作的关键,干得好 :) - fire in the hole
@Bill_BsB 也许你可以在纸上手动执行算法,这样会有所帮助吧? - w00t

6
我用这个C++实现得到了100分:
#include <map>
#include <algorithm>

inline bool mySortFunction(pair<int,int> p1, pair<int,int> p2)
{
    return ( p1.first < p2.first );
}

int number_of_disc_intersections ( const vector<int> &A ) {
    int i, size = A.size();
    if ( size <= 1 ) return 0;
    // Compute lower boundary of all discs and sort them in ascending order
    vector< pair<int,int> > lowBounds(size);
    for(i=0; i<size; i++) lowBounds[i] = pair<int,int>(i-A[i],i+A[i]);
    sort(lowBounds.begin(), lowBounds.end(), mySortFunction);
    // Browse discs
    int nbIntersect = 0;
    for(i=0; i<size; i++)
    {
        int curBound = lowBounds[i].second;
        for(int j=i+1; j<size && lowBounds[j].first<=curBound; j++)
        {
            nbIntersect++;
            // Maximal number of intersections
            if ( nbIntersect > 10000000 ) return -1;
        }
    }
    return nbIntersect;
}

我尝试了你的解决方案,得分为87。我稍微改动了一下,达到了93分,但不幸的是我只有一个测试没有通过。无论如何,你的想法很棒,易于理解! - Coder
1
运行此代码时得到了93%的结果。不明白为什么最坏情况下这里不会是O(N^2),考虑到有两个for循环和一个值为20亿的10万个整数的数组。 - Raul
也不太明白按下限排序的意义,虽然它似乎是有效的。你能解释一下吗?谢谢。 - Raul

3

100/100 c#

  class Solution
    {
        class Interval
        {
            public long Left;
            public long Right;
        }

        public int solution(int[] A)
        {
            if (A == null || A.Length < 1)
            {
                return 0;
            }
            var itervals = new Interval[A.Length];
            for (int i = 0; i < A.Length; i++)
            {
                // use long to avoid data overflow (eg. int.MaxValue + 1)
                long radius = A[i];
                itervals[i] = new Interval()
                {
                    Left = i - radius,
                    Right = i + radius
                };
            }
            itervals = itervals.OrderBy(i => i.Left).ToArray();

            int result = 0;
            for (int i = 0; i < itervals.Length; i++)
            {
                var right = itervals[i].Right;
                for (int j = i + 1; j < itervals.Length && itervals[j].Left <= right; j++)
                {
                    result++;
                    if (result > 10000000)
                    {
                        return -1;
                    }
                }
            }
            return result;
        }
    }

3
一个Python答案
from bisect import bisect_right

def number_of_disc_intersections(li):
    pairs = 0
    # treat as a series of intervals on the y axis at x=0
    intervals = sorted( [(i-li[i], i+li[i]) for i in range(len(li))] )
    # do this by creating a list of start points of each interval
    starts = [i[0] for i in intervals]
    for i in range(len(starts)):
        # find the index of the rightmost value less than or equal to the interval-end
        count = bisect_right(starts, intervals[i][1])
        # subtract current position to exclude previous matches, and subtract self
        count -= (i+1)
        pairs += count
        if pairs > 10000000:
            return -1
    return pairs

你能给我们一个简短的解释吗?它和被接受的答案有什么不同吗? - Austin Henley

3

我提供了另一种解决方案,因为我发现之前的解决方案中使用的计数原则不易理解。虽然结果相同,但更加直观的计数过程和解释值得呈现。

首先,考虑O(N^2)解决方案,按照它们的中心点顺序迭代遍历圆盘,并计算以当前圆盘为中心,位于其右侧且与之相交的圆盘数量,条件为current_center + radius >= other_center - radius。注意,我们可以使用条件current_center - radius <= other_center + radius统计位于当前圆盘左侧的圆盘数量,得到相同的结果。

def simple(A):
    """O(N^2) solution for validating more efficient solution."""
    N = len(A)
    unique_intersections = 0
    # Iterate over discs in order of their center positions 
    for j in range(N):
        # Iterate over discs whose center is to the right, to avoid double-counting.
        for k in range(j+1, N):
            # Increment cases where edge of current disk is at or right of the left edge of another disk.
            if j + A[j] >= k - A[k]:
                unique_intersections += 1
                # Stop early if we have enough intersections.
                # BUT: if the discs are small we still N^2 compare them all and time out.
                if unique_intersections > 10000000:
                    return -1
    return unique_intersections

如果我们能够“查找”与当前圆盘相交的右侧(或左侧)圆盘数,就能将时间复杂度从O(N^2)降至O(N)。关键的洞见是重新解释相交条件为“一个圆盘的右侧边缘重叠了另一个圆盘的左侧边缘”,也就是说,只有边缘而非中心点才重要。
下一个洞见是尝试排序边缘,耗时O(N log N)。给定左侧边缘和右侧边缘的有序数组,在沿着数轴从左到右扫描时,左侧或右侧边缘在当前位置点左侧的数量仅仅是对应左侧边缘和右侧边缘的当前索引:常数时间推导。
最后,我们使用“右侧边缘 > 左侧边缘”的条件来推断当前圆盘与仅从当前圆盘左侧启动的圆盘(为避免重复)之间的交点数等于当前边缘左侧的左侧边缘数减去当前边缘左侧的右侧边缘数。也就是说,开始于此之前的圆盘数减去已关闭的圆盘数。
现在是经过Codility 100%测试的代码:
def solution(A):
    """O(N log N) due to sorting, with O(N) pass over sorted arrays"""
    N = len(A)
    # Left edges of the discs, in increasing order of position.
    left_edges = sorted([(p-r) for (p,r) in enumerate(A)])
    # Right edges of the discs, in increasing order of position. 
    right_edges = sorted([(p+r) for (p,r) in enumerate(A)])
    #print("left edges:", left_edges[:10])
    #print("right edges:", right_edges[:10])
    intersections = 0
    right_i = 0
    # Iterate over the discs in order of their leftmost edge position.
    for left_i in range(N):
        # Find the first right_edge that's right of or equal to the current left_edge, naively:
        # right_i = bisect.bisect_left(right_edges, left_edges[left_i])
        # Just scan from previous index until right edge is at or beyond current left:
        while right_edges[right_i] < left_edges[left_i]:
             right_i += 1
        # Count number of discs starting left of current, minus the ones that already closed.
        intersections += left_i - right_i
        # Return early if we find more than 10 million intersections.
        if intersections > 10000000:
            return -1
    #print("correct:", simple(A))
    return intersections

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接