01分數規劃

cn是大帅哥886發表於2024-08-14

基礎知識


定義


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

相關文章