给定一组范围S和重叠范围R,找到S中包含R

时间:2015-11-02 02:56:50

标签: algorithm range dynamic-programming

以下是某人给我的练习面试问题,我不确定最佳解决方案是什么:

  

给出一组范围:
  (例如 S = {(1, 4), (30, 40), (20, 91) ,(8, 10), (6, 7), (3, 9), (9, 12), (11, 14)} 。并且给定目标范围R(例如 R = (3, 13) - 意味着范围从3到13)。写一个算法到找到覆盖目标范围的最小范围集。集合中的所有范围必须重叠才能被视为跨越整个目标范围。(在此示例中,答案为 {(3, 9), (9, 12), (11, 14)}

解决此问题的最佳方法是什么?我以为这将使用贪婪算法完成。在上面的示例中,我们将查找与3相交的所有数字,并从具有最高max的数字中选择。然后我们会用我们刚刚选择的那个做同样的事情。所以,既然我们选择了(3,9),我们现在想要找到所有与9相交的范围,其中我们选择具有最高最大值的范围。在那次迭代中,我们选择了(9,12)。我们对那个做同样的事情,我们发现与12相交的下一个范围是最高的最大值是(11,14)。

在那次迭代之后,我们看到14大于13(我们的范围的最大值),所以我们可以停止。

我对这个算法的问题是,如何有效地查询交叉范围?如果我们尝试线性搜索,我们最终会得到一个 O(n^2) 的算法。我的下一个想法是每次运行循环时从我们的列表中删除任何相交的范围。所以在第一次迭代中,我们交叉(1,4)(3,9)。在下一次迭代中,我们交叉(9,12)(3,9)(8,10)。因此,在最后一次迭代中,我们所要看的只是 {(30,40),(20,91),(6,7)} 。我们可以通过跨越具有min> gt的所有内容来提高效率。 13,最大<问题是这仍然可能还不够。在我们的范围范围内存在大量重复序列的潜在问题。如果我们的范围列表包含 {(6,7),(6,7),(6,7),(6,7),(6,7)} 之类的内容,我们将不得不每次看一遍,即使它们对我们没用。即使我们只是存储唯一值(通过将它们全部置于一组中),我们可能会有一个非常大的范围,一系列范围在我们的目标范围内,但我们内部也有一个范围几乎跨越整个目标范围。

查询范围的有效方法是什么?或者可能,解决这个问题的更有效算法是什么?

4 个答案:

答案 0 :(得分:2)

如何使用间隔树进行查询? (https://en.m.wikipedia.org/wiki/Interval_tree)我不确定贪婪是否可以在这里发挥作用。如果我们查看最后一组选项,与R中的高点重叠,则每个选项之间可能存在重叠,例如:

R = (2,10) and we have (8,10) and (7,10) both overlapping with (6,8)

在这种情况下,我们只需要为(6,8)存储一个值作为路径的第二段;我们再次访问(6,8)因为我们已经知道R访问了(6,8),因为我们已经知道leg = 1 start with the possible end (or beginning) intervals label these intervals with leg until end of path is reached: remove the intervals labeled leg from the tree for each of those intervals labeled leg: list overlapping intervals in the chosen direction leg = leg + 1 label the listed overlapping intervals with leg 的行数较少。所以你在我们去的时候消除间隔的想法是有道理的。这样的事情能起作用吗?

{{1}}

答案 1 :(得分:1)

我可以建议使用复杂O(n log n)的算法,而不使用间隔树。

让我们介绍一些符号。我们应按时间间隔(X,Y)覆盖范围(x_i,y_i)

首先按起点给出间隔(x_i,y_i)。这需要O(n log n)

让我们选择时区(x_i,y_i)x_i <= X时间间隔为(x_k,y_k),最多为y_i。因为区间已经按起点排序,我们只能增加索引,而区间满足条件。如果y_k小于X,则对于给定的集合和范围没有解决方案。在其他情况下,间隔(x_k,y_k)包含&#39; X&#39;并且在包含X的区间中具有最大终点。

现在我们需要覆盖间隔(y_k, Y),以满足重叠条件。因为对于包含X的所有区间的终点小于y_k+1,我们可以从上一步的最后一个区间开始。

每个时间间隔在此阶段仅使用一次,因此此部分的时间复杂度为O(n),总计为O(n log n)

以下解决方案的代码段:

intervals // given intervals from set S
(X, Y) // range to cover
sort intervals 
i = 0 // start index
start = X // start point
result_set // set to store result
while start <= Y && i < len(intervals): 
   next_start =  intervals[i].y
   to_add = intervals[i]
   while intervals[i].x <= start && i < len(intervals):
        if next_start > intervals[i].y:
           next_start = intervals[i].y
           to_add = intervals[i]
        i++
   if(next_start < start):
        print 'No solution'
        exit
   start = next_start
   result_set add to_add

答案 2 :(得分:1)

好的,在尝试了很多不同的东西后,这是我的解决方案。它运行在 O(nlogn) 时间,并且不需要使用间隔树(尽管我可能会使用它,如果我能记住如何实现一个面试,但我认为这需要很长时间而不提供任何真正的好处)。

此算法的瓶颈在于排序。每个项目只触摸一次,但它只适用于排序数组,所以这是我们做的第一件事。因此 O(nlogn) 时间复杂度。因为它修改了原始数组,所以它具有 O(1) 空间复杂度,但如果我们不允许修改原始数组,我们可以只复制它,并保留其余部分算法相同,使空间复杂度 O(n)

import java.util.*;

class SmallestRangingSet {
    static class Interval implements Comparable<Interval>{
        Integer min;
        Integer max;
        public Interval(int min, int max) {
            this.min = min;
            this.max = max;
        }

        boolean intersects(int num) {
            return (min <= num && max >= num);
        }

        //Overrides the compareTo method so it will be sorted
        //in order relative to the min value
        @Override
        public int compareTo(Interval obj) {
            if (min > obj.min) return 1;
            else if (min < obj.min) return -1;
            else return 0;
        }
    }

    public static Set<Interval> smallestIntervalSet(Interval[] set, Interval target) {
        //Bottleneck is here. The array is sorted, giving this algorithm O(nlogn) time
        Arrays.sort(set);

        //Create a set to store our ranges in
        Set<Interval> smallSet = new HashSet<Interval>();
        //Create a variable to keep track of the most optimal range, relative
        //to the range before it, at all times.
        Interval bestOfCurr = null;
        //Keep track of the specific number that any given range will need to
        //intersect with. Initialize it to the target-min-value.
        int currBestNum = target.min;
        //Go through each element in our sorted array.
        for (int i = 0; i < set.length; i++) {
            Interval currInterval = set[i];
            //If we have already passed our target max, break.
            if (currBestNum >= target.max)
                break;
            //Otherwise, if the current interval intersects with
            //our currBestNum
            if (currInterval.intersects(currBestNum)) {
                //If the current interval, which intersects currBestNum
                //has a greater max, then our current bestOfCurr
                //Update bestOfCurr to be equal to currInterval.
                if (bestOfCurr == null || currInterval.max >= bestOfCurr.max) {
                    bestOfCurr = currInterval;
                }
            }
            //If our range does not intersect, we can assume that the most recently
            //updated bestOfCurr is probably the most optimal new range to add to 
            //our set. However, if bestOfCurr is null, it means it was never updated,
            //because there is a gap somewhere when trying to fill our target range.
            //So we must check for null first.
            else if (bestOfCurr != null) {
                //If it's not null, add bestOfCurr to our set
                smallSet.add(bestOfCurr);
                //Update currBestNum to look for intervals that
                //intersect with bestOfCurr.max
                currBestNum = bestOfCurr.max;
                //This line is here because without it, it actually skips over
                //the next Interval, which is problematic if your sorted array
                //has two optimal Intervals next to eachother.
                i--;
                //set bestOfCurr to null, so that it won't run
                //this section of code twice on the same Interval.
                bestOfCurr = null;
            }

        }

        //Now we should just make sure that we have in fact covered the entire
        //target range. If we haven't, then we are going to return an empty list.
        if (currBestNum < target.max)
            smallSet.clear();
        return smallSet;
    }

    public static void main(String[] args) {
        //{(1, 4), (30, 40), (20, 91) ,(8, 10), (6, 7), (3, 9), (9, 12), (11, 14)}
        Interval[] interv = {
                new Interval(1, 4),
                new Interval(30, 40),
                new Interval(20, 91),
                new Interval(8, 10),
                new Interval(6, 7),
                new Interval(3, 9),
                new Interval(9, 12),
                new Interval(11, 14)
        };
        Set<Interval> newSet = smallestIntervalSet(interv, new Interval(3,14));
        for (Interval intrv : newSet) {
            System.out.print("(" + intrv.min + ", " + intrv.max + ") ");
        }

    }
}

输出

(3, 9) (9, 12) (11, 14)

答案 3 :(得分:0)

你的作业引起了我的兴趣,所以我编写了一个C ++程序,通过迭代与目标范围左侧重叠的范围来解决问题,并递归搜索覆盖剩余范围的最小数量的范围(右侧)目标范围。

对该算法的一个重要优化(在该程序中未示出)将是,对于每个递归级别,使用与目标范围的左侧重叠最大量的范围,并且从进一步考虑中丢弃所有范围左侧重叠较小的量。通过使用这个规则,我相信在递归调用树中最多只会有一次下降。这种优化将产生具有复杂度O(n log(n))的算法。 (n表示递归的深度,而log(n)表示二进制搜索,以找到重叠最多的范围。)

该程序产生以下输出:

{ (3, 9) (9, 12) (11, 14) }


这是程序:

#include <utility>  // for std::pair
#include <vector>   // for std::vector
#include <iostream> // for std::cout & std::endl

typedef std::pair<int, int> range;
typedef std::vector<range> rangelist;

// function declarations
rangelist findRanges (range targetRange, rangelist candidateRanges);
void print (rangelist list);


int main()
{
    range target_range = { 3, 13 };

    rangelist candidate_ranges =
        { { 1, 4 }, { 30, 40 }, { 20, 91 }, { 8, 10 }, { 6, 7 }, { 3, 9 }, { 9, 12 }, { 11, 14 } };

    rangelist result = findRanges (target_range, candidate_ranges);

    print (result);
    return 0;
}


// Recursive function that returns the smallest subset of candidateRanges that
// covers the given targetRange.
// If there is no subset that covers the targetRange, then this function
// returns an empty rangelist.
//
rangelist findRanges (range targetRange, rangelist candidateRanges)
{
    rangelist::iterator it;
    rangelist smallest_list_so_far;

    for (it = candidateRanges.begin (); it != candidateRanges.end (); ++it) {

        // if this candidate range overlaps the beginning of the target range
        if (it->first <= targetRange.first && it->second >= targetRange.first) {

            // if this candidate range also overlaps the end of the target range
            if (it->second >= targetRange.second) {

                // done with this level - return a list of ranges consisting only of
                // this single candidate range
                return { *it };
            }
            else {
                // prepare new version of targetRange that excludes the subrange
                // overlapped by the present range
                range newTargetRange = { it->second + 1, targetRange.second };

                // prepare new version of candidateRanges that excludes the present range
                // from the list of ranges
                rangelist newCandidateRanges;
                rangelist::iterator it2;
                // copy all ranges up to but not including the present range
                for (it2 = candidateRanges.begin (); it2 != it; ++it2) {
                    newCandidateRanges.push_back (*it2);
                }
                // skip the present range
                it2++;
                // copy the remainder of ranges in the list
                for (; it2 != candidateRanges.end(); ++it2) {
                        newCandidateRanges.push_back (*it2);
                }

                // recursive call to find the smallest list of ranges that cover the remainder
                // of the target range not covered by the present range
                rangelist subList = findRanges (newTargetRange, newCandidateRanges);

                if (subList.size () == 0) {
                    // no solution includes the present range
                    continue;
                }
                else if (smallest_list_so_far.size () == 0 ||               // - first subList that covers the remainder of the target range
                         subList.size () < smallest_list_so_far.size ())    // - this subList is smaller than all previous ones checked
                {
                    // add the present range to the subList, which represents a solution
                    // (though possibly not optimal yet) at the present level of recursion
                    subList.push_back (*it);
                    smallest_list_so_far = subList;
                }
            }
        }
    }
    return smallest_list_so_far;
}

// print list of ranges
void print (rangelist list)
{
    rangelist::reverse_iterator rit;
    std::cout << "{ ";
    for (rit = list.rbegin (); rit != list.rend (); ++rit) {
        std::cout << "(" << rit->first << ", " << rit->second << ") ";
    }
    std::cout << "}" << std::endl;
}