{大赦課之玖}

失蹤

\(INDEX\)

  • greedy

這我

  • 20729 蔡嘉晉
  • 失蹤的世宗
  • judging

 

  • 因為大家都用之前的綽號講爛梗所以換綽號了
  • 但還是一樣爛
  • 這邊推薦大家叫我本名

 

  • 店言學術
  • 資訊校隊
  • 北市賽佳作

{greedy}

要叫他貪心還是貪婪呢

一個不用思考的🌰

  • 一條數列 有正有負
  • 可以拿走部分的數字
  • 希望拿到的數字的總和最大

 

  • 啊不就看到正的就拿
  • 對啊就這樣
  • 貪婪 的拿走所有能讓你答案更好的東西

 

  • 當然正常來說不會這麼水

how to greedy

  • 用greedy算法的時候
  • 不是要找到一個最佳解
  • 而是要往一個不會比最佳解差的方向走
  • 也就是說你做某個操作
  • 你不確定他的結果
  • 但是你知道這樣還是能夠達到最佳解
  • 這樣把所有要操作的東西處理完
  • 因為你每次都維護著最佳解
  • 所以最後答案就會是好的

 

  • 好抽象 圖形化一下

一顆決策樹

  • 每個選擇就像在一棵樹上做決策
  • 每個樹上的點分別代表一種決策
  • 用剛剛拿陣列當🌰 假設有一個陣列{1, -3, 2, 4, -6}

0

1

1-3

1

1+2

1

3-4

3

3-6

3

this one

is good

¿SO?

  • 在決策樹中
  • greedy要做的不是找到所有最佳解可能存在的枝葉們
  • 而是要保證往下走的這顆子樹會有至少一個最佳解

0

1

1-3

1

1+2

1

3-4

3

3-6

3

this one

is good

來看個真正的greedy🌰

  • 眾所皆知 建電每次出去吃飯都會有些人吃飯特別慢
  • 比如吉米每次都在講話然後花了別人兩倍時間才吃完
  • 所以上菜及吃飯的先後順序就很重要了

 

  • TIOJ 1072 誰先晚餐
  • 給定每個人點的餐所需要的做菜時間 \(W_i\) 以及吃飯時間 \(C_i\)
  • 有一廚師專門為這桌人服務
  • 求最快何時所有人都能吃完飯

 

  • 誰要先吃?
  • 做飯快的? 做飯慢的? 吃飯慢的? 吃飯快的?

證明啊

  • 生活中我們可能會讓吃比較慢的人先吃 (\(C_1 \ge C_2 \ge ... \ge C_N\))
  • 但為什麼 證明啊 你證明啊
  • 若一吃飯序列中非全由吃較慢的人先吃
  • 必有二相鄰吃飯者其 \(C_i \lt C_{i+1}\)
  • 意即 \(i+1\) 吃比 \(i\) 慢
  • 考慮這兩個人誰該先吃

\(i\) 先吃

\(i+1\) 先吃

\(i\) 耗時

\(i+1\) 耗時

\(W_{i+1}+C_{i+1}\)

\(W_i+W_{i+1}+C_{i+1}\)

\(W_{i}+C_{i}\)

\(W_i+W_{i+1}+C_{i}\)

>

>

>

  • \(i+1\) 先吃的情況一定不比 \(i\) 差
#include <bits/stdc++.h>
using namespace std;
#define ss second
#define ff first
#define int long long
signed main() {
    ios_base::sync_with_stdio(false);cin.tie(0);
    int n;
    while(true){
        cin>>n;
        if(n==0) return 0;
        vector<pair<int, int>> people;
        for(int i=0; i<n; ++i){
            int a,b;
            cin>>a>>b;
            people.push_back({b, a});
        }
        sort(people.begin(), people.end(), greater<pair<int, int>>()); 
        // pair會先比較第一項再比較第二項
        int current=0; //廚師煮飯時間和
        int maximum=0;
        for(int i=0; i<n; ++i){
            current=current+people[i].ss; 
            maximum=max(maximum, current+people[i].ff); // curTime + arr[i].first是第i人離開的時間
        }
        cout<<maximum<<'\n';
    }
}
# PRESENTING CODE

AC CODE

{greedy-2}

為了不要讓上一條太深

🌰 - 2

  • 蘿莉切割問題 每次greedy都一定要講
  • 原版題序偏噁 和諧版如下
  • 把一個東西分成 \(N\) 塊
  • 每塊有一個權重 將一塊重量為 \(x\) 的東西分成 \(y\), \(z\)
  • 需花費 \(x\) 的代價
  • 已知所需分成的 \(N\) 塊個別所需的權重 求最小代價

 

  • 例:
  • 一塊東西要分成 3 2 2 3 代表原本有十單位
  • 可以 [10] -> [8, 2] -> [5, 3, 2] -> [3, 2, 3, 2] 代價 10+8+5 = 23
  • 或是 [10] -> [6, 4] -> [3, 3, 4] -> [3, 3, 2, 2] 代價 10+6+4 = 20
  • 第二種顯然比較好,同時也是所有分割方法的最佳方法

  • 正著切太難做了 不如反過來將想要切成的塊數合併吧
  • 用一顆樹來解釋
    • 不是決策樹

  • 序列: [8, 2, 3, 7, 5]
  • btw這是一顆二元樹

2

3

5

5

10

15

7

8

25

合併 / 切割

  • 這樣代價是多少?
  • (2+3)+(5+5)+(7+8)+(15+10)

2

3

5

5

10

15

7

8

25

  • 這樣代價是多少?
  • (2+3)+((2+3)+5)+(7+8)+((7+8)+(2+3+5))
  • 2 三次
  • 3 三次
  • 5 二次
  • 7 二次
  • 8 二次

2

3

5

5

10

15

7

8

25

  • 越底層的節點會被算到最多次
  • 如果把最後結果當作第0層
  • 他下面兩顆當第1層
  • 第 \(i\) 層的要算 \(i\) 次

2

3

5

5

10

15

7

8

25

  • 所以我們要把越小的數字放越下面
  • 最下層的會先算到才能算上層的
  • 所以每次選最小的兩個來算!

 

  • 也可以發現將樹上任二
  • 葉節點交換皆會使答案
  • 不變或增加

 

 

  • 葉節點的意思是下面沒有東西的節點
  • 7 8 2 3 5

2

3

5

5

10

15

7

8

25

證明?

  • 可以發現每次都拿最小的兩個來算就好了
  • 既然剛剛用樹來解釋了
  • 所以 樹歸
  • 數學歸納法
  •  

證明

  • 當 \(N = 2\)時,我們只有兩個數字\(a_1\)和\(a_2\)。,我們會先合併這兩個數字,成本為\(a_1+a_2\),最終只剩下一個數字,所以成本最小。
  • 假設對於\(N = k\ (k \ge 2)\)的情況,我們的貪婪策略總是能夠最終只剩下一個數字,並且成本最小。
  • 歸納步驟:現在考慮\(N = k + 1\)的情況,我們有\(k + 1\)個數字\(a_1, a_2, ..., a_{k+1}\)。根據貪婪策略,我們會先合併a1和a2,成本為\(a_1+a_2\)
  • 然後,我們得到了\(k\)個數字\(a_1+a_2, a_3, a_4, ..., a_{k+1}\)。根據歸納假設,可以使用貪婪策略將這\(k\)個數字合併成一個數字,並且成本最小
  • 因此,總成本為\(a_1+a_2+\) 最小成本(\(k\)個數字的合併成本)
  • 由於我們每次合併都是選擇最小的兩個數字,所以最小成本(\(k\)個數字的合併成本)也是最小的。因此,總成本最小。
  • 綜上所述,我們使用貪婪策略可以確保最終只剩下一個數字,並且成本最小。這就是這個問題的一個有效解法。

#include <bits/stdc++.h>
using namespace std;
#define int long long

signed main(){
    int n,m;
    while(cin>>n){
        priority_queue<int,vector<int>,greater<int> > p;
        while(n--){
            cin>>m;
            p.push(m);
        }
        int sum=0;
        int a,b;
        while(p.size()>1){
            a=p.top();
            p.pop();
            b=p.top();
            p.pop();
            sum+=a+b;
            p.push(a+b);
        }
        cout<<sum<<'\n';
        p.pop();
    }
}
# 誰先晚餐

AC CODE

記得開long long

{greedy-3}

additional

如果有時間再講

最大線段覆蓋

  • 在一個數線上
  • 有 \(N\) 個區間
  • 請取出最多的區間使所有區間不互相覆蓋

 

  • greedy的對區間右界排序並能拿就拿
  • why?

 

  • 可以試試看用數歸或反證法證明

題單

聽說我們有大赦賽

btw這我跟佑佑的美術作業

Made with Slides.com