算法挑战:合并日期范围

时间:2010-07-01 07:11:31

标签: c# linq algorithm optimization datetime

我正面临一个有趣的问题:

  • 我有几个可以重叠的日期范围
  • 他们每个人都有一个名字

是否有可能“重叠”这些范围?也就是说,生成:

  • 一组新的范围,其中没有一个与其他范围重叠
  • 每个新范围都有一个相应名称列表

也许我可以让它更具图形化。这就是我的第一个:

a   |------------------------------|
b                    |-------------------|
c          |-----------------|

这是我想要获得的:

    |------|---------|-------|-----|-----|
        a      a,c     a,b,c   a,b    b

我找到了一种有效的解决方案,但这并不优雅:

  1. 我将每个范围(从,到)转换为天数列表(d1,d2,d3等)
  2. 我按天分组名称
  3. 我聚合包含相同名称的组以重新创建范围
  4. 您能想到更好的解决方案吗?我正在使用C#,但任何与语言无关的想法都会非常感激。谢谢!

6 个答案:

答案 0 :(得分:10)

我会

  1. 保持无序的“开放”范围列表
  2. 从第1天开始,将第一个范围添加到开放范围列表中。
  3. 移动到下一个范围边界(开始或关闭)。创建您的第一个“输出”范围:从第1天开始,到当天结束。它是您的开放范围列表中的项目。
  4. 如果遇到的范围已经在开放范围列表中,请将其删除。否则,添加它。
  5. 重复3和4,沿着线移动。
  6. 我在解释它时确实做得不好。我很快就会为此编写一些代码。但在此之前,您的解决方案会发生什么:

    a   |------------------------------|
    b                    |-------------------|
    c          |-----------------|
    
    1.  Start at day 1, add A to open ranges list, which now contains [A]
    2.  Move to the start of C.  First RESULT RANGE: A range from Day 1 to C's start,
          with a value A. (what is in your open ranges list)
    3.  Add C to the open ranges list, which now contains [A,C]
    4.  Move to the start of B.  Second RESULT RANGE: A range from C's start to B's
          start, with a value A,C. (what is in your open ranges list)
    5.  Add B to the open ranges list, which now contains [A,C,B]
    6.  Move to the end of C.  Third RESULT RANGE: A range from B's start to C's end,
          with a value of A,C,B.
    7.  Remove C from the open ranges list, which now contains [A,B]
    8.  Move to the end of A.  Fourth RESULT RANGE: A range from C's end to A's end,
          with a value of A,B
    9.  Remove A from the open ranges list, which now contains [B]
    10. Move to the end of A.  Fourth RESULT RANGE: A range from A's end to B's end,
          with a value of B
    
    RESULT RANGES
    1. from Day 1 to C's start: A
    2. from C's start to B's start: A,C
    3. from B's start to C's end: A,C,B
    4. from C's end to A's end: A,B
    5. from A's end to B's end: B
    

    替代方法

    你可以这样做:

    1. 保留“输出范围”的有序列表。这样可以很容易地测试一个点是否在一个范围内,以及相互跟随的范围。
    2. 从输入范围中取一个范围。
    3. 检查所有输出范围之前或之后是否完全,并进行相应处理。如果是的话,请跳过后续步骤并返回步骤2.
    4. 将其起点与输出范围进行比较
    5. 如果它在任何其他输出范围之前,请添加从其开始到第一个输出范围开始的新输出范围。
    6. 如果它位于已存在的输出范围之间,则在该点分割该输出范围。第一部分将保持相同的“父母”,第二部分将具有相同的父母+新的输入范围。
    7. 现在,将其终点与输出范围进行比较。
    8. 如果它在任何其他输出范围之后,请添加从最后一个输出范围的末尾到结尾的新输出范围。
    9. 如果它位于已存在的输出范围之间,则在该点分割该输出范围。第二部分将保持相同的“父母”,第一部分将具有相同的父母+新的输入范围
    10. 将当前输入范围作为一部分添加到步骤6和9中两个“已处理”范围之间的所有范围(如果有)。
    11. 对所有输入范围重复2-6。
    12. 以下是前几个步骤,使用示例数据+另一个范围D: (“{处理”范围由**double stars**表示)

      a   |------------------------------|
      b                    |-------------------|
      c          |-----------------|
      d       |------------------------|
      
      
      1.
         Process A
         Output ranges: |--------------A---------------|
      2.
         Process B
           - Start of B is in **A**; split A in two:
                        |-------A-------|------AB------|
           - End of B is after any output range range;
               creating new output range at end
                        |-------A-------|------AB------|---B---|
          - Nothing was/is in between **A** and (nothing)
      3.
         Process C
           - Start of C is in **A**; split A in two:
                        |---A----|--AC--|------AB------|---B---|
           - End of C is in **AB**; split AB in two:
                        |---A----|--AC--|--ABC--|--AB--|---B---|
           - There were/are no ranges between **A** and **AB**
      
      4.
         Process D
           - Start of D is in **A**; split A in two:
                        |-A-|-AD-|--AC--|--ABC--|--AB--|---B---|
           - End of D is in **AB**; split AB in two:
                        |-A-|-AD-|--AC--|--ABC--|ABD|AB|---B---|
           - Ranges AC and ABC were/are in between **A** and **AB**
                        |-A-|-AD-|--ACD-|-ABCD--|ABD|AB|---B---|
      
      Final output:     |-A-|-AD-|--ACD-|-ABCD--|ABD|AB|---B---|
      

答案 1 :(得分:2)

您可能希望查看Interval Trees

答案 2 :(得分:2)

我有代码可以做到这一点。它依赖于由from然后to排序的输入集(即,如果它是SQL,它将是ORDER BY from_value, to_value),但之后它是非常优化的。

我的实现基本上执行@Justin L.answer描述的内容,因此如果您只想要文本描述,请查看他对算法的回答。

代码在这里:LVK.DataStructures,您要查看的文件是:

使用它:

List<Range<DateTime>> ranges = ...
var slices = ranges.Slice();

这将为每个切片提供一个新范围,对于每个这样的范围,您将拥有一个.Data属性,其中包含返回到贡献范围的引用。

即。对于您的原始示例,您将获得您想要的,单个范围,以及.Data属性中的贡献范围a,b,c等。

这些类可能会使用我的类库的其他部分,而这些部分就在那里。如果您决定使用它,只需复制出您想要使用的部分。

如果您只对实施感兴趣,可以在RangeExtensions.cs文件line 447及其后的InternalSlice方法中找到它。

答案 3 :(得分:1)

你可以:

  1. 对所有日期的列表进行排序(组合from和to日期)
  2. 然后沿着此列表运行,每个新范围将从一个日期到下一个开始或结束日期,与前一个日期不同。
  3. 对于命名新范围,使用当前新范围包含的原始范围名称列表是有意义的,并且每次到达结束日期时,从列表中删除旧范围名称,并且每个范围都是点击开始日期,将其名称添加到列表中。

答案 4 :(得分:0)

这样做:

创建一个Event类。

class DateEvent : IComparable<DateEvent>
{
    public Date Date;
    public int DateRangeId;
    public bool IsBegin; // is this the start of a range?

    public int CompareTo(DateEvent other)
    {
        if (Date < other.Date) return -1;
        if (Date > other.Date) return +1;
        if (IsBegin && !other.IsBegin) return -1;
        if (!IsBegin && other.IsBegin) return +1;
        return 0;
    }
}

定义List<DateEvent> events;

将每个范围的开始和结束日期添加到列表中:

for (int i = 0; i < dateRanges.Length; ++i)
{
    DateRange r = dateRanges[i];
    events.Add(new DateEvent(r.BeginDate, i, true));
    events.Add(new DateEvent(r.EndDate, i, false));
}

对事件进行排序。

events.Sort();

现在设置一个输出类:

class OutputDateRange
{
    public Date BeginDate;
    public Date EndDate;
    public List<string> Names;
}

最后,遍历事件,保持存在的范围:

List<int> activeRanges;
List<OutputDateRange> output;
Date current = events[0].Date;
int i = 0;

while (i < events.Length;)
{
    OutputDateRange out = new OutputDateRange();
    out.BeginDate = current;

    // Find ending date for this sub-range.
    while (i < events.Length && events[i].Date == current)
    {
        out.EndDate = events[i].Date;
        if (events[i].IsBegin)
            activeRanges.Add(events[i].DateRangeId);
        else
            activeRanges.Remove(events[i].DateRangeId);
        ++i;
    }

    if (i < events.Length)
        current = events[i].Date;

    foreach (int j in activeRanges)
        out.Names.Add(dateRanges[j].Name);

    output.Add(out);
}

这应该可以解决问题。请注意,我没有制作构造函数,代码有点难看,但希望能传达出一般的想法。

答案 5 :(得分:0)

  1. 我先列出一个清单,我不知道它的长度,但我有3个字符
  2. 检查一个插槽,如果有A?添加'A',如果B在那里?添加'B',如果有?添加'C'
  3. 转到另一个位置,像#2
  4. 一样循环
  5. 当没有添加到另一个新插槽
  6. 时结束
  7. 我得到了清单
  8. 压扁列表