一、带权中位数的含义:
比如有这样一串元素: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、有了带权中位数的概念,如果让我们查找带权中位数,我们应该会很快想到一个算法:
先将序列排序(时间复杂度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 的值才能使士兵们以最少的总移动步数排成一行?
问题分析:
①题目要求同一时刻任意网格点只能有一位士兵,通过适当的移动顺序和移动路线可以做到;
②题目要求士兵以最少的总移动步数排成一行,我们可以将问题转换为:求士兵站立的最终位置,使得每个士兵的移动步数和最小。