HDU6438 Buy and Resell 解題報告(一個有趣的貪心問題的嚴格證明)
寫在前面
此題是一個很容易想到的貪心題目,但是正確性的證明是非常復雜的。然而,目前網上所有題解並未給出本題貪心算法的任何正確性證明,全部僅停留在描述出一個貪心算法。本著對算法與計算機科學的熱愛(逃),我花了2周時間深入研究了這個問題,並請教了Apass.Jack 大牛,終於在他的幫助下證明了該貪心的正確性。接下來將給出詳細地證明過程。
PS:Apass.Jack提供了整個證明框架(盡管後來被我發現了一處錯誤並重新修正了證明),在此表示感謝!
題目描述
給定$n$($n \le 10^5)$個城市的物品價格,每個城市只能最多購買或賣出一個物品,可以既不買也不賣。當然,若當前手上沒有物品則不能賣出。問從城市1走到城市$n$,途中進行買賣後最多賺多少錢,在此前提下最少進行多少次交易。初始手上沒有任何物品,但有無限的錢。
Sample Input
3 4 1 2 10 9 5 9 5 9 10 5 2 2 1
Sample Output
16 4 5 2 0 0
算法描述
我們先不考慮最小化交易次數,只考慮利潤最大化。本題有一個比較好想的貪心算法:
從左往右遍歷城市,並維護每個城市當前的狀態(買了物品、賣了物品或什麽都不做)。對於城市$i$,我們找1到$i-1$中價格最低的$j$且$j$城市未買入(可以賣出),如果$j$城市價格比$i$城市低,則在$j$城市買入並在$i$城市賣出。同時更新$j$城市的買賣狀態:若之前狀態是賣出,則更新為什麽都不做;否則更新為買入。
這樣遍歷完1到$n$即可得出答案。實現時,顯然可用優先隊列維護。這樣時間復雜度$O(n\log n)$。
算法正確性證明
基本術語
為表述方便,引入以下記號:
(1)用$p_i$表示城市$i$的價格;
(2)一個策略定義為每個城市的買賣狀態集合,用$s_i$表示。其中,$s_i=0$表示什麽都不做;$s_i=1$表示買入;$s_i=-1$表示賣出。
(3)定義$s$的前綴和為$h_s(i)=\sum_{i=1}^n {s_i}$,並補充$h_s(0)=0$;顯然一個方案是合法的,當且僅當對任意$1 \le i \le n$,$h_s(i) \ge 0$。
(4)定義$(i,j)$表示一個升序二元組$(i < j)$;
(5)稱$(i,j)$被策略$s$分開,或稱在策略$s$中$(i,j)$不可能屬於同一次買賣,如果存在$i \le k < j$,使得$h_s(k)=0$。
證明思路
此題直接證明是非常困難的,因為它會動態修改之前的策略。我們考慮引入一個中間步驟,即得出最優解滿足的充要性質,然後證明貪心的解也滿足這個性質,那麽貪心就自然是正確的了。
接下來將證明以下幾個主要定理,分別建立最優解的性質以及貪心解的性質。
另外為了簡化證明,以下部分均假設所有$p_i$互不相同。顯然如果$p_i$互不相同時算法正確,那麽$p_i$可以相同時算法也必然正確(通過取極限)。
定理1
如果一個策略$s$是最優策略,則$h_s(n)=0$,且對於任意$(i,j)$一定滿足:
(1)若$s_i=s_j=0$或$s_i<s_j$,必有$p_i>p_j$。
(2)若$s_i=s_j=0$或$s_i>s_j$,且$p_i>p_j$,$(i,j)$必然被分開。
證明:$h_s(n)=0$顯然。
(1)若不然,我們將$s_i$加1,將$s_j$減1,顯然對任意$i$有$h_s(i) \ge 0$,因此是合法方案,但利潤更大了,矛盾。
(2)若不然,則對任意$i \le k<j$有$h_s(k)\ge1$。我們令$s_i$減1,$s_j$加1,顯然對任意$i$,$h_s(i)\ge 0$仍成立,因此是合法方案,但利潤更大了,矛盾。
定理2
滿足定理1的策略必然由貪心算法給出。
證明:用數學歸納法,$n=1$顯然成立。
考慮$n$時滿足定理1的一個策略,由於$h_s(n)=0$故必有$s_n=0$或$s_n=-1$。
(1)若$s_n=0$,那麽該策略在前$n-1$個城市中滿足定理1策略,已由貪心算法給出。當貪心算法到第$n$個城市時,它一定什麽都不做,若不然,必存在$p_j<p_n$且$s_j \le 0$。那麽二元組$(j,n)$和定理1(1)矛盾。
(2)若$s_n=-1$。設$k$是$0 \le k<n$中滿足$h_s(k)=0$的最大的$k$,$j$為$k<j\le n$中$s_j \ge 0$且價格最高的城市。
考慮把$s_j$減1而其它不變,這樣我們得到一個新策略,將其記為$w$,那麽新的策略$h_w(n-1)=0$且$h_w$恒非負,此時$w_1$到$w_{n-1}$便是前$n-1$個城市的一個合法方案。下證它滿足定理1的條件。
註意一個性質:如果$(i,j)$在$s$中被分開,在$w$中一定被分開,因為$w$只有$j$處比$s$小1,其它值都相同。利用該性質,對於策略$w$,任何不包含$j$的二元組必然滿足定理1的條件;我們只需考慮包含$j$的二元組是否滿足定理1條件即可。
對於定理1(1):
a):若$(i,j)$滿足$w_i=w_j=0$或$w_i<w_j$,必有$s_i<s_j$,由於$s$滿足定理1(1)故$p_i>p_j$;
b):若$(j,i)$滿足$w_j=w_i=0$或$w_j<w_i$,那麽$s_i=w_i \ge 0$,由於$j$是$k$之後$s$非負的價格最高者,故$p_j>p_i$。
對於定理1(2):
a):若$(i,j)$滿足$w_i>w_j$或$w_i=w_j=0$,且$p_i>p_j$,那麽$s_i=w_i \ge 0$,由於$j$是$k$之後$s$非負的價格最高者,故必有$i \le k$,故$(i,j)$被$s$分開;
b):若$(j,i)$滿足$w_j>w_i$或$w_i=w_j=0$,且$p_j>p_i$,那麽$s_j>s_i$,由定理1(2)$(j,i)$在$s$策略中被分開,這與$k$是最大的$h_s(k)=0$的城市矛盾,不會有這種情況。
綜上,$w$是滿足定理1的策略,由歸納知必然是貪心算法前$n-1$個城市的執行結果。最後考慮算法在第$n$個城市的操作。
首先必有$p_j<p_n$,若不然由$s_j \ge 0 >-1=s_n$但$(j,n)$並未在$s$中被分開可導出和定理1(2)的矛盾。而$w_j \le 0$,故算法必然會在$n$城市賣出(因為可以在$j$買入且能增大利潤),下面只需考慮是不是一定買入$j$城市。設算法買入的城市為$t$,且$p_j>p_t$,那麽$s_t \le 0$而$s_j \ge 0$,那麽$t$不能在$j$之前(否則與定理1(1)矛盾)。同樣$t$也不能在$j$之後(否則由定理1(2)$(j,t)$被$s$分開,與$k$是最大的$h_s(k)=0$的城市矛盾),因此買入的城市$t=j$。因此滿足定理1的策略確實完全由貪心算法給出。證畢。
定理3
滿足定理1的策略是唯一的,從而貪心算法正確。
證明:由於定理1的策略必由貪心算法給出,而貪心算法給出的策略只有一個,故滿足定理1的策略是唯一的。又由定理1,最優策略滿足定理1,故算法是正確的。
最小化交易次數
根據上面定理,當價格互不相同時最優策略是唯一的,但價格可以相同時則不一定,此時才存在最小化交易次數的問題。此時考慮修改算法,當優先隊列中最低價格不止一個時,優先取原本賣出物品的城市,這樣該城市的狀態會被修改成什麽都不做。這個正確性很顯然。對於價格相同的物品,它們對於後續城市是完全等價的,因此盡可能的將它們變成不買也不賣必然達到最小交易次數。
綜上,這道題目就完全解決了。
參考代碼
1 #include<cstdio> 2 #include<queue> 3 using namespace std; 4 int p[100001]; 5 struct Node{ 6 int i; 7 bool sell; 8 bool operator < (const Node t)const{ 9 return p[i] != p[t.i] ? p[i] > p[t.i] : !sell && t.sell; 10 } 11 }; 12 int main() 13 { 14 int test, n; 15 scanf("%d", &test); 16 while (test--){ 17 priority_queue<Node> q; 18 scanf("%d", &n); 19 for (int i = 0; i < n; i++) 20 scanf("%d", &p[i]); 21 long long ans = 0; 22 int cnt = 0; 23 q.push({ 0, 0 }); 24 for (int i = 1; i < n; i++){ 25 int j = q.top().i; 26 if (p[j] < p[i]){ 27 bool sell = q.top().sell; 28 q.pop(); 29 ans += p[i] - p[j]; 30 if (sell)q.push(Node{ j, 0 }); 31 else cnt += 2; 32 q.push(Node{ i, 1 }); 33 } 34 else q.push(Node{ i, 0 }); 35 } 36 printf("%lld %d\n", ans, cnt); 37 } 38 }
HDU6438 Buy and Resell 解題報告(一個有趣的貪心問題的嚴格證明)