实现运行速度最快的算法(概率求解)

17

为了进行算法竞赛训练(不是作业),我们拿到了去年的一道题目。由于另一个网站要求登录,所以我们把它发布到了这个网站。

问题如下: 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),对于最大测试用例而言,运行速度太慢了,超过1秒钟。请有人告诉我如何在最短的时间内完成(不需要代码,除非你想要),因为这个问题已经困扰我很久了。
编辑:一定有一种方法可以在小于O(L)的时间复杂度内完成吧?

5
你应该详细说明你的算法应该做什么,代码很酷,但自然语言有时也是有用的。 - AsTeR
@AsTeR 添加了更多关于我是如何做的的解释,我会尽力再改进一下。 - 197
有趣!我在做这个。@197,你知道如何在O(L)中解决一个更简单的问题吗?那里有一条直街,有L座房子。 - Kos
好的,我想我懂了!关键词:前缀和(有一个小技巧);你还想要更多提示吗? - Kos
我已经发布了一个答案,我会逐步扩展它(除非你真的想让我一次性发布整个解决方案?)而且我相信O(L)真的足够了。 - Kos
显示剩余2条评论
4个回答

10
假设列表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. 我们知道如何从房子i-1更新slope到房子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)。如果您想要最小化内存使用,则还可以从迭代器流式传输而不是列表。

简而言之,此解决方案实现了最低可行复杂度O(N)。


你确定这个可行吗?当我输入algorithm([(2, 3), (4, 1), (11, 1), (12, 2)], 3)时它返回了11,但正确答案应该是33,难道我的输入有误吗? - phant0m
此外,你写道“我们只需要检查实际存在的房屋或者相反的房屋”,但是我的算法认为位置8是理想的,但是既不是8也不是它的相反数5有房子。或者你的意思是别的什么? - phant0m
在我的解决方案中,位置从0开始,它们必须满足0 <= x < 4*L,因此您必须将12替换为0,或将(x,2*pop)改为(x%(4*L),2*pop)。当L = 3时,8的相反数是8-2*L = 2,这确实有一个房子。还请注意,有时候斜率可以为0,以便整个间隔都是最佳的。 - Generic Human
啊,我明白了,我把“opposite”解释错了。现在有意义了。基本上,你只需将我的算法中的所有步骤组合起来,W(F)W(B)不会改变。我可以将其纳入我的解决方案中,这甚至在图表中也是可见的,这些步骤可以跳过 :) PS:如果你包括@用户名,当你发表新评论时,你可以通知我(或其他人)。 - phant0m
谢谢,我认为我大部分理解了这个解决方案,除了不连续的部分。您能否以更简单的方式向我解释一下?我不明白为什么是“2 * population”,其中的“2”是什么意思?我认为我可以看出斜率如何工作,但对于不连续性还不确定。 - 197
@197:假设有3个人住在一个房子里。当房子越来越远时,每走一步就加上3,所以斜率为3。一旦开始靠近,每走一步就减去3。因此,斜率从+3变为-3,即它的变化量为2 * 人口数量。不连续性基本上是指函数“跳跃”(在这种情况下,从+3-3,中间没有任何值)。 - phant0m

9
这里是一个不太数学化的解决方案,可以在O(n)时间内运行。
让我们将房屋(从0开始索引)分为两个不相交的集合:
- F,“前面”,人们顺时针走到房子 - B,“后面”,人们逆时针走到房子
还有一个单独的房子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+1时,F的消费变化量(以及一些修正因为F本身会改变)。我已经尽力在这里描述它。

algorithm depiction

我们最终得到了总运行时间为 O(L)
该算法的程序在本文末尾。
但是我们可以做得更好。只要在集合之间没有房屋发生变化,添加的 cw 将为零。我们可以计算这些步骤的数量并一次性完成它们。
当房屋发生以下情况时,房屋会改变集合: - 当 p 在一个房屋上 - 当 p 在房屋的对面
在下面的图表中,我已经可视化了算法现在更新 CW 所需停止的位置。 突出显示导致算法停止的房屋。

optimized algorithm

算法从一个房子开始(或者说是它的对面,稍后我们会看到原因),在这种情况下,恰好是一个房子。
同样,我们有消耗量 C(B) = 3*1C(F) = 2 * 1。如果我们将 p 向右移动一位,那么就会给 C(B) 加上 4 并从 C(F) 中减去 1。如果我们再次将 p 向右移动一次,同样的事情会发生。
只要同样的两组房子分别向前和向后移动,C 的变化就是恒定的。
现在我们稍微改变一下 B 的定义:它现在也包括 p!(这不会改变关于该算法优化版本的上述段落)。
这是因为当我们进入下一步时,我们将重复添加远离的房屋的权重。当 p 向右移动时,当前位置的房屋正在向远处移动,因此 W(B) 是正确的加数。
另一种情况是房子停止远离并再次靠近。在这种情况下,由于6*weight从一个C转移到另一个C,因此Cs会发生巨大变化。这是我们需要停止的另一种情况。

new calculations

我希望你已经清楚了这个算法的工作原理和原因,所以我会在这里留下工作算法。如果有不清楚的地方,请询问。
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)

1
你用什么软件制作了这个插图? - Arnab Datta
1
@ArnabDatta 我使用了这个在线LaTeX编辑器和一些Photoshop的原始笔划。 - phant0m
2
不,它不是n^2。这两个循环只运行一次。实际上,consumptions()内部的循环控制了run()中的循环运行的次数,因为consumptions()生成用于在run()循环迭代的值。 - phant0m
@GrijeshChauhan 不用客气,也谢谢你。你的帖子很不错 ;) 顺便说一句:当你评论我的帖子时,不需要@我 ;) - phant0m
@phant0m,特别感谢你上一篇文章。你知道我因为糟糕的英语被老师打了好几次 :P - Grijesh Chauhan
显示剩余3条评论

0

我会提供一些技巧,让你仍然能够对自己有所挑战。


让我从一个大大简化的版本开始:
有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)内计算所有分数,因为它不包含嵌套循环。它与前缀和相关。你能找到它吗?


0

没有必要计算每个房子!!!

虽然这个想法还没有完全发展,但我认为值得思考:

模数N

N是所有房屋的数量,n应该是一些房屋的“地址”(编号)。

如果你在岛上走来走去,你会发现对于每一个你经过的房子,n都会加1。如果你到达了n=N的房子,那么下一个房子就是1号房子。

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

升数是n(模数N)的函数

您可以通过构建距离和居住人数的乘积总和来计算每个房屋编号的升数。

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

该函数是周期性的

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

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

我的意思是:如果N非常大,则从房屋a移动到其相邻的房屋a+1时,升高的升数不会发生很大变化。因此,您可以使用插值方法。

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

这是我的建议:

步骤1:选择一个大于1且小于N的距离d。我不能说为什么,但我会使用d=int(sqrt(N))(也许有更好的选择,请尝试一下)。
步骤2:计算房屋0、d、2d、3d等的升数。
步骤3:您将找到一些值比它们两个邻居都高。使用这些高点及其邻居通过插值方法来计算接近高点的更多点(间隔分割)。

重复此过程,直到您有时间(您有1秒钟,这是很长的时间!)进行其他高点的插值。

如果您看到全局最大值必须在其他地方,请从一个高点跳到另一个高点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接