轉載請注明出處:http://www.cnblogs.com/KirisameMarisa/p/4187637.html
題目鏈接:http://acm.nyist.net/JudgeOnline/problem.php?pid=914
問題描述:有N個物體,它們的利益用v[i]表示,代價用c[i]表示。現在要在這N個物體中選取K個物體,使得選出來的這K個物體的總利益除以總代價達到最大值。即取得最大值。
問題轉化:構造一個x[N]的數組,表示每個數取或不取的狀態,顯然每一個x[i]只有兩個取值:0和1,其中1表示取,0表示不取。則整個式子也就可以變成目標式:
值得注意的是:上式中的r是每一組x對應求得的當前答案,而我們的目的就是要找到一組x使得求出來的r得到最大值R!(這很重要!)
一、二分法
進一步對式子進行處理:
簡單的一項處理:
構造一個函數:
其中F(r)在平面坐標系上體現為一條直線,每一組x[i]都分別唯一地對應一條直線,這些直線的截距均大於等於0、斜率均小於等於0。而這些直線在X軸上的截距就是這一組x求出來的r,而截距的最大值就是我們要求的R。(如下圖所示)
在X軸上面任取一個r,如果至少有一條直線的F(r)>0,那么說明了什么呢?
說明至少還有一條直線與X軸的交點在它的右邊,那么這個r一定不是最大值,真正的最大值在它的右邊。反之,如果所有的F(r)都小於0,那么真正的最大值在它的左邊
那么前面的結論就可以換種說法,因為我只需判斷最大的那個F(r)的正負性就行了:
隨便取一個r,如果F(r)max>0,則結果R>r,反之若F(r)max<0,則結果R<r。直到找到使F(r)max=0的r,那個r就是我們要找的結果R
顯然這是一個令人感到興奮的結論,因為我們自然而然就想到了一種方法:二分法!
至此,還有一個小小的問題沒有解決,那就是F(r)max怎么求?
至此,問題全部解決!
#define eps 1e-8 #define zero(a) fabs(a)<eps double v[MAXN],c[MAXN],d[MAXN]; int main() { int N,K; scanf("%d%d",&N,&K); for(int i=0;i<N;i++) scanf("%lf%lf",&v[i],&c[i]); double l=0.0,r=10000000.0; while(!(zero(r-l))) //二分終止條件,利用double的精度控制 { double mid=(r+l)/2.0; for(int i=0;i<N;i++) d[i]=v[i]-mid*c[i]; sort(d,d+N); double sum=0.0; for(int i=N-1;i>=N-K;i--) sum+=d[i]; if(sum>=0) l=mid; else r=mid; } double ans=l; printf("%.2lf\n",ans); return 0; }
二分法可以輕松水過南陽理工學院oj上的原題和ghbgh出的上大oj上的改題甚至是POJ的2976這些最基本的0-1分數規划問題。不過,用這種方法去做POJ上面的3111時卻直接TLE了orz。。。。
二分是一個非常通用的辦法,但是我們來考慮這樣的一個問題,二分的時候我們只是用到了F(r)>0這個條件,而對於使得F(r)>0的這組解所求到的R值沒有使用。因為F(r)>0,我們已經知道了R是一個更優的解,與其漫無目的的二分,為什么不將解移動到R上去呢?
我們再次回到函數表達圖:
這個是二分的原理表現,在圖中,我們取左界為0,右界為足夠大,畫出中間的(左+右)/2,發現此時F(r)max是大於0的,那么繼續將左界移向(左+右)/2,繼續二分,直到找到R為止。
但是我們換種思路,我們在判斷一個當前的r的時候需要去求一個F(r)max,在二分之中我們僅僅判斷了F(r)max與0的關系,這是利用率比較低的。其實我們可以將F(r)max利用起來。找到F(r)max所在的那一條直線,然后將r移動到這條直線的截距上面去(如下圖,找到當前的F(r)max所在的直線,將r移動到r4上面去,這樣做甚至只要2步即可到位)
這就是下面要介紹的方法:
二、Dinkelbach算法
它實質上是一種迭代,他是基於這樣的一個思想:他並不會去二分答案,而是先隨便給定一個答案,然后根據更優的解不斷移動答案,逼近最優解。由於他對每次判定使用的更加充分,所以它比二分會快上很多。
在這個算法中,我們可以將r初始化為任意值,不過由於所有直線都只有y軸右邊的部分,所以一般將r初始化為0。
double m[50000+10],w[50000+10];
struct node{double num;int ord;} d[50000+10];
bool cmp(node a,node b){return a.num>b.num;}
int main() { int N,K; scanf("%d%d",&N,&K); for(int i=0;i<N;i++) scanf("%lf%lf",&m[i],&w[i]); double l=0.0,ans; while(1) { ans=l; for(int i=0;i<N;i++) { d[i].num=m[i]-ans*w[i]; d[i].ord=i; } sort(d,d+N,cmp); double p=0.0,q=0.0; for(int i=0;i<K;i++) { p+=m[d[i].ord]; q+=w[d[i].ord]; } l=p/q; if(zero(ans-l)) break; } printf("%.2f\n",ans); return 0; }
不過需要注意的是,並不是可以放棄二分全用Dinkelbach算法。這只是最基本的0-1規划問題, Dinkelbach算法的弊端就是需要保存解。在更加復雜的問題中,有的時候二分更快,有時Dinkelbach算法更快。
二分和Dinkelbach算法寫法都非常簡單,各有長處,大家要根據題目謹慎使用。
by---Kirisame_Marisa 2014-12-26 22:31:43