【问题标题】:Algorithm (prob. solving) achieving fastest runtime算法(问题求解)实现最快的运行时间
【发布时间】:2012-07-22 13:22:33
【问题描述】:

对于算法竞赛训练(不是家庭作业),我们收到了过去一年的这个问题。将其发布到此站点,因为其他站点需要登录。

这就是问题所在: http://pastehtml.com/view/c5nhqhdcw.html

图片没用,所以贴在这里:

它必须在不到一秒的时间内运行,我只能考虑最慢的方法,这是我尝试过的:

with open('islandin.txt') as fin:
    num_houses, length = map(int, fin.readline().split())
    tot_length = length * 4 # side length of square
    houses = [map(int, line.split()) for line in fin] # inhabited houses read into list from text file

def cost(house_no):
    money = 0
    for h, p in houses:
        if h == house_no: # Skip this house since you don't count the one you build on
            continue
        d = abs(h - house_no)
        shortest_dist = min(d, tot_length - d)    
        money += shortest_dist * p
    return money


def paths():
    for house_no in xrange(1, length * 4 + 1):
        yield house_no, cost(house_no)
        print house_no, cost(house_no) # for testing

print max(paths(), key=lambda (h, m): m) # Gets max path based on the money it makes

我目前正在做的是遍历每个位置,然后遍历该位置的每个有人居住的房屋,以找到最大收入的位置。

伪代码:

max_money = 0
max_location = 0
for every location in 1 to length * 4 + 1
    money = 0
    for house in inhabited_houses:
        money = money + shortest_dist * num_people_in_this_house
    if money > max_money
        max_money = money
        max_location = location

这太慢了,因为它是 O(LN) 并且对于最大的测试用例不会在一秒钟内运行。有人可以简单地告诉我如何在最短的运行时间内完成它(除非您愿意,否则不需要代码),因为这一直困扰着我多年。

编辑:一定有办法在少于 O(L) 的时间内做到这一点,对吧?

【问题讨论】:

  • 你应该详细说明你的算法应该做什么,代码很酷,但自然语言有时也很有用
  • @AsTeR 添加了更多关于我如何做的解释,我会尝试进一步改进它。
  • 有趣!我在那个。 @197,你知道如何在 O(L) 中解决一个更简单的问题,那里有 L 个房子的直街吗?
  • 好的,我想我明白了!关键字:前缀和(有一个扭曲);想要更多提示吗?
  • @Kos 我没有研究过直线,但解决方案不是必须小于 O(L) 吗?是的,我想要更多提示!

标签: python algorithm


【解决方案1】:

假设列表houses(x,pop)0 <= x < 4*L 位置和pop 人口对组成。

我们想要最大化的目标函数是

def revenue(i):
    return sum(pop * min((i-j)%(4*L), 4*L - (i-j)%(4*L)) for j,pop in houses)

朴素算法 O(LN) 算法很简单:

max_revenue = max(revenue(i) for i in range(4*L))

但是为每个位置重新评估 revenue 是非常浪费的。

为避免这种情况,请注意这是一个分段线性函数;所以它的导数是分段常数,在两种点处不连续:

  • i 内部,导数从slope 变为slope + 2*population[i]
  • 在岛上房子i对面的点,导数从slope变为slope - 2*population[i]

这让事情变得非常简单:

  1. 我们只需检查实际房屋或对面房屋,因此复杂度降至 O(N²)。
  2. 我们知道如何将 slope 从 house i-1 更新为 house i,并且只需要 O(1) 时间。
  3. 由于我们知道位置 0 的收入和斜率,并且由于我们知道如何迭代更新 slope,因此复杂度实际上下降到 O(N):在两个连续房屋之间/对面的房子,我们可以用斜率乘以距离来获得收入的差异。

所以完整的算法是:

def algorithm(houses, L):
    def revenue(i):
        return sum(pop * min((i-j)%(4*L), 4*L - (i-j)%(4*L)) for j,pop in houses)

    slope_changes = sorted(
            [(x, 2*pop) for x,pop in houses] +
            [((x+2*L)%(4*L), -2*pop) for x,pop in houses])

    current_x = 0
    current_revenue = revenue(0)
    current_slope = current_revenue - revenue(4*L-1)
    best_revenue = current_revenue

    for x, slope_delta in slope_changes:
        current_revenue += (x-current_x) * current_slope
        current_slope += slope_delta
        current_x = x
        best_revenue = max(best_revenue, current_revenue)

    return best_revenue

为了简单起见,我使用sorted() 来合并两种类型的斜率变化,但这不是最优的,因为它具有 O(N log N) 复杂度.如果你想要更高的效率,你可以在O(N)时间内生成一个对应于对面房屋的排序列表,并将其与O(N)(例如,使用标准库的 heapq.merge)。如果您想最小化内存使用量,您还可以从迭代器而不是列表进行流式传输。

TLDR:此解决方案实现了 O(N) 的最低可行复杂度。

【讨论】:

  • 你确定这行得通吗? algorithm([(2, 3), (4, 1), (11, 1), (12, 2)], 3) 什么时候应该给1133 还是我的输入错误?
  • 进一步,你写We only have to examine actual houses or opposite-of-houses,然而,我的算法认为8的位置是理想的,但8和它的对面5都没有房子。还是你的意思是别的?
  • 在我的解决方案中,位置从 0 开始,它们必须满足 0 <= x < 4*L,因此您必须将 12 替换为 0 或将 (x, 2*pop) 更改为 (x%(4*L), 2*pop)。当L = 3 时,8 的反面是8-2*L = 2,它确实有房子。另请注意,有时斜率可以为 0,因此整个区间可以是最佳的。
  • 啊,我明白了,我误解了“相反”的意思。现在它是有道理的。基本上你只需在我的算法W(F)W(B) 中组合所有这些步骤就不会改变。我可以将其合并到我的解决方案中,甚至可以在图表中看到这些步骤可以跳过 :) PS:如果您包含@username,您可以在发表新评论时通知我(或其他人)。跨度>
  • 谢谢,我想我主要理解这个解决方案,除了不连续部分。你有什么办法可以更简单地向我解释一下吗?我不明白为什么是2 * population2 是干什么用的?我想我可以看到斜率是如何工作的,但不确定是否有不连续性。
【解决方案2】:

这是一个在 O(n) 中工作的数学倾向较小的解决方案。

让我们将房屋(索引从 0 开始)分成两个不相交的集合:

  • F,“前”,人们从 CCW 走到房子的地方
  • B,“后退”,人们沿着 CW 走到房子的地方

还有一栋房子p,它标志着工厂将建在的当前位置。

我的插图基于图片中给出的示例。

按照惯例,让我们将一半的房子分配给F,正好少一栋分配给B

  • F 包含 6 个房子
  • B 包含 5 间房屋

通过简单的模运算,我们可以轻松地通过(p + offset) % 12 访问房屋,这要归功于Python 对模运算符的合理实现,这与some other popular languages 不同。

如果我们为p任意选择一个位置,我们可以很容易地确定O(L)的耗水量。

我们可以对p 的不同位置重新执行此操作,以达到O(L^2) 的运行时间。

但是,如果我们只将p移动一个位置,如果我们稍微巧妙地观察一下,我们可以确定O(1)的新消费:F(或B分别居住的人数)确定当我们设置p' = p+1F 的消耗变化了多少。 (以及一些更正,因为F 本身会改变)。我已经尽我所能在这里描述了这一点。

我们最终的总运行时间为O(L)

这个算法的程序在文末。

但我们可以做得更好。只要集合之间没有房屋变化,添加的cs 和ws 将为零。我们可以计算出这些步骤有多少,一步完成。

在以下情况下房屋会发生变化: - 当p 在房子上时 - 当p在房子对面时

在下图中,我已经可视化了算法现在为更新Cs 和Ws 所做的停止。 突出显示的是房子,它会导致算法停止。

算法从一所房子开始(或与之相反,我们稍后会看到原因),在这种情况下,它恰好是一所房子。

同样,我们同时拥有消费C(B) = 3*1C(F) = 2 * 1。如果我们将p 向右移动一位,我们将4 添加到C(B) 并从C(F) 中减去1。如果我们再次转移p,就会发生完全相同的事情。

只要相同的两组房子分别靠近和远离,Cs的变化是不变的。

我们现在稍微改变B 的定义:它现在也将包含p! (这不会改变上述关于算法优化版本的段落)。

这样做是因为当我们移动到下一步时,我们会增加重复移动的房屋的重量。当p向右移动时,当前位置的房子正在移动,因此W(B)是正确的和数。

另一种情况是当房子停止移动并再次靠近时。在这种情况下,Cs 会发生巨大变化,因为6*weight 从一个C 变为另一个。这是我们需要停止的另一种情况。

我希望清楚它的工作原理和原因,所以我将把工作算法留在这里。有不清楚的地方请追问。

O(n):

import itertools

def hippo_island(houses, L):
    return PlantBuilder(houses, L).solution

class PlantBuilder:
    def __init__(self, houses, L):
        self.L = L
        self.houses = sorted(houses)
        self.changes = sorted(
            [((pos + L /2) % L, -transfer) for pos, transfer in self.houses] + 
            self.houses)
        self.starting_position = min(self.changes)[0]

        def is_front(pos_population):
            pos = pos_population[0]
            pos += L if pos < self.starting_position else 0
            return self.starting_position < pos <= self.starting_position + L // 2

        front_houses = filter(is_front, self.houses)
        back_houses = list(itertools.ifilterfalse(is_front, self.houses))

        self.front_count = len(houses) // 2
        self.back_count = len(houses) - self.front_count - 1
        (self.back_weight, self.back_consumption) = self._initialize_back(back_houses)
        (self.front_weight, self.front_consumption) = self._initialize_front(front_houses)
        self.solution = (0, self.back_weight + self.front_weight)
        self.run()

    def distance(self, i, j):
        return min((i - j) % self.L, self.L - (i - j) % self.L)

    def run(self):
        for (position, weight) in self.consumptions():
            self.update_solution(position, weight)

    def consumptions(self):
        last_position = self.starting_position
        for position, transfer in self.changes[1:]:
            distance = position - last_position
            self.front_consumption -= distance * self.front_weight
            self.front_consumption += distance * self.back_weight

            self.back_weight += transfer
            self.front_weight -= transfer

            # We are opposite of a house, it will change from B to F
            if transfer < 0:
                self.front_consumption -= self.L/2 * transfer
                self.front_consumption += self.L/2 * transfer


            last_position = position
            yield (position, self.back_consumption + self.front_consumption)

    def update_solution(self, position, weight):
        (best_position, best_weight) = self.solution
        if weight > best_weight:
            self.solution = (position, weight)

    def _initialize_front(self, front_houses):
        weight = 0
        consumption = 0
        for position, population in front_houses:
            distance = self.distance(self.starting_position, position)
            consumption += distance * population
            weight += population
        return (weight, consumption)

    def _initialize_back(self, back_houses):
        weight = back_houses[0][1]
        consumption = 0
        for position, population in back_houses[1:]:
            distance = self.distance(self.starting_position, position)
            consumption += distance * population
            weight += population
        return (weight, consumption)

O(L)

def hippo_island(houses):
    return PlantBuilder(houses).solution

class PlantBuilder:
    def __init__(self, houses):
        self.houses = houses
        self.front_count = len(houses) // 2
        self.back_count = len(houses) - self.front_count - 1
        (self.back_weight, self.back_consumption) = self.initialize_back()
        (self.front_weight, self.front_consumption) = self.initialize_front()
        self.solution = (0, self.back_weight + self.front_weight)
        self.run()

    def run(self):
        for (position, weight) in self.consumptions():
            self.update_solution(position, weight)

    def consumptions(self):
        for position in range(1, len(self.houses)):
            self.remove_current_position_from_front(position)

            self.add_house_furthest_from_back_to_front(position)
            self.remove_furthest_house_from_back(position)

            self.add_house_at_last_position_to_back(position)
            yield (position, self.back_consumption + self.front_consumption)

    def add_house_at_last_position_to_back(self, position):
        self.back_weight += self.houses[position - 1]
        self.back_consumption += self.back_weight

    def remove_furthest_house_from_back(self, position):
        house_position = position - self.back_count - 1
        distance = self.back_count
        self.back_weight -= self.houses[house_position]
        self.back_consumption -= distance * self.houses[house_position]

    def add_house_furthest_from_back_to_front(self, position):
        house_position = position - self.back_count - 1
        distance = self.front_count
        self.front_weight += self.houses[house_position]
        self.front_consumption += distance * self.houses[house_position]

    def remove_current_position_from_front(self, position):
        self.front_consumption -= self.front_weight
        self.front_weight -= self.houses[position]

    def update_solution(self, position, weight):
        (best_position, best_weight) = self.solution
        if weight > best_weight:
            self.solution = (position, weight)

    def initialize_front(self):
        weight = 0
        consumption = 0
        for distance in range(1, self.front_count + 1):
            consumption += distance * self.houses[distance]
            weight += self.houses[distance]
        return (weight, consumption)

    def initialize_back(self):
        weight = 0
        consumption = 0
        for distance in range(1, self.back_count + 1):
            consumption += distance * self.houses[-distance]
            weight += self.houses[-distance]
        return (weight, consumption)

结果:

>>> hippo_island([0, 3, 0, 1, 0, 0, 0, 0, 0, 0, 1, 2])
(7, 33)

【讨论】:

  • 你用什么样的软件来制作插图?
  • @ArnabDatta 我在 Photoshop 中使用过this online LaTeX editor 和一些原始的笔触。
  • 我不熟悉Python语法但是“运行”和“消耗”方法中有for循环,是不是意味着它是n^2?
  • 不,不是n^2。两个循环只运行一次。事实上,consumptions() 中的循环控制了run() 中的循环运行的次数,因为consumptions() generates 中的循环在run() 中迭代的值。
  • @phant0m 感谢改进我的帖子,我刚刚在我的通知框中看到了..不错的答案 +1。
【解决方案3】:

我将提供一些提示,以便您仍然可以挑战自己。


让我从一个高度简化的版本开始:

一条笔直的街道上有 N 栋房子,要么有人居住,要么空无一人。

0 1 1 0 1

让我们计算他们的分数,知道第 n 个房子的分数等于到其他非空房子的所有距离的总和。所以第一个房子的分数是1+2+4 = 7,因为还有3个其他人住的房子,它们的距离是1、2、4。

完整的分数数组如下所示:

7 4 3 4 5

如何计算?显而易见的方法是......

for every house i
    score(i) = 0
    for every other house j
        if j is populated, score(i) += distance(i, j)

这会给你 O(N^2) 的复杂度。但是有一种更快的方法可以计算 O(N) 中的所有分数,因为它没有嵌套循环。它与前缀和有关。你能找到吗?

【讨论】:

    【解决方案4】:

    不用每家都计算!!!

    它还没有完全开发,但我认为值得考虑:

    取模N

    N为所有房屋的数量,n应为部分房屋的“地址”(数量)。

    如果你在岛上走一圈,你会发现每经过一个房子,n 就加 1。如果你到达 nN 的房子,那么下一个房子的数字是 1。

    让我们使用不同的编号系统:将每个门牌号加 1。然后 n 从 0 变为 N-1。这与数字模 N 的行为方式相同。

    升是门牌号的函数n(模N

    您可以通过构建所有距离乘积和居住在那里的人的总和来计算每个门牌号的升数。

    您还可以绘制该函数的图形:x 是 n,y 是升数。

    函数是周期性的

    如果您了解模数的含义,您就会明白您刚刚绘制的图形只是周期函数的一个周期,因为 Litre(n) 等于 Litre(n + x * N) 其中 x 是一个整数(也可能是负数)。

    如果N很大,则函数是“伪连续”

    我的意思是:如果 N 真的很大,那么如果你从房子 a 搬到它的邻居房子,升的量不会有太大变化一个+1。所以你可以使用插值的方法。

    您正在寻找周期性伪连续函数的“全局”最大值的位置(仅在一个周期内真正全局)

    这是我的建议:

    第一步:选择一个大于1小于N的距离d。我不能说为什么,但我会使用 d=int(sqrt(N)) (也许有更好的选择,试试看)。
    第 2 步:计算房屋 0、d、2d、3d、...的升数 第 3 步:您会发现一些值高于它们的两个邻居。使用这个高点和他们的邻居使用插值方法来计算更多接近高点的点(区间分割)。

    只要你有时间,就对其他高点重复这个插值(你有 1 秒,这是很长的时间!)

    如果你看到,从一个高点跳到另一个高点,全局最大值一定在别处。

    【讨论】:

      猜你喜欢
      • 2023-01-31
      • 2018-09-20
      • 2012-02-28
      • 2011-02-15
      • 2015-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多