消除嵌套循环

4
我被分配了一个任务,需要构建一个暴力算法。该算法必须找到包含14400个顶点的图中的最佳路径,每天24小时,每小时600个顶点。在这600个顶点中,每个顶点都可以选择下一个小时的480个顶点。
我尝试构建了一个算法,但现在的问题是无法遍历整个图,因为我得到了很多嵌套循环。由于我对Python不熟悉,是否有任何方法可以改进算法?
Path = [0] * 2
S = [12, 9, 20];
K = 10
S[:] = [x - K for x in S]

for x in range(0,600):     #1.hour              
Path[0]= x
for y in range(-240,240):    # 2.hour
    hour2 = x+y
    if 0 <= hour2 <= 600:
         Path[1] = hour2             
         for y in range(-240,240):   # 3.hour
             hour3 = hour2 + y
             if 0 <= hour3 <= 600:
                 Path[2]= hour3
                 price = sum([a*b for a,b in zip(Path,S)])
                 if maxprice < price:
                     maxprice = price
                     Optimalpath = list(Path)
print Optimalpath
print maxprice

我只展示了前三个小时的嵌套循环,但如果可能的话,它必须迭代所有24小时。

或者我对这个问题的考虑方式有误?


2
递归,递归,递归。 - BJ Myers
2个回答

4
在每个24个阶段中,至少有240种可能性(通常多达480种)。因此,至少有24**240种可能的路径。这比10**57条路径还要多。你无法通过蛮力解决这个问题。但是,可以使用线性规划方法来解决这个问题。

正如BJ Myers建议的那样,您可以使用递归来生成所有可能的路径。 假设您有一个生成器函数,它生成长度为1的所有可能路径。那很容易:

def generate_paths1():
    for i in range(600):
        yield [i]

你可以使用generate_paths1生成长度为2的所有可能路径:
def generate_paths2():
    for path in generate_paths1():
        current = path[-1]
        low = max(current-240, 0)
        high = min(current+240, 600)
        for i in range(low, high):
            yield path+[i]

你可以使用generate_paths2生成所有长度为3的路径:

def generate_paths3():
    for path in generate_paths2():
        current = path[-1]
        low = max(current-240, 0)
        high = min(current+240, 600)
        for i in range(low, high):
            yield path+[i]

但是等等!generate_paths3 函数实际上与 generate_paths2 函数几乎相同。肯定有更好的方法。我们可以编写一个递归函数,可以执行 generate_paths1generate_paths2generate_paths3 能够做到的一切 - 甚至更多:

def generate_paths(N):
    # moves = range(0, 601, 120)   # see below for why this is an improvement
    moves = range(601)
    if N == 1:
        for i in moves:
            yield [i]
    else:
        for path in generate_paths(N-1):
            current = path[-1]
            low = max(current-240, 0)
            high = min(current+240, 600)
            for i in [i for i in moves if low <= i <= high]:
                yield path+[i]

N = 3
for path in generate_paths(N):
    ...

虽然能够生成所有路径很棒,但是路径太多了。 如果我们将问题识别为线性规划(LP)问题,我们可以做得更好。 你的问题可以像这样表示为LP问题:
Maximize price = sum([a*b for a, b in zip(S, path)])
Subject to:

    x[1] - x[0] <= 240
    x[0] - x[1] <= 240
    x[2] - x[1] <= 240
    x[1] - x[2] <= 240
    ... 

线性规划问题的解之一的特性是:

如果存在可行解并且(线性)目标函数有界,则最优值总是在最优水平集的边界上达到。(我强调)

因此,您可以将 moves = range(601) 替换为

    moves = range(0, 601, 120)
    # [0, 120, 240, 360, 480, 600]

因为当S为正时,最优解倾向于使用600来最大化价格,并且在S为负时使用0来最小化损失。介于两者之间的其他值是最优解从0到600或从600到0移动所需的最大跳数。
这将减少路径数量至6**24,远远小于240**24,但仍然太大以致于无法进行暴力求解。

使用scipy.optimimize.linprog,您可以像这样解决最优路径问题,即使是完整的24阶段问题:

import numpy as np
import scipy.optimize as optimize

"""
Minimize: np.dot(S, x)
Subject to: np.dot(A, x) <= b
"""
N = 24
K = 10
S = np.random.randint(-K//2, +K//2, size=N)
A = np.zeros((2*(N-1), N), dtype=int)
for i in range(N-1):
    A[2*i, i:i+2] = (1, -1)
    A[2*i+1, i:i+2] = (-1, 1)
b = np.array([240]*A.shape[0])
bounds = [(0, 600)]*N
result = optimize.linprog(c=-S, A_ub=A, b_ub=b, bounds=bounds)

optimal_path = result.x
max_price = np.dot(S, optimal_path)
print('''S: {}
optimal_path: {}
price: {}'''.format(S, optimal_path, max_price))

产生类似以下结果:

S: [ 0  1  3  4 -5 -1  0 -3 -4  0  3  2 -5  1 -4 -1 -3  2  0 -2  0  4 -2  2]
optimal_path: [ 360.  600.  600.  360.  120.    0.    0.    0.    0.  240.  480.  240.
    0.  240.    0.    0.    0.  240.    0.  120.  360.  600.  360.  600.]
price: 8520.0

非常感谢您的帮助。我有一个问题,您是如何计算出最优解所需的最大跳数的? - Christian Andersen
我通过 brute force 方法找到了非常小的 N 的解决方案,从而获得了一些直觉。后来我意识到这是一个线性规划问题,因此知道最优解总是出现在“边界”上(考虑到约束条件)。在这里,“边界”意味着移动最大量——240。 - unutbu
1
换句话说,如果你从0开始想要到达600,但是每次最多只能跳240,那么你会先跳240,然后480,最后落在600。反之,如果你从600开始想要到达0,你会跳(最大可能的距离)到360(=600-240),然后再跳120到达0。因此,最优解使用的唯一值为[0, 120, 240, 360, 480, 600] - unutbu
又有一个问题。在暴力方法中,我可以看到你编写的递归函数是有效的。但我不明白它是如何创建路径的,因为从我的角度来看,N并没有改变。 - Christian Andersen
编写(和阅读)递归函数涉及“假定性思维”。你“希望”有一个神奇的黑匣子——函数generate_paths——它返回所有长度为N的路径。在编写之前,你假设已经有了这个函数。在定义的过程中使用黑匣子。如果你处理基本情况N=1而没有递归调用,那么剩下的部分就会“神奇地”工作。 - unutbu
显示剩余2条评论

2

您可以使用以下组合。


考虑将循环体制成一个函数。

for x in ...
    for y in ...
        for z in ...
            ...

三重循环看起来很艰难。但是,请考虑以下内容:

def process_xy(x, y):
    for z in ...

for x in ...
    for y in ...
        process_xy(x, y)

你不仅减少了代码缩进,还做了以下工作:

  1. 你创建了一个可以独立调试和测试的较小单元(process_xy)。
  2. 其余的嵌套循环虽然存在,但只是简单地调用一个函数。

请注意:

for x0 in a0:
    for x1 in a1:
        for x2 in a2:
             ....

等同于

import itertools

for (x0, x1, x2) in itertools.product(a0, a1, a2):
    ...

当嵌套范围不依赖于外部范围时,这将非常有用。


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