K Best

صورت مساله

n عدد جواهر داریم که جواهر i-ام دارای ارزش v_i و وزن w_i است. می خواهیم زیرمجموعه ی k عضوی S = ({i_1, i_2, ..., i_k}) را از این جواهرها را انتخاب کنیم به صورتی که عبارت زیر بیشنه شود:

قیود مساله:

  • 1 ~<=~ k ~<=~ n ~<=~ 100000
  • 0 ~<= v_i <=~ 10^6
  • 1 ~<= w_i <=~ 10^6

توضیح و بررسی

گمان کنم این مساله یکی از آن مسائلی باشد که برای بسیاری از افراد شدیدا آموزنده باشد و آن ها را با روش جدیدی برای مواجه شدن با برخی مسائل آشنا کند.

راه حل من برای حل این مساله دو گام دارد:

  • گام اول: بیشترین مقدار برای عبارت بالا را پیدا می کنم.
  • گام دوم: با استفاده از این بیشترین مقدار جواب، را می سازم.

گام اول

فرض کنید روشی داریم که با آن می توانیم با آن می توانیم بدست بیاوریم که آیا زير مجموعه ی k تایی از جواهرها وجود دارد که عبارت فوق برای آن ها بیشتر از مقدار مفروض x است یا نه. اسم این تابع را test می گذاریم (بعدا به این تابع خواهیم پرداخت):

bool test(int x);

در این صورت می توانیم با استفاده از این تابع و جستجوی دودوئی بیشترین مقدار را برای عبارت بالا پیدا کنیم:

double Left = 0;
double Right = 1000000;
double Result = 0;
while(fabs(Left - Right) > 1e-6)
{
	double Mid = (Left + Right) / 2;
	if(test(Mid))
	{
		Result = Mid;
		Left = Mid;
	}
	else
		Right = Mid;
}

اکنون به توضیح اینکه چگونه می توانیم بفهمیم که “آیا زير مجموعه ی k تایی از جواهرها وجود دارد که عبارت فوق برای آن ها بیشتر از مقدار مفروض x است یا نه”. در واقع باید زیر مجموعه از جواهرات وجود داشته باشد که در نامساوی زیر صدق کند:

{{sum{j=1}{k}{v_j}}/{sum{j=1}{k}{w_j}}}~>=~x

اگر x را به سمت چپ معادله ببریم:

{{sum{j=1}{k}{v_j}}/{sum{j=1}{k}{w_j}}}-x~>=~0 ~~=>~~ {{sum{j=1}{k}{v_j}-~~x~*~sum{j=1}{k}{w_j}}/{sum{j=1}{k}{w_j}}}~>=~0 ~~=>~~ {{sum{j=1}{k}{(v_j ~-~ x ~*~ w_j)}}/{sum{j=1}{k}{w_j}}}~>=~0

و چون همواره داریم sum{j=1}{k}{w_j}~>=~0، پس نتیجه می گیریم که زیرمجموعه ی موردنظر باید در عبارت زیر صدق کند:

{sum{j=1}{k}{(v_j ~-~ x ~*~ w_j)}}~>=~0

و برای اینکه بفهمیم آیا زير مجموعه ای وجود دارد که در این رابطه صدق می کند، بیشترین مقدار را برای {sum{j=1}{k}{(v_j ~-~ x ~*~ w_j)}} پیدا می کنیم. اگر این بیشترین مقدار بزرگتر از صفر بود، چنین زیر مجموعه ای وجود دارد و گرنه وجود ندارد.


برای اینکه بیشینه ی این عبارت را پیدا کنیم، کافی است که v_j~-~x~*~w_j را برای هر کدام از جواهرها حساب کنیم و k عنصر بزرگتر آرایه ی حاصل را انتخاب کنیم و آن ها را با هم جمع کنیم.

پس کد تابع تست به این صورت خواهد بود:

bool test(double x)
{
	for(int i = 0; i < n; i ++)
		arr[i] = v[i] - x * w[i];
	sort(arr, arr + n);
	double sum = 0;
	for(int i = 0; i < k ;i ++)
	    sum += arr[n-1-i];
	if(sum >= 0)
		return true;
	else
		return false;
}

گام دوم

خُب! گمان کنم اگر بخش قبل را خوب فهمیده باشید، انجام دادن این گام برایتان به آسانی خوردن یک سیب باشد!

در واقع تابعی نیاز داریم که برای مقدار x زیرمجموعه ای از اشیاء انتخاب کند که عبارتی که قبلا درباره ی آن بحث کردیم درباره ی آن زیرمجموعه صدق کند. در اینجا نیز باید مانند بالا جواهرها را بر حسب v_j~-~x~*~w_j مرتب کنیم و k تا بزرگترین را انتخاب کنیم. تابع construct همین کار را انجام می دهد:

vector<int> construct(double x)
{
	vector<P> arr(n);
	for(int i = 0; i < n; i += 1)
		arr[i] = P(v[i] - x * w[i], i);
	sort(arr.begin(), arr.end());
	reverse(arr.begin(), arr.end());
	vector<int> result(k);
	for(int i = 0; i < k; i ++)
		result[i] = arr[i].second+1;
	return result;
}

کد من برای حل این مساله

#include <map>
#include <iostream>
#include <string>
#include <cstdio>
#include <cmath>
#include <vector>
#include <algorithm>
 
using namespace std;
 
#define P pair<double, int>
#define eps 1e-7
 
int n, v[200000], w[200000], k;
double arr[200000];
 
int check(double x)
{
	for(int i = 0; i < n; i ++)
		arr[i] = v[i] - x * w[i];
	sort(arr, arr + n);
	double sum = 0;
	for(int i = 0; i < k ;i ++)
	{
		sum += arr[n-1-i];
		if(sum < -eps)
			return false;
	}
	if(sum >= -eps)
		return true;
	else
		return false;
}
 
vector<int> construct(double x)
{
	vector<P> arr(n);
	for(int i = 0; i < n; i += 1)
		arr[i] = P(v[i] - x * w[i], i);
	sort(arr.begin(), arr.end());
	reverse(arr.begin(), arr.end());
	vector<int> result(k);
	for(int i = 0; i < k; i ++)
		result[i] = arr[i].second+1;
	return result;
}
 
int main()
{
	while(scanf("%d %d", &n, &k) == 2)
	{
		for(int i = 0; i < n; i ++)
			scanf("%d %d", v + i, w + i);
		double Left = 0, Right = 10000000;
		double result = 0;
		while(fabs(Left - Right) > 1e-6)
		{
			double Mid = (Left + Right) / 2;
			if(check(Mid))
			{
				result = Mid;
				Left = Mid;
			}
			else
				Right = Mid;
		}
		vector<int> vresult = construct(result);
		sort(vresult.begin(), vresult.end());
		for(int i = 0; i < vresult.size(); i ++)
		{
			if(i)
				cout << " ";
			cout << vresult[i];
		}
		cout << endl;
	}
	return 0;
}

 
k_best.txt · Last modified: 2007/12/25 03:03 by hadi
 
Recent changes RSS feed Creative Commons License Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki