【算法概论】分治算法:查找带权中位数及中位数算法的妙用

一、带权中位数的含义:

       比如有这样一串元素:0.1, 0.35, 0.05, 0.1, 0.15, 0.05, 0.2

       这串元素的中位数是几呢,这很容易可以得出:0.1,具体查找中位数的方法可见【算法概论】查找中位数。

       而带权中位数,则是:0.2。

       → 将上述元素排序:0.05, 0.05, 0.1, 0.1, 0.15, 0.2, 0.35

       → 0.05 + 0.05 + 0.1 + 0.1 + 0.15 = 0.45 < 0.5,0.45 + 0.2 > 0.5;0.35 < 0.5,0.35 + 0.2 > 0.5。

       通过上面这个例子,应该对带权中位数已经有些概念了叭,下面给出带权中位数的定义:

       (图来源:https://blog.csdn.net/qq_40828805/article/details/79372362)

【算法概论】分治算法:查找带权中位数及中位数算法的妙用_第1张图片

二、查找带权中位数

       1、有了带权中位数的概念,如果让我们查找带权中位数,我们应该会很快想到一个算法:

       先将序列排序(时间复杂度O(n*logn)),然后从前往后遍历数组,并将遍历到的元素的权重相加,直到所有权重和 > 0.5,返回当前的元素,则该数就是带权中位数。

       2、然而,上述算法的时间复杂度为O(n*logn),我们是否还能将其的算法复杂度降低呢?可以,在查找中位数的算法中,最优的时间复杂度为O(n),原因在于我们没有将数组排序。其实这个题目我们也不需要对数组排序,与查找中位数的算法思想相似,我们只需要在n个元素中找出较小的k个元素,将它们的权重相加,使得它们的和 < 1/2,而后 n-k-1的元素,将他们的权重相加,和也 < 1/2。

#include 
#include 

using namespace std;

double WeightedMid(vector data, int k);
double Selection(vector data, int k);

int main()
{
	//int data[] = { 0.1, 0.35, 0.05, 0.1, 0.15, 0.05, 0.2 };

	//输入一串数字
	vector data;
	double temp;
	while (cin >> temp)
	{
		data.push_back(temp);
		char c = cin.get();
		if (c == '\n')		//键入回车停止输入
		{
			break;
		}
	}

	int n = data.size();
	
	cout << WeightedMid(data, n / 2) << endl;

	return 0;
}

//找到带权中位数
//data为待查找数组,k为带权中位数的位置
double WeightedMid(vector data, int k)
{
	//找到中位数
	double mid = Selection(data, k);

	//分别计算左右两部分的权重和
	double sum_sl = 0;			//左半部分数组的权重和
	double sum_sr = 0;			//右半部分数组的权重和
	for (int i = 0; i < int(data.size()); ++i)
	{
		if (data[i] < mid)
		{
			sum_sl += data[i];
		}
		else if (data[i] > mid)
		{
			sum_sr += data[i];
		}
	}

	//比较左右两部分权重和与0.5的关系
	if (sum_sl >= 0.5)
	{
		return WeightedMid(data, k - 1);
	}
	else if (sum_sr > 0.5)
	{
		return WeightedMid(data, k + 1);
	}
	else
	{
		return mid;
	}
}

//找到中位数
//data为待查找的数组,k为查找的中位数的位置
double Selection(vector data, int k)
{
	//基准值
	double pivot = data[k];

	//对小于基准值、等于基准值、大于基准值的元素进行划分
	int sl = 0, sv = 0, sr = 0;
	vector SL;
	vector SV;
	vector SR;

	int n = data.size();
	for (int i = 0; i < n; ++i)
	{
		if (data[i] < pivot)
		{
			SL.push_back(data[i]);
			++sl;
		}
		else if (data[i] == pivot)
		{
			SV.push_back(data[i]);
			++sv;
		}
		else
		{
			SR.push_back(data[i]);
			++sr;
		}
	}

	//比较k和子集所含元素的大小关系
	if (k < sl)
	{
		return Selection(SL, k);
	}
	else if (k < sv + sl)
	{
		return data[k];
	}
	else
	{
		return Selection(SR, k - sl - sv);
	}
}

三、该算法的应用

1、部分背包问题:

问题描述:

       一个窃贼去一家商店偷窃,有n件商品:第 i 件商品价值 vi 元,重 wi 磅(vi、wi都是整数),他的背包最多只能装下 w 磅物品,每个商品他可以选择一部分带走,问他最多能带走多贵的物品?

问题分析:

       窃贼可对每件商品选择一部分带走,这不同于0-1背包问题,这里的商品就像是金粉?

算法分析:

       我们可以对每件商品的价值 —— 每磅的价值 进行排序,然后按价值大小依次往背包里装,这样,时间复杂度为 O(n*logn)。这里,时间复杂度主要是排序,那么,可以不排序吗?

       当然OK。就像是上面查找带权中位数的算法,排序只是为了将单价高的商品筛选出来,所以我们并不需要将所有的商品价值进行排序。

       采用分治法(divide and conquer),先找出中位数!

       找出其中位数,将数组分为 SL[单价大于中位数]、SV[单价等于中位数]、SR[单价小于中位数] 三个集合;

       接下来分3种情况:

       ①如果 SL 数组中所有商品的重量 ≥ w,则问题在 SL 中递归解决;

       ②如果 SL + SV ≥ w,则先将 SL 中的物品全部装入包中,然后在 SV 中递归解决;

       ③如果 SL + SV < w,则将 SL 和 SV 中的物品全部装入背包中,再在 SR 中递归解决。

时间复杂度分析:

        T(n) ≤ T(n/2) + O(n)   → T(n) = O(n)

2、N个数中有一个数出现的次数大于1/2

算法分析:

       同样,该题也可以对数组先排序再进行扫描,但这样的时间复杂度略高。其实,一个数出现的次数超过一半,这个数就是中位数,该算法即 查找中位数的算法。

3、士兵站队问题

问题描述:

       在一个划分成网格的操场上,n 个士兵散乱地站在网格点上。网格点由整数坐标(x, y)表示。士兵们可以沿网格边上、下、左、右移动一步,但在同一时刻任一网格点上只能有一名士兵。按照军官的命令,士兵们要整齐地列成一个水平队列,即排列成(x, y),(x+1, y), … ,(x+n-1, y)。如何选择 x 和 y 的值才能使士兵们以最少的总移动步数排成一行?

【算法概论】分治算法:查找带权中位数及中位数算法的妙用_第2张图片

问题分析:

       ①题目要求同一时刻任意网格点只能有一位士兵,通过适当的移动顺序和移动路线可以做到;

       ②题目要求士兵以最少的总移动步数排成一行,我们可以将问题转换为:求士兵站立的最终位置,使得每个士兵的移动步数和最小。

你可能感兴趣的:(数据结构与算法)