01分数规划 —— 入门介绍

本文介绍了01分数规划的基本概念和解决方法,包括问题定义、二分法求解,并详细探讨了最优比率生成树问题,特别是Dinkelbach算法的应用,提供相关例题和C++代码实现。

01分数规划

问题介绍

01分数规划是大家学习之路上较为经典的一类问题,首先对于这个名字,01的含义一般来说就是“选与不选”,问题原型如下

当前有n个物品,每个物品都有其价值 v i v_i vi 与成本 w i w_i wi,要求我们从中选出k(k<=n)个物品,使得总价值与总成本的比例最大(最小),也就是对于选出的这k个物品
1 < = j < = k 1 <= j <= k 1<=j<=k
∑ 1 k v j / ∑ 1 k w j \sum_1^k{v_j} / \sum_1^k{w_j} 1kvj/1kwj 最大(最小),求这个最值

  • PS:对于这个问题,网上有些介绍可能少了其中的“k个”,而是描述为“选一些”,当然这样可能更加宽泛,但是容易出现误解,我一开始就误以为“一些”是”任意个“的意思,结果发现这样想的话,只用选性价比最高的那个物品就行了,显然这样是不行的

解决方法/算法过程

  • 1.式子变形

一开始要对这个式子进行变形,以最大值为例,设答案为 a n s ans ans 则在选出的k个中有:
∑ 1 k v j / ∑ 1 k w j ≤ a n s \sum_1^k{v_j} / \sum_1^k{w_j} \le ans 1kvj/1kwjans


∑ 1 k v j ≤ a n s ∗ ∑ 1 k w j \sum_1^k{v_j} \le ans * \sum_1^k{w_j} 1kvjans1kwj


∑ 1 k v j − a n s ∗ ∑ 1 k w j ≤ 0 \sum_1^k{v_j} - ans * \sum_1^k{w_j} \le 0 1kvjans1kwj0

合并为
∑ 1 k ( v j − a n s ∗ w j ) ≤ 0 \sum_1^k{(v_j - ans * w_j)} \le 0 1k(vjanswj)0

可以看出这是个一次不等式,不等式左边可以看成一个以 a n s ans ans 为变量的一次函数,当 ∑ 1 k ( v j − a n s ∗ w j ) < 0 \sum_1^k{(v_j - ans * w_j)} < 0 1k(vjanswj)<0时,说明ans太大了,当 ∑ 1 k ( v j − a n s ∗ w j ) > 0 \sum_1^k{(v_j - ans * w_j)} > 0 1k(vjanswj)>0时,说明有比ans更大的解,当式子两边相等时,说明我们找到了真正的ans

  • 2.枚举答案

    上面这个不等式性质很明显了,单调函数求零点,二分答案就可以了

    • 1.设左边界 l = 0 l = 0 l=0, 右边界 r = max ⁡ ( 1 ≤ i ≤ n ) v i / w i r = \max_{(1 \le i \le n)}{v_i/w_i} r=max(1in)vi/wi,进行二分枚举

    • 对于每次的 m i d mid mid,我们处理出所有的
      d i = v i − a n s ∗ w i d_i = v_i - ans * w_i di=vianswi
      然后对 d i d_i di 进行从大到小的排序

    • 把前k个 d i d_i di 相加,得到的结果如果大于0,则答案在左半区间,反之在右半区间

例题/模板题

poj2976 题目链接

是的,这是很与原型非常接近的一道题,不过要注意精度,还有就是如果用了double,则在printf输出时用%.0f才能过,虽然不能归咎于玄学,但是确实网上还没有找到对此的解释,大家都是用的这种方法

c++代码

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const double cc = 1e-7;

int n, k;
double aa[1005], bb[1005];
double dd[1005];

bool cmp(double a, double b)
{
	return a > b;
}

int main()
{
	while (scanf("%d%d", &n, &k) == 2)
	{
		if (!n && !k)
		{
			break;
		}
		for (int i = 1; i <= n; ++i)
		{
			scanf("%lf", &aa[i]);
			aa[i] *= 100;
		}
		double l = 0, r = 0;
		double ans = -1;
		for (int i = 1; i <= n; ++i)
		{
			scanf("%lf", &bb[i]);
			r = max(r, aa[i] / bb[i]);
		}
		while (l + cc < r)
		{
			double mid = (l + r) / 2;
			for (int i = 1; i <= n; ++i)
			{
				dd[i] = aa[i] - bb[i] * mid;
			}
			sort(dd + 1, dd + n + 1, cmp);
			double acc = 0;
			for (int i = 1; i <= n - k; ++i)
			{
				acc += dd[i];
			}
			if (acc > 0)
			{
				l = mid;
			}
			else
			{
				r = mid;
			}
		}
		ans = (l + r) / 2;
		printf("%.0f\n", ans);
	}
	return 0;
}

拓展问题:最优比率生成树与Dinkelbach算法

最优比率生成树,不是指树的形状如何,而是指生成树的边的某种比例(如边长和/花费和)最小或者最大,本质上还是01分数规划问题

题中要求得出具有最小的“花费和/边长和”的生成树,我们可以用kruskal算法,每次也就是从m条边中选n-1条边,那么类比上面的01分数规划原问题,我们kruskal排序用来比较的值也应该是
d i = c o s t i − a n s ∗ d i s i d_i = cost_i - ans * dis_i di=costiansdisi

每次二分后,排序优先选择di最小的边建立生成树就可以了

但这样是过不了的,因为这是个完全图,导致kruskal算法比较慢, O ( 2 ∗ n 2 ∗ l o g 2 ( n ) ) O(2*n^2*log^2(n)) O(2n2log2(n)) 复杂度太大,解决方案之一是使用prim,不过更为方便的方法是使用Dinkelbach算法

  • Dinkelbach算法

    Dinkelbach算法是对二分法的优化,我们可以认为它相比二分,更新ans时换了一种迭代方式

    • 1.我们去掉二分所需要的l, r, mid
    • 2.我们初始化ans==0
    • 3.每次按照 c o s t i − a n s ∗ d i s i cost_i - ans * dis_i costiansdisi 对边进行排序并选出生成树
    • 4.同时记录这n-1条边所形成的的一次函数
      f ( a n s ) = ∑ ( c o s t i − a n s ∗ d i s i ) f(ans) = \sum{(cost_i - ans * dis_i)} f(ans)=(costiansdisi)
      的零点 x 0 x_0 x0
      x 0 = ∑ c o s t i / ∑ d i s i x_0 = \sum{cost_i} / \sum{dis_i} x0=costi/disi
    • 5.检验 a n s ans ans x 0 x_0 x0的偏差,如果在精度范围内则结束,否则 a n s = x 0 ans = x_0 ans=x0

Dinkelbach算法每次的ans收敛方向与二分法是一致的,但是收敛速度比二分法要快不少,事实上,此题在使用kruskal算法时,正好卡了二分法,若使用Dinkelbach算法则刚好可以过

  • 注意:通过多次提交与验证,算法中有两个需要注意的地方,一是代码中r变量(即上述ans)一开始最好置为0,否则可能会WA,二是与之前的poj2976类似,最终答案的占位符最好用%f而不要用%lf,原因还未找到,但是既然已经重复遇到了这种问题,以后可以优先考虑%f。

c++代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>

using namespace std;

const double cc = 1e-4;		//精度 

int n;
struct edges
{
	int u;
	int v;
	double d;
	double w;
	double dv;
} ee[2000005];

double r;

bool cmp(struct edges a, struct edges b)
{
	return a.w - r * a.d < b.w - r * b.d;
}

int ff[1005] = {0};

double xs[1005], ys[1005], zs[1005];

int find(int xx)
{
	if (ff[xx] != xx)
	{
		ff[xx] = find(ff[xx]);
	}
	return ff[xx];
}

int main()
{
	while (scanf("%d", &n) == 1)
	{
		if (!n)
		{
			break;
		}
		for (int i = 1; i <= n; ++i)
		{
			scanf("%lf%lf%lf", &xs[i], &ys[i], &zs[i]);
		}
		int co = 0;				//边的数量 n * (n - 1) / 2 
		for (int i = 1; i <= n; ++i)
		{
			for (int j = i + 1; j <= n; ++j)
			{
				ee[++co].u = i;
				ee[co].v = j;
				ee[co].d = sqrt((xs[i] - xs[j]) * (xs[i] - xs[j]) + (ys[i] - ys[j]) * (ys[i] - ys[j]));
				ee[co].w = fabs(zs[i] - zs[j]);
			}
		}
		r = 0;				//如果不置为0可能会WA 
		while (1)
		{
			sort(ee + 1, ee + co + 1, cmp);
			int cnt = 0;
			for (int i = 1; i <= n; ++i)
			{
				ff[i] = i;
			}
			double A = 0, B = 0;
			for (int i = 1; i <= co; ++i)
			{
				int fu = find(ee[i].u);
				int fv = find(ee[i].v);
				if (fu != fv)
				{
					ff[fu] = fv;
					A += ee[i].w;
					B += ee[i].d;
					++cnt;
					if (cnt == n - 1)
					{
						break;
					}
				}
			}
			double nex = A / B;
			if (fabs(r - nex) <= cc)
			{
				break;
			}
			else
			{
				r = nex;
			}
		}
		printf("%.3f\n", r);		//如果写lf可能会WA 
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值