寻找最长不重叠序列的算法

18
我试图找到解决以下问题的最佳方法。我所说的最佳方法是指复杂度更低。
作为输入,给定一个元组列表 (起始位置,长度),例如:
[(0,5),(0,1),(1,9),(5,5),(5,7),(10,1)]
每个元素通过它的起始位置和长度表示一个序列,例如(5,7)相当于序列(5,6,7,8,9,10,11) - 从5开始的7个元素列表。可以假设元组按其起始元素排序。
输出应返回表示最长连续序列的不重叠元组的组合。这意味着,解决方案是没有重叠和间隙的范围子集,并且是可能最长的 - 虽然可能有多个。
例如,对于给定的输入,解决方案是:[(0,5),(5,7)] ,相当于(0,1,2,3,4,5,6,7,8,9,10,11)
回溯法是解决此问题的最佳方法吗?
我对人们提出的任何不同方法都感兴趣。
此外,如果有人知道此问题的正式参考或类似问题的其他参考资料,请告诉我。
顺便说一下 - 这不是作业。
编辑
为了避免一些错误,这是预期行为的另一个示例。
对于像[(0,1),(1,7),(3,20),(8,5)]这样的输入,正确答案是[(3,20)],相当于(3,4,5,..,22)的长度为20。收到的一些答案将给出[(0,1),(1,7),(8,5)],相当于(0,1,2,...,11,12)作为正确答案。但是这个最后的答案不正确,因为它比[(3,20)]短。

1
你所说的最长连续线是什么意思?一条长度为7的单独线比两条长度为5的分离线更好吗(例如[(0,5),(7,5)])? - Rafał Dowgird
我所说的“最长”是指例如[(0,100)]比[(0,10),(10,5)]更长。因为第一个覆盖了序列(0,1,2,...,99),而第二个只覆盖了(0,1,..,9,10,..,14)。 - Manuel Salvadores
@msalvadores:所以你的意思并不是“连续”的,对吗?只是表示数字的总数?如果是这样,我建议从你的问题中删除“连续”这个词,并稍微改一下措辞。 - j_random_hacker
我认为他确实是指“连续的”。 - Prasad Chalasani
已编辑为另一个情况。 - Manuel Salvadores
显示剩余2条评论
9个回答

13

使用给定的排序(按开始元素)对元组列表进行迭代,同时使用哈希映射来跟踪以某个索引结束的最长连续序列的长度。

伪代码,省略了哈希映射中不存在的项目(假设如果未找到则返回0):

int bestEnd = 0;
hashmap<int,int> seq // seq[key] = length of the longest sequence ending on key-1, or 0 if not found
foreach (tuple in orderedTuples) {
    int seqLength = seq[tuple.start] + tuple.length
    int tupleEnd = tuple.start+tuple.length;
    seq[tupleEnd] = max(seq[tupleEnd], seqLength)
    if (seqLength > seq[bestEnd]) bestEnd = tupleEnd
}
return new tuple(bestEnd-seq[bestEnd], seq[bestEnd])

这是一个O(N)算法。

如果你需要组成该序列的实际元组,你需要保留一个由结束索引哈希的元组链表,并在此端点的最大长度更新时进行更新。

更新:我的Python知识相当有限,但根据你粘贴的Python代码,我创建了下面的代码,以返回实际序列而不仅仅是长度:

def get_longest(arr):
    bestEnd = 0;
    seqLengths = dict() #seqLengths[key] = length of the longest sequence ending on key-1, or 0 if not found
    seqTuples = dict() #seqTuples[key] = the last tuple used in this longest sequence
    for t in arr:
        seqLength = seqLengths.get(t[0],0) + t[1]
        tupleEnd = t[0] + t[1]
        if (seqLength > seqLengths.get(tupleEnd,0)):
            seqLengths[tupleEnd] = seqLength
            seqTuples[tupleEnd] = t
            if seqLength > seqLengths.get(bestEnd,0):
                bestEnd = tupleEnd
    longestSeq = []
    while (bestEnd in seqTuples):
        longestSeq.append(seqTuples[bestEnd])
        bestEnd -= seqTuples[bestEnd][1]
    longestSeq.reverse()
    return longestSeq


if __name__ == "__main__":
    a = [(0,3),(1,4),(1,1),(1,8),(5,2),(5,5),(5,6),(10,2)]
    print(get_longest(a))

1
就像我在我的答案中所说的那样,保持一个由实际元组组成的链表应该相对容易。每当您找到一个更长的序列时,只需取出先前的链接列表并将新元组添加到其中即可。您可以将这些元组列表存储在单独的哈希映射中,也可以扩展现有的哈希映射以达到此目的,使值成为一个类,该类结合了最大长度以及组成此最大长度的元组的链接列表。 - Luke Hutteman
2
更新是正确的,也是一个很好的答案。http://paste.ideaslabs.com/show/uOR5k0db5 对更新后的Python算法进行了微调,以处理边缘情况,并将输出反转以从最低元组开始。 - orangepips
@orangepips:我认为!= 0不再必要了;我们可以简单地迭代,直到当前索引不再在seqTuples中。此外,如果输入包含负起始索引(例如a = [(-1,1),(0,2)]),则!= 0可能会有害。 - Luke Hutteman
@LukeHutteman 如果输入元组按开始时间未排序,这个算法不会给出错误的答案吗? - Aseem Goyal
@aseem:是的,但问题明确说明“可以假设元组按开始元素排序。”所以我选择在我的算法中使用它。 - Luke Hutteman
显示剩余5条评论

2

修订后的算法:

create a hashtable of start->list of tuples that start there
put all tuples in a queue of tupleSets
set the longestTupleSet to the first tuple
while the queue is not empty
    take tupleSet from the queue
    if any tuples start where the tupleSet ends
        foreach tuple that starts where the tupleSet ends
            enqueue new tupleSet of tupleSet + tuple
        continue

    if tupleSet is longer than longestTupleSet
        replace longestTupleSet with tupleSet

return longestTupleSet

c#实现

public static IList<Pair<int, int>> FindLongestNonOverlappingRangeSet(IList<Pair<int, int>> input)
{
    var rangeStarts = input.ToLookup(x => x.First, x => x);
    var adjacentTuples = new Queue<List<Pair<int, int>>>(
        input.Select(x => new List<Pair<int, int>>
            {
                x
            }));

    var longest = new List<Pair<int, int>>
        {
            input[0]
        };
    int longestLength = input[0].Second - input[0].First;

    while (adjacentTuples.Count > 0)
    {
        var tupleSet = adjacentTuples.Dequeue();
        var last = tupleSet.Last();
        int end = last.First + last.Second;
        var sameStart = rangeStarts[end];
        if (sameStart.Any())
        {
            foreach (var nextTuple in sameStart)
            {
                adjacentTuples.Enqueue(tupleSet.Concat(new[] { nextTuple }).ToList());
            }
            continue;
        }
        int length = end - tupleSet.First().First;
        if (length > longestLength)
        {
            longestLength = length;
            longest = tupleSet;
        }
    }

    return longest;
}

测试:

[Test]
public void Given_the_first_problem_sample()
{
    var input = new[]
        {
            new Pair<int, int>(0, 5),
            new Pair<int, int>(0, 1),
            new Pair<int, int>(1, 9),
            new Pair<int, int>(5, 5),
            new Pair<int, int>(5, 7),
            new Pair<int, int>(10, 1)
        };
    var result = FindLongestNonOverlappingRangeSet(input);
    result.Count.ShouldBeEqualTo(2);
    result.First().ShouldBeSameInstanceAs(input[0]);
    result.Last().ShouldBeSameInstanceAs(input[4]);
}

[Test]
public void Given_the_second_problem_sample()
{
    var input = new[]
        {
            new Pair<int, int>(0, 1),
            new Pair<int, int>(1, 7),
            new Pair<int, int>(3, 20),
            new Pair<int, int>(8, 5)
        };
    var result = FindLongestNonOverlappingRangeSet(input);
    result.Count.ShouldBeEqualTo(1);
    result.First().ShouldBeSameInstanceAs(input[2]);
}

@Handcraftsman 我认为这个算法不适用于以下情况。输入 [(0,2),(2,8),(3,50)],正确答案是 [(3,50)],因为它包含了此情况下最长的序列 - 50个元素。 - Manuel Salvadores
哎呀,我显然误解了这个问题。我需要更多地思考一下。 - Handcraftsman
@Handcraftsman 谢谢!算法易于阅读。我可以理解你的伪代码并编写一个很好运行的Python版本!!http://paste.ideaslabs.com/show/65N44moAqJ 对复杂度有什么评论吗? - Manuel Salvadores
1
@msalvadores,你可能想在“if (tupleSet[-1] ...”块的末尾添加一个Python“continue”等效项。被添加到队列中的元组可能还没有达到它们的最大长度,因此没有理由将它们与最长项进行比较,这可能会产生潜在的成本。 - Handcraftsman
虽然这种方法可以工作,但最坏情况下的复杂度是指数级的,因为对于每个元组集合你出队列,你可能会入队列几个新的。 - Luke Hutteman

2
这是加权有向无环图的最长路径问题的一个特例。
图中的节点是序列的起始点和最后一个元素后面的点,下一个序列可以从它们开始。
问题很特殊,因为两个节点之间的距离必须独立于路径相同。

1

编辑以将伪代码替换为实际的Python代码

再次编辑以更改代码;原始算法在解决方案中,但我误解了对中的第二个值!幸运的是,基本算法是相同的,我能够进行更改。

这里有一个想法,可以在O(N log N)中解决问题,而且不使用哈希映射(因此没有隐藏时间)。对于内存,我们将使用N * 2个“东西”。

我们将向每个元组添加两个值:(BackCount,BackLink)。在成功的组合中,BackLink将从最右边的元组向左链接到最左边的元组。 BackCount将是给定BackLink的累积计数值。

这是一些Python代码:

def FindTuplesStartingWith(tuples, frm):
    # The Log(N) algorithm is left as an excersise for the user
    ret=[]
    for i in range(len(tuples)):
        if (tuples[i][0]==frm): ret.append(i)
    return ret

def FindLongestSequence(tuples):

    # Prepare (BackCount, BackLink) array
    bb=[] # (BackCount, BackLink)
    for OneTuple in tuples: bb.append((-1,-1))

    # Prepare
    LongestSequenceLen=-1
    LongestSequenceTail=-1

    # Algorithm
    for i in range(len(tuples)):
        if (bb[i][0] == -1): bb[i] = (0, bb[i][1])
        # Is this single pair the longest possible pair all by itself?
        if (tuples[i][1] + bb[i][0]) > LongestSequenceLen:
            LongestSequenceLen = tuples[i][1] + bb[i][0]
            LongestSequenceTail = i
        # Find next segment
        for j in FindTuplesStartingWith(tuples, tuples[i][0] + tuples[i][1]):
            if ((bb[j][0] == -1) or (bb[j][0] < (bb[i][0] + tuples[i][1]))):
                # can be linked
                bb[j] = (bb[i][0] + tuples[i][1], i)
                if ((bb[j][0] + tuples[j][1]) > LongestSequenceLen):
                    LongestSequenceLen = bb[j][0] + tuples[j][1]
                    LongestSequenceTail=j

    # Done! I'll now build up the solution
    ret=[]
    while (LongestSequenceTail > -1):
        ret.insert(0, tuples[LongestSequenceTail])
        LongestSequenceTail = bb[LongestSequenceTail][1]
    return ret

# Call the algoritm
print FindLongestSequence([(0,5), (0,1), (1,9), (5,5), (5,7), (10,1)])
>>>>>> [(0, 5), (5, 7)]
print FindLongestSequence([(0,1), (1,7), (3,20), (8,5)])    
>>>>>> [(3, 20)]

整个算法的关键在于代码中的“THIS IS THE KEY”注释所在的位置。我们知道当前的StartTuple可以与EndTuple相连。如果存在以EndTuple.To结尾的更长序列,则在我们到达此点时已经找到,因为它必须从较小的StartTuple.From开始,并且数组按“From”排序!

1

这是一个简单的reduce操作。给定一对连续的元组,它们可以或者不能被合并。因此定义成对组合函数:

def combo(first,second):
    if first[0]+first[1] == second[0]:
        return [(first[0],first[1]+second[1])]
    else:
        return [first,second]

这只是返回一个列表,其中包含两个参数的一个元素,或者原始的两个元素。

然后定义一个函数来迭代第一个列表并组合成对:

def collapse(tupleList):
    first = tupleList.pop(0)
    newList = []
    for item in tupleList:
        collapsed = combo(first,item)
        if len(collapsed)==2:
            newList.append(collapsed[0])
        first = collapsed.pop()
    newList.append(first)
    return newList

这个方法会保留列表中的第一个元素,用它来与列表中的当前项(从第二项开始)进行比较。当无法合并时,它将第一个元素放入新列表中,并将first替换为这两个元素中的第二个。

然后只需使用元组列表调用collapse函数:

>>> collapse( [(5, 7), (12, 3), (0, 5), (0, 7), (7, 2), (9, 3)] )
[(5, 10), (0, 5), (0, 12)]

[编辑] 最后,迭代结果以获取最长序列。
def longest(seqs):
    collapsed = collapse(seqs)
    return max(collapsed, key=lambda x: x[1])

复杂度 O(N)。为了获得额外的分数,将其反转,以便初始的pop(0)变为pop(),您不必重新索引数组或移动迭代器。对于最高分,请将其作为成对的reduce操作运行,以实现多线程好处。


在Python中运行它,collapse函数中应该使用combo([first,item])而不是combo(first,item)。 - Manuel Salvadores
我已经修复了,谢谢。combo现在需要两个元组参数而不是列表。 - Phil H
@msalvadores:已添加了一个快速函数来查找折叠序列中最长的序列。我忘记了最后一步!假设你想要折叠后的序列,而不是展开后的序列。 - Phil H

1

我删除了之前的解决方案,因为它没有经过测试。

问题是在“加权有向无环图”中找到最长路径,可以在线性时间内解决:

http://en.wikipedia.org/wiki/Longest_path_problem#Weighted_directed_acyclic_graphs

将一组{起始位置}的并集{(起始位置+结束位置)}作为顶点。例如,它可能是{0、1、5、10、11、12}。

对于顶点v0,v1,如果存在一个结束值w,使得v0+w=v1,则添加一个有向边连接v0和v1,并将w作为其权重。

现在按照维基百科页面中的伪代码进行操作。由于顶点数是2xn的最大值(n是元组数),因此该问题仍然可以在线性时间内解决。


嗨,我认为你的答案没有考虑到可能有多个三元组从相同的索引开始。如果是这样,请更好地澄清“哈希键”表的构建方式 - 谢谢!!! - Manuel Salvadores

1

仅考虑基本算法,这个方案可行吗?

(抱歉语法不好,但我试图保持语言无关性)

首先是最简单的形式:找到最长的连续对。

循环遍历每个成员,并将其与具有更高起始位置的每个其他成员进行比较。如果第二个成员的起始位置等于第一个成员的起始位置和长度之和,则它们是连续的。如果是这样,请使用较低的起始位置和组合长度形成一个新的成员集以表示此内容。

然后,将这些成对物品与所有具有更高起始位置的单个成员进行比较并重复,形成一组新的连续三元组(如果存在)。

继续这种模式,直到没有新的集合为止。

然后棘手的部分是您必须比较每个集合中每个成员的长度,以找到真正的最长链。

我相信这不如其他方法有效,但我认为这是一种可行的强制解决方案。

我希望得到反馈,以及我可能忽略的任何错误。


0

这听起来像是一个完美的“动态规划”问题...

最简单的程序是暴力求解(例如递归),但这具有指数复杂度。

使用动态规划,您可以设置一个长度为n的数组a,其中n是您的问题的所有(start+length)值的最大值,其中a[i]表示到a[i]的最长非重叠序列。然后,您可以遍历所有元组,更新a。此算法的复杂度将为O(n*k),其中k是输入值的数量。


0
  • 创建一个有序数组,其中包含所有的起始和结束点,并将它们全部初始化为1
  • 对于元组中的每个项,将该项的端点(起始点和结束点)与有序数组中的项进行比较,如果任何一点在它们之间(例如,数组中的点为5,而你有长度为4的起始值为2),则将该值更改为零。
  • 完成循环后,开始沿着有序数组移动,并在看到1时创建一个条带,同时在看到1的情况下添加到现有条带中,在遇到任何零的情况下,关闭条带等等。
  • 最后检查条带的长度

我认为复杂度大约为O(4-5*N)

(请参见更新)

N是元组中项目的数量。


更新

正如你所想象的那样,复杂度并不准确,但肯定非常小,因为它是线条拉伸数量(元组项)的函数。

因此,如果N是线条拉伸的数量,则排序为O(2N * log2N)。比较为O(2N)。查找线条拉伸也是O(2N)。因此总体上是O(2N(log2N + 2))


聪明啊,端点数量是有限的。但是如何在线性时间内创建一个有序的起始点和终点数组呢?我至少期望这需要O(n log n)的时间复杂度(起始点已经排序,但最坏情况下终点只会递减或递增)。而且在更新所有点(O(n))时,在排序数组中查找一个点可能也需要log n的时间复杂度... - ivy
2
负复杂度类?哇塞! - moinudin
@Ivy 你说得对。我没有考虑到排序,所以我们需要 O(2N * log2N) 的时间来进行排序。我会更新我的答案。 - Aliostad
我认为第二点会将原始示例中的(0,5)设置为0(因为来自(0,1)的1介于两者之间),这似乎可能导致错误的解决方案。 - Rafał Dowgird

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