好,我們下課
Greedy by ... Your Primal Instincts...
給你一個整數 N (N ≥ 0),請找到最小的 Q (Q ≥ 0),
使得在 Q 中所有數字(digit)的乘積等於 N 。
舉例來說:
怎麼做?
給你一個整數 N (N ≥ 0),請找到最小的 Q (Q ≥ 0),
使得在 Q 中所有數字(digit)的乘積等於 N 。
怎麼做?
從2找到9,找到因數就放第一位數,以此循環
N = 16,你會輸出 2222。
想辦法輸出的位數最小化!
最後再由小的輸出?
有特例嗎?
給你一個整數 N (N ≥ 0),請找到最小的 Q (Q ≥ 0),
使得在 Q 中所有數字(digit)的乘積等於 N 。
T = int(input())
for _ in range(T):
N = int(input())
ans = []
for factor in range(9, 1, -1):
while N % factor == 0:
N //= factor
ans.append(str(factor))
if N == 1 and ans:
print(''.join(ans[::-1]))
elif N == 1:
print(1)
else:
print(-1)
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int T;
scanf("%d", &T);
while(T--) {
int N;
scanf("%d", &N);
string ans;
for (int factor=9; factor>1; factor--) {
while (N % factor == 0) {
N /= factor;
ans += '0' + factor;
}
}
reverse(ans.begin(), ans.end());
if (N == 1 && ans.length())
cout << ans << endl;
else if (N==1)
cout << 1 << endl;
else
cout << -1 << endl;
}
return 0;
}
給你一個整數 N (N ≥ 0),請找到最小的 Q (Q ≥ 0),
使得在 Q 中所有數字(digit)的乘積等於 N 。
雖然可以 AC,但是你真的知道
為什麼可以由9到1嗎?
首先,我們只需要考慮和數:9,8,6,4。
如果 N 有 2 * 2 * 3 你會選擇 4 * 3 還是 2 * 6?
如果 N 有 2 * 3 * 3 你會選擇 2 * 9 還是 3 * 6?
有沒有發現其實這題沒有你想像中的簡單?
如果 N 有 2 * 2 * 2 你會選擇 4 * 2 還是 8?
你該怎麼選擇 S 才可以把所有點包住呢?
這種題目很難 (被稱為NPC 問題)
所以如果廣泛的說,他其實不能貪心,
只是題目的設計剛好可以讓它貪心。
找零問題 - Change-making Problem
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
舉例來說:
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
但考慮比較奇怪的 case:
目標 8 塊
5 + (目標 3 塊)
4 + (目標 4 塊)
5 + 1 + 1 + 1
(四個硬幣)
4 + 4
(兩個硬幣)
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
總結來說,這題只能爆搜。
也就是你幾乎必須試遍所有可能。
那不是很慢嗎?
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
想想看,在什麼樣的條件下,
貪心演算法的解才是最佳解呢?
How to Greedy?
找零錢問題
Product of Digits
在所有可選的數字中,
優先選合併最多的。
在所有可找的零錢,
優先選面額最大的。
在眾多可能中,只選了一種 "看似最佳" 的解答。
那就表示你很貪心。
當然,有時候貪心解就是最佳解,
但大多時候都不是,通常這時就用動態規劃DP。
在講之前,回憶一下枚舉吧!
f(8)
5 + f(3)
4 + f(4)
1 + f(7)
5 + 1 + f(2)
4 + 4
4 + 1 + f(3)
5 + 1 + 1 + f(1)
4 + 1 + 1 + f(2)
1 + 5 + f(2)
1 + 4 + f(3)
1 + 1 + f(6)
1 + 5 + 1 + f(1)
1 + 4 + 1 + f(2)
1 + 1 + 5 + f(1)
在動態規劃的課程中,
我們說問題如果具有兩個性質,
那麼它就適合動態規劃 (DP)
講人話就是這個大問題可以透過小問題解決。
講人話就是解決大問題時,小問題被問不只一次。
overlapping subproblems
optimal substructure
找零問題中,答案可以暴搜一開始選的硬幣+遞迴解決
找零問題中,遞迴時會發現每個問題有可能被問很多次
在貪心演算法的課程中,
我們說問題如果具有兩個性質,
那麼它就適合貪心演算法 (Greedy)
講人話就是這個大問題可以透過小問題解決。
optimal substructure
找零問題中,答案可以暴搜一開始選的硬幣+遞迴解決
講人話就是你可以在所有可能選擇到最佳解的方向。
greedy choice property
找零問題中,優先選最大面額「可能」就會是最佳解
這真的有人講中文嗎?
大膽假設,小心求證
現在有一堆餅乾跟一堆小孩,
餅乾有各自的「滿足度」,小孩有各自的「貪心度」。
如果小孩吃的餅乾「滿足度」大於等於自己的「貪心度」,他會很開心。
每個小孩最多只能給一片餅乾,請問你最多可以讓幾位小孩開心?
舉例來說:
直觀來說,你會怎麼做?
現在有一堆餅乾跟一堆小孩,
餅乾有各自的「滿足度」,小孩有各自的「貪心度」。
如果小孩吃的餅乾「滿足度」大於等於自己的「貪心度」,他會很開心。
每個小孩最多只能給一片餅乾,請問你最多可以讓幾位小孩開心?
直觀來說,你會怎麼做?
1. 從貪心度最小的餅乾開始給,
優先滿足最不貪心的小孩。
因為不貪心的小孩最好被滿足。
2. 從貪心度最大的餅乾開始給,
優先滿足最貪心的小孩。
因為可以保證餅乾可以被
最適合的貪心小孩吃掉
1. 從貪心度最小的餅乾開始給,
優先滿足最不貪心的小孩。
2. 從貪心度最大的餅乾開始給,
優先滿足最貪心的小孩。
g.sort()
s.sort()
ans = 0
while s and g:
if s[-1] >= g[-1]:
ans += 1
del g[-1], s[-1]
else:
del g[-1]
return ans
g.sort(reverse=True)
s.sort(reverse=True)
ans = 0
while s and g:
if s[-1] >= g[-1]:
ans += 1
del g[-1], s[-1]
else:
del s[-1]
return ans
都可以AC!
1. 從貪心度最小的餅乾開始給,
優先滿足最不貪心的小孩。
2. 從貪心度最大的餅乾開始給,
優先滿足最貪心的小孩。
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int ans = 0;
while (!s.empty() && !g.empty()) {
if (s.back() >= g.back()) {
ans += 1;
s.pop_back(), g.pop_back();
} else {
g.pop_back();
}
}
return ans;
都可以AC!
sort(g.begin(), g.end(), greater<int>());
sort(s.begin(), s.end(), greater<int>());
int ans = 0;
while (!s.empty() && !g.empty()) {
if (s.back() >= g.back()) {
ans += 1;
s.pop_back(), g.pop_back();
} else {
s.pop_back();
}
}
return ans;
優先滿足最不貪心的小孩。
?
你能證明看看第二個方法的正確性嗎?
現在有一堆人要點餐,每個人點的餐有「煮的時間」跟「吃的時間」。
廚師一次只能準備一個餐點,求出最後一個人吃完時的最短時間
舉例來說:
直觀來說,你會怎麼做?
1
1
2
2
3
3
第一個人
第二個人
第三個人
廚師
7
現在有一堆人要點餐,每個人點的餐有「煮的時間」跟「吃的時間」。
廚師一次只能準備一個餐點,求出最後一個人吃完時的最短時間
直觀來說,你會怎麼做?
1. 從準備時間最短的開始做。
因為這樣可以讓客人先吃。
3. 從吃最久的的開始做。
因為這樣可以讓廚師沒事做的時間最小
2. 從準備時間最長的開始做。
因為這樣可以優先處理掉最佔時間的
哪個是對的?
找得出反例嗎?
如果不會通靈,你數學最好強一點。
C1
E1
C2
E2
C1
E1
C2
E2
C1
E1
C2
E2
C1
E1
E2
C2
優先做吃
最久的人。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main(){
int n, C, E;
while(cin >> n && n != 0){
vector< pair<int,int> > V;
for(int i=0; i<n; i++){
cin >> C >> E;
V.push_back({E,C});
}
sort(V.begin(), V.end());
reverse(V.begin(),V.end());
int now_time = 0, ans = 0;
for(auto [E, C] : V){
// 紀錄現在煮多久了。
now_time += C;
// 算出這個客人啥時離開,並更新最大值。
ans = max(ans, now_time + E);
}
cout << ans << endl;
}
return 0;
}
while True:
n = int(input())
if n == 0:
break
V = []
for _ in range(n):
C, E = map(int, input().split())
V.append((E, C))
V.sort(reverse=True)
now_time, ans = 0, 0
for E, C in V:
now_time += C
ans = max(ans, now_time + E)
print(ans)
現在有一堆任務,每個任務有起始時間跟結束時間。你一次只能做一個任務,問最少需要放棄任務的數量?
直觀來說,你會怎麼做?
舉例來說:
[1, 2]
[2, 3]
[3, 4]
[1, 3]
現在有一堆任務,每個任務有起始時間跟結束時間。你一次只能做一個任務,問最少需要放棄任務的數量?
直觀來說,你會怎麼做?
放棄任務的數量...不太好想。
換個角度想:如果是最多可以做多少任務呢?
1. 從任務所花時間最短的開始選,因為他的費時最小。
3. 從任務結束時間最早的開始做,因為早做完就可以快選下一個。
2. 從任務開始時間最早的開始做,早做完早享受。
哪個是對的?
找得出反例嗎?
不失一般性,我們先根據任務的結束時間做排序。
常用來證明貪心的技巧
接著我們來介紹一種...
[u1, v1]
[?]
[u2, v2]
假設 [u1, v1] 在我們的算法中,會被選為最佳解。
考量之後會遇到的 [u2, v2] ,他跟 [u1, v1] 重疊
(也就是選了 [u1, v1]後不能選 [u2, v2]),那麼會有幾種狀況。
假設 [u1, v1] 在我們的算法中,會被選為最佳解。
考量之後會遇到的 [u2, v2] ,他跟 [u1, v1] 重疊
(也就是選了 [u1, v1]後不能選 [u2, v2]),那麼會有幾種狀況。
顯然不管怎麼選
都是[u1,v1]。
[u1, v1]
[u2, v2]
[u1, v1]
[u2, v2]
如果 u1 被選,那麼表示 v2 只會卡到下一個選擇。
假設 [u1, v1] 在我們的算法中,會被選為最佳解。
顯然不管怎麼選
都是[u1,v1]。
如果 u1 被選,那麼表示 v2 只會卡到下一個選擇。
如果有個答案 [u2, v2] 沒被我們選,
他一定不會比我們選的答案好。QED
Trivial.
如果拿 [u1, v1] 換 [u2, v2]
你有可能會把 [u1, v1] 後面的任務被犧牲。 (可能2換1)
intervals.sort(key = lambda a: a[1])
cur_r = -float('inf')
ans = 0
for l, r in intervals:
if cur_r <= l:
cur_r = r
ans += 1
return len(intervals) - ans
sort(intervals.begin(), intervals.end(),
[](auto &a, auto &b) {
return a[1] < b[1];
});
int current_end = -50000, chosen = 0;
for (auto &interval : intervals) {
if (current_end <= interval[0]) {
current_end = interval[1];
chosen ++;
}
}
return intervals.size() - chosen;
[u1, v1]
[u2, v2]
如果 u1 被選,那麼表示 v2 只會卡到下一個選擇。
我們雖然當時這樣講,不過會不會有這樣的情況呢?
1
2
3
1
2
4
3
好好的重新想一遍吧!
大膽假設,小心求證
題目名稱 | 來源 | 備註 |
---|---|---|
物品堆疊 (Stacking) | Zerojudge c471 |
APCS 2017/10 - 4 直觀困難,試試數學 |
The Bus Driver Problem | Zerojudge e538 | 非常經典的題目! |
Jump Game | Leetcode 55 | 跟後面題目有關 |
Jump Game II | Leetcode 45 | |
Largest Number | Leetcode 179 | Coding Bar py-進階 |
Largest Merge Of Two Strings | Leetcode 1754 |
只是你還沒看過很難的貪心而已 XD
來看看更複雜的貪心題吧!
但通常 APCS 題目的是
在上之後的課程請你複習一下:
Priority Queue ( Python 是 heapq)
set, map, lower_bound
二分搜尋法 (自己用 while 迴圈寫出來的作法)
我們定義兩數合併為 merge(x , y) → (x + y),成本為 x + y。
給你一堆數字,求將所有數字合併成一個數字的最小成本。
直觀來說,你會怎麼做?
舉例來說:
現在需要合併的數字為 [1, 2, 3]
那麼答案會是 9。
1
2
3
6
cost : 3
cost : 6
3
我們定義兩數合併為 merge(x , y) → (x + y),成本為 x + y。
給你一堆數字,求將所有數字合併成一個數字的最小成本。
直觀來說,你會怎麼做?
由小排到大,從一開始慢慢開始合併。
這樣好像會有點問題...?
合併完的數字要放哪裡?
由小排到大,從一開始慢慢開始合併,合併完放前面
由小排到大,從一開始慢慢開始合併,合併完放後面
由小排到大,
合併完放前面
由小排到大,
合併完放後面
考慮測資 : [2, 2, 3, 3]
考慮測資 : [1, 1, 3, 3]
但最佳解是:
但最佳解是:
不能每次都拿最小的兩個嗎?
不能每次都拿最小的兩個嗎?
Priority Queue / heapq!
每次拿最小的兩個合併,合併完丟回去 Priority Queue / heapq
不過,你會證明嗎?
非常的不太適合給高中生證明XD,見這裡
from queue import PriorityQueue
while True:
n = int(input())
if n == 0:
break
Q = PriorityQueue()
for v in map(int, input().split()):
Q.put(v)
cost = 0
while Q.qsize() > 1:
A, B = Q.get(), Q.get()
cost += A+B
Q.put(A+B)
print(cost)
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, x;
while (cin>>n && n != 0) {
priority_queue<long long> Q;
while(n--) {
cin>>x;
Q.push(-x);
}
long long ans=0, A, B;
while (Q.size() != 1) {
A = -Q.top(); Q.pop();
B = -Q.top(); Q.pop();
ans += A + B;
Q.push(-(A + B));
}
cout << ans << endl;
}
}
import heapq
while True:
n = int(input())
if n == 0:
break
Q = list(map(int, input().split()))
heapq.heapify(Q)
cost = 0
while len(Q) > 1:
A, B = heapq.heappop(Q), heapq.heappop(Q)
cost += A+B
heapq.heappush(Q, A+B)
print(cost)
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度期望值最低?
什麼叫做編碼?
文本:
Fuwawa Mococo
字符 | 次數 | 編碼 |
---|---|---|
F | 1 | 000 |
U | 2 | 001 |
W | 2 | 010 |
A | 2 | 011 |
M | 1 | 100 |
O | 3 | 101 |
C | 2 | 110 |
000 001 010 011 010 011
100 101 110 101 110 101
經過編碼:
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度最低呢?
不能亂編碼!會互撞!
字符 | 次數 | 編碼 |
---|---|---|
F | 1 | 0 |
U | 2 | 1 |
W | 2 | 10 |
A | 2 | 11 |
M | 1 | 100 |
O | 3 | 101 |
C | 2 | 110 |
0 1 10 11 10 11
100 101 110 101 110 101
文本:
Fuwawa Mococo
經過編碼:
0 -> F,這沒問題。
110 要看成是 UW 還是 C 呢?
可以把他轉成一個編碼樹,這樣就可以保證不會相撞!
字符 | 次數 | 編碼 |
---|---|---|
F | 1 | 000 |
U | 2 | 001 |
W | 2 | 010 |
A | 2 | 011 |
M | 1 | 100 |
O | 3 | 101 |
C | 2 | 110 |
F
U
W
A
M
O
C
0
0
0
0
0
0
1
1
1
1
1
1
0
開始!
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度最低呢?
字符 | 次數 | 編碼 |
---|---|---|
F | 1 | 0000 |
U | 2 | 001 |
W | 2 | 010 |
A | 2 | 011 |
M | 1 | 0001 |
O | 3 | 11 |
C | 2 | 10 |
F
U
W
A
M
O
C
0
0
0
0
0
1
1
1
1
1
開始!
1
0
讓我們來換個編碼樹!
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度最低呢?
字符 | 次數 | 編碼 |
---|---|---|
F | 1 | 0000 |
U | 2 | 001 |
W | 2 | 010 |
A | 2 | 011 |
M | 1 | 0001 |
O | 3 | 11 |
C | 2 | 10 |
文本:
Fuwawa Mococo
0000 001 010 011 010 011
0001 11 10 11 10 11
經過編碼:
原本: 36個 bit
現在: 33個 bit,更短了!
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度最低呢?
字符 | 次數 | 編碼 |
---|---|---|
F | 1 | 0000 |
U | 2 | 001 |
W | 2 | 010 |
A | 2 | 011 |
M | 1 | 0001 |
O | 3 | 11 |
C | 2 | 10 |
你把兩個點的次數合併起來,
其實就會多花這兩個點的次數來編碼!
把 F 跟 M 合併,
就表示你會花 (F的次數) + (M的次數) 個 bit 來編碼!
這樣你知道跟合併數字的關係了嗎?
F
M
0
1
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度最低呢?
現在有個一維道路。你想開車到 target,但燃料有限。
所幸路上有很多加油站,每個加油站有其位置跟燃料數量。
問你最少加油幾次才可以到target?
start
target
=100
🚘
起始
10 燃料
10
+60 燃料
20
+30 燃料
+30 燃料
30
+40 燃料
60
≈
直觀來說,你會怎麼做?
現在有個一維道路。你想開車到 target,但燃料有限。
所幸路上有很多加油站,每個加油站有其位置跟燃料數量。
問你最少加油幾次才可以到target?
直觀來說,你會怎麼做?
比較多油比較賺的,從最多油的開始加?
如果我們每次看到加油站的時候才決定要不要加,
好像有點困難?
從最多的燃料開始加嗎?
如果從最多燃料的加油站開始加,有沒有什麼條件呢?
start
target
=100
🚘
起始
30 燃料
10
+60 燃料
20
+30 燃料
+30 燃料
40
+40 燃料
60
≈
"後悔沒有加" 的清單
🚘
🚘
🚘
🚘
🚘
+60 燃料
+30 燃料
+30 燃料
+40 燃料
用過了!
用過了!
🚘
30
通常都會使用 priority_queue 來實作。
核心概念就是先做選擇,出事想辦法彌補!
priority_queue<int> Q;
stations.push_back(vector<int> {target, 0});
sort(stations.begin(), stations.end());
int reach = startFuel, used = 0;
for (auto station : stations) {
// 如果走不到下個加油站,
// 就從後悔清單拿到可以走到為止
while (!Q.empty() && reach < station[0]) {
reach += Q.top();
used += 1;
Q.pop();
}
// 已經到達target或者到不了下個加油站,結束。
if (reach >= target || reach < station[0])
break;
Q.push(station[1]);
}
return reach >= target ? used : -1;
reach, used = startFuel, 0
stations.append([target, 0])
stations.sort()
# Heapq is min heap by default.
# We need *-1 to negate values.
Q = []
for position, fuel in stations:
# 如果走不到下個加油站,
# 就從後悔清單拿到可以走到為止
while Q and reach < position:
reach += -heapq.heappop(Q)
used += 1
# 已經到達target或者到不了下個加油站,結束。
if reach >= target or reach < position:
break
heapq.heappush(Q, -fuel)
return used if reach >= target else -1
你需要放置 k 個基地台在一維城鎮上,目標是想讓所有住家都可以被覆蓋到。
那麼請問基地台的覆蓋半徑最小可以是多少?
(所有基地台的半徑一樣,基地台可以自己設定。)
1
3
11
1
3
11
直徑=10
直徑=2
直徑=2
完全沒概念... 不妨換個角度想想?
直觀來說,你會怎麼做?
放 k 個基地台,
問最小 r 多少才可以覆蓋所有住家?
放 k 個基地台,
給定半徑 r,問可不可以覆蓋所有住家?
1
3
11
5
9
12
原本題目
簡單化題目
直觀來說,你會怎麼做?
二分搜尋可以在一些很奇怪的地方出現。
跟貪心一樣 - 可能會出現在任何地方!
TODO
#include <stdio.h>
#include <algorithm>
int ary[50001], n, k;
bool is_ok(int x){
int last = -x-1, now_put = 0;
for(int i=0; i<n; i++){
if(last + x < ary[i]){
now_put ++;
last = ary[i];
if(now_put > k){
return false;
}
}
}
return true;
}
int main(){
scanf("%d%d", &n, &k);
for(int i=0; i<n; i++)
scanf("%d", &ary[i]);
std::sort(ary, ary+n);
int L=0, R=1000000000;
while(R-L != 1){
int mid = (L+R) / 2;
if(is_ok(mid))
R = mid;
else
L = mid;
}
printf("%d\n", R);
return 0;
}
能不能夠根據你的貪心來寫出答案,
還是要看你實作的強度。
讓我用下個題目告訴你,實作與想法的碰撞!
給你一個直方圖,求其中的最大矩形面積。
舉例來說:
直觀來說,不考慮實作,你會怎麼做?
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
觀察: 如果我們看到最小的數字,那麼以他為準的答案就是 n * 這個數字
[2, 1, 5, 6, 2, 3]
那之後呢? 之後的高度都不該穿過 1,
所以應該把它切開
[2] / [5, 6, 2, 3]
[5, 6] / [3]
/ [6]
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
理論通過,開始實作。
[2, 1, 5, 6, 2, 3]
[2] / [5, 6, 2, 3]
將陣列每次都拆開...
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
[2, 1, 5, 6, 2, 3]
[2] / [5, 6, 2, 3]
將陣列每次都拆開 (僅用左右界表示子陣列)...
最原始的線段樹是為了解決 RMQ 問題
(Range Minimum Query)。
大方向就是把樹的點當成區間維護。
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
[2, 1, 5, 6, 2, 3]
[2] / [5, 6, 2, 3]
將陣列每次都拆開 (僅用左右界表示子陣列)...
struct SegTree {
// min number, it's index
pair<int, int> min_info;
SegTree *L, *R;
SegTree(SegTree *L, SegTree *R, pair<int, int> min_info={INT_MAX, -1}):
L(L), R(R), min_info(min_info){}
void pull() {
min_info = min(L->min_info, R->min_info);
}
};
SegTree* build(int L, int R, vector<int>& A) {
// [l, r]
if (L == R)
return new SegTree(NULL, NULL, {A[L], L});
int M = (L + R) / 2;
SegTree *now = new SegTree(build(L, M, A), build(M+1, R, A));
now->pull();
return now;
}
pair<int, int> query(SegTree* root, int L, int R, int x, int y) {
if (R < x || y < L) return {INT_MAX, -1};
if (x <= L && R <= y) return root->min_info;
int M = (L + R) / 2;
return min(query(root->L, L, M, x, y), query(root->R, M+1, R, x, y));
}
class Solution {
public:
int dc(SegTree *root, int x, int y, int N) {
auto [min_v, min_idx] = query(root, 0, N-1, x, y);
if (min_idx == -1)
return INT_MIN;
return max({
(y - x + 1) * min_v,
dc(root, x, min_idx-1, N),
dc(root, min_idx+1, y, N)
});
}
int largestRectangleArea(vector<int>& heights) {
// Special Case:
bool flag = true;
for (auto h: heights)
if (h != heights[0]) {
flag = false;
break;
}
if (flag)
return heights[0] * heights.size();
SegTree *root = build(0, heights.size()-1, heights);
return dc(root, 0, heights.size()-1, heights.size());
}
};
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
[2, 1, 5, 6, 2, 3]
[2] / [5, 6, 2, 3]
將陣列每次都拆開 (僅用左右界表示子陣列)...
給你一個直方圖,求其中的最大矩形面積。
[2, 1, 5, 6, 2, 3]
[2] / [5, 6, 2, 3]
或是 ... 我們每次都找下一個最小值的區間在哪裡。
數字的排名: (1, 0, 4, 5, 2, 3)
[0, 6)
[0, 1), [2, 6)
/ / [5, 6, 2, 3]
[0, 0), [1, 1), [2, 6)
/ / [5, 6] / [3]
[0, 0), [1, 1), [2, 4), [5, 6)
給你一個直方圖,求其中的最大矩形面積。
或是 ... 我們每次都找下一個最小值的區間在哪裡。
/ / [5, 6, 2, 3]
[0, 0), [1, 1), [2, 6)
/ / [5, 6] / [3]
[0, 0), [1, 1), [2, 4), [5, 6)
[2] / [5, 6, 2, 3]
[0, 1), [2, 6)
vector<pair<int, int>> order;
for (int i = 0; i < heights.size(); i++) {
order.push_back({heights[i], i});
}
sort(order.begin(), order.end());
int ans = 0;
set<pair<int, int>> intervals{{0, heights.size()}};
for (auto &[height, idx] : order) {
auto it = --intervals.lower_bound({idx, INT_MAX});
ans = max(ans, height * (it->second - it->first));
intervals.insert({it->first, idx});
intervals.insert({idx+1, it->second});
intervals.erase(it);
}
return ans;
Python 沒有內建平衡樹可以用...
所以 Python 不太能這樣寫。
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
給你一個直方圖,求其中的最大矩形面積。
最大高度開始。
[2, 1, 5, 6, 2, 3]
觀察: 如果我們看到最大的數字,那麼他為準的答案就是這個數字
[2, 1, (5, 5), 2, 3]
[2, 1, (5, 5), 2, 3]
[2, 1, (2, 2, 2, 2)]
由大到小看數字,
合併兩側比自己更大的數字。
並且每次更新答案就可以了。
我們用 Doubly Linked List 來合併!
給你一個直方圖,求其中的最大矩形面積。
最大高度開始。
[2, 1, 5, 6, 2, 3]
[2, 1, (5, 5), 2, 3]
[2, 1, (5, 5), 2, 3]
[2, 1, (2, 2, 2, 2)]
2
1
5
6
2
2
3
2
1
5
5
2
2
3
2
1
5 x 2
2
2
3
由大到小看數字,合併兩側比自己更大的數字。
並且每次更新答案就可以了。
6 x 1
5 x 2
答案
3 x 1
2 x 4
2
1
2 x 2
3
2
1
2 x 4
2
struct node {
node *prev, *next;
int h, w;
node(int h, int w):
h(h), w(w), prev(NULL), next(NULL) {}
int merge(set<node *> &deleted) {
// left
while (prev && h <= prev->h) {
w += prev->w;
node *tmp = prev;
if (prev->prev)
prev->prev->next = this;
prev = prev->prev;
deleted.insert(tmp);
delete tmp;
}
// right
while (next && h <= next->h) {
w += next->w;
node *tmp = next;
if (next->next)
next->next->prev = this;
next = next->next;
deleted.insert(tmp);
delete tmp;
}
return w * h;
}
};
vector<pair<int, node *>> order;
for (int i = 0; i < heights.size(); i++) {
node *cur = new node(heights[i], 1);
if (i) {
order.back().second->next = cur;
cur->prev = order.back().second;
}
order.push_back({heights[i], cur});
}
sort(order.begin(), order.end());
reverse(order.begin(), order.end());
int ans = 0;
set<node *> deleted;
for (auto &[h, cur] : order) {
if (deleted.find(cur) == deleted.end()) {
ans = max(ans, cur->merge(deleted));
}
}
return ans;
遇到重複的數字會很麻煩
TODO
Python 還沒寫
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
給你一個直方圖,求其中的最大矩形面積。
最大高度開始。
[2, 1, 5, 6, 2, 3]
觀察: 如果我們看到最大的數字,那麼他為準的答案就是這個數字
[2, 1, (5, 5), 2, 3]
[2, 1, (5, 5), 2, 3]
[2, 1, (2, 2, 2, 2)]
我們用 Disjoint Set 來合併!
由大到小看數字,
合併兩側比自己更大的數字。
並且每次更新答案就可以了。
給你一個直方圖,求其中的最大矩形面積。
最大高度開始。
由大到小看數字,合併兩側比自己更大的數字。
並且每次更新答案就可以了。
答案
2
1
5
6
2
3
[(1, 1, 1, 1, 1, 1)]
1 x 6
[2, 1, (2, 2, 2, 2)]
2 x 1
[2, 1, (2, 2, 2, 2)]
2 x 4
[2, 1, (5, 5), 2, 3]
3 x 1
[2, 1, (5, 5), 2, 3]
5 x 2
[2, 1, 5, 6, 2, 3]
6 x 1
TODO
boss = [i for i in range(len(nums))]
size = [1 for _ in nums]
ans = 0
def find_boss(i):
if boss[i] != i:
boss[i] = find_boss(boss[i])
return boss[i]
def merge(i, j):
i, j = find_boss(i), find_boss(j)
if i != j:
if nums[i] > nums[j]:
i, j = j, i
boss[j] = i
size[i] += size[j]
ary = sorted([(num, i) for i, num in enumerate(nums)]
for num, i in reversed(ary):
if i > 0 and nums[i-1] >= nums[i]:
merge(i-1, i)
if i+1 < len(nums) and nums[i+1] >= nums[i]:
merge(i, i+1)
I = find_boss(i)
ans = max(ans, nums[I] * size[I])
return ans
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
3. 單調隊列
給你一個直方圖,求其中的最大矩形面積。
單調隊列
我們由左往右看數字:
2 1 5 6 2 3
你會發現 2 遇到 1 的時候,
2 的作用就完全消失了
2 1 ?
維護一個遞增數列就好。
給你一個直方圖,求其中的最大矩形面積。
單調隊列
2 1 5 6 2 3
維護一個遞增數列就好。
怎麼更新答案呢?
考量第二個 2
我們會結算 5 跟 6,
因為之後 5 跟 6 就再也沒用了。
結算方法:(數字大小) x (離 2 的距離)
// Terminate all
heights.push_back(0);
deque<pair<int, int>> Q;
int ans = 0;
for (int i = 0; i < heights.size(); i++) {
// Pop
pair<int, int> cur{heights[i], i};
while (!Q.empty() && Q.back() > cur) {
ans = max(ans, Q.back().first * (i - Q.back().second));
cur.second = Q.back().second;
Q.pop_back();
}
Q.push_back(cur);
}
return ans;
nums.append(0)
q = deque()
ans = 0
for i, height in enumerate(nums):
cur = [height, i]
while q and q[-1] > cur:
ans = max(ans, q[-1][0] * (i-q[-1][1]))
cur[1] = q[-1][1]
q.pop()
q.append(cur)
return ans
為了方便,會在最後放 0。
這是為了結算所有的答案。
題目名稱 | 來源 | 備註 |
---|---|---|
Course Schedule III | Leetcode 630 | 不相交區間困難版 |