贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
贪心算法还是比较好理解的一个算法,以前我也是这样认为的,感觉贪心就是每一步都做到最优解就可以了,但是后来结合问题发现自己的理解存在着一些问题。贪心算法比较经典的题目之一就是单源最短路径问题,这个问题在一些步骤上面我想了很久,有些细节想不通。这个问题以后有机会再讲。本次讲一讲背包问题。
贪心算法两个最重要的性质:
(1)贪心选择性质;
(2)最优子结构性质;
其中,贪心选择性质:自顶向下进行决策,每次做出的决策都是局部最优解,且每次做出决策后问题规模都变小了;最优子结构性质:即问题的最优解结构中包含子问题的最优解;
动态规划算法的两个最重要的性质:
(1)重叠子问题性质;
(2)最优子结构性质;
其中最优解子结构性质和贪心算法相似,唯一不同的是重叠子问题性质,因为动态规划算法是自底向上的算法,它需要首先将原始问题分解为若干个相互有联系的子问题,在计算的时候有的子问题可能会被计算很多次,所以动态规划算法会将这些子问题的解存在一个表格中,使得最终对于这些子问题只需要求解一次(可以使原来需要再指数时间内解决的问题可以在多项式问题中得到解决)
背包问题就是有若干物品,每个物品有自己的价值和重量。背包有总重量。问题就是怎样将背包装的最大价值。背包问题也分很多种,贪心算法解决的是物品可以拆分的背包问题(就是物品可以分成几份装入)。这个问题用贪心还是比较好解决的。贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。此问题就是将每次的放入看成每一步,要想解决问题,就是将每一步都放入最优解。也就是说,每一次的放入都要放入最佳的选择。讲到这里,就要说一说最佳的选择,每一次的放入的最佳的选择就是每次放入的物品都是剩余的物品中价值最大且质量最小的,这里就要引入一个物品的属性,物品的权重值。物品的权重值就是指物品的价值除以物品的质量。所以,本问题的每一次的最佳选择就是每次都选出权重值最大的物品。
问题的大致思路说完了,下面就讲一讲具体的算法。算法最开始是先声明物品类,因为后面要用到很多的物品属性,如果使用数组会有点麻烦,物品的属性有背包ID,物品价值,物品质量,物品权重值。在声明的时候,只要输入物品的前三个属性就可以了,物品的权重值可以由前三个推导出来。算法的接下来就是将物品的数组按物品的权重值排序,权重值大的排在数组的前面,方便后面的运算。算法的主体就是从数组中取出物品对象,计算比较物品的质量和当前背包剩余重量的大小,如果大于,就计算要放入的百分比。如果小于,就进行下一步的最佳选择。算法的大致思路及时这样。下面粘贴代码:
#ifndef MERGE_SORT_H
#define MERGE_SORT_H
template <class Type>
void MergeSort(Type a[], int n);
#endif
//merge_sort.template实现部分
#include "merge_sort.h"
template <class Type>
void MergeSort(Type *a , Type *v, int n) //a是重量,v是价值
{
Type *b = new Type[n];
int s = 1;
while (s < n)
{
MergePass(a, b, v , s, n); //合并到数组b
s += s;
MergePass(b, a, v , s, n); //合并到数组a
s += s;
}
delete b;
}
template <class Type>
void MergePass(Type *x, Type *y, Type *v, int s, int n)
{
int i = 0;
while (i <= n - s * 2)
{
Merge(x, y ,v , i, i + s - 1, i + 2 * s - 1); //合并大小为s的相邻两段子数组
i += s * 2;
}
if (i + s < n) //剩下的元素少于2s
Merge(x, y, v, i, i + s - 1, n - 1);
else for (int j = i; j <= n - 1; j++)
y[j] = x[j];
}
template <class Type>
void Merge(Type *c, Type *d, Type *v , int l, int m, int r) //合并c[l:m]和c[m+1:r]到d[l:r],其中c[l:m]和c[m+1:r]都是已经经过升序排好的数组
{
int i = l, j = m + 1, k = l;
while ((i <= m) && (j <= r))
{
if ((v[i] / c[i]) >= (v[j] / c[j])) //这里使用降序排序
d[k++] = c[i++];
else
d[k++] = c[j++];
}
if (i > m)
for (int q = j; q <= r; q++)
d[k++] = c[q];
else for (int q = i; q <= m; q++)
d[k++] = c[q];
}
//背包问题,使用贪心算法进行求解
//======================================================
#include <iostream>
#include "merge_sort.cpp"
using namespace std;
void init_data(float *v, float *w, float *x, int n) //初始化数据
{
cout << "请输入每类物体的价值:" << endl;
for (int i = 0; i < n; i++)
{
cin >> v[i];
}
cout << "请输入每类物体的重量: " << endl;
for (int i = 0; i < n; i++)
{
cin >> w[i];
}
for (int i = 0; i < n; i++)
x[i] = 0;
}
void Knapsack(int n, float M, float *v, float *w, float *x)
//n是物体的种类数,M是背包容量,v是每类物体的价值,w是每类物体的重量,x是每类物体装入的份额,属于[0,1]
{
int i = 0;
float c = M;
MergeSort(w , v , n); //v[i]/w[i]是每一类物体的单位重量价值,然后对它们进行降序排序
for (i = 0; i < n; i++)
{
if (w[i] > c)
break;
x[i] = 1;
c -= w[i];
}
if (i < n)
x[i] = c / w[i];
}
int main(void)
{
float M = 0.0; //背包容量
cout << "请输入背包容量: ";
cin >> M;
int n = 0; //物体数量
cout << "\n请输入物体数量 n : ";
cin >> n;
float *v = new float[n];
float *w = new float[n];
float *x = new float[n];
init_data(v, w, x, n); //初始化数据
Knapsack(n, M, v, w, x);
cout << "排好序的w[i]: " << endl;
for (int i = 0; i < n; i++)
{
cout << w[i] << " ";
}
cout << "\n\n输出最后的决策x[i] : " << endl;
for (int i = 0; i < n; i++)
{
cout << x[i] << " ";
}
/*MergeSort(w, v, n);
cout << "输出排好序的w[i] : " << endl;
for (int i = 0; i < n; i++)
{
cout << w[i] << " ";
}*/
system("pause");
delete v , w , x;
return 0;
}
以上算法的时间复杂度和空间复杂度为O(n2),其中时间复杂度基本已经不能再优化了,但空间复杂度可以优化到O(n)。
背包问题与0-1背包问题的不同,虽然这两个问题极为相似,但背包问题可以用贪心算法求解,而对于0-1背包问题,贪心选择算法不能得到最优解。因为在0-1背包问题的这种情况下,它无法保证最后能将背包装满,部分闲置的背包空间,使每公斤背包的价值降低了。动态规划算法可以有效的解0-1背包问题。