1. 程式人生 > >HDU6438 Buy and Resell 解題報告(一個有趣的貪心問題的嚴格證明)

HDU6438 Buy and Resell 解題報告(一個有趣的貪心問題的嚴格證明)

include 目前 users 更新 完全 計算 解題報告 tput 有一個

寫在前面

此題是一個很容易想到的貪心題目,但是正確性的證明是非常復雜的。然而,目前網上所有題解並未給出本題貪心算法的任何正確性證明,全部僅停留在描述出一個貪心算法。本著對算法與計算機科學的熱愛(逃),我花了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 解題報告(一個有趣的貪心問題的嚴格證明)