贪心策略练习与总结

一.感性认识“贪心策略”

1.基本概念

所谓贪心算法是指,在对问题求解时,总是只做出在当前看来是最好的选择即可,并不穷尽各种可能来真正找出最优解。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
贪心策略不追求整体最优解,只希望得到较为满意的解(然而在许多情况下能达到最优解)。贪心策略一般可以快速得到满意的解,因为它省去了为找最优解要穷尽所有可能而必须耗费的大量时间。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

2.基本思想

贪心算法的基本思想如下:
  1.建立数学模型来描述源问题;
  2.把求解的问题分成若干个子问题;
  3.对每一子问题求解,得到子问题的局部最优解;
  4.把子问题的解局部最优解合成原来解问题的一个解。

3.适用的情况

贪心策略适用的前提是:局部最优策略能导致产生全局最优解。实际上,贪心算法适用的情况很少。对于一个具体的问题,我们怎么知道是否可用贪心算法来解此问题呢?以及能否得到问题的一个最优解呢?这个问题很难给予肯定的回答。但是,从许多可以用贪心算法求解的问题中我们看到它们一般具有两个重要的性质:贪心选择性质(最难的在于证明他能形成最优解)和最优子结构性质。

1).贪心策略的选择

所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。在动态规划算法中,每步所作的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能作出选择。而在贪心算法中,仅在当前状态下作出最好选择,即局部最优选择。然后再去解作出这个选择后产生的相应的子问题。

贪心算法所作的贪心选择可以依赖于以往所作过的选择,但决不依赖于将来所作的选择,也不依赖于子问题的解。正是由于这种差别,动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为一个规模更小的子问题。

2).最优子结构性质

当一个问题的最优解包含着它的子问题的最优解时,称此问题具有最优子结构性质。问题所具有的这个性质是该问题可用动态规划算法或贪心算法求解的一个关键特征。


二.理性认识“贪心策略”


1,例子一,活动选择问题

1),问题描述


有一个由n个活动组成的集合S = {a1, ..., an}
1. 这些活动使用同一个资源,而这个资源在某一时刻只供一个活动使用
2. 每个活动都有一个开始和结束时间si/fi;如果被选中,则任务ai发生在半开时间区间[si, fi)
3. 如果两个活动ai和aj不重叠,则称两个活动兼容
活动选择问题,希望选出一个最大兼容活动集
初始化数据:
Si={1,3,0,5,3,5,6,8,8,2,12}
Fi={4,5,6,7,9,9,10,11,12,14,16},即如下表格:
i 1 2 3 4 5 6 7 8 9 10 11
si 1 3 0 5 3 5 6 8 8 2 12
fi 4 5 6 7 8 9 10 11 12 13 14

2),分析过程

A)描述贪心选择标准

以第一个活动作为起点(因为他是最早结束的,这样才可以尽可能增加多次活动的“潜力”),总是选择满足兼容条件下的最早结束的活动,因为这样可以使得剩下的时间资源可以更多的其他活动使用,这种选择方式一定可以选出最大兼容活动数。


3),代码如下:

// ConsoleAppGreedyTest1.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"


#include "iostream"
using namespace std;
#define len 11
int GreedyActivitySelector(int *s,int *f,int *A);
int _tmain(int argc, _TCHAR* argv[])
{
	int Si[len] = { 1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12 };//开始时间
	int Fi[len] = { 4, 5, 6, 7, 9, 9, 10, 11, 12, 14, 16 };//结束时间
	int A[len] = { 0 };//兼容活动子集
	int l = 0;
	l=GreedyActivitySelector(Si, Fi, A);//获取最大活动兼容个数
	for (int i = 0; i < l;i++)
	{
		cout << A[i] << " ";
	}
	cout << endl;
	system("pause");
	return 0;
}

int GreedyActivitySelector(int *s, int *f, int *A)
{
	int longlen = 0;
	A[0] = 0;//直接选取第一个活动作为最大兼容活动集的起点
	int k = 0;//记录最近加入的活动的下标
	int count = 1;
	for (int m = 1; m < len;m++)
	{
		if (s[m]>f[k])//s[m]是下一个活动的开始时间,f[k]是上一个活动的结速时间,我们总是希望找到下一个活动的开始时间比上一个的结束时间大,就可以了
		{
			A[count] = m;
			count++;
			k = m;
		}
	}
	return count;
}

贪心策略练习与总结_第1张图片



三,练习贪心策略


练习一,最长上升子序列

题目描述:

给定一个整型数组, 求这个数组的最长严格递增子序列的长度。 譬如序列1 2 2 4 3 的最长严格递增子序列为1,2,4或1,2,3.他们的长度为3。

输入:

输入可能包含多个测试案例。
对于每个测试案例,输入的第一行为一个整数n(1<=n<=100000):代表将要输入的序列长度
输入的第二行包括n个整数,代表这个数组中的数字。整数均在int范围内。

输出:

对于每个测试案例,输出其最长严格递增子序列长度。

样例输入:
4
4 2 1 3
5
1 1 1 1 1
样例输出:
2
1
贪心分析:

开辟一个栈,每次取“栈”(这里采用数组来实现)顶元素s和读到的元素a做比较,如果a>s,  则加入栈(将会有序);如果a最长序列长度没有改变,但序列Q的''潜力''增大,我们总是贪心的营造这种能产生最长长度的“潜力”,并且不影响其长度。  
举例:原序列为1,5,8,3,6,7  
栈为1,5,8,此时读到3,则用3替换5,得到栈中元素为1,3,8,  再读6,用6替换8,得到1,3,6,再读7,得到最终栈为1,3,6,7  ,最长递增子序列为长度4。 


#include 
#include "vector" 
 
using namespace std;
int maxLongNoDrop(const vector &Arr)
{
    vector stackVec;
    stackVec.push_back(-999999);//初始化第一个数为一个尽可能小的值
    for (size_t i = 0; i < Arr.size(); i++)
    {
        if (Arr[i] > stackVec.back())//stackVec.back()该数组最前面的这个值
        {
            stackVec.push_back(Arr[i]);//将形成一个有序的数组
        }
        else
        {
            int mid = 0, low = 1, high = stackVec.size()-1;
            //二分检索“数组栈”(有序的)中比当前Arr[i]大的第一个数的位置(为low)
            while (low <= high)
            {
                mid = (low + high) / 2;
                if (Arr[i] > stackVec[mid])
                    low = mid + 1;
                else
                    high = mid - 1;
            }
            //替换之
            stackVec[low] = Arr[i];
        }
    }
 
    return (stackVec.size() - 1);
}
 
int main()
{
    int n = 0;
    while (cin>>n)
    {
        vector srcArr(n, 0);
        for (size_t i = 0; i < n; i++)
            cin >> srcArr[i];
        cout << maxLongNoDrop(srcArr) << endl;
    }
    return 0;
}


练习二,招聘会

题目描述:

又到毕业季,很多大公司来学校招聘,招聘会分散在不同时间段,小明想知道自己最多能完整的参加多少个招聘会(参加一个招聘会的时候不能中断或离开)。

输入:

第一行n,有n个招聘会,接下来n行每行两个整数表示起止时间,由从招聘会第一天0点开始的小时数表示。
n <= 1000 。

输出:

最多参加的招聘会个数。

样例输入:
3
9 10
10 20
8 15
样例输出:
2
#include "vector"
#include "string"
#include "algorithm"
#include 

#include 
using namespace std;
int GreedyActSelector(vector &s, vector &f);
int main()
{
	int n = 0;
	while (cin >> n&&n <= 1000)
	{
		vector vecstart(n);
		vector vecend(n);

		for (int i = 0; i < n; i++)
			cin >> vecstart[i] >> vecend[i];
		for (int i = 0; i < n; i++)
		{
			for (int j = i+1; j < n; j++)
			{
				if (vecend[i]>vecend[j])//对结束时间排序,由小到大
				{
					int temp1 = vecend[i];
					vecend[i] = vecend[j];
					vecend[j] = temp1;

					int temp2 = vecstart[i];//开始时间也相应变换,但不一定有序
					vecstart[i] = vecstart[j];
					vecstart[j] = temp2;
				}
			}
		}
		cout << GreedyActSelector(vecstart, vecend) << endl;
	}
	return 0;
}
//以第一个招聘会作为起点(因为他是最早结束的,这样才可以尽可能增加多次活动的“潜力”),
//总是选择满足兼容条件下的最早结束的招聘会,因为这样可以使得剩下的时间资源可以更多的其他招聘会使用,
//这种选择方式一定可以选出最大兼容招聘会数。
int GreedyActSelector(vector &start, vector &finish)
{
	int longlen = 0;
	int k = 0;//记录最近加入的活动的下标  
	int count = 1;
	for (int m = 1; m < start.size(); m++)
	{
		if (start[m] >= finish[k])//start[m]是下一个招聘会的开始时间,finish[k]是上一个招聘会的结速时间,我们总贪婪的找到下一个活动的开始时间比上一个的结束时间大  
		{
			count++;
			k = m;
		}
	}
	return count;
}


练习三,还是畅通工程

题目描述:
    某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。
输入:

    测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。
    当N为0时,输入结束,该用例不被处理。

输出:

    对每个测试用例,在1行里输出最小的公路总长度。

样例输入:
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
0
样例输出:
3
5


#include "vector"
#include "string"
#include "algorithm"
#include 
#include "stack"
#include 
#include 
 
using namespace std;
 
class Edge
{
public:
    int acity;//城市a
    int bcity;//城市b
    int cost;  //建成a到b的路的花费
    bool operator < (const Edge &q) const//注意返回值的类型,运算符重载。
    {  
        return cost> n, n > 0)
    {
        int m = n*(n - 1) / 2;
 
        UFSet uset(10000);
        uset.makeSet(n);//初始化每个城市的祖先为自身
 
        for (int i = 0; i < m; i++)
            cin>> edge[i].acity>> edge[i].bcity>> edge[i].cost;
 
        int mincost = uset.getMinCost(m);
        cout << mincost << endl;
    }
 
    return 0;
}



练习四,继续畅通工程

题目描述:
    省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。现得到城镇道路统计表,表中列出了任意两城镇间修建道路的费用,以及该道路是否已经修通的状态。现请你编写程序,计算出全省畅通需要的最低成本。
输入:
    测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( 1< N < 100 );随后的 N(N-1)/2 行对应村庄间道路的成本及修建状态,每行给4个正整数,分别是两个村庄的编号(从1编号到N),此两村庄间道路的成本,以及修建状态:1表示已建,0表示未建。

    当N为0时输入结束。
输出:
    每个测试用例的输出占一行,输出全省畅通需要的最低成本。
样例输入:
3
1 2 1 0
1 3 2 0
2 3 4 0
3
1 2 1 0
1 3 2 0
2 3 4 1
3
1 2 1 0
1 3 2 1
2 3 4 1
0
样例输出:
3
1
0


#include "algorithm"
#include 
#include "stack"
#include 
#include 

using namespace std;

class Edge
{
public:
	int acity;//城市a
	int bcity;//城市b
	int cost;  //建成a到b的路的花费
	bool isBuild; //标记路是否建成
	bool operator < (const Edge &q) const//注意返回值的类型,运算符重载。
	{  
		return cost> n, n > 0)
	{
		int m = n*(n - 1) / 2;

		UFSet uset(100);
		uset.makeSet(n);//初始化每个城市的祖先为自身
		for (int i = 0; i < m; i++)
		{
			cin>> edge[i].acity>> edge[i].bcity>> edge[i].cost>> edge[i].isBuild;
			if (edge[i].isBuild == 1)
				uset.unionSet(edge[i].acity, edge[i].bcity);//将已经建成的两个城市编号建立连接
		}
		int mincost = uset.getMinCost(m);
		cout << mincost << endl;
	}

	return 0;
}



练习五,货币问题



题目描述:

已知有面值为1元,2元,5元,10元,20元,50元,100元的货币若干(可认为无穷多),需支付价格为x的物品,并需要恰好支付,即没有找零产生。
求,至少需要几张货币才能完成支付。
如,若支付价格为12元的物品,最少需要一张10元和一张2元,即两张货币就可完成支付。

输入:

输入包含多组测试数据,每组仅包含一个整数p(1<=p<=100000000),为需支付的物品价格。

输出:

对于每组输入数据,输出仅一个整数,代表最少需要的货币张数。

样例输入:
10
11
13
样例输出:
1
2
3
#include 
 
using namespace std;
 
int getAns(int key)
{//贪心选择的结果
    if (key == 0)
        return 0;
    if (key == 8 || key == 9)
        return 3;
    else if (key == 1 || key == 2 || key == 5)
        return 1;
    else
        return 2;
}
 
int main()
{
    int n;
    while (cin >> n)
    {
        int count=0;
        count += n / 100;//计算百位及其以上
        n = n % 100;//保留个位和十位
        count += getAns(n / 10);//计算十位
        count += getAns(n % 10);//计算个位
        cout << count << endl;
    }
    return 0;
}
/**************************************************************
    Problem: 1549
    User: EbowTang
    Language: C++
    Result: Accepted
    Time:50 ms
    Memory:1520 kb
****************************************************************/

或者代码这样写

#include 
#include "stack"
#include 
#include 
 
using namespace std;
 
const int money[] = { 100, 50, 20, 10, 5, 2, 1 };//注意这些面值能凑出任意正整数面值
 
int main()
{
    int n;
    while (cin >> n)
    {
        int count = 0;
        for (int i = 0; i < 7; i++)
        {
            count += n / money[i];//总是贪心的选择面值大的
            n = n%money[i];
        }
        cout << count << endl;
    }
    return 0;
}
/**************************************************************
    Problem: 1549
    User: EbowTang
    Language: C++
    Result: Accepted
    Time:50 ms
    Memory:1520 kb
****************************************************************/





参考资源

【1】《算法导论》,第十六章,贪心算法
【2】《百度百科》
【3】《维基百科》
【4】http://www.cppblog.com/3522021224/archive/2007/06/16/26429.aspx
【5】https://en.wikipedia.org/wiki/Greedy_algorithm
【6】http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741375.html
【7】http://www.icodeguru.com/cpp/10book/


注:

本文部分文字学习并整理自网络资源,代码参考并改写于《算法导论》.

如果侵犯了您的版权,请联系本人[email protected],本人将及时编辑掉!


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