1 贪心算法简介
贪心算法每一步都做出当时看起来最佳的选择,即:它总是做出局部最优的选择,寄希望这样的选择能导致全局的最优解。
贪心算法并不能保证得到全局最优解,但实际情况中确实可以用该方法得到全局最优解。
2 贪心算法原理
2.1 如何判断一个问题能否用贪心算法求解?
需要满足两个性质:
第一:局部最优解可以构造出全局最优解;
第二:最优子结构(同动态规划)。
2.2 贪心算法与动态规划之间的关系
贪心算法是动态规划的一种特殊情况,且贪心算法更像是动态规划中带备忘录的自顶向下方法。贪心算法通过贪心选择可以降低子问题的求解数量,实际上是只剩下一个子问题,这也就避免了动态规划中对多个子问题“选择”的过程,大大提高了速度。但要注意的是贪心算法并不能保证得到全局最优解,它只能解决满足上述具有两项特质的问题。
贪心算法用的也是自顶向下,编码过程中也会用到递归,这和自顶向下的动态规划是相似的。自顶向下的动态规划中会用到备忘录,备忘录的目的是用来防止子问题被重复求解;而在贪心算法中欧并不需要备忘录,因为不存在重复子问题的求解。
动态规划和贪心算法都要做出选择。自顶向下的动态规划是将大问题转化为小问题,这常用状态转移方程进行描述,状态转移方程描述的就是通过特定的“选择”将大问题转化为小问题,并且注意递归的出口即可;对于自底向上的动态规划,则是提前找到一个base case,并且再逐步计算出一个一个的子问题,然后通过“选择”,用这些个子问题构造出对大问题的求解,这个过程同样也是用状态转移方程进行描述。贪心算法通过做出贪心选择,导致了后续待求解子问题的减少,实际上只剩下了一个子问题,然后通过递归再对子问题同样做出贪心选择来逐步降低子问题的粒度,直至求解完成。
贪心选择与子问题的最优解组合在一起构建出原问题的全局最优解,这种方法隐含了对子问题使用数学归纳法,该归纳法可证明每个步骤的贪心选择会生成原问题的最优解。
3 例子(最大兼容活动《算法导论3rd-P237》)
3.1 公共代码实现
//使用快速排序给活动序列按照结束时间进行原地排序
int partition(int *a, int p, int r, int *b)
{
int value = a[r];
int j = p - 1;
for (int i=p;i<=r-1;++i)
{
if (a[i] <= value)
{
swap(a[++j], a[i]);
swap(b[j], b[i]);
}
}
swap(a[++j], a[r]);
swap(b[j], b[r]);
return j;
}
void qSort(int *a, int p, int r, int *b)
{
if (p < r)
{
int q = partition(a, p, r, b);
qSort(a, p, q-1, b);
qSort(a, q+1, r, b);
}
}
void PRINT_RESULT(int *s, int *f, const vector<int>& result)
{
for (int i = 0; i < result.size(); ++i)
{
cout << "(" << s[result[i]] << "," << f[result[i]] << ")";
}
cout << endl;
}
3.2 动态规划求解-不需要事先排序-不带输出活动序列(仅仅含有最大活动数)-未使用memo
int activity_DynamicUp2Bottom_NoResult(int *s, int *f, const vector<int>& vec)
{
int q = 0, maxIndex = 0;
for (int i=0;i<vec.size();++i)
{
int k = vec[i];
vector<int> s1, s2;
for (int j = 0; j < vec.size(); ++j)
{
if (j == i)
{
continue;
}
if (f[vec[j]] <= s[k])
{
s1.push_back(vec[j]);
continue;
}
if (s[vec[j]] >= f[k])
{
s2.push_back(vec[j]);
}
}
int max1 = 0, max2 = 0;
if (!s1.empty())
{
max1 = activity_DynamicUp2Bottom_NoResult(s, f, s1);
}
if (!s2.empty())
{
max2 = activity_DynamicUp2Bottom_NoResult(s, f, s2);
}
if (q < max1 + max2 +1)
{
q = max1 + max2 + 1;
maxIndex = k;
}
}
return q;
}
void test_UnOrdered_DynamicUp2Bottom_NoResult()
{
//未按照结束时间排序
//int s[] = { 6,2,3,1,5,8 };//结束时间
//int f[] = { 8,4,5,5,9,10 };//开始时间
//未按照结束时间排序
int s[] = { 8,3,0,5,3,5,6,8,2,1,12 };//结束时间
int f[] = { 12,5,6,7,9,9,10,11,14,4,16 };//开始时间
int n = sizeof(s) / sizeof(s[0]);
vector<int> vec;
for (int i = 0; i < n; ++i)
{
vec.push_back(i);
}
//不带输出结果
int maxActivity = activity_DynamicUp2Bottom_NoResult(s, f, vec);
}
3.3 动态规划求解-不需要事先排序-带输出活动序列(含有最大活动数目) -未使用memo
int activity_DynamicUp2Bottom_WithResult(int *s, int *f, const vector<int>& vec, vector<int>& r1)
{
map<int, vector<int>> r;
int q = 0, maxIndex = 0;
for (int i = 0; i < vec.size(); ++i)
{
int k = vec[i];
vector<int> s1, s2;
for (int j = 0; j < vec.size(); ++j)
{
if (j == i)
{
continue;
}
if (f[vec[j]] <= s[k])
{
s1.push_back(vec[j]);
continue;
}
if (s[vec[j]] >= f[k])
{
s2.push_back(vec[j]);
}
}
int max1 = 0, max2 = 0;
if (!s1.empty())
{
max1 = activity_DynamicUp2Bottom_WithResult(s, f, s1, r[k]);
}
if (!s2.empty())
{
max2 = activity_DynamicUp2Bottom_WithResult(s, f, s2, r[k]);
}
if (q < max1 + max2 + 1)
{
q = max1 + max2 + 1;
maxIndex = k;
}
}
//保存最优结果
for (int i=0;i<r[maxIndex].size(); ++i)
{
r1.push_back(r[maxIndex][i]);
}
r1.push_back(maxIndex);
return q;
}
void test_UnOrdered_DynamicUp2Bottom_WithResult()
{
//未按照结束时间排序
//int s[] = { 6,2,3,1,5,8 };//结束时间
//int f[] = { 8,4,5,5,9,10 };//开始时间
//未按照结束时间排序
int s[] = { 8,3,0,5,3,5,6,8,2,1,12 };//结束时间
int f[] = { 12,5,6,7,9,9,10,11,14,4,16 };//开始时间
int n = sizeof(s) / sizeof(s[0]);
vector<int> vec;
for (int i = 0; i < n; ++i)
{
vec.push_back(i);
}
//带输出结果
vector<int> result;
int maxActivity = activity_DynamicUp2Bottom_WithResult(s, f, vec, result);
PRINT_RESULT(s, f, result);
}
3.4 动态规划求解-不需要事先排序-带输出活动序列(含有最大活动数目)- 增加了memo(memo是相对于某一个活动集合而言的)
int activity_DynamicUp2BottomMemo_WithResult(int *s, int *f, const vector<int>& vec, vector<int>& r1, map<vector<int>, vector<int>>& memo)
{
map<int, vector<int>> r;
int q = 0, maxIndex = 0;
for (int i = 0; i < vec.size(); ++i)
{
int k = vec[i];
vector<int> s1, s2;
for (int j = 0; j < vec.size(); ++j)
{
if (j == i)
{
continue;
}
if (f[vec[j]] <= s[k])
{
s1.push_back(vec[j]);
continue;
}
if (s[vec[j]] >= f[k])
{
s2.push_back(vec[j]);
}
}
int max1 = 0, max2 = 0;
if (memo.find(s1) != memo.end())
{
r[k] = memo[s1];
}
else
{
if (!s1.empty())
{
max1 = activity_DynamicUp2BottomMemo_WithResult(s, f, s1, r[k], memo);
}
}
if (memo.find(s2) != memo.end())
{
r[k] = memo[s2];
}
else
{
if (!s2.empty())
{
max2 = activity_DynamicUp2BottomMemo_WithResult(s, f, s2, r[k], memo);
}
}
if (q < max1 + max2 + 1)
{
q = max1 + max2 + 1;
maxIndex = k;
}
}
//保存最优结果
for (int i = 0; i < r[maxIndex].size(); ++i)
{
r1.push_back(r[maxIndex][i]);
}
r1.push_back(maxIndex);
//放入memo
memo[vec] = r1;
return q;
}
void test_UnOrdered_DynamicUp2BottomMemo_WithResult()
{
//未按照结束时间排序
//int s[] = { 6,2,3,1,5,8 };//结束时间
//int f[] = { 8,4,5,5,9,10 };//开始时间
//未按照结束时间排序
int s[] = { 8,3,0,5,3,5,6,8,2,1,12 };//结束时间
int f[] = { 12,5,6,7,9,9,10,11,14,4,16 };//开始时间
int n = sizeof(s) / sizeof(s[0]);
vector<int> vec;
for (int i = 0; i < n; ++i)
{
vec.push_back(i);
}
//带输出结果
vector<int> result;
map<vector<int>, vector<int>> memo;
int maxActivity = activity_DynamicUp2BottomMemo_WithResult(s, f, vec, result, memo);
PRINT_RESULT(s, f, result);
}
3.5 贪心算法-递归实现-事先按照结束时间进行从小到大排序
int activity_Greedy_Recursive(int *s, int *f, int startIndex, int n, vector<int>& result)
{
int m = startIndex + 1;
while (m<=n && s[m]<f[startIndex])//找到第一个起始时间大于等于f[startIndex]的活动
{
++m;
}
if (m<=n)
{
result.push_back(m);
//这里使用的是逗号表达式,逗号表达式中的每一个表达式都会执行,但逗号表达式最右边的表达式的结果最为整个逗号表达式的结果
return activity_Greedy_Recursive(s, f, m, n, result), result.size();
}
else
{
return 0;
}
}
void test_Ordered_Greedy_Recursive()
{
//未按照结束时间排序,手动添加虚拟活动a0,起始时间和结束时间都是0
//int s[] = { 0,6,2,3,1,5,8 };//结束时间
//int f[] = { 0,8,4,5,5,9,10 };//开始时间
//未按照结束时间排序,手动添加虚拟活动a0,起始时间和结束时间都是0
int s[] = { 0,8,3,0,5,3,5,6,8,2,1,12 };//结束时间
int f[] = { 0,12,5,6,7,9,9,10,11,14,4,16 };//开始时间
int n = sizeof(s) / sizeof(s[0]);
//快速排序
qSort(f, 0, n - 1, s);
//带输出结果
vector<int> result;
int maxActivity = activity_Greedy_Recursive(s, f, 0, n-1, result);
PRINT_RESULT(s, f, result);
}
3.6 贪心算法-迭代实现-事先按照结束时间进行从小到大排序
int activity_Greedy_Iteration(int *s, int *f, int startIndex, int n, vector<int>& result)
{
result.push_back(startIndex);
for (int i = startIndex + 1; i <= n; ++i)
{
if (s[i] >= f[startIndex])
{
result.push_back(i);
startIndex = i;
}
}
return result.size();
}
void test_Ordered_Greedy_Iteration()
{
//未按照结束时间排序,迭代版本不需要虚拟活动a0
//int s[] = { 6,2,3,1,5,8 };//结束时间
//int f[] = { 8,4,5,5,9,10 };//开始时间
//未按照结束时间排序,迭代版本不需要虚拟活动a0
int s[] = { 8,3,0,5,3,5,6,8,2,1,12 };//结束时间
int f[] = { 12,5,6,7,9,9,10,11,14,4,16 };//开始时间
int n = sizeof(s) / sizeof(s[0]);
//快速排序
qSort(f, 0, n - 1, s);
//带输出结果
vector<int> result;
int maxActivity = activity_Greedy_Iteration(s, f, 0, n - 1, result);
PRINT_RESULT(s, f, result);
}