好,我們下課
找零問題 - Change-making Problem
簡單上來說就是有 n 個物品,
你會從裡面挑幾個 (沒有順序) 當成是最佳解。
你寫過中的題目哪些是這樣的呢?
找硬幣問題:
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
通常這類的題目通常
一開始可以選個具有「最佳性質」的選項開始選。
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
舉例來說:
但考慮比較奇怪的 case:
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
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)
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
f(8)
5 + f(3)
4 + f(4)
1 + f(7)
5 + 1 + f(2)
4 + 4
4 + 1 + f(3)
1 + 5 + f(2)
1 + 4 + f(3)
1 + 1 + f(6)
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
這種類型的題目,大部分
會使用 DP 求解
不過如果面額長得像
台灣的面額一樣的話...
f(12)
10 + f(2)
5 + f(7)
1 + f(11)
5 + 1 + f(1)
5 + 5 + f(2)
5 + 1 + f(6)
1 + 10 + f(1)
1 + 5 + f(6)
1 + 1 + f(10)
給你一個國家的面額種類,
請問最少可以用多少個硬幣湊出 amount?
不過如果面額長得像
台灣的面額一樣的話...
你會發現優先
使用最大面額
一定會拿到最佳解
你找的到一條規則可以走到最佳解,這就是
貪心法。
在動態規劃的課程中,
我們說問題如果具有兩個性質,
那麼它就適合動態規劃 (DP)
講人話就是這個大問題可以透過小問題解決。
講人話就是解決大問題時,小問題被問不只一次。
overlapping subproblems
optimal substructure
找零問題中,答案可以暴搜一開始選的硬幣+遞迴解決
找零問題中,遞迴時會發現每個問題有可能被問很多次
在貪心演算法的課程中,
我們說問題如果具有兩個性質,
那麼它就適合貪心演算法 (Greedy)
講人話就是這個大問題可以透過小問題解決。
optimal substructure
找零問題中,答案可以暴搜一開始選的硬幣+遞迴解決
講人話就是你可以在所有可能選擇到最佳解的方向。
greedy choice property
找零問題中,優先選最大面額「可能」就會是最佳解
這真的有人講中文嗎?
接下來會介紹各式各樣的貪心題,
帶你一起尋找這個所謂的「規則」。
你找的到一條規則
可以走到最佳解,這就是
貪心法
(廣義的) Sweep-line Algorithm
掃描線源自於幾何題目中的算法,
例如線性規劃(LP)應該也算是掃描線?
總而言之就是隨著一條線(不一定是垂直線)移動去決定解。
凸包演算法 - Andrew's monotone chain 示意圖
*APCS不考凸包,
但學科會考喔!
給定 n 個區間,請算出有多少數字
被至少一個區間所覆蓋。
舉例來說,如果區間長這樣:
那麼答案為6。因為被覆蓋的區間為
給定 n 個區間,請算出有多少數字
被至少一個區間所覆蓋。
大概想像有一條掃描線,先對區間的開頭做排序,
從左往右掃就可以了。
每次都去記錄現在灰色最遠可以到哪。
給定 n 個區間,請算出有多少數字
被至少一個區間所覆蓋。
1. 不用管
2. 更新 cur_R
3. 從零計算
基本上你的掃描線只會遇到三種狀況。
cur_R: 灰色最遠到哪
給定 n 個區間,請算出有多少數字
被至少一個區間所覆蓋。
n = int(input())
L = []
for _ in range(n):
l, r = map(int, input().split())
L.append((l, r))
L.sort()
ans, cur_R = 0, -1
for l, r in L:
if cur_R < l:
cur_R = r
ans += r - l
elif r > cur_R:
ans += r - cur_R
cur_R = r
print(ans)
int n, l, r;
scanf("%d", &n);
vector<pair<int, int>> V;
while (n--){
scanf("%d%d", &l, &r);
V.push_back({l, r});
}
sort(V.begin(), V.end());
int ans = 0, cur_R = -1;
for (auto [l, r] : V) {
if (cur_R < l) {
cur_R = r;
ans += r - l;
}
else if (r > cur_R) {
ans += r - cur_R;
cur_R = r;
}
}
printf("%d\n", ans);
C++
Python
例如線段覆蓋長度就是:
* 其實我不覺得掃描線是貪心,不過就...算了
給定一個數列 A,請輸出子區間和最大為多少。
直觀來說,你會怎麼做?
舉例來說:
有沒有一個方法在經過一些前處理後,
就可以快速的 (O(1)) 算出區間和呢?
prefix sum / interval (range) sum
preprocess
這是有一套標準做法的。
首先我們先做出這樣的陣列 (前綴和)
這件事情應該用一個迴圈就可以寫完。
S[0] = A[0];
for (int i=1; i<n; i++)
S[i] = S[i-1] + A[i]
S = [A[0]]
for x in A[1:]:
S.append(S[-1]+x)
C++
Python
有沒有一個方法在經過一些前處理後,
就可以快速的 (O(1)) 算出區間和呢?
prefix sum / interval (range) sum
preprocess
我們要怎麼利用 S 還算出 f(l, r) 呢?
觀察一下...
將前綴和作處理就變區間和了!
這樣我們就可以在預處理 O(n) 的情況下做到 O(1) 查詢!
給定一個數列 A,請輸出子區間和最大為多少。
回到原題,那麼就會變成這樣。
這樣要怎麼寫呢?假設我們開掃描線枚舉 r
拿 S_r 去減掉之前「最小的前綴和」
就是這個 r 最大的區間和了!
給定一個數列 A,請輸出子區間和最大為多少。
pre_sum, ans, min_sum = 0, nums[0], 0
for x in nums:
pre_sum += x
ans = max(ans, pre_sum - min_sum)
min_sum = min(min_sum, pre_sum)
return ans
int pre_sum=0, min_sum=0, ans=nums[0];
for (auto x : nums) {
pre_sum += x;
ans = max(ans, pre_sum - min_sum);
min_sum = min(min_sum, pre_sum);
}
return ans;
C++
Python
發現這件事情的你,其實就不用真的寫區間和啦...
拿 S_r 去減掉之前「最小的前綴和」
就是這個 r 最大的區間和了!
假設我們開掃描線枚舉 r
區間和是一個(在APCS)很常使用的技巧,
一定一定一定要好好的熟練!
可能哪天你就會突然碰到區間和的公式
(尤其遞迴 / 動態規劃),
這個時候你就要回憶起來!
有個笑話:
有人線段樹寫到魔征,
比賽看到區間和用線段樹還不自知
給 n 根長棍,你可以任選其中三根
組成的三角形,
那麼你可以組成的三角形中,
周長最大可以是多少。
(如果不能組成任何三角形,輸出0)
直觀來說,你會怎麼做?
解貪心的第一件事情就是:通靈
如果你沒有想法的話...
給 n 個長棍,請找出可以組成的三角形中,
周長最大可以是多少。
(如果不能組成任何三角形,輸出0)
考量每根棍子當 c ,怎麼選 a, b 呢?
形成三角形的條件是什麼呢?
找 a, b 比 c 小,並且這個 a, b 是最大的,
如果可以組成,
那就是選這根棍子當 c 的最大周長三角形
對於三個邊長 :
sort(nums.begin(), nums.end());
while (nums.size() >= 3) {
int n = nums.size();
if (nums[n-1] < nums[n-2] + nums[n-3])
return nums[n-1] + nums[n-2] + nums[n-3];
nums.pop_back();
}
return 0;
nums.sort()
while len(nums) >= 3:
if nums[-1] < nums[-2] + nums[-3]:
return nums[-1] + nums[-2] + nums[-3]
nums.pop()
return 0
C++
Python
考量每根棍子當 c ,怎麼選 a, b 呢?
找 a, b 比 c 小,並且這個 a, b 是最大的,
對於三個邊長 :
基本上遇到兩個 / 三個都是搜尋左右
class Solution:
def maxSumOfThreeSubarrays(self, nums: List[int], k: int) -> List[int]:
dp = defaultdict(lambda: (0, None))
PS = list(accumulate(nums))
IS = lambda L, R: PS[R] - (PS[L-1] if L>0 else 0)
IS_k = [0] * len(nums)
for i in range(len(nums)-k+1):
IS_k[i] = IS(i, i+k-1)
pre = [(-IS_k[0], 0)]
for i in range(1, len(nums)):
pre.append(min(pre[-1], (-IS_k[i], i)))
suf = [(inf, len(nums)-k)]
for i in range(len(nums)-2, -1, -1):
suf.append(min(suf[-1], (-IS_k[i], i)))
suf = suf[::-1]
ans = (math.inf, None)
for mid in range(k, len(nums)-k):
val = IS_k[mid] - pre[mid-k][0] - suf[mid+k][0]
pair = (pre[mid-k][1], mid, suf[mid+k][1])
ans = min(ans, (-val, pair))
return ans[1]
有 nnn 台機器,每台機器生產一份資料需要 ttt 單位時間。
這些機器需要被放置在位置 [1,n][1, n][1,n],每個位置只能放一台機器。
有 mmm 個任務,每個任務指定一個範圍 [l,r][l, r][l,r],
要求範圍內的每台機器都生產 www 份資料。
請問完成所有任務的最小總時間是多少?
舉例來說:
直觀來說,你會怎麼做?
🖥️D
🖥️A
🖥️B
🖥️C
🖥️E
要求0秒
要求1秒
要求2秒
要求3秒
要求0秒
答案=1+2+3=6
🖥️A 1
🖥️B 2
🖥️C 3
🖥️D 4
🖥️E 5
產生一單位
需要的秒數
1
2
3
4
5
任務:範圍 [2,4] 要求 1 單位
預先處理一下,先算出每個位置各需要多少資料吧!
怎麼做呢?
已經算完的
正在算的
紀錄掃描線有多少 w
還沒算完的
掃描線!
有 nnn 台機器,每台機器生產一份資料需要 ttt 單位時間。
這些機器需要被放置在位置 [1,n][1, n][1,n],每個位置只能放一台機器。
有 mmm 個任務,每個任務指定一個範圍 [l,r][l, r][l,r],
要求範圍內的每台機器都生產 www 份資料。
請問完成所有任務的最小總時間是多少?
[l, r, w]
紀錄掃描線有多少 w
掃描線!
預先處理一下,先算出每個位置各需要多少資料吧!
預先處理一下,先算出每個位置各需要多少資料吧!
再開一個掃描線!紀錄什麼時候要多扣誰!
[l, r, w]
[l, ∞, w]
紀錄之後要-多少 w
[r+1, ∞, -w]
紅色(的前綴和) - 橘色(的前綴和) 就是你的答案。
紀錄掃描線有 + 多少 w
預先處理一下,先算出每個位置各需要多少資料吧!
再開一個掃描線!紀錄什麼時候要多扣誰!
橘色(的前綴和) - 紅色(的前綴和) 就是你的答案。
from itertools import accumulate
n, m = map(int, input().split())
work = []
needs = [0] * n
for _ in range(m):
l, r, w = map(int, input().split())
needs[l-1] += w
if r != n:
needs[r] -= w
needs = list(accumulate(needs))
Python
int n, m;
scanf("%d%d", &n, &m);
vector<int> table(n), needs(n);
for (int i=0; i<m; i++) {
int l, r, w;
scanf("%d%d%d", &l, &r, &w);
table[l-1] += w;
if (r!=n)
table[r] -= w;
}
// 內建的前綴和,需要
// #include <numeric>
partial_sum(table.begin(),
table.end(), needs.begin());
C++
預先處理一下,先算出每個位置各需要多少資料吧!
真的做掃描線,然後開一個 counting table 在 r 的位置扣掉。
n, m = map(int, input().split())
work = []
for _ in range(m):
l, r, w = map(int, input().split())
work.append((l, r, w))
work.sort(reverse=True)
needs = [0] * (n+2)
minus = [0] * (n+2)
cur = 0
for i in range(1, n+1):
while work and work[-1][0] == i:
l, r, w = work.pop()
cur += w
minus[r+1] = w
cur -= minus[i]
needs[i] = cur
needs = needs[1:-1]
Python
int n, m;
scanf("%d%d", &n, &m);
vector<vector<int>> works(m, vector<int>(3));
for (auto &work : works) {
scanf("%d%d%d", &work[0], &work[1], &work[2]);
}
sort(works.begin(), works.end());
vector<int> needs(n), minus(n);
int cur_need = 0, work_ptr = 0;
for (int i=1; i<=n; i++) {
while (work_ptr != m && works[work_ptr][0] == i) {
auto work = works[work_ptr];
int l = work[0], r = work[1], w = work[2];
if (r != n)
minus[r] += w;
cur_need += w;
work_ptr++;
}
cur_need -= minus[i-1];
needs[i-1] = cur_need;
}
C++
接下來的問題變成什麼樣呢?
有 nnn 台機器,每台機器生產一份資料需要 ttt 單位時間。
有 nnn 個位置,分別需要 x 份資料。
請問要怎麼放機器才可以最小化總時間?
或者寫的數學一點:
怎麼解呢?留給之後吧!
現在 A, B 各有 n 個數字,請排列 A 和 B 使得以下最小化
現在有 n 個數字,數字代表水平面高度。
每個地方可以花 1 秒往左右擴散 1 個高度,
請問擴散到最後需要幾秒?
舉例來說:
耗時 3 秒
[4, 0, 0, 0]
[3, 1, 0, 0]
[2, 1, 1, 0]
耗時 3 秒
[1, 1, 1, 1]
[2, 6, 0, 4]
[2, 5, 2, 3]
[2, 4, 3, 3]
[3, 3, 3, 3]
直觀來說,你會怎麼做?
現在有 n 個數字,數字代表水平面高度。
每個地方可以花 1 秒往左右擴散 1 個高度,
請問擴散到最後需要幾秒?
前處理:
如果 (總和 / n) 除不盡,回傳 -1。
讓最後的水平面變成 0,會比較好看一點。
[4, 0, 0, 0]
[2, 6, 0, 4]
[3, -1, -1, -1]
[-1, 3, -3, 1]
現在有 n 個數字,數字代表水平面高度。
每個地方可以花 1 秒往左右擴散 1 個高度,
請問擴散到最後需要幾秒?
如果 L1 是 4,那就代表一定花四秒往右流 4。
如果 L1 是 -4,那就代表一定花四秒往左流 4。
考慮每個點要往左往右流多少。
大概可以知道掃描線往右然後...
現在有 n 個數字,數字代表水平面高度。
每個地方可以花 1 秒往左右擴散 1 個高度,
請問擴散到最後需要幾秒?
如果 L1+L2+L3 < 0
L4需要往左流補左邊的洞
如果流完左邊 L4 還有剩..
L4需要往右流剩下的水
掃描線由左往右就做可以算完了!
level = sum(machines) // len(machines)
if len(machines) * level != sum(machines):
return -1
height = [machine-level for machine in machines]
ans = 0
for i in range(len(height)):
L_flow, R_flow = 0, 0
# 如果左邊 < 0,表示要優先流左邊
if i and height[i-1] < 0:
L_flow = -height[i-1]
height[i] += height[i-1]
# height[i-1] = 0
# 如果還有剩下的,那麼一定往右流
if height[i] > 0:
R_flow = height[i]
height[i+1] += height[i]
# height[i] = 0
ans = max(ans, L_flow + R_flow)
return ans
Python
int S = 0;
for (int h : machines)
S += h;
if (S % machines.size() != 0)
return -1;
for (int &h : machines)
h -= S / machines.size();
int ans = 0;
for (int i=0; i<machines.size(); i++) {
int L_flow=0, R_flow=0;
// 如果左邊 < 0,優先流左
if (i && machines[i-1] < 0) {
L_flow = -machines[i-1];
machines[i] += machines[i-1];
// machines[i-1] = 0;
}
// 如果還有剩下的,那麼一定往右流
if (machines[i] > 0) {
R_flow = machines[i];
machines[i+1] += machines[i];
// machines[i] = 0;
}
ans = max(ans, L_flow+R_flow);
}
return ans;
C++
你需要放置 k 個基地台在一維城鎮上,目標是想讓所有住家都可以被覆蓋到。
那麼請問基地台的覆蓋直徑最小可以是多少?
(所有基地台的半徑一樣,基地台可以自己設定。)
1
3
11
1
3
11
直徑=10
直徑=2
直徑=2
完全沒概念... 不妨換個角度想想?
直觀來說,你會怎麼做?
放 k 個基地台,
問最小 d 多少才可以覆蓋所有住家?
放 k 個基地台,
給定直徑 d,問可不可以覆蓋所有住家?
1
3
11
5
9
12
原本題目
簡單化題目
直觀來說,你會怎麼做?
知道了簡化版題目,要怎麼解原題呢?
放 k 個基地台,
問最小 d 多少才可以覆蓋所有住家?
放 k 個基地台,
給定直徑 d,問可不可以覆蓋所有住家?
原本題目
簡單化題目
對答案作二分搜!
原題:
最小直徑 d* 是多少?
d = 100 怎麼樣?
T,可以涵蓋。試試看小的
d = 50 怎麼樣?
F,不能涵蓋。試試看大的
d = 75 怎麼樣?
F,不能涵蓋。試試看大的
❓
n, k = map(int, input().split())
A = list(map(int, input().split()))
A.sort()
def trail(d):
last, quota = -d-1, k
for cur in A:
if last + d < cur:
if not quota:
return False
quota -= 1
last = cur
return True
L, R = 0, 10**10
while R-L != 1:
mid = (L+R) // 2
if trail(mid):
R = mid
else:
L = mid
print(R)
#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;
}
C++
Python
通常會把簡化版題目寫成函式,
這樣會比較好看。
背景圖是 Voronoi diagram,
算出兩點的中線交集
掃描線往右移動
已經算出答案,
或者已經維護好東西了
還沒算出答案,
或者東西還沒維護
掃描線就是
一步一步地隨著掃描線的移動
去維護答案。
掃描線往右移動
已經算出答案,
或者已經維護好東西了
還沒算出答案,
或者東西還沒維護
最大子區間和
最小前綴和 / 現在的前綴和
還沒被算的右界
超級洗衣機
已經流完變穩定的洗衣機
還沒穩定的洗衣機
已經被覆蓋到的房子
簡化的基地台
還沒被覆蓋到的房子
通靈解法
觀察題目
可能是
貪心題
想到一個
「可能對」
的解法
構造反例
Edge Case
是錯的!
找不到反例
開始寫!
通靈不出解法
數學分析
自己生測資看看
大膽假設,小心求證
簡單上來說就是有 n 個物品,
你會從裡面找一個順序具有最佳解。
動態規劃通常比較難通常找順序的最佳解,
所以這類題目通常就真的要貪心或暴搜。
簡單上來說就是有 n 個物品,
你會從裡面挑幾個 (沒有順序) 當成是最佳解。
基本上選擇類貪心都會有個動態規劃解,
因為選擇類的題目動態規劃很好出。
簡單上來說就是有 n 個物品,
你會從裡面找一個順序具有最佳解。
怎麼找到順序類貪心的最佳解呢?
接下來讓我們看看順序類貪心
的題目都長怎樣吧!
現在有一堆人要點餐,每個人點的餐有「煮的時間」跟「吃的時間」。
廚師一次只能準備一個餐點,求出最後一個人吃完時的最短時間
舉例來說:
直觀來說,你會怎麼做?
1
1
2
2
3
3
第一個人
第二個人
第三個人
廚師
7
現在有一堆人要點餐,每個人點的餐有「煮的時間」跟「吃的時間」。
廚師一次只能準備一個餐點,求出最後一個人吃完時的最短時間
直觀來說,你會怎麼做?
1. 從準備時間最短的開始做。
因為這樣可以讓客人先吃。
3. 從吃最久的的開始做。
因為這樣可以讓廚師沒事做的時間最小
2. 從準備時間最長的開始做。
因為這樣可以優先處理掉最佔時間的
哪個是對的?
找得出反例嗎?
3. 從吃最久的的開始做。
因為這樣可以讓廚師沒事做的時間最小
#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)
C++
Python
我連想出那三個選項都有問題...
那麼我們來試試從頭推導吧!
如何構造反例?
給定 n 個人,每個人有 C(ook), E(at) 時間,請排出一個順序 σ,使得以下公式最小化:
我們在寫誰先晚餐,都是用猜的再驗證。
啊我就真的連一些假解都通不出來...😭
試試看數學分析吧!
如果交換 (2, 3),中間差了什麼呢?
交換
什麼樣的條件,1234 會比 1324 一樣或更好呢?
只要題目是要求一個最佳順序,
都可以嘗試這樣想!
* 如果前面證不出來
可能是 135 排 246 排
第二種證明 (Optional)
C1
E1
C2
E2
C1
E1
C2
E2
C1
E1
C2
E2
C1
E1
E2
C2
優先做吃
最久的人。
現在有 n 個物品,每個物品有其 f 和 w,
請排出一個順序,使其以下數值越小越好:
直觀來說,你會怎麼做?
舉例來說:
* 你可以看原題了解
f 跟 w 的意義是什麼,
如果你需要的話
現在有 n 個物品,每個物品有其 f 和 w,
請排出一個順序,使其以下數值越小越好:
直觀來說,你會怎麼做?
老實說,這題要直觀想出來 + 有信心 AC
不是每個人都可以做到的,
所以我們嘗試分析看看吧!
目標是一個順序,所以像誰先晚餐一樣分析看看如果換中間的兩個物品,對答案會有什麼樣的影響
如果交換 (2, 3),中間差了什麼呢?
交換
如果交換 (2, 3),中間差了什麼呢?
交換
什麼條件會使得 1234 比 1324 還要好呢?
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
int n, tmp;
scanf("%d", &n);
vector<vector<double>> V;
vector<double> w(n), f(n);
for (int i=0; i<n; i++)
scanf("%lf", &w[i]);
for (int i=0; i<n; i++) {
scanf("%lf", &f[i]);
if (f[i])
V.push_back({w[i]/f[i], w[i], f[i]});
}
sort(V.begin(), V.end());
unsigned long long ans=0, pre_sum=0;
for (auto v : V) {
int w = v[1], f = v[2];
ans += f * pre_sum;
pre_sum += w;
}
printf("%llu\n", ans);
}
n = int(input())
w = list(map(int,input().split()))
f = list(map(int,input().split()))
l = [(w/f, w, f) for w,f in zip(w, f) if f]
l.sort()
ans, prefix = 0, 0
for _, wi, fi in l:
ans += prefix * fi
prefix += wi
print(ans)
C++
Python
小心如果 w, f 是 int,
w / f 也會是 int。
還有小心 int overflow。
我們定義兩數合併為 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 | 1 | 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 | 1 | 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 | 1 | 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 | 1 | 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 | 1 | 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
現在有一堆文字,
怎麼編碼才可以讓總編碼的長度最低呢?
背包問題但 W/V 最大化
http://poj.org/problem?id=2976
或
http://poj.org/problem?id=3111
, 排順序後貪心 + 二分搜
Optional
如果你在選擇的過程中,發現有比較好的點!
那我就反悔!
可以反悔的清單
A
B
C
D
...
A
B
❌
可以選!
可以選!
選不進去!
好像可以把B換掉選C?
可以選!
❌
C
D
通常使用 priority_queue / heap
來找到「哪個最值得被換掉」。
現在有個一維道路。你想開車到 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<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
C++
Python
現有個 n 個計畫,而你初始有 w 塊錢。
每個計劃都有其 c (花費), p (利潤)。
要啟動一個計劃需要花該計畫的 c 來額外獲得 p 塊錢。
如果最多能選 k 個計畫,最多可以賺多少錢?(不計成本)
直觀來說,你會怎麼做?
舉例來說:
開頭必須要做 (0, 1),
獲得1塊錢後做 (1, 3),再獲得 3 塊,總共獲得 4 塊。
現有個 n 個計畫,而你有 w 塊錢。
每個計劃都有其 c (花費), p (利潤)。
要啟動一個計劃就需要花該計畫的 c 來獲得 p 塊錢。
如果你最多能選 k 個計畫,你最多可以賺多少錢?
你可以啟動並且賺最多錢的!
那第二個計劃要選誰呢?
一樣選你可以啟動的項目裡面最多錢的!
向反悔貪心一樣,用PQ來找範圍內最大值!
現有個 n 個計畫,而你有 w 塊錢。
每個計劃都有其 c (花費), p (利潤)。
要啟動一個計劃就需要花該計畫的 c 來獲得 p 塊錢。
如果你最多能選 k 個計畫,你最多可以賺多少錢?
"後悔沒有賺" 的清單
選了!
(0, 1)
(1, 2)
(1, 3)
(2, 3)
w=0
w=1
w=4
選了!
選了!
已選擇的計畫:
vector<pair<int, int>> V;
for (int i=0; i<profits.size(); i++)
V.push_back({capital[i], profits[i]});
sort(V.begin(), V.end());
auto ptr = V.begin();
priority_queue<int> bucket;
for (int i=0; i<k; i++) {
while (ptr != V.end() && w >= ptr->first) {
auto [c, p] = *ptr;
bucket.push(p);
ptr++;
}
if (!bucket.empty()) {
w += bucket.top();
bucket.pop();
}
}
return w;
L = sorted(zip(capital, profits))[::-1]
bucket = []
profit = w
for i in range(k):
while L and profit >= L[-1][0]:
c, p = L.pop()
if p > 0:
heapq.heappush(bucket, -p)
if bucket:
profit -= bucket[0]
heapq.heappop(bucket)
return profit
C++
Python
現有個 n 個計畫,而你有 w 塊錢。
每個計劃都有其 c (花費), p (利潤)。
要啟動一個計劃就需要花該計畫的 c 來獲得 p 塊錢。
如果你最多能選 k 個計畫,你最多可以賺多少錢?
現在有個一維道路。你想開車到 target,但燃料有限。
所幸路上有很多加油站,每個加油站有其位置跟燃料數量。
問你最少加油幾次才可以到target?
仔細想想其實這兩題根本是一樣的。
仔細想想其實這兩題根本是一樣的。
互為對偶問題,所以兩個解法一樣。
所以寫貪心記得要想想自己曾經寫過什麼喔!
真的對 C 做排序是好的嗎?來試試看推導!(補充)
1234 跟 1324 利潤一樣,1234比1324好的條件:
de morgan 定理: A&B&!(C&D) = (A&B&!C) | (A&B&!D)
簡單上來說就是有 n 個物品,
你會從裡面挑幾個 (有順序) 當成是最佳解。
簡單上來說就是有 n 個物品,
你會從裡面挑幾個 (沒有順序) 當成是最佳解。
簡單上來說就是有 n 個物品,
你會從裡面找一個順序具有最佳解。
通常這類題目會先找順序,
有了順序之後再做選擇。
簡單上來說就是有 n 個物品,
你會從裡面挑幾個 (有順序) 當成是最佳解。
通常這類題目會先找順序,
有了順序之後再做選擇。
A
B
D
C
H
E
F
G
為什麼呢?假設有 n 個物品要選
假設最佳解是按照著字典序的
A
C
D
G
A
B
D
C
H
E
F
G
那麼你先對原本 n 個物品排序
A
C
D
G
你再選擇 k 個就可以了。
現在有一堆任務,每個任務有起始時間跟結束時間。你一次只能做一個任務,問最少需要放棄任務的數量?
直觀來說,你會怎麼做?
舉例來說:
[1, 2]
[2, 3]
[3, 4]
[1, 3]
直觀來說,你會怎麼做?
放棄任務的數量...不太好想。
換個角度想:如果是最多可以做多少任務呢?
1. 從任務所花時間最短的開始選,因為他的費時最小。
3. 從任務結束時間最早的開始做,因為早做完就可以快選下一個。
2. 從任務開始時間最早的開始做,早做完早享受。
3. 從任務結束時間最早的開始做,因為早做完就可以快選下一個。
現在有一堆任務,每個任務有起始時間跟結束時間。你一次只能做一個任務,問最少需要放棄任務的數量?
現在有一堆任務,每個任務有起始時間跟結束時間。你一次只能做一個任務,問最多可以做多少任務?
話是這麼說,但怎麼從頭分析呢?
現在有 n 個任務,已經確定這 n 個任務可以全部完成,請你給出這個任務順序。
我們先從簡單版題目來決定順序:
既然任務彼此都是不相交...
那麼對開始或對結束排序都可以吧?
接著讓我們來選原本的題目!
現在有一堆對開始時間做排序的任務表,
你一次只能做一個任務,
問最多可以做多少任務?
既然都是不相交...
那麼對開始或對結束排序都可以吧?
現在有一堆對開始時間做排序的任務表,
你一次只能做一個任務,
問最多可以做多少任務?
[S1, E1]
[S3, E3]
對開始做排序,會不會要拿之前選的二換現在的一呢?
[S2, E2]
如果你需要二換一才可以插入 3,
那麼一定長得像上面這樣。
但是因為 S2 <= S3,
所以絕對不會二換一。
對開始做排序,對 E 做反悔貪心
// saved with (e, s)
sort(intervals.begin(), intervals.end());
priority_queue<pair<int, int>> Q;
for (auto &tmp : intervals) {
int s=tmp[0], e=tmp[1];
if (Q.empty() || s >= Q.top().first) {
Q.push({e, s});
} else if (e <= Q.top().first) {
Q.pop();
Q.push({e, s});
}
}
return intervals.size() - Q.size();
C++
intervals.sort()
Q = [] # Save (-ei, si)
for s, e in intervals:
if not Q or s >= -Q[0][0]:
heapq.heappush(Q, (-e, s))
elif e < -Q[0][0]:
heapq.heappop(Q)
heapq.heappush(Q, (-e, s))
return len(intervals) - len(Q)
Python
現在有一堆對結束時間做排序的任務表,
你一次只能做一個任務,
問最多可以做多少任務?
現在有一堆對結束時間做排序的任務表,
你一次只能做一個任務,
問最多可以做多少任務?
對結束時間做排序呢?
現在有一堆對結束時間做排序的任務表,
你一次只能做一個任務,
問最多可以做多少任務?
對結束時間做排序呢?
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;
C++
Python
現在有 n 個物品,每個物品有其 H 和 L,
請從中選出 k 個物品,排出一個順序,
使其滿足以下條件。( k 越大越好 )
* 你可以看原題了解
H 跟 L 的意義是什麼,
如果你需要的話
舉例來說:
直觀來說,你會怎麼做?
現在有 n 個物品,每個物品有其 H 和 L,
請從中選出 k 個物品,排出一個順序,
使其滿足以下條件。( k 越大越好 )
直觀來說,你會怎麼做?
這題直觀其實有點困難,我們先從順序開始想。
現在有 n 個物品,每個物品有其 H 和 L,
請從中選出 k 個物品,排出一個順序,
使其滿足以下條件。( k 越大越好 )
現在有 n 個物品,每個物品有其 H 和 L,
保證有一個順序可以使以下式子成立。
請找出順序。
簡化版題目:
現在有 n 個物品,每個物品有其 H 和 L,
保證有一個順序可以使以下式子成立。
請找出順序。
簡化版題目:
這種找順序問題我們好像有一套方法可以分析?
我們都是麻瓜,所以我們來試試看吧!
什麼條件下,選2會比選3還要好呢?
考慮 <1, 2, 3, 4> 跟 <1, 3, 2, 4>
交換
什麼條件下,選2會比選3還要好呢?
如果 不成立,那麼 也不成立,
這樣 2 跟 3 都不能選,沒什麼好比較的。
什麼條件下,選2會比選3還要好呢?
成立
不成立
我們已經找出順序了!
不過我們要怎麼解出原題呢?
如果你選了 k 個當成解答,
那麼對 H + L 排序一定可以把這 k 的按照順序放好。
怎麼挑選 k 個?
我們可以先把所有東西按照 H + L 排序後再挑!
補充:如果你覺得刪掉 L2 >= H1 很怪,那就不要刪試試看。
1234 比 1324 好的條件是甚麼?
成立
不成立
你會得到一樣的結論,那就是對 L+H 排序。
我們已經找出順序了!
不過我們要怎麼解出原題呢?
現在有 n 個物品,每個物品有其 H 和 L,
請從中選出 k 個物品,排出一個順序,
使其滿足以下條件。( k 越大越好 )
好像可以動態規劃了!
DP[n, k] = 在前n個物品中,選出k個的最小高度
怎麼定義遞迴呢?
Hint: 回憶一下 LIS 的題目吧!
int n, h, l;
scanf("%d", &n);
vector<pair<int, int>> V;
vector<int> DP(n+1, INT_MAX);
DP[0] = 0;
for (int i=0; i<n; i++) {
scanf("%d%d", &h, &l);
V.push_back({h+l, h});
}
int ans = 0;
sort(V.begin(), V.end());
for (auto [lim, h] : V) {
for (int k=n; k>=1; k--) {
if (DP[k-1] <= lim-h) {
DP[k] = min(DP[k], DP[k-1] + h);
ans = max(ans, k);
}
}
}
printf("%d\n", ans);
n = int(input())
L = []
for _ in range(n):
h, l = map(int, input().split())
L.append((h, l))
L.sort(key=lambda x: x[0] + x[1])
DP = [0] + [float('inf')] * n
ans = 0
for h, l in L:
for k in range(n, 0, -1):
if DP[k-1] <= l:
DP[k] = min(DP[k], DP[k-1]+h)
ans = max(ans, k)
print(ans)
C++
O(n^2) 會 TLE ... (但還是有90%)
有沒有其他方法呢?
Python
(b 是被你選的人之中 H 最高的那個)
import heapq
n = int(input())
L = []
for _ in range(n):
l, h = map(int, input().split())
L.append((h, l))
L.sort(key=lambda x: x[0] + x[1])
pq = []
ans, cur_h = 0, 0
for l, h in L:
if l >= cur_h:
cur_h += h
heapq.heappush(pq, -h)
ans += 1
elif -pq[0] > h:
cur_h += h - (-pq[0])
heapq.heappop(pq)
heapq.heappush(pq, -h)
print(ans)
int n, h, l;
scanf("%d", &n);
vector<pair<int, int>> V;
for (int i=0; i<n; i++) {
scanf("%d%d", &h, &l);
V.push_back({h+l, h});
}
sort(V.begin(), V.end());
priority_queue<int> PQ;
int ans = 0, cur_h=0;
for (auto [lim, h] : V) {
if (lim-h >= cur_h) {
PQ.push(h);
cur_h += h;
ans += 1;
} else if(PQ.top() > h) {
PQ.pop();
PQ.push(h);
cur_h += h - PQ.top();
}
}
printf("%d\n", ans);
C++
Python
甚至 2021 年有人出了一個證明論文
基本上題目都會是給你兩群東西,
請你找出誰跟誰配隊會有最佳解。
A 群
B 群
基本上題目都會是給你兩群東西,
請你找出誰跟誰配隊會有最佳解。
通常這類題目每一堆只會有一個數字,
所以通常都是兩堆先排序,
不是順著就是逆著配對。
現在有一堆餅乾跟一堆小孩,
餅乾有各自的「滿足度」,小孩有各自的「貪心度」。
如果小孩吃的餅乾「滿足度」大於等於自己的「貪心度」,他會很開心。
每個小孩最多只能給一片餅乾,請問你最多可以讓幾位小孩開心?
舉例來說:
直觀來說,你會怎麼做?
現在有一堆餅乾跟一堆小孩,
餅乾有各自的「滿足度」,小孩有各自的「貪心度」。
如果小孩吃的餅乾「滿足度」大於等於自己的「貪心度」,他會很開心。
每個小孩最多只能給一片餅乾,請問你最多可以讓幾位小孩開心?
直觀來說,你會怎麼做?
1. 從貪心度最小的餅乾開始給,
優先滿足最不貪心的小孩。
因為不貪心的小孩最好被滿足。
2. 從貪心度最大的餅乾開始給,
優先滿足最貪心的小孩。
因為可以保證餅乾可以被
最適合的貪心小孩吃掉
1. 從貪心度最小的餅乾開始給,
優先滿足最不貪心的小孩。
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!
sort(g.rbegin(), g.rend());
sort(s.rbegin(), s.rend());
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;
C++
Python
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!
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
C++
Python
優先滿足最不貪心的小孩。
?
你能證明看看第二個方法的正確性嗎?
有 nnn 台機器,每台機器生產一份資料需要 ttt 單位時間。
這些機器需要被放置在位置 [1,n][1, n][1,n],每個位置只能放一台機器。
有 mmm 個任務,每個任務指定一個範圍 [l,r][l, r][l,r],
要求範圍內的每台機器都生產 www 份資料。
請問完成所有任務的總時間是多少?
舉例來說:
直觀來說,你會怎麼做?
🖥️D
🖥️A
🖥️B
🖥️C
🖥️E
要求0秒
要求1秒
要求2秒
要求3秒
要求0秒
答案=1+2+3=6
🖥️A 1
🖥️B 2
🖥️C 3
🖥️D 4
🖥️E 5
產生一單位
需要的秒數
1
2
3
4
5
任務:範圍 [2,4] 要求 1單位
[l, r, w]
紀錄掃描線有多少 w
掃描線!
預先處理一下,先算出每個位置各需要多少資料吧!
接下來的問題變成什麼樣呢?
有 nnn 台機器,每台機器生產一份資料需要 ttt 單位時間。
有 nnn 個位置,分別需要 x 份資料。
請問要怎麼放機器才可以最小化總時間?
或者長這樣:
怎麼解呢?
現在 A, B 各有 n 個數字,請排列 A 和 B 使得以下最小化
試試通靈!
感覺上應該是大的 * 小的總合會比較小。
可是我不會通靈 😭...
老樣子,試試看數學分析吧!
現在 A, B 各有 n 個數字,請排列 A 和 B 使得以下最小化
現在 A, B 各有 n 個數字,請排列 A 和 B 使得以下最小化
老樣子,試試看數學分析吧!
考慮 n = 2,什麼條件下 A1 會跟 B1 配,A2 會跟 B2 配呢?
考慮 n = 2,什麼條件下 A1 會跟 B1 配,A2 會跟 B2 配呢?
現在 A, B 各有 n 個數字,請排列 A 和 B 使得以下最小化
先對 A 排序應該不影響題目。
那麼 B 應該就會是反著排序,因為可以一直交換。
現在 A, B 各有 n 個數字,請排列 A 和 B 使得以下最小化
考慮 n = 2,什麼條件下 A1 會跟 B1 配,A2 會跟 B2 配呢?
// include 省略
using namespace std;
int main() {
int n, m;
scanf("%d%d", &n, &m);
vector<int> table(n), needs(n);
for (int i=0; i<m; i++) {
int l, r, w;
scanf("%d%d%d", &l, &r, &w);
table[l-1] += w;
if (r!=n)
table[r] -= w;
}
partial_sum(table.begin(), table.end(), needs.begin());
vector<int> machines(n);
for (int i=0; i<n; i++)
scanf("%d", &machines[i]);
sort(needs.rbegin(), needs.rend());
sort(machines.begin(), machines.end());
long long ans = 0;
for (int i=0; i<n; i++) {
ans += 1ULL * needs[i] * machines[i];
}
printf("%lld", ans);
return 0;
}
from itertools import accumulate
n, m = map(int, input().split())
work = []
needs = [0] * n
for _ in range(m):
l, r, w = map(int, input().split())
needs[l-1] += w
if r != n:
needs[r] -= w
needs = list(accumulate(needs))
machines = list(map(int, input().split()))
machines.sort(reverse=True)
needs.sort()
ans = 0
for a, b in zip(machines ,needs):
ans += a * b
print(ans)
C++
Python
貪心選擇,但保留後路
很難,可惜 APCS 不會考
如果你在選擇的過程中,發現有比較好的點!
那我就反悔!
在反悔貪心 I 中,我們會將直接將反悔選項
放到堆裡面,等著我們反悔。
在反悔貪心 II 中,我們無法直接將反悔選項
放到堆裡面,我們必須要事先處理反悔選項,才可以把它放進反悔堆內。
接著來讓我們看怎麼個「事先處理」吧!
接下來的兩題有點難,不過這個難度 APCS 絕對不會考 (但比賽有可能),所以就當作課外聽聽看就好!
Pizza with 3n slices (leetcode 1388)
標程是 DP O(n²),但可以貪心 O(nlogn)
有 3nnn 片圓形的 Pizza,每片的大小都不一樣,
要分給你,Alice 和 Bob 三個人。
你優先選擇其中一片 Pizza,Alice,Bob就會選擇你那片的兩側。
總共你會選擇 n 片,此時 3n 片 Pizza 就會分完。
問你怎麼選才可以使吃到的 Pizza 最多?
舉例來說:
最佳解: 拿了 4 + 6 = 10
最佳解: 拿了 8 + 8 = 16
有 3nnn 片圓形的 Pizza,每片的大小都不一樣,
要分給你,Alice 和 Bob 三個人。
你優先選擇其中一片 Pizza,Alice,Bob就會選擇你那片的兩側。
總共你會選擇 n 片,此時 3n 片 Pizza 就會分完。
問你怎麼選才可以使吃到的 Pizza 最多?
這題上本質其實就是在
一個陣列選 n 個數字,
但帶有些限制。
你覺得條件是甚麼呢?
[8, 9, 8, 6, 1, 1]
不能選相鄰的數字。
而這其實就是最緊條件
證明:選(包含頭尾)不相鄰的數字 = 合法的披薩選法
證明 → (數歸 + 構造法)
選
不選
選
證明 ← : Trivial
得證:選不相鄰的數字 ⇔ 合法的披薩選法。
現在有 3nnn 個數字陣列 A,請最大化你選的 n 個數字。
你選的數字不能相鄰,包含頭尾。
想到這裡其實你就已經有一個DP解了。(不考慮頭尾同時選)
DP[n][k][c] = 前 n 個數字中,選 k 個數字,有沒有選到第n個數字 (有的話 c = 1,否則 c = 0)
不過要怎麼處理頭尾不同時選呢?
現在有 3nnn 個數字陣列 A,請最大化你選的 n 個數字。
你選的數字不能相鄰,包含頭尾。
想到這裡其實你就已經有一個DP解了。
但這題其實有貪心解!他可以做到
k = 2, [8, 9, 8, 6, 1, 1]
如果我們按照排序來選數字呢?
k = 1, [8, 9, 8, 6, 1, 1]
這不是不行嗎...?
看起來沒有貪心選擇的性質啊?
現在有 3nnn 個數字陣列 A,請最大化你選的 n 個數字。
你選的數字不能相鄰,包含頭尾。
k = 2, [8, 9, 8, 6, 1, 1]
k = 1, [8, 9, 8, 6, 1, 1]
看起來沒有貪心選擇的性質啊?
有沒有機制讓我們反悔呢...?
假設最大值是 9,那麼只會有兩種狀況
現在有 3nnn 個數字陣列 A,請最大化你選的 n 個數字。
你選的數字不能相鄰,包含頭尾。
int n = slices.size() / 3, ans = 0;
while (n--) {
auto m = max_element(slices.begin(), slices.end());
auto r = (m + 1 == slices.end() ? slices.begin() : m + 1);
auto l = (m == slices.begin() ? slices.end() : m - 1);
ans += *m;
*m = *l + *r - *m;
if (l < r)
slices.erase(r), slices.erase(l);
else
slices.erase(l), slices.erase(r);
}
return ans;
ans = 0
n = len(slices) // 3
for _ in range(n):
idx = slices.index(max(slices))
ans += slices[idx]
slices[idx] = slices[idx-1] + \
slices[(idx+1) % len(slices)] - slices[idx]
if idx == len(slices)-1:
del slices[idx-1]
del slices[0]
else:
del slices[idx+1]
del slices[idx-1]
return ans
C++
Python
這不是 嗎...
選 C = 扣掉 BCD 的最佳解 + C
證明: [..., A, B, C, D, E, F, ...] 中,
其中 C 為最大值,那麼最佳解只會有兩種狀況
選 D 不選 B : 扣掉 BCDE 的最佳解 + D
上面比較大,
因為有更多選擇
上面比較大,
因為前提 C >= D
選 B 不選 D 同理。
所以如果你不選 C,你一定會同時選 C 的兩側
按照同樣的邏輯,如果 B + D - C > A, E 但 B D 不在最佳解,那麼 A C E 一定在最佳解。
選 BD = 扣掉 ABCDE 的最佳解 + (B + D)
證明: [...Y, A, B, C, D, E, F, X...] 中,
其中 B + D - C > A, E,那麼最佳解只會有兩種狀況
選 CE 不選 A : 扣掉 ABCDEF 的最佳解 + (C + E)
上面比較大,
因為有更多選擇
上面比較大,
因為前提
選 AC 不選 E 同理。
這也就表示著你的反悔總是在反轉黑白交替的鍊。
選變不選 / 不選變選
如果兩個黑白相間的鍊碰再一起呢?自己證明看看吧!
這不是 嗎...
其實我們可以使用 Doubly Linked List + Priority Queue 來實作這個功能!
8
9
8
6
1
1
Doubly Linked List
Priority Queue / heap
[(9, 1), (8, 0), (8, 2), (6, 3), (1, 4), (1, 5)]
struct Node{
int l, r, v;
};
int n = slices.size() / 3, ans = 0, cnt = 0;
// Build Circular Doubly Linked List
int top = 3*n;
vector<Node> LR(4*n+1);
priority_queue<pair<int, int>> PQ;
for (int i=0; i<3*n; i++) {
LR[i] = {i ? i-1 : 3*n-1, // l
(i == 3*n-1) ? 0 : i+1, // r
slices[i]}; // v
PQ.push({slices[i], i});
}
while (cnt != n) {
auto [_, m] = PQ.top();
PQ.pop();
if (LR[m].v == -1)
continue;
auto [l, r, v] = LR[m];
int ll=LR[l].l, rr=LR[r].r;
ans += v, cnt += 1;
int new_v = LR[l].v + LR[r].v - v;
LR[ll].r = LR[rr].l = top;
LR[top] = {ll, rr, new_v};
LR[l].v = LR[m].v = LR[r].v = -1;
PQ.push({new_v, top++});
}
return ans;
n = len(slices) // 3
# Build Circular Doubly Linked List
LR = [None] * (4*n+1)
for i in range(3*n):
LR[i] = [i-1, i+1, slices[i]]
LR[0][0] = 3*n-1
LR[3*n-1][1] = 0
# Make Heap
q = [(-x, i) for i, x in enumerate(slices)]
heapq.heapify(q)
ans = []
top = 3*n
while len(ans) != n:
v, m = heapq.heappop(q)
if LR[m] is None:
continue
l, r, v = LR[m]
ans.append(v)
new_v = LR[l][2] + LR[r][2] - v
ll, rr = LR[l][0], LR[r][1]
LR[ll][1], LR[rr][0] = top, top
LR[top] = [ll, rr, new_v]
LR[l] = LR[m] = LR[r] = None
heapq.heappush(q, (-new_v, top))
top += 1
return sum(ans)
C++
Python
你可以真的寫一個 Linked List,但我好懶
Best Time to Buy and Sell IV (leetcode 188)
標程是 DP O(nk),但可以貪心 O(n) / O(nlogn)
AI-666 賺多少 (2017 學科全國賽 - P6, tioj 2039)
根據出題者說,這題標程就是貪心 O(n) / O(nlogn),
但一票人都用 DP + Aliens 優化 O(nlogC)
給定數列 P(第 i 天的股票價格)和整數 k,
求最多進行 k 筆買賣時的最大利潤。
此外,你只能同時持有一張股票。
舉例來說:
(4 - 1) = 3
Prices = [1,3,2,4]
k = 1
[1,3,2,4]
(3 - 1) + (4 - 2) = 4
k = 2
[1,3,2,4]
直觀來說,你會怎麼做?
好像真的是太難了...
好像真的是太難了...
先從簡單的開始想
限制 k = 1
"最大子區間和" 的弱化版。
利用掃描線紀錄:
m = prices[0]
a = 0
for p in prices:
m = min(m, p)
a = max(a, p-m)
return a
Python
給定數列 P(第 i 天的股票價格)和整數 k,
求最多進行 k 筆買賣時的最大利潤。
此外,你只能同時持有一張股票。
k = ∞,可以當天賣了又買
ans = 0
for i in range(1, len(prices)):
if prices[i-1] < prices[i]:
ans += prices[i] - prices[i-1]
return ans
Python
給定數列 P(第 i 天的股票價格)和整數 k,
求最多進行 k 筆買賣時的最大利潤。
此外,你只能同時持有一張股票。
如果我們可以當天賣了又買,
那麼其實我們可以考量昨天跟今天。
那麼如果你想要解原題該怎麼辦呢?
我們「大概」會從賺最多的開始選。
給定數列 P(第 i 天的股票價格)和整數 k,
求最多進行 k 筆買賣時的最大利潤。
此外,你只能同時持有一張股票。
我們「大概」會從賺最多的開始選。
等等,這是對的嗎?
A
B
C
如果事先處理掉這種 Case 呢?
那就沒有其他 Case 嗎?
考慮所有相鄰的兩次交易
≈
≈
v: valley 山谷
p: peak 山峰
考慮所有相鄰的兩次交易
≈
符合原本先選大的貪心!
但 (v1, p1) 有可能之後合併,所以等後面處理。
≈
考慮所有相鄰的兩次交易
≈
≈
≈
≈
直接合併
合併 + 反悔
等後面合併
放棄 (v₀, p₀)
等後面合併
考慮所有相鄰的兩次交易
實際做做看吧!
Prices = [1, 5, 2, 3, 6, 0, 2]
等待合併的
不等合併的
(1, 5)
(2, 3)
(3, 6)
(0, 2)
(2, 6)
(1, 6)
Rule 1: 合併
Rule 2: 合併 + 反悔
(2, 5)
Rule 3: 等待
(1, 6)
從 (1, 6), (2, 5), (0, 2) 按照賺最多的開始選。
stack<pair<int, int>> S;
vector<int> options;
for (int i=1; i<prices.size(); i++) {
if (prices[i-1] < prices[i])
S.push({prices[i-1], prices[i]});
while (S.size() >= 2) {
auto [v1, p1] = S.top(); S.pop();
auto [v0, p0] = S.top(); S.pop();
if (p0 <= v1) {
// Case 1: 合併
S.push({v0, p1});
} else if (v0 >= v1) {
// Case 4: 踢掉 (v0, p0)
options.push_back(p0 - v0);
S.push({v1, p1});
} else if (p1 >= p0) {
// Case 2: 合併 + 反悔
options.push_back(p0 - v1);
S.push({v0, p1});
} else {
// Case 3: 不做事,等待下次合併
S.push({v0, p0});
S.push({v1, p1});
break;
}
}
}
while (!S.empty()) {
auto [v, p] = S.top();
S.pop();
options.push_back(p - v);
}
sort(options.begin(), options.end(), greater{});
int ans = 0;
for (int i=0; i<k && i < options.size(); i++)
ans += options[i];
return ans;
pairs = deque()
regret = []
for i in range(len(prices)-1):
v1, p1 = prices[i], prices[i+1]
# 如果價格變差,跳過
if v1 >= p1:
continue
pairs.append([v1, p1])
while len(pairs) >= 2:
(v0, p0), (v1, p1) = pairs[-2], pairs[-1]
# Case 1: 合併
if v0 <= p0 <= v1 <= p1:
pairs[-2][1] = p1
pairs.pop()
# Case 2: 合併 + 反悔
elif v0 <= v1 <= p0 <= p1:
pairs[-2][1] = p1
pairs.pop()
regret.append([v1, p0])
# Case 3: 等待下次合併 (直接跳掉)
elif v0 <= v1 <= p1 <= p0:
break
# Case 4: 踢掉 (v0, p0)
elif v0 >= v1:
del pairs[-2]
regret.append([v0, p0])
# 選最大的 k 個總和
profits = sorted([b-a for a, b in regret + list(pairs)], reverse=True)
return sum(profits[:k])
C++
Python
你可能會想說:
雖然前面是 O(n) 處理沒錯,
但最後一步不是要 sort 後選前 k 個加起來嗎?
sort 應該是 O(n log n)?
其實,給定一個未排序的序列找出第 k 大數字是 O(n)
這個算法叫做 Quick Select (一個非常麻煩的演算法)
使用這個方法後,用 for 迴圈把比第 k 個數字還要小的加起來就是答案了。
Quick Select 的 C++ built-in function
k = min(k, (int)options.size());
nth_element(options.begin(), options.begin()+k, options.end(), greater{});
int ans = 0;
for (int i=0; i<k; i++)
ans += options[i];
return ans;
Python 沒有,自己寫吧🙃
給定數列 P(第 i 天的股票價格)和整數 k,
求最多進行 k 筆買賣時的最大利潤。
此外,你只能同時持有一張股票。
仔細想想其實有 DP 解,時間複雜度
DP[n][k][c] = 前 n 天買了 k 次,有(c=1)沒有(c=0)持股時的最大利潤
雖然DP比較好想但貪心複雜度是 ,快很多!
不過這題 DP 可以優化,叫做 Aliens 優化,可以做到 O(N log P)
int dp[1000][101][2] = {}, visit[1000][101][2] = {};
int rec(vector<int>& prices, int n, int k, bool c) {
if (n == -1 && k == 0 && c == 0)
return 0;
if (n == -1 || k == -1)
return INT_MIN + 1000;
if (visit[n][k][c])
return dp[n][k][c];
visit[n][k][c] = true;
if (c == 1)
return dp[n][k][c] = max(
rec(prices, n-1, k-1, 0)-prices[n],
rec(prices, n-1, k, 1));
else
return dp[n][k][c] = max(
rec(prices, n-1, k, 1)+prices[n],
rec(prices, n-1, k, 0));
}
int maxProfit(int k, vector<int>& prices) {
int ans = 0;
for (int i=0; i<=k; i++)
ans = max(ans, rec(prices, prices.size()-1, i, 0));
return ans;
}
C++
def maxProfit(self, k: int, prices: List[int]) -> int:
dp = {}
def rec(prices, n, k, c):
if n == -1 and k == 0 and c == 0:
return 0
if n == -1 or k == -1:
return float('-inf')
if (n, k, c) not in dp:
if c == 1:
dp[n, k, c] = max(
rec(prices, n-1, k-1, 0) - prices[n],
rec(prices, n-1, k, 1)
)
else:
dp[n, k, c] = max(
rec(prices, n-1, k, 1) + prices[n],
rec(prices, n-1, k, 0)
)
return dp[n, k, c]
ans = 0
for i in range(0, k+1):
ans = max(ans, rec(prices, len(prices)-1, i, 0))
return ans
Python
通靈解法
觀察題目
可能是
貪心題
想到一個
「可能對」
的解法
構造反例
Edge Case
是錯的!
找不到反例
開始寫!
通靈不出解法
數學分析
自己生測資看看
主題 | 題目名稱 | 大概作法 |
---|---|---|
掃描線貪心 | 線段覆蓋長度 (APCS) | 掃描線紀錄當前覆蓋到哪。 |
最大子區間和 | 先算出區間和,再想辦法對區間和掃描。 | |
最大周長三角形 |
數學推導,觀察從最大開始掃描。 |
|
生產線 (APCS) | 跟線段覆蓋長度作法差不多。 | |
超級洗衣機 | 維護掃描線前的性質 (已經被擴散完)。 | |
基地台 (APCS) | 簡化問題後,用掃描線判斷 r 是否夠大 + 對 r 二分搜尋。 | |
順序類貪心 | 誰先晚餐 | 盲猜吃最久的先做。或者利用交換判別誰該放前面。 |
物品堆疊 (APCS) | 基本上只能數學推導,很難通靈。 | |
Add All | 通靈每次都使用最小的兩個數字,需要用 Priority Queue。 | |
反悔貪心 | 加油站問題 | 將之後有可能會反悔的選項,利用反悔堆 (PQ) 紀錄起來 |
IPO | 加油站的對偶問題,做個問題轉換後題目就等於加油站。 | |
順序+選擇類貪心 | 不相交區間 | 利用數學推導推出順序,再思考需不需要反悔貪心。 |
湖畔大樓 | 利用數學推導推出順序,再思考如何反悔貪心。 | |
匹配類貪心 | 餅乾分配 | 觀察條件最嚴苛的變數 (最難被滿足的小孩),以此推規律 |
生產線 (APCS) | 數學推導判斷怎麼匹配,或者依照算幾不等式的直覺。 | |
反悔貪心 II | 分 Pizza 問題 | 將題目轉變後,思考如果選擇一個選項,想反悔要怎麼辦。 |
買賣股票問題 | 同 Pizza 問題,想想甚麼條件下可以做出反悔選項。 |
題目名稱 | 來源 | 備註 |
---|---|---|
Product of Digits | Zerojudge d418 |
題目名稱 | 來源 | 備註 |
---|---|---|
支點切割 | APCS 2018 / 2 - 3, zj h028 | 用區間和做轉移 |
找最接近 k 的矩形和 | Leetcode 363 | 做二維的區間和是 O(n^4) |
投資遊戲 | zj m373 | APCS 2023/10 - 4 (40%) 100% 用貪心也可以但難 |
Jump Game | Leetcode 55 | |
Jump Game II | Leetcode 45 | |
Set Intersection Size at least two | Leetcode 757 | |
砍樹 | APCS 2020 / 1 - 3, zj h028 | 類洗衣機 |
Split Array Largest Sum | Leetcode 410 | +二分搜 |
雙子大廈 TwinTower | TIOJ 1320 | IOI Warmup 3 |
題目名稱 | 來源 | 備註 |
---|---|---|
Largest Number | Leetcode 179 | 經典題 |
髮廊服務優化問題 | zj l243 | 2021 全國學科 pA |
筆電販賣機 | NPSC 題目 |
題目名稱 | 來源 | 備註 |
---|---|---|
Least Cost Bracket Sequence | TIOJ 1708 | Codeforce 3D |
題目名稱 | 來源 | 備註 |
---|---|---|
機器出租 | Zerojudge j608 |
APCS 2023 / 1 - 4,選K個不相交區間 |
Course Schedule III | Leetcode 630 | 經典題,任務要求是時長+終點 |
烏龜塔問題 | zj f347 |
題目名稱 | 來源 | 備註 |
---|---|---|
The Bus Driver Problem | Zerojudge e538 | 非常經典的題目! |
Two city scheduling | Leetcode 1029 | |
Advantage Shuffle | Leetcode 870 |
題目名稱 | 來源 | 備註 |
---|---|---|
Largest Merge Of Two Strings | Leetcode 1754 | 常見的類型 |
給你一個直方圖,求其中的最大矩形面積。
舉例來說:
直觀來說,不考慮實作,你會怎麼做?
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜掃描?
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜掃描?
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
觀察: 如果我們看到最小的數字,那麼以他為準的答案就是 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));
}
C++ 線段樹 Code
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());
}
};
C++ Main Code
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜掃描?
給你一個直方圖,求其中的最大矩形面積。
從最小高度開始
[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
給你一個直方圖,求其中的最大矩形面積。
像逆序數對一樣,拆兩半後分成三種 Case
類雙指針(?) 通常簡單 DC 題
就是會用雙指針處理
給你一個直方圖,求其中的最大矩形面積。
直觀來說,不考慮實作,你會怎麼做?
暴力搜尋它?
有沒有一個比較好的順序去爆搜?
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。
這是為了結算所有的答案。