bangye wu
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.

      Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

      Explore these features while you wait
      Complete general settings
      Bookmark and like published notes
      Write a few more notes
      Complete general settings
      Write a few more notes
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note No publishing access yet

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.

    Your account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Your team account was recently created. Publishing will be available soon, allowing you to share notes on your public page and in search results.

    Explore these features while you wait
    Complete general settings
    Bookmark and like published notes
    Write a few more notes
    Complete general settings
    Write a few more notes
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # AP325-Python 第4章 貪心演算法與掃瞄線演算法 「貪心通常比較簡單,但多半不能解決問題,在演算法與人生中都是這樣。」 孔子說:「及其老也,血氣既衰,戒之在得。」日常生活中最常看到貪的狀況就是政治人物,而想到政治人物就想到金庸的笑傲江湖。這一章的例題習題部分就以笑傲江湖中的人物來串場,不過不必擔心,沒看過故事不影響解題。 --- 這一章介紹貪心演算法(Greedy algorithm)以及掃描線演算法(sweep-line algorithm),這兩種演算法有點類似,所以放在同一章。這裡的貪心沒有負面的意思,單純指以當前最有利的方式做決定。貪心演算法通常很直覺,所以寫程式往往簡單,但不是對所有問題都能夠找到最佳解。Greedy通常需要證明,但上機考試與競賽的場合,不需要證明,所以很多人不知道怎麼做的時候就用Greedy寫下去猜猜看,考試本來就是該猜就要猜,不過貪心也要貪的正確。 掃描線演算法的名字出自計算幾何,是指解題過程想像用一條掃描線沿著某個方向掃描過去,他是一個很好的思考模式,很多經典的分治演算法都有掃瞄線的版本。有一些數列的問題往往可以看成數線上的問題,所以也可以用掃描線的方式來思考。 貪心演算法與掃描線演算法通常需要搭配排序或優先佇列(priority_queue,常簡稱PQ),我們在第二章介紹過排序了,所以這裡不再重複。優先佇列是一種重要的資料結構,在大部分資料結構的課本中都有介紹自製的方法(heap)以及應用,它在Python中也有庫存函數可以呼叫,但它應該不能算「常用簡單」的資料結構,所以在APCS考試中應該不會出現非用它不可的題目,我們會對PQ做簡單的介紹。 ## 基本原理 所謂貪心演算法指的是:**一個算法由一連串的決定組成,每次決定時都以當時最有利的方式挑選,而且一旦挑選後就不再更改**。我們先以舉一個很簡單的例子來說明。 --- ##### P-4-1. 少林寺的代幣 令狐沖去少林寺觀光,少林寺有四種代幣,面額分別是$(1, 5, 10, 50)$,令狐沖拿了 $n$ 元去換代幣,請問以這四種代幣湊成 $n$ 元,最少要多少枚代幣? Time limit: 1秒 輸入格式:第一行是一個正整數 $m$,代表有幾筆測資,以下有 $m$ 行,每行是一筆測資,每筆測資是一個正整數 $n$,$n<1000$。 輸出:依序每一行輸出每一筆測資的最少的代幣數量。 範例輸入: 3 21 50 123 範例結果: 3 1 7 說明:$21 = 10\times 2 + 1$, $50 = 50\times 1$, $123 = 50\times 2 + 10\times 2 + 1\times 3$。 --- 因為代幣的數量希望要最少,直覺貪心的想法就是要盡量使用面額大的硬幣,代幣由大面額的往小的考慮,能換盡量換,剩下不足的再考慮比較小的面額。以下是這樣的程式。 ```python= # P-4-1 num of coin, greedy m = int(input()) coin = [1,5,10,50] for i in range(m): n = int(input()) num = 0 for c in coin[::-1]: # large to small num += n//c # num of coin c n %= c # remaining print(num) # ``` 程式寫起來很簡單,簡單到如果考試碰到這種題目,會懷疑是不是有陷阱。這個方法很直覺,但怎麼知道它是對的呢?有些人可能會說:就很直覺啊,路邊隨便找個少林寺掃地的也會這樣做。 那麼請看看下面的狀況:假設代幣的面額是$(1, 4, 5)$三種,上面的貪心方法會找到正確答案嗎?假設$n=8$,貪心的方法會先換一個5元,然後剩下3元就換3個1元代幣,所以8元換了4個代幣。但是如果拿兩個4元代幣就可以湊8元了,所以正確答案應該是2而不是4。 請不要懷疑人生,P-4-1的做法是對的,只是並不是能用在任何代幣面額的狀況,我們必須證明。如何證明呢? 在代幣是$(1, 5, 10, 50)$的情形下,如果1元的代幣總額不小於5,一定可以拿出5個換成一個5元,所以1元代幣的個數不會超過4個;同樣的你可以證明5元代幣不會超過1個,10元代幣不會超過4個。也就是說,對任意第$j$種代幣$coin[j]$,小於$coin[j]$的代幣總額不會超過$coin[j]$,所以Greedy是對的。在$(1, 4, 5)$的狀況,就沒有這個性質了。順便一提,不要小看少林寺的掃地僧,掃地僧往往是最厲害的。 這個例子中我們看到了貪心法的一些特質。 * 算法由一連串決定組成(由大到小逐一考慮各種代幣的數量),每次考慮當前最好決定(能換盡量換),選擇後就不再更改。 * 算法的正確性必須經過證明,內容就是要證明所下的決定是對的,也就是一定有一個最佳解包含了貪心法所做的決定,而證明的方法通常都是用換的:假設有一個最佳解跟貪心法做的決定不一樣,那麼,我們可以換成一個與貪心法一樣的決定,而解會更好,或至少不會變差。 在下一節中我們會看到更多的例子。貪心算法往往都是要不斷的找最大值或最小值,如果資料不會變動,可以在開始的時候做一次排序,如果資料是會變動的呢?就必須用一個動態的資料結構來處理,以下我們對優先佇列做一點介紹。 ### 優先佇列 (priority_queue) (\*) 第三章介紹過佇列,佇列跟現實生活中排隊的狀況一樣,是先進者先出,但更貼近現實生活中的狀況是有些事情後發生的卻必須先處理,因為緊急或是更重要。 優先佇列就是具有優先權的排列,不管進入的順序,優先權高的會排在前面。Python標準函數庫中的heapq提供了PQ基本操作函數,資料結構的課本中也都有教以heap來實作,heap是一個完全二元樹(complete binary tree),但實作在陣列中,也就是心中有樹但程式中沒有樹。heap的實作並不算困難,但考試與比賽時間寶貴,通常能使用內建的就不要自己寫,因此本書中並不介紹heap的實作,有興趣的可以人可以自己查詢網路或書籍。 Python的heapq說明可以參考[heapq --- 堆積佇列 (heap queue) 演算法](https://docs.python.org/zh-tw/3/library/heapq.html),使用時自己準備一個list,然後呼叫它所提供的函數來對list進行操作。常用操作的時間複雜度如下,其中$n$表示成員的數量,也就是佇列的大小: * 加入新元素(push),$O(\log n)$。 * 查詢最小值(top),$O(1)$。 * 刪除最小值(pop),$O(\log n)$。 * 檢查是否為空(empty),$O(1)$。 請注意刪除與查詢只能針對最小值,若要改變大小順序,最簡單的方法就是改變key的正負號。以下的範例程式介紹基本用法,說明請見程式內註解。 ```python= import heapq h1 = [] # initial empty list as heap heapq.heappush(h1, 5) # push, O(logn) for v in [4,1,5]: heapq.heappush(h1, v) # print(h1,"min is ",h1[0]) # min element at [0] q = heapq.heappop(h1) # pop min to q, O(logn) print("previous min =",q, "current min =",h1[0]) print("current size:",len(h1)) # size of heap O(1) if len(h1): print('not empty') # check if empty O(1) else : print('empty') a = [2,-4,-5,-4,8] # initial from a list heapq.heapify(a) # initial a nonempty heap, O(n) print(a,"min =",a[0]) # min at top a.sort() # can also initialized by sort but O(nlogn) # to make a max-heap, using -v for b = [-x for x in a] print("minus value of a =",b) heapq.heapify(b) imax = -heapq.heappop(b) # original max in a print("max of a is",imax) # element can be list a = [2,-4,-3,-4,8] pa = [[v,i] for i,v in enumerate(a)] #[value,index] print(a); print(pa) heapq.heapify(pa) v,i = heapq.heappop(pa) print("min of a is a[",i,"] = ",v) ``` 輸出結果如下: ``` [1, 5, 4, 5] min is 1 previous min = 1 current min = 4 current size: 3 not empty [-5, -4, 2, -4, 8] min = -5 minus value of a = [5, 4, 4, -2, -8] max of a is 8 [2, -4, -3, -4, 8] [[2, 0], [-4, 1], [-3, 2], [-4, 3], [8, 4]] min of a is a[ 1 ] = -4 ``` ## 例題與習題 這一節我們來看貪心與掃瞄線演算法的題目,我們將題型區分為以下幾類,每一類挑選一些經典題來說明,另外在下一節有一些其他的補充習題: * 單欄位資料排序 * 多欄位資料排序 * 以PQ處理動態資料 * 貪心搭配二分搜 * 掃描線演算法 很多貪心算法的重點在證明,但本書以程式實作為主,不以證明為重點,所以有些題目的證明只簡略說明重點,或者請讀者上網路查詢。 ### 單欄位資料排序 --- ##### P-4-2. 笑傲江湖之三戰 笑傲江湖的遊戲中,你扮演令狐沖的角色,因為「長恨此身非我有,何時忘卻盈盈」,所以你率領任我行等魔教高手前往少林寺搭救任盈盈,欲知故事原委可參見笑傲江湖第27回「三戰」。 套句丸尾班長的口頭禪,總而言之,少林寺與五嶽劍派等正教派出$n$位高手,而你方也有$n$位高手,每個人都有一個戰力值。雙方要進行一對一的對戰,每個人不管輸或贏都只能比一場,假設兩人對戰時,戰力值高的獲勝。對於對方的每一位高手,你可以指定派哪一位與其對戰,為了解救盈盈,你希望贏越多場越好,平手則沒有幫助。請計算出最多能贏多少場。 Time limit: 1秒 輸入格式:第一行是一個正整數$n$,第二行有$n$個非負整數是對方的戰力,第三行有$n$個非負整數是我方的戰力,同行數字間以空白間格。$n$與戰力值不超過1e5。 輸出:輸出最多可能的勝場數。 範例輸入: 5 3 1 7 0 2 8 3 5 0 0 範例結果: 3 --- 我們只要關心可以戰勝的那些場次需要誰去出戰,其他場次贏不了,就在剩下的人裡面隨便挑選去應戰。假設我方目前最弱的戰力是$a$而對方最弱的是$b$,如果$a \le b$,則$a$對誰都不會贏,是沒用的,可以忽略。否則$a > b$,我們宣稱「一定有一個最佳解是派$a$去出戰$b$」,證明如下: 假設在一個最佳解中,$a$對戰$x$而$y$對戰$b$,我們將其交換改成「$a$對$b$而$y$對$x$」。根據我們的假設$y \ge a$,交換後$a$可以贏$b$而$y$對$x$的戰績不會比$a$對$x$差,所以交換後勝場數不會變少。 既然如此,我們可以確定派$a$對戰$b$一定可以得到最佳解,並將$a$與$b$移除後,繼續依照上述原則挑選。 範例程式如下,因為要不斷的挑選最小值,而戰力不會改變,所以是個靜態的資料。我們可以將雙方戰力先進行一次排序,然後依序由小往大考慮即可。 ```python= # P_4_2, one-on-one O(nlogn), sorting+greedy n = int(input()) enemy = [int(x) for x in input().split()] ours = [int(x) for x in input().split()] enemy.sort() ours.sort() win = 0 # num of win ei = 0 # index of weakest enemy for p in ours: # each of our power if p > enemy[ei]: # can win some enemy win += 1 ei += 1 # next enemy # otherwise, p is useless, do nothing # print(win) ``` 這一題也可以由大到小考慮,基本原則是能贏的贏一點就好,當然可以用優先佇列(PQ),在範例程式中有以PQ寫的方式,做法簡單,我們就不在此說明,有興趣練習PQ的可以自行參考。 --- ##### P-4-3. 十年磨一劍 (最少完成時間) 人們常用說「十年磨一劍」來比喻下的功夫深,但是這句話對於華山磨劍坊就不適用了,因為要磨劍的客人非常的多,一把劍如果磨太久,客人等待的時間過長是會被客訴的。華山磨劍坊目前有$n$筆訂單,每筆訂單要磨一把劍,每把劍所需的時間不盡相同。磨劍師傅每次只能磨一把劍,現在希望每一筆訂單的完成時間之總和能夠最小,希望能找出最好的磨劍順序。 舉例來說,如果有四把劍,磨劍需要的時間分別是(3,1,3,4),如果以(3,1,3,4)的順序來磨,第一把的完成時間是3,第二把完成時間是3+1=4,第三把是3+1+3=7,第四把是3+1+3+4=11,總和是3+4+7+11=25。如果把順序改成(1,3,3,4),那麼完成時間分別是(1,4,7,11),總和是23,這是這一題最好的解。 Time limit: 1秒 輸入格式:第一行是一個正整數$n$,第二行有$n$個正整數是每一把劍需要的時間,同行數字間以空白間格。$n$與每把劍的時間都不超過1e5。 輸出:輸出最小的完成時間總和。 範例輸入: 4 3 1 3 4 範例結果: 23 --- 有$n$個工作要安排一個順序後依序完成,希望每個工作的完成時間總和最小,這是個經典的minimum completion time問題。直覺上短的工作要先做,放在越前面的工作,等它的人越多。如果把計算時間的方式仔細看一下,你會發現如果$job[i]$是第$i$個工作所需的時間,那麼完成總時間是 $\sum_{i=0}^{n-1} (n-i)\times job [i]$, 所以shortest-job-first是對的。另外一個證明方法是假設$job[i]$的順序是最佳解而存在某個工作$job[i]>job[i+1]$,那麼,我們交換這兩個相鄰工作,其他的工作的完成時間不會改變,但這兩個工作的完成時間總和會減少,如此可知,最佳解中不可能大的排在小的前面。這種考慮相鄰交換的情形是個證明的技巧,因為其它的工作不會改變,可以得到很簡單的證明。 程式非常簡單,排序後計算就好了,但是不要把計算寫成$O(n^2)$的笨方法了,運用prefix-sum的手法就很容易在排序後以$O(n)$完成計算,另外一個方法是用上面的公式。 ```python= # P_4_3, min completion time, sorting+greedy n = int(input()) job = [int(x) for x in input().split()] job.sort() # shortest job first curr = 0 # current completion time total = 0 # total completion time for t in job: curr += t # completion time of i total += curr # total completion time # print(total) ``` ### 多欄位資料排序 在上面兩個例題中,每個資料都只是一個數字,接下來我們要看需要資料具有多欄位(項目)的狀況。在第二章我們介紹排序時,說明過python中list與tuple是以字典順序比較大小,以及sort()中使用key的方法,如果不熟悉的人,請再次參考第二章的方法。 下面這一題是很有名的經典題 **Activity selection problem**。 --- ##### P-4-4. 幾場華山論劍 (activity selection) 自從華山論劍聲名大噪之後,想參加的人絡繹不絕,因此華山派決定每年都舉辦非常多場的華山論劍,每一場都有自己的開始時間與結束時間,參加者必須全程在場,所以不能在同一時間參加兩場。 令狐沖拿到了今年所有場次的資料,希望能參加越多場越好,以便盡速累積經驗值,請你幫忙計算最多可以參加幾場。請注意,所挑選活動必須完全不重疊,兩場活動只在端點重疊也是不可以同時參加的,也就是說**前一場的結束時間必須小於下一場的開始時間**。 Time limit: 2秒 輸入格式:第一行是一個正整數$n$,代表一共舉辦了$n$場華山論劍,接下來$n$行,每行兩個非負整數$s$與$t$,代表這場活動的時間區間是$[s,t]$,兩數字間以空白間格。$n$不超過1e5,$s < t < 1e9$。 輸出:最多參加幾場活動。 範例輸入: 4 1 4 0 3 3 4 4 6 範例結果: 2 說明:可以挑選[0,3]與[4,6]兩場活動。 --- 這個題目簡單說就是:數線上有若干線段,要挑選出最多的不重疊的線段。第一個想法可能是盡量挑短的,但是範例中有一個反例,挑短的不是正確答案。我們宣稱下面這個的性質: **「若$[x,y]$是所有現存線段中右端點最小的,那麼一定有個最佳解是挑選了$[x,y]$。」** 證明:假設最佳解沒有挑$[x,y]$,令$[a,b]$是最佳解中右端點最小的。根據我們的假設,$y \le b$,因此將$[x,y]$取代$[a,b]$不會重疊到任何最佳解中的其他線段,我們可以得到另外一個包含$[x,y]$的最佳解。 根據此性質,我們可以不斷地將最小右端的線段挑選進解答,然後略過任何與它重疊的線段。因為線段不會改變,一開始依照右端由小排到大,然後從頭到尾掃描一次就可以得到答案。 以下是範例程式,在此程式中我們用一個list $[start,finish]$來表示一個活動的起始與結束時間,排序時用 key=lambda x: x[1] 來定義排序的key值是一個元素$x$的第1個欄位$x[1]$,也就是結束時間,對排序lambda函數不熟悉的請複習第二章。 在排序後,以一個迴圈從頭到尾掃描一次,以變數endtime保持著前一個已挑選活動的結束時間,初值給 -1 因為輸入的時間皆為非負,迴圈內以if來判斷開始時間是否大於endtime,若是,挑選起來,修改答案與endtime;若否,可以直接丟棄它,不做任何事。 ```python= # P_4_4,activity selection, sorting+greedy n = int(input()) act = [] # [start,finish] for i in range(n): act.append([int(x) for x in input().split()]) act.sort(key=lambda x:x[1]) # sort by finish time endtime = -1 # last finished time total = 0 # num of selected activity for start,finish in act: if start > endtime: # compatible, select it total += 1 endtime = finish # else do nothing # print(total) ``` 如果因為不熟悉排序時下key的方式,可以將要比較的值放在每一個元素的第0個欄位,因為每個元素是list,兩個list比較時是先比第0個欄位的字典順序,所以會依照我們的需要的方式排序而省略寫key函數。在本題中,我們希望以右端點結束時間排序,我們就將右端點放在前面就行了。 以下是範例程式,大致上與前一支相同,在第6行讀入每一個活動的起始與結束時間時,我們以結束時間在前的順序放入$act$中,第8行排序時間就沒有給key了。留意在第11行歷遍時,每一筆資料取出的是finish,start,結束時間在前。 ```python= # P_4_4,activity selection, sorting+greedy # without key lambda n = int(input()) act = [] for i in range(n): s,t = [int(x) for x in input().split()] # [start,finish] act.append([t,s]) # finish at first act.sort() # sort by finish time endtime = -1 # last finished time total = 0 # num of selected activity for finish,start in act: if start > endtime: # compatible, select it total += 1 endtime = finish # else do nothing # print(total) ``` 本題中我們只需安排欄位的順序,但利用這個方式來避免寫key函數有的時候會多花記憶體,例如要比較的是幾個欄位運算的結果(例如下一題),如果不下key,就變成要在每筆資料前方多開一個欄位,所以並非一定可行。 在P-4-3十年磨一劍的例題中,每一件磨劍工作,無論需要時間的長短,重要性都是一樣的,我們要求的只是完成時間的總和最小化,下面是一個變化題,每一個工作延後的代價不同。 --- ##### P-4-5. 嵩山磨劍坊的問題(加權最小完成時間) 嵩山磨劍坊接了$n$筆磨劍工作,磨劍師傅每次只能磨一把劍。除了每把劍各有所需的時間之外,每件工作的重要性也不同。假設第$i$筆訂單需要的時間是$t[i]$,而重要性是$w[i]$。磨劍坊的計價方式是:每件工作都已經先收了一筆款項,假設第$i$筆訂單在時間$f$時完成,則需要扣款$f\times w[i]$,現在希望將$n$筆磨劍工作安排最好的順序,使得扣款總額越小越好。嵩山派掌門左冷禪是非常嚴厲的老闆,希望你能幫磨劍師傅找出最好的順序以免他遭到處罰。 舉例來說,如果有四把劍,磨劍需要的時間分別是$t=(1,4,5,6)$,而重要性依序是$w=(1,3,4,7)$。如果依訂單編號順序$(1,2,3,4)$來磨,也剛好是工作時間由短到長的順序,每件工作的完成時間依序是$(1,5,10,16)$,扣款總額是$1\times 1 + 5\times 3 + 10\times 4 + 16\times 7 = 168$。如果依照訂單編號順序$(4,1,3,2)$ 來磨,則$t$與$w$重新排列後分別是$t=(6,1,5,4)$,$w=(7,1,4,3)$。完工時間是$(6,7,12,16)$,扣款總額是$6\times 7 + 7\times 1 + 12\times 4 + 16\times 3 = 145$。這是這一題24種排列中最好的解。 Time limit: 2秒 輸入格式:輸入的第一行工作數$N$,$N<1e5$。第二行有$N$個正整數,依序是各訂單所需時間$t[1],t[2],\ldots,t[N]$。第三行有$N$個非負整數,依序是各訂單的重要性$w[1],w[2],\ldots,w[N]$,時間與重要性皆不超過1000,相鄰以空白間隔。 輸出:輸出最小的扣款總額。答案不超過1e18。 範例輸入: 4 1 4 5 6 1 3 4 7 範例結果: 145 --- 如果每個工作(訂單)的重要性(w)都是一樣的話,這一題跟P-4-3就是一樣的,答案就是依照工作時間以Shortest-job first的方式排列,但是在有權重的狀況下,題目範例中就告訴你時間短的不一定在前面。我們再想想看,只看時間的話,時間短的應該排前面;那麼如果只看權重的話呢?權重大的應該排前面,因為延遲的話罰得重。但是兩個都要考慮的話,就必須兩個因子都列入考慮。 我們宣稱以下特性:**最佳解中,排在前面的,$t/w$值一定比較小**。證明?跟P-4-3類似,考慮排在相鄰的兩個工作,假設$t[i]/w[i] > t[i+1]/w[i+1]$。我們交換兩工作,其他的工作完成時間不會改變,而$i$延後了$t[i+1]$的時間,$i+1$則提早了$t[i]$的時間,因此變化量為$w[i]\times t[i+1] - w[i+1]\times t[i] < 0$,也就是解會變好。 程式的寫法不難,但是需要兩個欄位相除後的結果來排序。這裡要提醒,相除的結果是個浮點數,在類似的情形下也許有精準度的問題。以這一題的範圍,1~1000,不至於造成誤差。第二個問題,分母是否有可能是0?這一題是會。所以需要特殊處理一下,處理的方式是若$w=0$,則將key設為無窮大(本題1001就夠),也可以先把$w=0$的都刪除,因為權重為0的沒有罰款,可以直接刪除。 ```python= # P_4_4,activity selection, sorting+greedy # without key lambda n = int(input()) t = [int(x) for x in input().split()] w = [int(x) for x in input().split()] job = [(x,y) for x,y in zip(t,w)] job.sort(key=lambda x: x[0]/x[1] if x[1] else 1001) # sort by t/w, if w==0, key=oo curr = 0 # current completion time total = 0 # total weighted completion time for t,w in job: curr += t # completion time of i total += curr*w # total completion time # print(total) ``` 下面一題很類似,因此留做習題,不多解釋。 --- ##### Q-4-6. 少林寺的自動寄物櫃 (APCS201710) 少林寺的自動寄物櫃系統存放物品時,是將$N$個物品堆在一個垂直的貨架上,每個物品各佔一層。系統運作的方式如下:每次只會取用一個物品,取用時必須先將在其上方的物品貨架升高,取用後必須將該物品放回,然後將剛才升起的貨架降回原始位置,之後才會進行下一個物品的取用。 每一次升高貨架所需要消耗的能量是以這些物品的總重來計算。現在有$N$個物品,第$i$個物品的重量是$w(i)$而需要取用的次數為$f(i)$,我們需要決定如何擺放這些物品的順序來讓消耗的能量越小越好。 舉例來說,有兩個物品$w(1)=1$、$w(2)=2$、$f(1)=3$、$f(2)=4$,也就是說物品1的重量是1需取用3次,物品2的重量是2需取用4次。我們有兩個可能的擺放順序(由上而下): * (1,2),也就是物品1放在上方,2在下方。那麼,取用1的時候不需要能量,而每次取用2的能量消耗是$w(1)=1$,因為2需取用$f(2)=4$次,所以消耗能量數為$w(1)\times f(2)=4$。 * (2,1)。取用2的時候不需要能量,而每次取用1的能量消耗是$w(2)=2$,因為1需取用$f(1)=3$次,所以消耗能量數$=w(2)\times f(1)=6$。 在所有可能的兩種擺放順序中,最少的能量是4,所以答案是4。再舉一例,若有三物品而$w(1)=3,w(2)=4,w(3)=5$而$f(1)=1,f(2)=2,f(3)=3$。假設由上而下以$(3,2,1)$的順序,此時能量計算方式如下:取用物品3不需要能量,取用物品2消耗$w(3)\times f(2)=10$,取用物品1消耗$(w(3)+w(2))\times f(1)=9$,總計能量為19。如果以(1,2,3)的順序,則消耗能量為$3\times 2+(3+4)\times 3=27$。事實上,我們一共有3!=6種可能的擺放順序,其中順序(3,2,1)可以得到最小消耗能量19。 Time limit: 1秒 輸入格式:輸入的第一行是物品件數$N$,$N<1e5$。第二行有$N$個正整數,依序是各物品的重量$w(1),w(2),\ldots,w(N)$。第三行有$N$個正整數,依序是各物品的取用次數$f(1),f(2),\ldots,f(N)$,重量與次數皆不超過1000,相鄰以空白間隔。 輸出:輸出最小能量消耗值。答案不超過1e18。 範例輸入: 3 3 4 5 1 2 3 範例結果: 19 --- ### 以PQ處理動態資料(\*) 這一小節中我們提出需使用優先佇列(priority queue, PQ)的題目。由於APCS的範圍應該不包含PQ,所以照理說不太會出現在APCS考試中。 --- ##### P-4-7. 岳不群的併派問題(Two-way merge) (\*) 江湖上門派林立,華山派掌門岳不群認為要消除門派的衝突就是要合併各門派,但不能操之過急,必須每次挑兩個門派合併,如此逐步合併,最後將所有門派合併成一派。合併門派需要一些成本,每個門派都有他的成員數,第 i 個門派的成員數是 $m(i)$,合併兩派的成本就是雙方成員數之和,而兩派合併後,雙方成員就會歸到新合併的門派。岳不群不希望耗費太多成本,輸入各派人數,請幫他計算最少的合併總成本。 舉例來說,一開始如果有三派,$m=(3, 5, 1)$,若3人與5人的兩派先合併,成本是3+5=8,合併後剩下兩派,人數是(8,1),再合併這兩派,成本是8+1=9,此時已經只剩下一派,也就是完成所有合併統一江湖了,總成本是8+9=17。如果我們更換合併的順序,先合併3人與1人的兩派(成本4),再合併剩下的5人(成本4+5=9),則總成本只有4+9=13。這是這個例子的最低成本。 Time limit: 2秒 輸入格式:第一行是一個正整數 $n$,代表初始的門派數量,第二行有 $n$ 個正整數是每派的成員數,同行數字間以空白間格。$n$ 不超過2e5,每個門派初始人數都不超過 1e5。 輸出:第一行輸出總人數,第二行輸出最小的合併總成本。 範例輸入: 3 3 5 1 範例結果: 9 13 --- 這一題事實上是Two-way merge tree的問題,與一個有名的Huffman code是等價的。正確的合併順序的計算方法是:每次挑選人數最少的兩派合併,重複此步驟直到剩下一派為止。任何一種合併的過程可以用一個二元樹表示,下圖是一個例子。 ![image](https://hackmd.io/_uploads/HyxvMhiUa.png) 圖中每個方塊表示一個初始的門派,圓圈表示一個合併。初始人數$m(i)$ 是 (7,2,3,5),先合併 (2,3) 變成5,再合併(5,5) 變成10,最後合併 (7,10) 變成 17。合併的成本圓圈中的數字相加,這個例子 5+10+17=32 就是是最低成本。這一題的證明稍嫌麻煩,我們在此不給完整的證明,只做一些說明。總成本也可用另外一個方式計算,每一個方塊往上走到頂會經過幾次圓圈(稱為深度),就代表他經過幾次合併,所以每個 $m(i)$ 乘上他的深度加總後就是總成本,所以直覺上看得出來,數字越大的要放在深度小的地方,也就是說數字越小的要越早合併,完整的證明留給讀者上網搜尋Huffman code或two-way merge tree。 雖然證明有點麻煩,但是程式非常好寫,我們只要每次找出最小的兩個數,將他們刪除,再將他們的和放入。重複這個步驟直到剩下一個數字。但是要有效率的做到必須借助資料結構,如果使用排序,每次刪除最小值或許不是大問題(記住有效位置,每次後移兩格就好),但是插入兩數相加的和就會很浪費時間了。以下是用陣列的方法寫的範例程式,時間複雜度是 $O(n^2)$。雖然他效率不夠好,但是我們也使用了一些技巧來減少資料搬移的動作,所以也算個好的練習。 我們維持list $m$永遠是排好序的狀態,每次要找最小的兩個元素把它們移除後,再將他們相加後的和插入 $m$ 中並保持排好序的狀態。插入排序好的list可以用bisect中的insort,或是bisect_left加上insert,但必須是上升遞增;要移除方便,最好刪除的是在尾端。因此我們用負值放入進行遞增排序,這樣是原始資料的遞減排序。每次從尾端pop兩個資料就是最小的兩個。但是insort還是$O(n)$,所以整體時間還是$O(n^2)$。 ```python= # P_4_7 greedy, slow n^2 method using array import bisect n = int(input()) m = [-int(x) for x in input().split()] # negative m.sort() # origibal val large to small total = 0 # total cost for i in range(n-1): # merge n-1 times cost = m.pop() # smallest cost += m.pop() # second smallest total += cost # insert into increasing list bisect.insort(m, cost) # print(-m[0]) print(-total) ``` 要能夠有效率的做到增刪資料,我們必須借助資料結構,優先佇列每次可以找到最小值,是這一題最適合的資料結構,只要每次pop出兩個資料,相加後push回去就可以了。 ```python= # P_4_7 greedy, O(nlogn) using pq import heapq n = int(input()) m = [int(x) for x in input().split()] heapq.heapify(m) # heap initialization total = 0 # total cost for i in range(n-1): # merge n-1 times cost = heapq.heappop(m) # smallest cost += heapq.heappop(m) # second smallest total += cost # insert into heap heapq.heappush(m,cost) # print(m[0]) print(total) ``` 下面一個題目是在工作排程時的問題,做法簡單,但證明需要想一下,留做習題,如果資料量不大,可以用陣列做,但資料量大時,必須使用資料結構。 --- ##### Q-4-8. 先到先服務 (\*) 有 $n$ 個客人要分配到 $m$ 個櫃台來服務,客人依編號順序到達,第 $i$ 個客人需要 $t(i)$ 的時間來完成(無論分配到哪一個櫃台),而且同一位客人的需求必須在同一個櫃台完成,不能分拆。因為公平的因素,**先到客人不可以比晚到客人更晚開始服務**。現在希望安排每個客人的服務櫃檯,以便可以在最短的時間內完成所有客人的需求。 舉例來說,如果有三位客人與兩個櫃台,需求的時間依序是(3,1,5),我們可以如下分配,最後的完成時間是6。 櫃台1:3 櫃台2:1+5 請注意,我們不可以如下分配,雖然這樣的完成時間會減少到5,但是第3個客人(5)比第2個客人(1)早開始服務,違反了先到先服務的原則。 櫃台1:3+1 櫃台2:5 Time limit: 2秒 輸入格式:輸入兩行,第一行是正整數 $n$ 與 $m$,第二行是 $n$ 個正整數$t(1), t(2),\ldots ,t(n)$,同行數字間以空白間格。$n$ 與 $m$ 不超過2e5,$t(i)$ 不超過1e4。 輸出:最早可能服務完所有客人的時間。 範例輸入: 3 2 3 1 5 範例結果: 6 --- **End of Chapter 4 (I)** 下接 [AP325-Python 第4章 貪心演算法與掃瞄線演算法 (II)](/mEi9wqzjRCuj2aixy3Ea2Q)

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password
    or
    Sign in via Google Sign in via Facebook Sign in via X(Twitter) Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    By signing in, you agree to our terms of service.

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully