解决算法问题:预订酒店的可能性

本文的翻译是专门为“开发人员算法”课程的学生准备的。



本文是有关如何解决算法问题的系列文章的一部分。 根据我的个人经验,我发现大多数资源只是简单地详细描述了解决方案。 不幸的是,对能够找到有效解决方案的推理主线的解释不是很普遍。 因此,本系列文章的目的是描述从头解决问题的可能推理过程。



挑战赛



酒店经理必须在下一个季节处理N个预订单。 他的酒店有K间客房。 预订信息包含入住日期和退房日期。 经理想找出酒店中是否有足够的房间来满足需求。

输入数据:

-第一个输入有关到达时间信息的列表
-第二-包含出发时间信息的列表
-第三-K,表示房间数

输出数据:
-表示预订房间能力的逻辑值
false表示该酒店没有足够的房间来预订N
true表示酒店有足够的房间可预订N次

一个例子:

输入数据:
-签到= [1、3、5]
-出发= [2,6,10]
-K = 1

输出:false。 一天= 5,酒店有2位客人。 但是我们只有一个号码。



决策过程


在我看来,这项任务很有趣,因为有许多不同的解决方法。 让我们看一下选项。

每天存储计数器的结构
第一个想法可能是我们需要一种结构来存储每天的订单数量。 该结构可以是具有固定大小(由最长出发日期确定)的数组。
输入数据:
-条目= [1、3、5]
-离场= [2,6,10]
-k = 1

对于此示例,数组的大小将为10(因为第10天的最后一个出口)。 为了填充此数组,我们浏览了进入和退出列表,并增加或减少了相应日期的计数器。 伪代码示例:

int[] counts = new int[maxDepartures(departures)] for each arr in arrivals { counts[arr]++ } for each dep in departures { counts[dep]-- } 

结果,我们得到以下数组:

: 1 0 1 1 2 1 1 1 1 0
: 1 2 3 4 5 6 7 8 9 10


阵列装满后,只需检查一下所有元素是否不超过k (房间数)即可。

在前面的示例中,最大房间数为1。由于在第5天有2个预订,因此返回false

此解决方案的时间复杂度为O(n),其中n为预留数,空间为O(m),其中m为最大出发日。 从理论上讲还不错,但是它可能会为一个大型数组分配很多内存,尽管其中大部分不会被使用。

例如:
输入数据:
-条目= [1、3、5]
-出发次数= [2,10000,10]
-k = 1

在这种情况下,将分配一万个整数的数组。

让我们看看其他解决方案。

事件收集存储


还有什么其他选择? 让我们再次看看以前的结构发生了什么:

: 1 0 1 1 2 1 1 1 1 0
: 1 2 3 4 5 6 7 8 9 10


我们看到一些信息是重复的。 例如,在6到9天之间,预订数量没有变化,因为我们知道在这段时间内什么也不会发生。

如果存储事件,会更好吗? 让我们再次以相同的示例:
输入数据:
-条目= [1、3、5]
-离场= [2,6,10]
第一天:+1预订
第2天:-1预订
第3天:+1预订
第6天:-1预订
第5天:+1预订
第10天:-1预订

解决方案是遍历这些事件以增加或减少计数器。 如果在某个时候计数器大于k ,则返回false 。 但是,为了进行迭代,需要对事件的集合进行排序。

在这里最好使用哪种结构? 让我们总结一下我们的要求:

  • 搜索以检查是否已经存在这一天
  • 添加新的一天,
  • 查看结构以在每个排序的日期进行迭代。

如何使用二叉搜索树(BST)?

每个节点可以表示如下:

 class Node { int day int count Node left Node right } 

排序将在day字段中完成。

让我们看一下时间复杂度的后果:

  • 搜索以检查是否已经存在这样的日子:一般情况下为O(log(n)),最坏情况下为O(n),
  • 添加新的一天:一般情况下为O(log(n)),最坏情况下为O(n),
  • 查看用于在每个排序的日期进行迭代的结构:使用深度搜索的O(n)。

由于我们必须遍历每个元素并将其插入树中,因此算法的复杂度通常为O(n log(n)),最坏情况为O(n²)。

另一种选择是在添加所有事件之后使用哈希表并对键进行排序:

  • 搜索以检查是否已经存在这样的一天:一般情况下为O(1),最坏情况下为O(n)(概率取决于关联数组的容量),
  • 添加新的一天:一般情况下为O(1),最坏情况下为O(n),
  • 查看用于遍历每个已排序日期的结构:O(n log(n))用于排序关键字,O(n)用于排序。

最后,解决方案在中间情况下(归因于排序操作)为O(n log(n)),在最坏情况下为O(n²)。 该解决方案似乎与基于树的解决方案具有相同的复杂性。

让我们看一下Java中使用排序关联数组的可能实现:

 public boolean hotel(ArrayList<Integer> arrivals, ArrayList<Integer> departures, int k) { //   Map<Integer, Integer> events = new HashMap<>(); //   int n = arrivals.size(); for (int i = 0; i < n; i++) { int arrival = arrivals.get(i); int departure = departures.get(i); //      Integer current = events.get(arrival); events.put(arrival, current == null ? 1 : current + 1); //      current = events.get(departure); events.put(departure, current == null ? -1 : current - 1); } //    Map<Integer, Integer> sortedEvents = new TreeMap<>(events); int count = 0; for (Map.Entry<Integer, Integer> entry : sortedEvents.entrySet()) { count += entry.getValue(); //  count     if (count > k) { return false; } } return true; } 

恒定的空间复杂度


如果要优化算法,则需要考虑是否真的需要存储所有这些事件? 我们不能仅对收集数据(进入和退出)进行排序并检查预订限制吗?

一种解决方案是可能的,但是为此,有必要通过预先排序来简化输入数据。

如果两个集合都已排序,我们可以使用两个指针(一个用于入口,另一个用于出口)简单地遍历每个元素,并动态检查限制。

如您所见,在每次迭代过程中,我们仍然需要检查arrivals.get(indexArrival) departures.get(indexDeparture)之间的最小值,以找出需要更新的指针。

通常,由于排序操作,该算法具有恒定的时间和空间复杂度O(n log(n))。

Source: https://habr.com/ru/post/zh-CN470676/


All Articles