关于时间区间分割算法问题

背景:

对 非连续的多个有序的时间区间进行 分割,结果是拆分出更小的时间区间,基本规则就是 对于时间区间 A来说,如果有 时间区间B 与A有交集,则 用B对A进行分割,举例如下:

1-9:表示 1号到9号

时间区间A: 1-9、10-29、30-31

例子1:

时间区间B: 1-8、9-19、20-31

那么用B对A进行分割,结果应该是:
1-8、9-9、10-19、20-29、30-31

例子2:

时间区间B: 4-8、9-19、20-28

那么用B对A进行分割,结果应该是:
1-3、4-8、9-9、10-19、20-28、29-29、30-31

1-9、10-29、30-31

例子3:

时间区间B: 8-8、9-19、20-31

那么用B对A进行分割,结果应该是:
1-7、8-8、9-9、10-19、20-29、30-31

求助该如何进行切割?


回答:

方便其他看,加一段说明
[1,9],[10,19],[20,31] 这个时间段,我们理解为一个月休3次。分别是[9,10],[19,20],[31,1],也就是0.5,9.5和19.5
也就是说有个隐藏的[32,0]
而这三个点,我们实际上可以用0,9,19来表示,需要右端的时候,再+1即可。
我们用二进制按位取的思路来记录这个时间点就是 1 000 000 000 100 000 001
从右向左看第0,9,19位分别是 1,表示休息日。函数getLeft就做了如上处理,把带端点的数组转化为二进制数。


试了一下,早上的回答有问题,join不能这么用,重新写一个
实际上我们只需要定位分割点就可以了
那观察问题,不难看出是要对月内的日期分割,那么自然是个整数日期,而且一个月最多只有31天,与整数最大31位不谋而合。于是嘛。。。

function getLeft(params: number[][]) {

return params.reduce((pre, cur) => {

let left = 1 << cur[0] - 1

let right = 1 << cur[1] % 31 // 超出31日的相当于次月

return pre | left | right

}, 0)

}

function parting(a: number[][], b:number[][]) {

let lefts = getLeft(a) | getLeft(b)

let index = 31

let result:number[][] = [[31]]

while (index>=0) {

let bParting = lefts & 1 << index

if (bParting ) { // bParting != 0 表示被占位

result[result.length-1].push(index+1)

if(index>0)

result.push([index])

}

index--;

}

return result

}

测试一下

let a = [[1,9], [10, 29], [30, 31]]

parting(a, [[1, 8], [9, 19], [20, 31]])

// [ [ 31, 30 ], [ 29, 20 ], [ 19, 10 ], [ 9, 9 ], [ 8, 1 ] ]

parting(a, [[4, 8], [9, 19], [20, 28]])

// [[31, 30], [29, 29], [28, 20], [19, 10], [9, 9], [8, 4], [3, 1] ]

parting(a, [[8, 8], [9, 19], [20, 31]])

// [ [ 31, 30 ], [ 29, 20 ], [ 19, 10 ], [ 9, 9 ], [ 8, 8 ], [ 7, 1 ] ]

旧答案

如果是整数分隔的话,我有个想法,可以试一试

function active(a: number[][], b: number[][]) {

let c = a.join(0)

let d = b.join(0)

while (d[0] != 1) {

d = [d[0] - 1, …d]

}

let f: number[] = []

let index = 0

while (c[index] || d[index]) {

let q = c[index]

let w = d[index]

if (q) f.join(q)

if (w) f.join(w)

}

}

return f.spilt(0) // 分隔前做一步去重就可以了

试试

let a = [[1,2,3…9],[10,11…28,29],[30,31]]

let b = [[1,2,3…8],[9,11…18,19],[20,21…31]]

c.log ( active(a, b) )


回答:

虽然描述的是用小区间切割大区间,实际则为数字组,对一整个月的重排切分。
思路是首先将数字组按开始时间升序排,取两条比较,按情况分类讨论。

(1)两端对齐,取短的一端作为结束;
(2)开端t2>t1,则截取的区间为[t1.start,t2.start)
更新两条线段的起始值,如果其中一条线段被消耗完了,则取一条新的线段。
由于原始两条线段被处理过,新取的线段起始时间可能会更小,所以采用swap,始终保持t1的起始时间更小,同时可以减少上述讨论的情况(毕竟是对称的)。

最后注意处理循环结束的边界情况,把剩余的一条放入结果集合。
思路大概就是这样,代码写的比较口水化,看看就行

public static List<Date> timeRangeSplit(List<Tuple2<Date, Date>> baseSeg, List<Tuple2<Date, Date>> seg1) {

List<Date> res = new ArrayList<>();

// 所有数据按起始时间排序

baseSeg.addAll(seg1);

baseSeg = baseSeg.stream().sorted().collect(Collectors.toList());

Tuple2<Date, Date> t1 = baseSeg.get(0);

Tuple2<Date, Date> t2 = baseSeg.get(1);

Integer curIdx = 2;

while (null != t1 && null != t2) {

// 保证t1的起始时间更小

Date end;

if (t1._1.compareTo(t2._1) == 0) {

if (t1._2.compareTo(t2._2) <= 0) {

// t1短

end = t1._2;

} else {

end = t2._2;

}

} else {

// t2的起点把t1截成两段

end = DateDealUtils.minusByDay(t2._1, 1);

}

res.add(t1._1);

res.add(end);

t1 = t1.update1(DateDealUtils.plusByDay(end, 1));

t2 = t2.update1(DateDealUtils.plusByDay(end, 1));

if (t1._1.compareTo(t1._2) > 0) {

if (curIdx < baseSeg.size()) {

t1 = baseSeg.get(curIdx++);

} else {

t1 = null;

break;

}

}

if (t2._1.compareTo(t2._2) > 0) {

if (curIdx < baseSeg.size()) {

t2 = baseSeg.get(curIdx++);

} else {

t2 = null;

break;

}

}

if (t1._1.compareTo(t2._1) > 0) {

// swap 始终保持t1开始数字小

Tuple2<Date, Date> tmp = Tuple.of(t1._1, t1._2);

t1 = Tuple.of(t2._1, t2._2);

t2 = Tuple.of(tmp._1, tmp._2);

}

}

// 循环终止后,把剩余部分放入结果

if (null != t1 && t1._1.compareTo(t1._2) < 0) {

res.add(t1._1);

res.add(t1._2);

}

if (null != t2 && t2._1.compareTo(t2._2) < 0) {

res.add(t2._1);

res.add(t2._2);

}

return res;

}


回答:

你的 \( A \) 区间序列是这样的

前后区间是紧贴在一起的,如果是这种的话,可以用连接处端点序列来表示:

$$

1-9、10-29、30-31 \Rightarrow 1、10、30、32

$$

然后就可以用合并两个有序数组的算法,合并 \( AB \),取 \( A \) 首端点到 \( A \) 末端点就得到结果
参考代码(js):

const minusAndIntersect = (A, B) => {

const R = []

if (A.length == 0) return R

let iB = 0

while (iB < B.length && B[iB] <= A[0]) ++iB

R.push(A[0])

for (let iA = 1; iA < A.length; ++iA) {

while (iB < B.length && B[iB] <= A[iA]) R.push(B[iB++])

if (A[iA] > R.at(-1)) R.push(A[iA])

}

return R

}

const A = [1, 10, 30, 32]

console.log(minusAndIntersect(A, [1, 9, 20, 32])) // [1, 9, 10, 20, 30, 32]

console.log(minusAndIntersect(A, [4, 9, 20, 29])) // [1, 4, 9, 10, 20, 29, 30, 32]

console.log(minusAndIntersect(A, [8, 9, 20, 32])) // [1, 8, 9, 10, 20, 30, 32]

如果前后区间不是紧贴的,如 \( 1-10、21-30 \)

那么用 \( B \) 的端点对 \( A \) 进行分割即可
参考代码(js):

const minusAndIntersect = (A, B) => {

const R = []

if (A.length == 0) return R

B = B.flat()

for (let i = 1; i < B.length; i += 2) ++B[i]

let iB = 0

for (const a of A) {

let [begin, end] = a

while (iB < B.length && B[iB] <= begin) ++iB

while (iB < B.length && B[iB] <= end) {

if (B[iB] > begin) {

R.push([begin, B[iB] - 1])

begin = B[iB]

}

++iB

}

R.push([begin, end])

}

return R

}

const A = [[1, 9], [10, 29], [30, 31]]

console.log(minusAndIntersect(A, [[1, 8], [9, 19], [20, 31]])) // [[1,8],[9,9],[10,19],[20,29],[30,31]]

console.log(minusAndIntersect(A, [[4, 8], [9, 19], [20, 28]])) // [[1,3],[4,8],[9,9],[10,19],[20,28],[29,29],[30,31]]

console.log(minusAndIntersect(A, [[8, 8], [9, 19], [20, 31]])) // [[1,7],[8,8],[9,9],[10,19],[20,29],[30,31]]

// 前后区间不紧贴的例子

console.log(minusAndIntersect([[1, 10], [21, 30]], [[6, 25]])) // [[1,5],[6,10],[21,25],[26,30]]

参考代码(java):

public static void main(String[] args) {

MinusAndIntersect f = new MinusAndIntersect(ranges(1, 9, 10, 29, 30, 31));

System.out.println(f.exec(ranges(1, 8, 9, 19, 20, 31)));

System.out.println(f.exec(ranges(4, 8, 9, 19, 20, 28)));

System.out.println(f.exec(ranges(8, 8, 9, 19, 20, 31)));

// 前后区间不紧贴的例子

System.out.println(new MinusAndIntersect(ranges(1, 10, 21, 30)).exec(ranges(6, 25)));

/*

[1-8, 9-9, 10-19, 20-29, 30-31]

[1-3, 4-8, 9-9, 10-19, 20-28, 29-29, 30-31]

[1-7, 8-8, 9-9, 10-19, 20-29, 30-31]

[1-5, 6-10, 21-25, 26-30]

*/

}

static List<Range> ranges(int... beginAndEnd) {

int n = beginAndEnd.length;

List<Range> ranges = new ArrayList<>(n >>> 1);

for (int i = 0; i < n; ranges.add(new Range(beginAndEnd[i++], beginAndEnd[i++])));

return ranges;

}

static class Range {

int begin, end;

Range(int beginInclusive, int endInclusive) {

begin = beginInclusive;

end = endInclusive;

}

@Override

public String toString() {

return begin + "-" + end;

}

}

static class MinusAndIntersect {

Range[] A;

List<Range> R;

int i = -1, begin, end;

MinusAndIntersect(List<Range> A) {

this.A = A.toArray(new Range[A.size()]);

}

List<Range> exec(List<Range> B) {

R = new ArrayList<>();

if (next()) {

for (Range b : B)

if (!addUntil(b.begin - 1) || !addUntil(b.end)) return R;

addUntil(Integer.MAX_VALUE);

}

return R;

}

boolean addUntil(int last) {

while (begin <= last) {

if (end <= last) {

R.add(new Range(begin, end));

if (!next()) return false;

} else {

R.add(new Range(begin, last));

begin = last + 1;

break;

}

}

return true;

}

boolean next() {

if (++i == A.length) {

i = -1;

return false;

}

Range a = A[i];

begin = a.begin;

end = a.end;

return true;

}

}


回答:

两个时间段列表是有序的,均取最小的一个时间段来处理,他们可能有如下几种关系(以及处理)

  • 完全分离,

    把小的那个取出来,大的那个留着下一轮比较用

  • 存在交叉

    按交叉点切割成 3 部分,取前 2 部分,最后一部分留着下一轮比较用

  • 重合

    • 完全重合

      任取一个,两个都不留

    • 前端重合

      最小的一个,大的那个切割后留着下一轮比较用

    • 后端重合

      切割成两段,都取走不留

    • 包含

      切割成三段,取走前两段,留下最后一段

如果这个分析没问题,就是一种一种情况去处理,目前还没发现简洁算法。昨天想了一个,但是代码验证是错的。先看看大家有没有简单点的算法,再来考虑代码怎么实现。

以上是 关于时间区间分割算法问题 的全部内容, 来源链接: utcz.com/p/944556.html

回到顶部