给定一个约会列表,找出所有冲突的约会 代码输出复杂性代码结果:

问题描述

我有一个关于查找冲突约会的问题。

Appointments: [[4,5],[2,3],[3,6],[5,7],[7,8]]
Output: 
[4,5] and [3,6] conflict. 
[3,6] and [5,7] conflict. 

我试图自己解决这个问题,但失败了。我做了一些谷歌,但是,我不确定答案是否正确。你介意用 Python 分享你的想法吗? 我目前的想法是先对间隔进行排序,但不确定接下来要做什么。 感谢您的帮助。

更新
我用 O(n**2) 找出了低效的方法,并想用 O(nlogn) 寻找答案。谢谢。

这是一个测试用例,如果它比 O(nlgn) 慢,则有助于拒绝您的解决方

n = 10
intervals = []
for i in range(n):
    intervals.append((i,n))
print(intervals)

解决方法

另一个复杂度更高的答案 O(n log n)

代码

appointments = [[4,5],[2,3],[3,6],[5,7],[7,8]]


appointments.sort(key= lambda x:x[0]) #appointments get sorted by start time.

for i,a in enumerate(appointments):
    for i2 in range(i+1,len(appointments)):
        b = appointments[i2]
        if a[1] > b[0]: #if start of second appointment is before end of first appointment
            print(f"{a} and {b} conflict")
        else:
            break

输出

[3,6] and [4,5] conflict
[3,6] and [5,7] conflict

说明

通过先排序,代码变得更有效率。我们首先检查第一次约会并将其结束时间与第二次约会的开始时间进行比较。如果它们重叠,我们添加打印对并继续比较第一次约会与第三次、第四次、第五次约会,直到它们不再重叠。由于我们按开始时间对约会进行了排序,因此我们知道之后的所有约会都不会相交。

然后我们继续比较第二次约会和它之后的约会,依此类推。

复杂性

python 中的排序是 O(n log n)

因为我假设只有很少的冲突约会并且它们的长度相似,所以我们可以推断出检查冲突的循环是 O(n)。考虑到约会密度保持不变O(n),只有外循环随 O(1) 扩展,内循环复杂性根本不会增加。这是组合 O(n)。 (最坏的情况是 O(n^2),但只有当每个事件与其他每个事件相交时才会发生这种情况,这会很奇怪。)

但是因为预先排序需要O(n log n),所以算法作为一个洞也会运行一个O(n log n)

验证复杂性

要查看算法的执行情况,我们可以为不同大小的约会列表生成执行时间。我使用以下函数生成了应该接近真实世界数据的数据集:

def generate_test_data(test_size):
    appointments = []
    start_time = 0
    for i in range(test_size):
        appointments.append([start_time,start_time+choice(range(1,3))])
        start_time += choice(range(8))
    shuffle(appointments)
    return appointments

这会生成类似于@QiangSuper 示例但具有特定长度的列表。使用它,为不同的 n_(输入长度)_ 的算法计时并绘制运行时间。

我以我的算法和@Paddy3118s算法为例:

w = []
p = []
repetitions = 10**2 #runs 100 times for higher accuracy
for test_size in range(0,1000): 
    w.append(0); p.append(0)
    for i in range(repetitions):
        a = generate_test_data(test_size*10)

        b = deepcopy(a)
        start = timer()
        conflicts_w1(b)
        end = timer()
        w[test_size] += end-start

        b = deepcopy(a)
        start = timer()
        conflicts_p1(b)
        end = timer()
        p[test_size] += end - start

    print(f"{test_size*10},{w[test_size]/repetitions},{p[test_size]/repetitions}")

这会产生以下结果:

Runtime over n

可以看到蓝线线性上升,而橙色线表现得像一个二次函数,尽管两种实现看起来非常相似。但这种差异是可以解释的。

我的算法以 O(n log n) 进行缩放。但是因为 python 的 sort-function 是在 c 中实现的,所以它的效果只对更大的集合才可见。所以我的大部分算法运行时都可以归因于 for 循环,它线性扩展。

对于@Paddy3118s 算法,主要区别在于a[i+1:] 的使用。使用 O(n) 以 python 缩放的切片。所以他算法中的 for 循环用 O(n^2) 进行缩放。

如果我们在对数日志图上绘制相同的数据,则会得到以下结果:

log Runtime over log n

我们可以看到@Paddy3118s 算法确实更快,正如他已经声称并成功证明的那样。但仅适用于小于 100 项的列表。这就是为什么 OP 在特定情况下询问复杂性而不是速度的原因。

但我们生活在一个自由的世界,所以每个人都可以选择他们最喜欢的算法。

,

注意:项目之间的正确比较用于给出陈述的结果。其他示例中至少有一个不会给出所陈述的答案,尽管它们确实有很好的解释。

您需要先对约会进行排序。无需花费额外的费用将排序限制在开始时间,因为该算法仅取决于所有约会的开始时间是在结束时间之前还是在结束时间;正如所给。

conflicts 通过比较列表理解中的约会的结束时间和下一个约会的开始时间来提取。

注意:项目之间的正确比较用于给出陈述的结果。其他示例中至少有一个不会给出所陈述的答案,尽管它们确实有很好的解释。

您需要先对约会进行排序。无需花费额外的费用将排序限制在开始时间,因为该算法仅取决于所有约会的开始时间是在结束时间之前还是在结束时间;正如所给。

通过将约会的结束时间与列表理解中下一个约会的开始时间进行比较来提取冲突,直到它们发生冲突。

a = [[4,8]]
a.sort() # No need to sort on only start time
conflicts = []
for i,this in enumerate(a):
    for next_ in a[i+1:]:
        if this[1] > next_[0]:  # this ends *after* next_ starts
            conflicts.append((this,next_))
        else:
            break  # Don't need to check any more

print(conflicts)    #  [([3,[4,5]),([3,7])]

时间安排

有一个关于算法效率的讨论,所以我想我会在这个和@wuerfelfreaks 算法上运行计时,(用轻微的修改来收集和返回所有冲突作为列表):

修改后的代码

a = [[4,8]]

def conflicts_p1(a):
    "paddy3118's code as a function returning list of tuple pairs"
    a.sort()
    conflicts = []
    for i,this in enumerate(a):
        for next_ in a[i+1:]:
            if this[1] > next_[0]:  # this ends *after* next_ starts
                conflicts.append((this,next_))
            else:
                break  # Don't need to check any more
    return conflicts

def conflicts_w1(appointments):
    "@wuerfelfreak's answer returning a list of tuple pairs"
    appointments.sort(key= lambda x:x[0]) #appointments get sorted by start time.
    conflicts = []
    for i,a in enumerate(appointments):
        for i2 in range(i+1,len(appointments)):
            b = appointments[i2]
            if a[1] > b[0]: #if start of second appointment is before end of first appointment
                conflicts.append((a,b))
            else:
                break
    return conflicts

assert conflicts_p1(a) == conflicts_w1(a)

Ipython 计时

In [2]: # Paddy3118 timings

In [3]: %timeit conflicts_p1(a)
5.52 µs ± 38.6 ns per loop (mean ± std. dev. of 7 runs,100000 loops each)

In [4]: %timeit conflicts_p1(a)
5.42 µs ± 23.3 ns per loop (mean ± std. dev. of 7 runs,100000 loops each)

In [5]: %timeit conflicts_p1(a)
5.53 µs ± 438 ns per loop (mean ± std. dev. of 7 runs,100000 loops each)

In [6]: # wuerfelfreak timings

In [7]: %timeit conflicts_w1(a)
8.34 µs ± 141 ns per loop (mean ± std. dev. of 7 runs,100000 loops each)

In [8]: %timeit conflicts_w1(a)
7.95 µs ± 296 ns per loop (mean ± std. dev. of 7 runs,100000 loops each)

In [9]: %timeit conflicts_w1(a)
8.38 µs ± 371 ns per loop (mean ± std. dev. of 7 runs,100000 loops each)

In [10]: 

总结

对于给定的示例,我的(Paddy3118 的)代码比 wuerfelfreak 的代码快。

,

代码

def attend_all_appointments(intervals):
  start,end = 0,1
  intervals.sort(key=lambda x:x[start])
  prev_interval = intervals[0]
  # can_attend_all_appointments = True
  for i in range(1,len(intervals)):
    interval = intervals[i]
    if interval[start] < prev_interval[end]:
      print(f"{prev_interval} and {interval} conflict")
      # can_attend_all_appointments = False
    else:
      prev_interval[start] = interval[start]
      prev_interval[end] = interval[end]
  # return can_attend_all_appointments

时间复杂度:O(n log n) 因为我们需要在开始时排序并且只迭代一次间隔。此解决方案不需要嵌套循环。

,

O(n^2)

代码

from itertools import combinations
a = [[4,8]]

def intersects(elem): #Checks for intersections by seeing if a start/endpoint is enclosed by the start and endpoint of the other appointment
    return  (elem[0][0] < elem[1][0] < elem[0][1]) or \
            (elem[0][0] < elem[1][1] < elem[0][1]) or \
            (elem[1][0] < elem[0][0] < elem[1][1]) or \
            (elem[1][0] < elem[0][1] < elem[1][1])


conflicting = [elem for elem in combinations(a,2) if intersects(elem) ] #checks for every possible pair of events.
for conf in conflicting:
    print(f"{conf[0]} and {conf[1]} conflict")

结果:

[4,5] and [3,6] conflict
[3,7] conflict
,

也许是这样的?不是真正的基于范围的 for 循环的粉丝,但使用集合可以使这更直接。

appointments = [
    [4,8]
]

sets = [set(range(*appointment)) for appointment in appointments]

for current_index in range(len(sets)):
    current_set = sets[current_index]
    for other_index,other_set in enumerate(sets[current_index+1:],start=current_index+1):
        if current_set & other_set:
            print(f"{appointments[current_index]} and {appointments[other_index]} conflict.")
,

可能有点晚了...

from itertools import combinations

a = [[4,8]]

for x in combinations(a,2):
    b,c = x

    if b[0] > c[0] and b[1] < c[1] or \
            b[1] > c[0] and b[1] < c[1] or \
            b[0] < c[0] and b[1] > c[1]:
        print(f"{b} and {c} conflict")