【问题标题】:fast algorithm for merging overlapping intervals用于合并重叠区间的快速算法
【发布时间】:2020-02-06 13:52:31
【问题描述】:

我正在尝试编写一个足够快的算法来将区间与值合并。

例如有3个区间(包括from和to):

  1. {from:1, to: 3, amount: 1}
  2. {from:5, to: 7, amount: 1}
  3. {from:2, to: 6, amount: 1}

结果:

  1. {from:1, to: 1, amount: 1}
  2. {from:2, to: 3, amount: 2}
  3. {from:4, to: 4, amount: 1}
  4. {from:5, to: 6, amount: 2}
  5. {from:7, to: 7,amount: 1}

当间隔数达到 5000 时,我的算法中的计算可能需要大约一分钟。 (在合并的每个新间隔与大量其他间隔重叠的情况下)。 也许有人可以建议一些适合此任务的已知算法,或者他已经遇到过类似的问题并会分享他的解决方案?

input result

       function mergeingIntervals(intervals: IInterval[]): IInterval[] {
            //sort by length
            intervals.sort((a, b) => b.to - b.from - a.to + a.from);
            const result = [intervals[0]];
            intervals.forEach(interval => addInterval(result, interval));
            result.sort((a, b) => a.from - b.from);

            return result;
        }

        function addInterval(result: IInterval[], interval: IInterval): void {
            const firstIntersectionIndex = result.findIndex(row =>
                row.from >= interval.from && row.to <= interval.to ||
                row.from >= interval.from && row.from <= interval.to ||
                row.from <= interval.from && row.to >= interval.to ||
                row.to >= interval.from && row.to <= interval.to,
            );

            if (firstIntersectionIndex === -1) {
                const indexToInsertInto = result.findIndex(row => row.from > interval.to);
                if (indexToInsertInto !== -1) {
                    result.splice(indexToInsertInto, 0, interval);
                } else {
                    result.unshift(interval);
                }
            } else {
                const valueToMergeInto = result[firstIntersectionIndex];
                if (valueToMergeInto.from <= interval.from && valueToMergeInto.to >= interval.to) {
                    const newIntervals: IInterval[] = [];
                    if (valueToMergeInto.from !== interval.from) {
                        newIntervals.push({
                            from: valueToMergeInto.from,
                            to: subtractHour(interval.from),
                            value: valueToMergeInto.value,
                        });
                    }
                    newIntervals.push({
                        from: interval.from,
                        to: interval.to,
                        value: interval.value + valueToMergeInto.value,
                    });
                    if (valueToMergeInto.to !== interval.to) {
                        newIntervals.push({
                            from: addHour(interval.to),
                            to: valueToMergeInto.to,
                            value: valueToMergeInto.value,
                        });
                    }
                    result.splice(firstIntersectionIndex, 1, ...newIntervals);
                } else {
                    let count = 1;
                    result.slice(firstIntersectionIndex).forEach(sumValue => {
                        if (sumValue.to <= interval.to) {
                            count++;
                            return false;
                        } else {
                            return true;
                        }
                    });
                    if (count < 5) {
                        result.splice(firstIntersectionIndex, 1);
                        const newIntervals: IInterval[] = [];
                        if (interval.from < valueToMergeInto.from) {
                            newIntervals.push({
                                from: interval.from,
                                to: subtractHour(valueToMergeInto.from),
                                value: interval.value,
                            }, {
                                from: valueToMergeInto.from,
                                to: Math.min(valueToMergeInto.to, interval.to),
                                value: valueToMergeInto.value + interval.value,
                            });
                            if (interval.to !== valueToMergeInto.to) {
                                newIntervals.push({
                                    from: addHour(Math.min(valueToMergeInto.to, interval.to)),
                                    to: Math.max(valueToMergeInto.to, interval.to),
                                    value: valueToMergeInto.to < interval.to ? interval.value : valueToMergeInto.value,
                                });
                            }
                        } else {
                            if (interval.from !== valueToMergeInto.from) {
                                newIntervals.push({
                                    from: valueToMergeInto.from,
                                    to: subtractHour(interval.from),
                                    value: valueToMergeInto.value,
                                });
                            }
                            newIntervals.push({
                                from: interval.from,
                                to: Math.min(interval.to, valueToMergeInto.to),
                                value: interval.value + valueToMergeInto.value,
                            });
                            if (interval.to !== valueToMergeInto.to) {
                                newIntervals.push({
                                    from: addHour(Math.min(valueToMergeInto.to, interval.to)),
                                    to: Math.max(valueToMergeInto.to, interval.to),
                                    value: valueToMergeInto.to < interval.to ? interval.value : valueToMergeInto.value,
                                });
                            }
                        }
                        newIntervals.forEach(period => addInterval(result, period));
                    } else {
                        const newIntervals: IInterval[] = [];
                        if (interval.from < valueToMergeInto.from) {
                            newIntervals.push({
                                from: interval.from,
                                to: subtractHour(valueToMergeInto.from),
                                value: interval.value,
                            }, {
                                from: valueToMergeInto.from,
                                to: Math.min(valueToMergeInto.to, interval.to),
                                value: interval.value,
                            });
                            if (interval.to !== valueToMergeInto.to) {
                                newIntervals.push({
                                    from: addHour(Math.min(valueToMergeInto.to, interval.to)),
                                    to: Math.max(valueToMergeInto.to, interval.to),
                                    value: valueToMergeInto.to < interval.to ? interval.value : 0,
                                });
                            }
                        } else {
                            if (interval.from !== valueToMergeInto.from) {
                                newIntervals.push({
                                    from: valueToMergeInto.from,
                                    to: subtractHour(interval.from),
                                    value: 0,
                                });
                            }
                            newIntervals.push({
                                from: interval.from,
                                to: Math.min(interval.to, valueToMergeInto.to),
                                value: interval.value,
                            });
                            if (interval.to !== valueToMergeInto.to) {
                                newIntervals.push({
                                    from: addHour(Math.min(valueToMergeInto.to, interval.to)),
                                    to: Math.max(valueToMergeInto.to, interval.to),
                                    value: valueToMergeInto.to < interval.to ? interval.value : 0,
                                });
                            }
                        }
                        const valuesToMergeIntoArray = result.splice(firstIntersectionIndex, count, ...newIntervals);
                        valuesToMergeIntoArray.forEach(period => addInterval(interval, period));
                    }
                }
            }
        }

【问题讨论】:

  • 我不明白你是如何从输入到输出的,你需要解释你实际在做什么。 (最好展示您当前的解决方案,即使它不理想/功能不全)
  • 我加了一张说明合并原理的图
  • 图像并不能真正告诉我们什么,我们需要看到输入和输出之间的获取过程,而不仅仅是相同输入/输出数据的图形版本。由于您已经有了工作代码,请将其包含在 minimal example
  • @greatstone 我的第一步是根据开始时间和结束时间对输入数据进行排序。
  • 您的 from/to 值是否仅限于整数或可以是任何数字(即 4.55)?

标签: javascript algorithm intervals


【解决方案1】:

您可以为单点(及时)发生的金额变化创建条目,因此您实际上会使输入的条目数量增加一倍。然后按那个键排序。使该排序数组中的键唯一,聚合应用于同一键的更改量。最后构建该数组的输出。

function mergedIntervals(intervals) {
    let arr = [];
    // Store separate entries for "from" and for "to".
    for (let {from, to, amount} of data) {
        arr.push({ key: from, amountChange:  amount, countChange:  1});
        arr.push({ key: to+1, amountChange: -amount, countChange: -1});
    }
    // sort by key
    arr.sort((a, b) => a.key - b.key);
    // aggregate by key
    let last = {};
    let unique = [];
    let count = 0;
    let amount = 0;
    for (let {key, amountChange, countChange } of arr) {
        amount += amountChange
        count += countChange;
        if (key === last.key) {
            last.amount = amount;
            last.count = count;
        } else {
            unique.push(last = { key, amount, count });
        }
    }
    // generate result
    let result = [];
    last = null;
    for (let { key, amount, count } of unique) {
        if (last) last.to = key - 1;
        if (!count) {
            last = null;
        } else if (!last || last.amount !== amount) {
            result.push(last = { from: key, to: null, amount });
        }
    }
    return result;
}

// example run
let data = [ 
    {from: -5, to: 10, amount:  0}, // negative from
    {from: -4, to:  0, amount:  1}, // overlapping
    {from:  1, to:  2, amount:  2}, // adjacent to previous
    {from:  0, to:  6, amount: -5}, // negative amount
    // no coverage between 11 and 14
    {from: 15, to: 20, amount:  3}, // after gap
    {from: 18, to: 25, amount:  4}, // partly overlapping
];
console.log(mergedIntervals(data));

【讨论】:

  • 谢谢你,@NinaScholz。这来自你,让我感到自豪:-)
  • 我做了一个更短的版本... :-)
  • @trincot,不幸的是,您的示例在使用 amount=0 的时间间隔内无法正常工作
  • @greatstone,你有一个例子和想要的结果吗?请为它编辑问题。
  • @greatstone,请添加一个零金额的示例及其想要的结果。
【解决方案2】:

这种方法是tricotanswer 的一种简化版本,它依赖于排序索引,如 Javascript 中的数组键和特定时间戳的组合单值。

结果由对象的排序条目组成,并检查是否是最后一个对象或者数量是否不为零(与最后一个条目相反)一个新对象被推送到结果集中。

function mergedIntervals(intervals) {
    let values = {},
        last,
        result = [],
        amount = 0;

    for (let { from, to, amount } of intervals) {
        values[from] = (values[from] || 0) + amount;
        values[to + 1] = (values[to] || 0) - amount;
    }

    for (let [k, v] of Object.entries(values)) {
        amount += v;
        if (last) {
            last.to = k - 1;
            if (amount === last.amount) continue;
        }
        result.push(last = { from: +k, to: null, amount });
    }
    result.pop();
    return result;
}

let data = [{ from: 1, to: 7, amount: 0 }, { from: 1, to: 2, amount: 1 }, { from: 3, to: 4, amount: 0 }, { from: 6, to: 7, amount: 1 }];

console.log(mergedIntervals(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }

【讨论】:

  • 这也不错。只有一个限制:它只能用于“数组索引”范围 [0..2^32) 中的 from/to,因此,如果需要超出该范围的值(负数或整数 >= 2^32),那么另一个需要解决方案。
猜你喜欢
  • 1970-01-01
  • 2011-02-05
  • 2022-01-20
  • 2015-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-05
  • 2015-12-11
相关资源
最近更新 更多