01分数规划

01分数规划

问题介绍

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

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

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

解决方法/算法过程

  • 1.式子变形

一开始要对这个式子进行变形,以最大值为例,设答案为 \(ans\) 则在选出的k个中有:

\[\sum_1^k{v_j} / \sum_1^k{w_j} \le ans \]

\[\sum_1^k{v_j} \le ans * \sum_1^k{w_j} \]

\[\sum_1^k{v_j} - ans * \sum_1^k{w_j} \le 0 \]

合并为

\[\sum_1^k{(v_j - ans * w_j)} \le 0 \]

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

  • 2.枚举答案

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

    • 1.设左边界 \(l = 0\), 右边界 \(r = \max_{(1 \le i \le n)}{v_i/w_i}\),进行二分枚举

    • 对于每次的 \(mid\),我们处理出所有的

    \[d_i = v_i - ans * w_i \]

    然后对 \(d_i\) 进行从大到小的排序

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

例题/模板题

poj2976 题目链接

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

c++代码

#include 
#include 
#include 
#include 

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分数规划问题

  • 例题:poj2728

题中要求得出具有最小的“花费和/边长和”的生成树,我们可以用kruskal算法,每次也就是从m条边中选n-1条边,那么类比上面的01分数规划原问题,我们kruskal排序用来比较的值也应该是

\[ d_i = cost_i - ans * dis_i \]

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

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

  • Dinkelbach算法

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

    • 1.我们去掉二分所需要的l, r, mid
    • 2.我们初始化ans==0
    • 3.每次按照\(cost_i - ans * dis_i\) 对边进行排序并选出生成树
    • 4.同时记录这n-1条边所形成的的一次函数

    \[\]

    \[\]

    \[\]

    \[\]

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

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

c++代码:

#include 
#include 
#include 
#include 
#include 

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;
}

你可能感兴趣的:(01分数规划)