Python解题:Dream to EC-Final 梦回长安

本文介绍了如何使用Python解决一个关于古代西安城市布局的数学谜题。问题涉及将城市街道旋转45度后计算新排列的交叉口面积。作者通过详细分析和尝试多种算法,包括穷举法、列表累加和卷积运算,探讨了不同方法的效率和可行性,并分享了使用Numpy库进行卷积运算的高效解决方案。文章适合对Python编程和数学感兴趣的读者。
摘要由CSDN通过智能技术生成

最近问哥在问答频道认识了不少朝气蓬勃的小伙伴,请我一起参与解题。有些题目真是十分烧脑又十分有趣,同时又有很多陷阱,一不小心就答错了。于是问哥忍不住想把一些题目写出来,新开一个专栏,分享问哥的解题思路,但相信一定不是最优的解法。抛砖引玉,期待和大家有思想上的碰撞。

注:本专栏所有谜题都以Python3.10解题。


今天想和大家分享的谜题叫做《Dream to EC-Final》。很抱歉,它是全英文的题目,这也是中国学生的一道坎。可以说,谜题的50%难度在英文阅读理解上。它的原文是长这样的(大家有兴趣可以当做阅读理解,忍不了的话就快速向下翻,后面问哥会解释得图文并茂):


原题:

In order to discover the landscape of the ancient Xi'an City, Derrick has already searched enough information about it. Just as Hundreds and Thousands of houses are like a Go game, and twelve streets are like a vegetable garden(百千家似围棋局,十二街如种菜畦)written by Juyi Bai. This poetry vividly demonstrates the structure of the ancient Xi'an City, it consists of several vertical streets and horizontal roads of the same lengths but different widths.

Supposed that the shape of whole city is an n×n square, which composed of n vertical streets x1​, x2​,..., xn​ and n horizontal roads y1​, y2​,..., yn​ within the same lengths and different widths from centre. Derrick is curious about calculating the area of all intersections, because he wants to calculate the traffic flow accurately at each intersection in the ancient Xi'an City.

However, Derrick considers this task is quite easy for you, so Derrick will spin the whole square with 45 degree at first, maybe clockwise or maybe counterclockwise. After that, you are asked to calculate the total area of all intersections for each row and each column.

Input Specification:

There are multiple test cases. The first line of the input contains an integer T, indicating the number of test cases. For each test case:

The first line contains two integers n (1≤n≤1000) and d, indicating the size of the square and the direction of rotating the square. When d=1, it means that the direction of rotation is clockwise. When d=0, it means that the direction of rotation is counterclockwise.

The second line contains n integers x1​, x2​,..., xn​ (0≤xi​≤5×10^{4}), indicating the width of vertical streets at the i-th position from centre.

The third line contains n integers y1​, y2​,..., yn​ (0≤yj​≤5×10^{4}), indicating the width of horizontal roads at the j-th position from centre.

It's guaranteed that the sum of n of all test cases will not exceed 2×10^{4}.

Output Specification:

For each case, in the first line output 2n−1 integers separated by a space, indicating the total area of intersections for each row after rotating 45 degree.

Similarly, in the second line print 2n−1 integers separated by a space, indicating the total area of intersections for each column after rotating 45 degree.

Note: Please, DO NOT output extra spaces at the end of each line, or your answer may be considered incorrect!

Sample Input:

1
3 0
1 2 3
3 2 1

Sample Output:

3 8 14 8 3
1 4 10 12 9

Hint:

For example, the initial state of 3×3 square, the clockwise state of 3×3 square and the counterclockwise state of 3×3 square are shown as the following, respectively:


解析:

怎么样,看懂了多少?

简单地说,一个叫Derrick的小伙子来西安参加比赛,顺带着想要分析十三朝古都西安的城市布局。

他发现西安的街道横平竖直,阡陌纵横,但是路的宽度却各不相同。于是Derrick突发奇想:如果计算出每个交叉路口的面积,将对估算交通流量有帮助。这里可以说是阅读理解的第一个难点,Derrick想要计算的是交叉路口的面积,也就是横竖道路宽度之积,而不是街道区块的面积。

然后他给每条纵向的道路用x来表示,从城中心向外依次为x1,x2,x3...xn,横向的道路用y来表示,从城中心向外依次为y1,y2,y3...yn,横向道路和纵向道路的数量相同。然后给出每条道路的宽度。比如给的例子里纵向道路x的宽度依次为1、2、3,而y的宽度为3、2、1。于是,可以很容易计算得出每个交叉路口的面积,如下图(白色数字代表交叉路口的面积):

这时,Derrick说他想把地图旋转一下,如果他说1,就顺时针旋转45度,如果是0,就逆时针旋转45度。示例中给的是数字0,所以我们把地图逆时针旋转45度,就得到下图:

这个时候,到了这道题的关键了,Derrick要我们计算出旋转之后新的横向和纵向的路口面积之和。很多小伙伴未能理解这里指的新的横向和纵向是怎么计算,其实如果结合题目给的提示案例:

3 8 14 8 3
1 4 10 12 9

稍微发散思考一下,就可以在纸上或脑中画出类似下图的样子:

看出来没?新的横向(row)和纵向(column)的面积,原来是指旋转45度后斜线的路口面积之和。展现方式为从上往下从左至右。这里也是本题的一个陷阱,因为3、8、14、8、3这一排数字正序倒序都一样,所以不能直观得出横向row的面积是从上往下,还是从下往上,但是文中Derrick提到过是从城中心向外给街道命名,而且结合后面提交的试算结果来看,应该是相对于中心位置,逆时针旋转的时候从下往上,顺时针的时候从上往下。参考下图:

其实这样也降低了难度,因为不难看出,顺时针旋转后的row就是逆时针旋转时的column,互相交换了row和column而已。

当然,如果再加以联想,这不就是我们熟悉的乘法运算吗?只不过在计算每一位的时候,都不需要进位,而且在计算纵向(column)面积的时候,要把y轴的数字颠倒过来,321变成123。

如果能理解到这里,这道题的谜题就已经解开80%了。剩下的就是考虑如何用程序来实现。


编程实现:

首先问哥想说,Python虽然语法简洁、易懂易学,但它并不是一门高效的编程语言。相比其他编译型语言来说,Python的CPU、内存占用率相对都要高一些。这也就造成在最后的提交中,最后几道测试一直无法通过。如果大家有优化Python的方法,或者是更好的解法,欢迎给问哥留言。这里先介绍问哥的思路,抛砖引玉。

首先,题目里输入数据的格式为,

  1. 首先从键盘接收一个变量T,用于保存一次测试多少个例子,也表示下面的输入将重复多少次;
  2. 然后再接收两个数字nd,分别表示有n条横向和纵向的道路,d为1表示顺时针旋转45度,为0则表示逆时针旋转;
  3. 最后分别是纵向道路的宽 x1, x2, ... xn,和横向道路的宽 y1, y2, ... yn
  4. 重复T遍步骤2和3。

Python实现框架如下,包括最后的循环输出部分:

T= int(input())
rows = []
columns = []
for _ in range(T):
    n, d = map(int, input().split())
    x = list(map(int,input().split()))
    y = list(map(int, input().split()))
    if d == 0:
        # 编写子程序area()来实现,返回row和column的结果
        rows.append(area(x,y,n)) 
        columns.append(area(x,y[::-1],n))
    else:
        # 如果为1,则为顺时针旋转45度,将参数互换就可以
        rows.append(area(x,y[::-1],n))
        columns.append(area(x,y,n))
for i in range(T):
    print(" ".join(rows[i]))
    print(" ".join(columns[i]))
  1. 首先将输入的字符串格式转成整数型 (这里使用了map()高阶函数,对输入的元素逐个使用int()方法转成整数,再用list()方法转成列表);
  2. 用 x 或 y 的列表保存纵向和横向的道路宽度;
  3. 打算自定义一个函数area(),用来返回旋转后的横向row和纵向column的面积列表;
  4. 如果d不是0,则计算顺时针旋转45度后的结果;
  5. 就像上面分析的,当逆时针旋转45度,计算纵向(column)面积的时候,要把y轴的数字颠倒过来,321变成123,于是列表使用切片取反[::-1];
  6. 同样如同上面分析过的,顺时针和逆时针旋转的时候row和column互换位置。

**值得一提的是,提交答题不需要等全部输入完了再输出,可以一边输入一边输出,所以上面的代码可以不需要rows和columns的列表用来存储每个测试的输出。

接下来我们来实现自定义函数area()的部分,问哥这里想到了三种方法(最后一种方法使用第三方模块numpy):

方法一:

这种方法属于“穷举法”,本身并不值得推荐,放在这里仅仅作为对比和铺垫。

仔细观察,我们想要得到的row和column其实是从二维列表中,“抽取出”相应的列表元素,组成新的列表求和。

比如例子中 x1y1路口的面积相当于读取列表a的元素a[0][0], 第二行x2y1和x1y2的和相当于a[1,0]+a[0,1],以此类推。读取的元素数量递增,但是到达列表长度(本例中为3)后又递减到1(a[2][2])。所以我们可以这样做:

  1. 先创建一个完整的二维列表;
  2. 利用循环得到每组元素组合成新的列表;
  3. 对新列表求和,返回各行各列新列表求和后的结果列表。

 代码实现如下:

def area2(m,n,i):
    a = [0]*(i+1)
    b = [0]*(i+1)
    for j in range(i+1):
        a[j]=(m[j][i-j])
        b[j]=(m[n-1-j][n-1-i+j])
    return (a,b)

def area(m,n):
    r1 = []
    r2 = []
    for i in range(n):
        a,b = area2(m,n,i)
        r1.append(sum(a))
        r2.append(sum(b))
    r = r1 + r2[-2::-1]
    return [str(i) for i in r[::-1]]

其中变量m是完整的二维列表。可以看到,为了精准地使用列表进行切片,组合成新的列表,我又创建了另一个自定义函数area2(),实在是太笨拙了。

问哥把生成二维列表的工作放在了主程序里,因为如果要计算column的乘积之和,需要把二维列表进行倒序翻转。

for _ in range(T):
    n, d = map(int, input().split())
    x = list(map(int,input().split()))
    y = list(map(int, input().split()))
    matrix = [[i*j for i in y] for j in x]
    m = matrix[::-1]
    if d == '0':
        rows.append(area(matrix,n)[::-1])
        columns.append(area(m,n))
    else:
        rows.append(area(m,n))
        columns.append(area(matrix,n)[::-1])

提交后可以通过部分测试,但是因为这种方法占用内存实在是太多了,轻轻松松跑满64M。

其实想想也是,题目规定的范围是在 2×10^{4}以内,而每条路的宽度又在 5×10^{4} 以内,这样全部乘下来,二维列表的体积将十分庞大。于是这种方法不可取。

方法二:

这也是问哥首先想到的方法,先实现类似下图的矩阵,十分和百分位的位置用0填充,然后在矩阵里对每一列求和。

但是如果这样做需要两步,第一步创建所有乘积的一维列表,如[1, 2, 3], [2, 4, 6], [3, 6, 9],再对每个列表的空位填充0组成二维矩阵。但是这样做未免有些繁琐,而且填充0的部分,问哥没有想到好的办法。所以我使用了累加的方法。

因为最终结果的数字列表长度是固定的(2n-1),所以我先创建一个全0的列表a,用来累加和保存最终结果,再创建一个全0的临时列表b,用来循环计算每次乘积,然后和a累加,再清零重新开始下一个循环。

程序实现如下:

def area(x, y, n):
    length = 2*n - 1
    a = [0] * length
    for i in range(n):
        b = [0] * length
        for j in range(n):
            b[i+j] = y[i]*x[j]
        a = [a[k]+b[k] for k in range(length)]
    return a

可是写完之后才发现这样的循环累加里,由于列表a和b进行累加的位置相同,列表b其实并不需要。

 于是把列表b删去,代码精简如下:

def area(x, y, n):
    a = [0] * (2*n - 1)
    for i in range(n):
        for j in range(n):
            a[i+j] += y[i]*x[j]
    return a

同样我们可以使用字典实现同样的事情,因为最终的列表长度是固定的,可以把索引值作为键,累加的数据作为值,最后只要把字典的值作为列表返回即可。

def area(x, y, n):
    a = {}
    for i in range(n):
        for j in range(n):
            a[i+j] = a.get(i+j, 0) + y[i]*x[j]
    a = [str(i) for i in a.values()]
    return a

此外,因为要不断的调用索引值,我们也可以使用另一个内置函数enumerate()。

def area(x, y, n):
    a = [0] * (2*n - 1)
    for i1,j1 in enumerate(y):
        for i2,j2 in enumerate(x):
            a[i1+i2] += j1*j2
    return a

**因为最后要输出到屏幕,所以需要将结果用str()转成字符串,这里暂且略过。

测试下来,功能也没有问题。但不幸的是,最后几个测试点全部超时(未能在2000ms内完成)。而且使用列表、字典、内置函数enumerate()的表现稍有差异。

使用字典的表现是最差(最慢)的,这倒是令我很惊讶,因为按道理python的字典是哈希散列表,读取的效率应该是O(1)。猜测可能是因为字典是在累加过程中不断创建新键,而列表是在开始累加之前就已经把位置预留好了。

排在第二的是单纯地列表累加,速度比字典稍微快几毫秒到几十毫秒:

表现最好的是使用Python的内置函数enumerate(),而且在极限情况下走运“擦边”通过了第4个测试(1,965ms)。猜测可能是因为enumerate()函数一次性把列表的索引和值调了出来,不需要再在循环里通过列表索引去读取元素值,所以效率会稍微高一些。

 但似乎代码已经没有优化的余地了,也许只能另谋出路。

方法三

如果大家学过高等数学,或者离散数学,也许听说过一个概念叫“卷积”。其实我们刚才在分析题目的时候列出竖式运算乘法表达式,也许就有同学能够反应过来了,这不就是卷积吗?例子中的计算也可转换成如下图的多项式的乘法,而我们所要的结果就是多项式乘法运算后的每项系数

至此,可以明白这道题在考什么了。化繁为简,去掉那些大段的故事性描述,这道题其实考的就是两个大数相乘的卷积运算

问哥高数学得不好,对于卷积也是一知半解,再多解释就露馅了。把它看成多项式的乘法可能更好理解一些。

但是问题来了,我们在方法二里已经用python完成了这样的运算,但还是由于超时而无法通过最后两项测试,还有什么办法可以优化呢?问哥也不知道,也欢迎知道怎样做的小伙伴给问哥留言,问哥虚心、真心请教。

但是问哥知道,使用第三方模块,比如 Numpy 可以轻松完成卷积的计算。

于是本例的代码,如果使用Numpy的话,一条语句就可以完成(甚至都用不上表示列表长度的参数n):

import numpy as np

def area(x, y, n):
    return [str(i) for i in np.convolve(x,y)]

而且使用Numpy的另一个多项式计算函数poly1d()也可以轻松实现:


尾声:

关于本谜题的讨论也差不多就到这里了。遗憾的是,提交答案的测试环境没有安装Numpy,所以无法使用。而如果不用第三方模块,问哥实在是找不到性能更好速度更快的Python方法了。毕竟Python作为一门解释型语言,执行效率比编译型语言确实要差许多。如果你有更好的办法,欢迎留言讨论。

但是对于这个谜题,可以说已经被我们解开了。问哥也尽可能详尽如实地把思路过程——包括弯路——展现出来,希望能对大家有所帮助。

感谢你们读到这里,下次再见!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我问哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值