1015 德才論 (25分) Python3 無超時優化過程

今天來個吹牛的題,此題在不管你是Google還是百度又或是Git上都無法查到Python3的解題答案。

此題據說公司的高人也否定了此題。認爲Python效率問題導致超時case很難過。
So,所以才能吹牛啊~

事先聲明牛是要吹的。不過此題最終能做出來,是因爲我的領導一步步提供思路才能做出來!所以可以說此題可能除了他,全網無解~牛批!

先來說下這個題:

德才論 是用Python3解 PAT 題目時,第一個遇到的真正難題。難點在於常規寫法,總有三個case總是超時~

此題根據輸入將考生分爲才德全盡德勝才德勝才其他,總分相同時,按其德分降序排列;若德分也並列,則按准考證號的升序輸出。但是考生數量爲 <= 100000。所以當計算和排序時,因爲數量太大,導致了3個超時~

下面是原題:

德才論

宋代史學家司馬光在《資治通鑑》中有一段著名的“德才論”:“是故才德全盡謂之聖人,才德兼亡謂之愚人,德勝才謂之君子,才勝德謂之小人。凡取人之術,苟不得聖人,君子而與之,與其得小人,不若得愚人。”

現給出一批考生的德才分數,請根據司馬光的理論給出錄取排名。

輸入格式:

輸入第一行給出 3 個正整數,分別爲:N(≤10^5),即考生總數;L(≥60),爲錄取最低分數線,即德分和才分均不低於 L 的考生纔有資格被考慮錄取;H(<100),爲優先錄取線——德分和才分均不低於此線的被定義爲“才德全盡”,此類考生按德才總分從高到低排序;才分不到但德分到線的一類考生屬於“德勝才”,也按總分排序,但排在第一類考生之後;德才分均低於 H,但是德分不低於才分的考生屬於“才德兼亡”但尚有“德勝才”者,按總分排序,但排在第二類考生之後;其他達到最低線 L 的考生也按總分排序,但排在第三類考生之後。

隨後 N 行,每行給出一位考生的信息,包括:准考證號 德分 才分,其中准考證號爲 8 位整數,德才分爲區間 [0, 100] 內的整數。數字間以空格分隔。

輸出格式:

輸出第一行首先給出達到最低分數線的考生人數 M,隨後 M 行,每行按照輸入格式輸出一位考生的信息,考生按輸入中說明的規則從高到低排序。當某類考生中有多人總分相同時,按其德分降序排列;若德分也並列,則按准考證號的升序輸出。

輸入樣例:

14 60 80
10000001 64 90
10000002 90 60
10000011 85 80
10000003 85 80
10000004 80 85
10000005 82 77
10000006 83 76
10000007 90 78
10000008 75 79
10000009 59 90
10000010 88 45
10000012 80 100
10000013 90 99
10000014 66 60

輸出樣例:

12
10000013 90 99
10000012 80 100
10000003 85 80
10000011 85 80
10000004 80 85
10000007 90 78
10000006 83 76
10000005 82 77
10000002 90 60
10000014 66 60
10000008 75 79
10000001 64 90
時間限制: 400 ms
內存限制: 64 MB
代碼長度限制: 16 KB

解題過程

因爲本人計算機基礎比較渣,可能描述的不夠準確。畢竟大學連計算機原理跟高數都沒學過的人==!

根據題目按部就班的實現

正常解法但是超時
# -*- coding: utf-8 -*-
n = input().split()  # 接收第一行輸入的三個參數
# 學生人數,最低分,最高分
N, L, H = int(n[0]), int(n[1]), int(n[2])
# 存放四類人才
l1, l2, l3, l4 = [], [], [], []
for i in range(N):
    info = input().split()  # 接收考生信息
    moral_score = int(info[1])  # 德分
    talent_score = int(info[2])  # 才分
    if moral_score >= L and talent_score >= L:
        if moral_score >= H and talent_score >= H:
            l1.append(info)
        elif H <= moral_score:
            l2.append(info)
        elif talent_score <= moral_score:
            l3.append(info)
        else:
            l4.append(info)

# 當某類考生中有多人總分相同時,按其德分降序排列;若德分也並列,則按准考證號的升序輸出
# 排序規則
order = lambda x: (-int(int(x[1]) + int(x[2])), -int(x[1]), int(x[0]))

l1 = sorted(l1, key=order)
l2 = sorted(l2, key=order)
l3 = sorted(l3, key=order)
l4 = sorted(l4, key=order)
result = l1 + l2 + l3 + l4
print(len(result))
for i in result:
    print(f'{i[0]} {i[1]} {i[2]}')

此代碼運行結果如最開始的運行結果圖,肯定會三個超時,如果運氣足夠好,可能是兩個超時~

第一個優化點

優化接收參數過程。將input() 接收參數,此處改爲 sys.stdin.readline()

  1. 在python3下兩者性能差異已經不大,讀取大量數據時,sys.stdin.readline 會快不少
  2. input 會默認將每行最後的換行符刪除掉,有一定的開銷。等同於 sys.stdin.readline.strip()
  3. 根據測算 sys.stdin.readlinesys.stdin.readline.strip() 快20% (100萬數據)
  4. 根據測算 sys.stdin.readlineinput 10倍(100萬數據)

print 改成 sys.stdout.write 思路類似。

第二個優化點

N, L, H = int(n[0]), int(n[1]), int(n[2]) 因爲只運行一次,所以不考慮 int()的開銷。
for 循環裏,將德分和才分轉換成了 int 類型。這個轉換是需要轉 N*2 次。如果我們轉換 200000 次,那麼開銷大概是 0.03 秒~
如果將這個過程改成從字典裏取值,那麼性能提升大概 53% ~ 70%!!!

看效果

import timeit

print(timeit.timeit('int("1")', number=200000))
print(timeit.timeit('a = {"1": 1};a.get("1")', number=200000))
0.029694400000000003
0.018485100000000004

所以要定義一個大字典 key 是字符串類型的 0 ~ 100valueint 類型的 0 ~ 100
可以程序運行時生成,score_dict = dict(zip([str(i) for i in range(101)], range(101))) ,但是爲了節省時間,直接寫死一個字典。

第三個優化點

在判斷考生屬於哪類人時,沒想到有什麼可以優化的,需要注意的是,因爲不知道case樣本是什麼樣,所以,一上來就先判斷得分跟才分是不是高於最低分,好像並不一定能節省時間~

優化存儲信息結構(重點)
  1. 定義一個三維列表,列表的索引爲學生德才總分,所以列表長度爲 0 ~ 200
  2. 已知總分相同,德分排序,所以在索引位置創建一個長度 0 ~ 100 的列表。使用時,依然採用分數當索引。
  3. 最裏層的列表存放德分相同的准考證號。
  4. 數據存儲完成後,也就完成了排序,且無 append() 操作。

這樣的好處在於提前開闢好一個已知大小的空間去存放可能出現的數據,減少列表的 append 操作,append操作本身也是比較耗時的。

之前代碼 l1, l2, l3, l4 = [], [], [], [],就改成了

l1 = [[[] for _ in range(101)] for _ in range(201)]
l2 = [[[] for _ in range(101)] for _ in range(201)]
l3 = [[[] for _ in range(101)] for _ in range(201)]
l4 = [[[] for _ in range(101)] for _ in range(201)]

題目要求,先輸出達標的人數,然後輸出詳細信息。
這裏需要注意,如果輸出時,如最開始代碼那樣,print實際上是把所有數字都轉換成了str,再進行輸出,又因爲輸入信息與輸出信息結構相同,所以最好的辦法是直接輸出完整的學生信息。

在這裏創建一個字典,key 爲準考證號,value 與輸入一致的一行考生信息。

這樣遍歷多維列表時,最裏層的准考證號取出來後,直接去字典裏取完整的考生信息輸出。

第四個優化點

這個點的優化直接從城鄉偶爾超時到基本無超時。
將代碼扔到定義的函數中去執行,提升很大。

import time

start = time.time()
a = []
for i in range(100000):
    a.append(i)
print(time.time() - start)

start = time.time()
def test():
    a = []
    for i in range(100000):
        a.append(i)
test()
print(time.time() - start)
# output
# 0.031200170516967773
# 0.015599727630615234
# -*- coding: utf-8 -*-
import sys


def print_result(l):
    for total in l[::-1]:
        if total:
            for d_score in total[::-1]:
                if d_score:
                    for k in sorted(d_score):
                        sys.stdout.write(student.get(k))


def set_result(list, total, d_score, card_no):
    if list[total] == 0:
        list[total] = [0] * 101
        list[total][d_score] = [card_no]
    elif list[total][d_score] == 0:
        list[total][d_score] = [card_no]
    else:
        list[total][d_score].append(card_no)


score_dict = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
              '10': 10, '11': 11, '12': 12, '13': 13, '14': 14, '15': 15, '16': 16, '17': 17,
              '18': 18, '19': 19, '20': 20, '21': 21, '22': 22, '23': 23, '24': 24, '25': 25,
              '26': 26, '27': 27, '28': 28, '29': 29, '30': 30, '31': 31, '32': 32, '33': 33,
              '34': 34, '35': 35, '36': 36, '37': 37, '38': 38, '39': 39, '40': 40, '41': 41,
              '42': 42, '43': 43, '44': 44, '45': 45, '46': 46, '47': 47, '48': 48, '49': 49,
              '50': 50, '51': 51, '52': 52, '53': 53, '54': 54, '55': 55, '56': 56, '57': 57,
              '58': 58, '59': 59, '60': 60, '61': 61, '62': 62, '63': 63, '64': 64, '65': 65,
              '66': 66, '67': 67, '68': 68, '69': 69, '70': 70, '71': 71, '72': 72, '73': 73,
              '74': 74, '75': 75, '76': 76, '77': 77, '78': 78, '79': 79, '80': 80, '81': 81,
              '82': 82, '83': 83, '84': 84, '85': 85, '86': 86, '87': 87, '88': 88, '89': 89,
              '90': 90, '91': 91, '92': 92, '93': 93, '94': 94, '95': 95, '96': 96, '97': 97,
              '98': 98, '99': 99, '100': 100}
n = sys.stdin.readline().split()
N = int(n[0])
Low, High = score_dict[n[1]], score_dict[n[2]]
student = {}


def main():
    l1, l2, l3, l4 = [0] * 201, [0] * 201, [0] * 201, [0] * 201
    for i in range(N):
        input_ = sys.stdin.readline()
        info = input_.split()
        card_no = info[0]
        d_score = score_dict[info[1]]  # 德分
        c_score = score_dict[info[2]]  # 才分
        total = d_score + c_score

        if d_score >= High and c_score >= High:
            set_result(l1, total, d_score, card_no)
            student[card_no] = input_
        elif d_score >= High and c_score >= Low:
            set_result(l2, total, d_score, card_no)
            student[card_no] = input_
        elif d_score >= c_score and c_score >= Low:
            set_result(l3, total, d_score, card_no)
            student[card_no] = input_
        elif d_score >= Low and c_score >= Low:
            set_result(l4, total, d_score, card_no)
            student[card_no] = input_

    print(len(student))
    print_result(l1)
    print_result(l2)
    print_result(l3)
    print_result(l4)


main()

優化後的代碼提交基本不會有超時~

在這裏插入圖片描述

如果有興趣可以自己做一下試試看,代碼中還有一些小細節,就不挨個說了。因爲我也說不好~

大晚上寫了這麼多,你又那麼好看,給波關注唄~
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章