BZOJ1492: [NOI2007]貨幣兌換Cash
Description
小Y最近在一家金券交易所工作。該金券交易所只發行交易兩種金券:A紀念券(以下簡稱A券)和 B紀念券(以下簡稱B券)。每個持有金券的顧客都有一個自己的帳戶。金券的數目可以是一個實數。每天隨著市場的起伏波動,兩種金券都有自己當時的價值,即每一單位金券當天可以兌換的人民幣數目。我們記錄第 K 天中 A券 和 B券 的價值分別為 AK 和 BK(元/單位金券)。為了方便顧客,金券交易所提供了一種非常方便的交易方式:比例交易法。比例交易法分為兩個方面:(a)賣出金券:顧客提供一個 [0,100] 內的實數 OP 作為賣出比例,其意義為:將OP% 的 A券和 OP% 的 B券以當時的價值兌換為人民幣;(b)買入金券:顧客支付 IP元人民幣,交易所將會兌換給用戶總價值為 IP的金券,並且,滿足提供給顧客的A券和B券的比例在第 K 天恰好為 RateK;例如,假定接下來 3 天內的 Ak、Bk、RateK 的變化分別為:
假定在第一天時,用戶手中有 100元人民幣但是沒有任何金券。用戶可以執行以下的操作:
註意到,同一天內可以進行多次操作。小Y是一個很有經濟頭腦的員工,通過較長時間的運作和行情測算,他已經知道了未來N天內的A券和B券的價值以及Rate。他還希望能夠計算出來,如果開始時擁有S元錢,那麽N天後最多能夠獲得多少元錢。
Input
輸入第一行兩個正整數N、S,分別表示小Y能預知的天數以及初始時擁有的錢數。接下來N行,第K行三個實數AK、BK、RateK,意義如題目中所述。對於100%的測試數據,滿足:\(0<AK≤10,0<BK≤10,0<RateK≤100,MaxProfit≤10^9\)
\(n\leq 10^5\)
【提示】
1.輸入文件可能很大,請采用快速的讀入方式。
2.必然存在一種最優的買賣方案滿足:
每次買進操作使用完所有的人民幣;每次賣出操作賣出所有的金券。
Output
只有一個實數MaxProfit,表示第N天的操作結束時能夠獲得的最大的金錢數目。答案保留3位小數。
Sample Input
3 100
1 1 1
1 2 2
2 2 3
Sample Output
225.000
HINT
Solution
首先題設比較復雜,不仔細讀題,後面的方程肯定寫不出來。
考慮到,如果有一天賣出會讓你盈利,肯定是全部賣出時盈利更多。
同理,如果有一天買入會使得之後某一天賣出時賺得更大利潤,那麽這一天一定會全部買入。
沒有上面兩點這題根本不可做。
對於動歸的狀態,記作 \(dp[i].ma\)和\(dp[i].mb\) 分別表示第 \(i\) 天如果全部買入,可以最多獲得 \(dp[i].ma\) 的 \(A\) 券,與此同時可以買到 \(dp[i].mb\) 的 \(B\) 券。\(dp[i].ma\) 是最主要的狀態。姑且把m看作質量吧,不知道該咋起名字
這樣就有了 DP
方程(註意 \(i,j\) 別亂了):
\[ \begin{equation*} \begin{aligned} dp[i].mb&=\frac{val}{A[i]*rate[i]+B[i]}\dp[i].ma&=dp[i].mb\times rate[i]\val&=max\{dp[j].ma*A[i]+dp[j].mb*B[i]\} \end{aligned} \end{equation*} \]
這樣得到的是一個 \(O(n^2)\) 的算法,val的最大值為最終答案
考慮對於一個決策點 \(t\),如果有兩個決策方案 \(x,y\) ,那麽 \(x\) 比 \(y\) 優當且僅當:
\[
\begin{equation*}
\begin{aligned}
val_x&>val_y\dp[x].ma\times A[t]+dp[x].mb\times B[t]&>dp[y].ma\times A[t]+dp[y].mb\times B[t]\A[t]\times(dp[x].ma-dp[y].ma)&>B[t]\times(dp[y].mb-dp[x].mb)
\end{aligned}
\end{equation*}
\]
這時,這已經很像斜率優化了,但是因為沒有各種單調性,所以沒法搞事。
我們直接令 \(dp[x].ma<dp[y].ma\) 使得其有一個單調性。
\[
\begin{equation*}
\begin{aligned}
\frac{A[t]}{B[t]}&<\frac{dp[y].mb-dp[x].mb}{dp[x].ma-dp[y].ma}\-\frac{A[t]}{B[t]}&>\frac{dp[x].mb-dp[y].mb}{dp[x].ma-dp[y].ma}
\end{aligned}
\end{equation*}
\]
這就可以斜率優化了這是一個上凸殼
接下來,由於 \(dp[x].ma\) 實際上並不單調,所以需要用平衡樹維護上凸包我沒寫這個。
或者使用 CDQ分治
。
首先對於區間 \([L,R]\),可以通過 \([L,mid]\) 的信息做出來一個上凸包,用於更新 \([mid+1,R]\) 的決策,不一定是最終的最優,但要保證是 \([L,mid]\) 轉移過來的最優情況。
考慮把 \(-\frac{A[t]}{B[t]}\) 看做第一維,\(dp[x].ma\) 看做第二維。
將第一維排序(升序降序都可以寫出來),對第二維進行類似歸並的過程。
具體過程是\(solve(l,r)\)
\(\rightarrow\)按下標進行對每一天信息的劃分(小於 \(mid\) 劃到 \([L,mid]\) 否則 \([mid+1,R]\)。由於之前排過序,劃分後\(-\frac{A[t]}{B[t]}\)仍然有序)
\(\rightarrow solve(l,mid)\)
\(\rightarrow\)構建\([L,mid]\)凸包。進行決策,由於\(-\frac{A[t]}{B[t]}\)和凸包都是單調的,可以\(O(n)\)算一下\([mid+1,R]\)所有的最優解。
\(\rightarrow solve(mid+1,r)\)
\(\rightarrow\)按照 \(dp[x].ma\) 升序排序,這是可以進行斜率優化的基礎。
這樣保證了斜率優化的可行性(\(dp[x].ma\)單調遞增),同時又可以不讓決策的先後錯亂(比如先在第三天操作再去第二天操作),是一個優秀的分治算法。時間復雜度 \(O(nlogn)\)。
/**************************************************************
Problem: 1492
User: zzzc18
Language: C++
Result: Accepted
Time:1512 ms
Memory:10592 kb
****************************************************************/
#include<cmath>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN = 100000+9;
const double EPS = 1e-9;
const double INF = 1e20;
int n;
double ans[MAXN];
struct DATA{
double A,B,rate;
int id;
}num[MAXN];
bool DATAcmp(const DATA &X,const DATA &Y){
return (-X.A/X.B)<(-Y.A/Y.B);
}
struct DP{
double ma,mb;//表示A的數量以及B的數量
}dp[MAXN];
double K(int x,int y){
if(dp[x].ma==dp[y].ma)return -INF;
return (dp[x].mb-dp[y].mb)/(dp[x].ma-dp[y].ma);
}
bool jud(int x,int y){
if(fabs(dp[x].ma-dp[y].ma)<EPS)
return dp[x].ma<dp[y].ma;
else
return dp[x].ma<dp[y].ma;
}
void CDQ(int l,int r){
if(l==r){
ans[l]=max(ans[l],ans[l-1]);
dp[l].mb=ans[l]/(num[l].A*num[l].rate+num[l].B);
dp[l].ma=dp[l].mb*num[l].rate;
return;
}
static DATA tmp1[MAXN];
static DP tmp2[MAXN];
int mid=l+r>>1;
int p1=l,p2=mid+1;
for(int i=l;i<=r;i++){
if(num[i].id<=mid)
tmp1[p1++]=num[i];
else
tmp1[p2++]=num[i];
}
for(int i=l;i<=r;i++)
num[i]=tmp1[i];
CDQ(l,mid);
static int top;
static int sta[MAXN];
top=1;
for(int i=l;i<=mid;i++){
while(top>2 && K(sta[top-1],sta[top-2])<K(sta[top-1],i))
top--;
sta[top++]=i;
}
int now=1;
for(int i=r;i>mid;i--){
while(now<top-1 && K(sta[now],sta[now+1])>=-num[i].A/num[i].B)
now++;
int t=num[i].id;
ans[t]=max(ans[t],dp[sta[now]].ma*num[i].A+dp[sta[now]].mb*num[i].B);
}
CDQ(mid+1,r);
p1=l,p2=mid+1;
for(int i=l;i<=r;i++){
if(p2>r || (p1<=mid && jud(p1,p2)))
tmp2[i]=dp[p1++];
else
tmp2[i]=dp[p2++];
}
for(int i=l;i<=r;i++)
dp[i]=tmp2[i];
}
void solve(){
sort(num+1,num+n+1,DATAcmp);
CDQ(1,n);
printf("%.3lf\n",ans[n]);
}
int main(){
scanf("%d",&n);
scanf("%lf",&ans[0]);
for(int i=1;i<=n;i++){
scanf("%lf%lf%lf",&num[i].A,&num[i].B,&num[i].rate);
num[i].id=i;
}
solve();
return 0;
}
BZOJ1492: [NOI2007]貨幣兌換Cash