基礎知識
定義
01分數規劃問題:
01分數規劃問題主要包含一般的01分數規劃、最優比率生成樹問題、最優比率環問題、最大密度子圖等。我們將會對這四個問題進行討論。
解決這類問題有兩種方法:二分法,Dinkelbach 演算法,下文我只會用二分法(通常也用二分法)
所謂的01分數規劃問題就是指這樣的一類問題,給定兩個陣列,a[i]表示選取i的收益,b[i]表示選取i的代價。如果選取i,定義x[i]=1,否則x[i]=0。
每一個物品只有選或者不選兩種方案,求一個選擇方案使得R=sigma(a[i]*x[i])/sigma(b[i]*x[i])取得最值,即所有選擇物品的總收益/總代價的值最大或是最小。 一般來講就是求一個最優比率。
當然選擇物品的時候可能會有限制
推導
永遠要記得,我們的目標是使R取到最值(R在不同問題中可能不一樣,取的是最大最小值也不一定)一定要記住這一點,在下文我會反覆提及。那麼如何讓R取到最值?我們可以透過數學推導來求出來
我們先不管選和不選,假設它一定選
R=sigma(a[i])/sigma(b[i]),求R的最值
我們假定現在有一個解ans
我們想讓R最大,那必然有: R>=ans
由於我們想讓R最大,那麼就讓ans越大。所以我們考慮二分ans即可,當然,在此之前我們要整理一下這個式子方便後續的二分,注意由於涉及除法,一般二分都要用分數二分,注意精度問題
sigma(a[i])/sigma(b[i]) >= ans
sigma(a[i]) >= sigma(b[i])*ans
sigma(a[i]) - sigma(b[i])*ans >=0 倒數第二步
sigma(a[i] - b[i]*ans) >=0 最後一步
到此,就可以了
從倒數第二步推到最後一步
sigma(b[i])*ans = sigma(b[i]*ans)
你可能不知道為什麼這樣是正確的,很簡單,我們簡單證明下。
sigma(b[i])*ans = (b[1]+b[2]+...+b[n])*ans = (b[1]*ans+b[2]*ans+.....+b[n]*ans)
sigma(b[i]*ans) = (b[1]*ans+b[2]*ans+.....+b[n]*ans)
好了證畢
部分程式碼
這裡貼上分數二分的板子:
double R=1e8, L=0; while (R-L>1e-5) { double mid=(L+R)/2; if (check(mid)) { ans=mid; L=mid; } else R=mid; }
至於check函式,就是我們上述要求的
sigma(a[i] - b[i]*ans) >=0
看看這個式子是否成立,也就是左式是否大於0
check函式的寫法有很多,因題而異,例如:貪心,dp,揹包,等等等等
可能與什麼知識點結合
分數規劃一般來講不會單獨成題,一般來講有以下幾種形式:
0.不與任何演算法結合,即分數規劃裸題
1.與01揹包結合,即最優比率揹包
2.與生成樹結合,即最優比率生成樹
3.與負環判定結合,即最優比率環
4.與網路流結合,即最大密度子圖
5.與其他的各種帶選擇的演算法亂套,即最優比率啥啥的...
例如:
6.與費用流結合,即最優比率流(這個是我亂叫的)
我們看一個例題加深一下印象
分數規劃裸題(板子題)
https://www.luogu.com.cn/problem/P1570
題目要求n個物品中選m個,使得 sigma(v[i]) / sigma(c[i]) 最大化,這樣看上去,不就是上述推導嗎?不過多了一個限制條件,我們就不重新推式子了,還是這個式子
sigma(v[i] - c[i]*ans) >=0
想讓這個式子最大,可以用貪心的思想,給每個這樣的值排個序,取前m個相加即可,很簡單
#include <bits/stdc++.h> using namespace std; const int N=205; int n, m, v[N], c[N]; double a[N], ans=0; bool cmp(double a, double b) { return a>b; } bool check(double x) { double res=0; for (int i=1; i<=n; i++) a[i]=v[i]*1.0-c[i]*x*1.0; sort(a+1, a+1+n, cmp); for (int i=1; i<=m; i++) res+=a[i]; return res>=0.0; } int main() { scanf("%d%d", &n, &m); for (int i=1; i<=n; i++) scanf("%d", &v[i]); for (int i=1; i<=n; i++) scanf("%d", &c[i]); double R=1e5, L=0; while (R-L>1e-5) { double mid=(L+R)/2; if (check(mid)) { ans=mid; L=mid; } else R=mid; } printf("%.3lf", ans); return 0; }
最優比率生成樹
https://www.luogu.com.cn/problem/P4951
題目要使 f-sigma(c[i]) / sigma(t[i]) 最大化,且這些值只在每條邊上,要選出n-1條邊使圖連通的前提下滿足這個式子最大值
乍一看式子改變了,限制條件也變了,那不簡單?重推一遍就好了
假定現問題有一個答案是ans
f-sigma(c[i]) / sigma(t[i]) >= ans
f-sigma(c[i]) >= sigma(t[i])*ans
f-sigma(c[i]) - sigma(t[i])*ans >=0
f-sigma(c[i]-t[i]*ans) >= 0
令res=sigma(c[i]-t[i]*ans),即
f-res>=0
由於在這個式子中,要使得成立,應該讓res儘可能的小,注意我們check函式求的就是res,所以這裡求的時候是要求最小而不是上題中的最大了!
我們現在就考慮check函式如何寫了。這裡因為要使圖連通,且滿足這個式子最小,那就可以考慮用最小生成樹了
注意check函式,因為要呼叫多次,所以要提前保留邊的資訊(u,v),不然每次進行排序都會打亂這些資訊,所以每次進入check要初始化這些邊的資訊!!!
#include <bits/stdc++.h> using namespace std; const int N=10005; struct node { int u, v; double w; }a[N]; int n, m, k, u1[N], v1[N], c[N], t[N], cnt=0, f[N]; double sum=0, ans=0; int find(int x) { if (f[x]==x) return x; return f[x]=find(f[x]); } bool cmp(node a, node b) { return a.w<b.w; } bool check(double x) { for (int i=1; i<=m; i++) { a[i].w=c[i]*1.0+t[i]*1.0*x; a[i].u=u1[i], a[i].v=v1[i]; } sort(a+1, a+1+m, cmp); for (int i=1; i<=n; i++) f[i]=i; cnt=0, sum=0; for (int i=1; i<=m; i++) { int u=a[i].u, v=a[i].v; double w=a[i].w; int fu=find(u), fv=find(v); if (fu!=fv) { f[fu]=fv; cnt++, sum+=w; } if (cnt==n-1) break; } return (k-sum)>=0; } int main() { scanf("%d%d%d", &n, &m, &k); for (int i=1; i<=m; i++) scanf("%d%d%d%d", &u1[i], &v1[i], &c[i], &t[i]); double R=1e12, L=0; while (R-L>1e-8) { double mid=(R+L)/2; if (check(mid)) { ans=mid; L=mid; } else R=mid; } printf("%.4lf", ans); return 0; }
至此,問題得到完美解決
最優比率揹包
https://www.luogu.com.cn/problem/P4377