参考:https://blog.csdn.net/qq_32400847/article/details/51336300
https://blog.csdn.net/effective_coder/article/details/8736718
贪心算法思想:
顾名思义,贪心算法总是作出在当前看来最好的选择。也就是说贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。虽然贪心算法不能对所有问题都得到整体最优解,但对许多问题它能产生整体最优解。如单源最短路经问题,最小生成树问题等。在一些情况下,即使贪心算法不能得到整体最优解,其最终结果却是最优解的很好近似。
贪心算法的基本要素:
1.贪心选择性质。所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2. 当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心算法求解的关键特征。
贪心算法的基本思路:
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到算法中的某一步不能再继续前进时,算法停止。
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。
实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解;
贪心算法经典问题:
1.活动安排问题(要求活动最大化)----最大选择区间不相交---参考例题:hdoj 2037
分析:右排序,从第一个f[i]开始,判断后一个s[i]>f[i-1],成立就k++
2.区间完全覆盖问题---
给定一个长度为m的区间,再给出n条线段的起点和终点(闭区间),求最少使用多少条线段可以将整个区间完全覆盖?
分析:左排序,从第一个f[i]开始,判断后一个s[i]<=f[i-1],成立就选择f[i]大的那个,k++
3.区间选点问题--
给定N个区间[a,b],取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以重复)。
分析:右排序,从第一个f[i]开始,判断后一个s[i]<=f[i-1],成立就不操作,不成立就k++,并且改变比较的f[i]为不成立的f[i];
(上面的三种情况,做题时要把问题简化成某个类型)
4.特殊背包问题----性价比问题;---参考例题:hdoj 1009
在选择物品i装入背包时,可以选择物品的一部分,而不一定要全部装入背包
分析:计算每种物品的单位重量价值作为贪心选择的依据指标,选择单位重量价值最高的物品,将尽可能多的该物品装入背包,依此策略一直地进行下去,直到背包装满为止
5乘船问题
第一种:
POJ1700是一道经典的贪心算法例题。题目大意是只有一艘船,能乘2人,船的运行速度为2人中较慢一人的速度,过去后还需一个人把船划回来,问把n个人运到对岸,最少需要多久。先将所有人过河所需的时间按照升序排序,我们考虑把单独过河所需要时间最多的两个旅行者送到对岸去,有两种方式:
1.最快的和次快的过河,然后最快的将船划回来;次慢的和最慢的过河,然后次快的将船划回来,所需时间为:t[0]+2*t[1]+t[n-1];
2.最快的和最慢的过河,然后最快的将船划回来,最快的和次慢的过河,然后最快的将船划回来,所需时间为:2*t[0]+t[n-2]+t[n-1]。
(这里我们考虑这两种方式的原因,主要是船能乘2人,应该尽量想办法把最慢的两人先送过去,保证最后一定是最快就和次快的人到对岸。这就有两种,一是只有最快的人回来,次快的在岸上等;二是最快和最慢的人都回来一次,也就是第一次先把次快的送过去,倒数第二次,次快的再回来和最快的一起过去。)
算一下就知道,除此之外的其它情况用的时间一定更多。每次都运送耗时最长的两人而不影响其它人,问题具有贪心子结构的性质。
n=3 之后的情况单独考虑
第二种:
有n个人,第i个人重量为wi。每艘船的最大载重量均为C,且最多只能乘两个人。用最少的船装载所有人。
分析:考虑最轻的人i,如果无人能和他一起坐船,那唯一的方法就是每个人都单独坐船;否则,就选最重的那个人和i一起坐船。
可以用两个下标i,j,每次将i++.j--;
6..多机调度问题
n个作业组成的作业集,可由m台相同机器加工处理。要求给出一种作业调度方案,使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。作业不能拆分成更小的子作业;每个作业均可在任何一台机器上加工处理。这个问题是NP完全问题,还没有有效的解法(求最优解),但是可以用贪心选择策略设计出较好的近似算法(求次优解)。当n<=m时,只要将作业时间区间分配给作业即可;当n>m时,首先将n个作业从大到小排序,然后依此顺序将作业分配给空闲的处理机。也就是说从剩下的作业中,选择需要处理时间最长的,然后依次选择处理时间次长的,直到所有的作业全部处理完毕,或者机器不能再处理其他作业为止。如果我们每次是将需要处理时间最短的作业分配给空闲的机器,那么可能就会出现其它所有作业都处理完了只剩所需时间最长的作业在处理的情况,这样势必效率较低。在下面的代码中没有讨论n和m的大小关系,把这两种情况合二为一了。
#include<iostream>
#include<algorithm>
using namespace std;
int speed[10010];
int mintime[110];
bool cmp( const int &x,const int &y)
{
return x>y;
}
int main()
{
int n,m;
memset(speed,0,sizeof(speed));
memset(mintime,0,sizeof(mintime));
cin>>n>>m;
for(int i=0;i<n;++i) cin>>speed[i];
sort(speed,speed+n,cmp);
for(int i=0;i<n;++i)
{
*min_element(mintime,mintime+m)+=speed[i]; //*min_element(mintime,mintime+m)是mintimel里面的最小值,相应的还有*max_element
}
cout<<*max_element(mintime,mintime+m)<<endl;
}
7.买卖东西问题
特殊:经过一个商店可以不进行操作,只能拿一个商品
分析:https://blog.csdn.net/qq_40725780/article/details/81168424
//这里之后,题目已经不止用了贪心的方法了,暂时还不懂,所以没有自己的分析,多是复制参考博客 1 的,先放着;
8.Huffman编码(结合树-)
我们可以有多种方式表示文件中的信息,如果用01串表示字符,采用定长编码表示,则需要3位表示一个字符,整个文件编码需要300000位;采用变长编码表示,给频率高的字符较短的编码,频率低的字符较长的编码,达到整体编码减少的目的,则整个文件编码需要(45×1+13×3+12×3+16×3+9×4+5×4)×1000=224000位,由此可见,变长码比定长码方案好,总码长减小约25%。
对每一个字符规定一个01串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀,这种编码称为前缀码。可能无前缀码是一个更好的名字,但是前缀码是一致认可的标准术语。编码的前缀性质可以使译码非常简单:例如001011101可以唯一的分解为0,0,101,1101,因而其译码为aabe。译码过程需要方便的取出编码的前缀,为此可以用二叉树作为前缀码的数据结构:树叶表示给定字符;从树根到树叶的路径当作该字符的前缀码;代码中每一位的0或1分别作为指示某节点到左儿子或右儿子的路标。
从上图可以看出,最优前缀编码码的二叉树总是一棵完全二叉树,而定长编码的二叉树不是一棵完全二叉树。 给定编码字符集C及频率分布f,C的一个前缀码编码方案对应于一棵二叉树T。字符c在树T中的深度记为dT(c),dT(c)也是字符c的前缀码长。则平均码长定义为:
使平均码长达到最小的前缀码编码方案称为C的最优前缀码。
Huffman编码的构造方法:先合并最小频率的2个字符对应的子树,计算合并后的子树的频率;重新排序各个子树;对上述排序后的子树序列进行合并;重复上述过程,将全部结点合并成1棵完整的二叉树;对二叉树中的边赋予0、1,得到各字符的变长编码。
POJ3253一道就是利用这一思想的典型例题。题目大意是有把一块无限长的木板锯成几块给定长度的小木板,每次锯都需要一定费用,费用就是当前锯的木板的长度。给定各个要求的小木板的长度以及小木板的个数,求最小的费用。以要求3块长度分别为5,8,5的木板为例:先从无限长的木板上锯下长度为21的木板,花费21;再从长度为21的木板上锯下长度为5的木板,花费5;再从长度为16的木板上锯下长度为8的木板,花费8;总花费=21+5+8=34。利用Huffman思想,要使总费用最小,那么每次只选取最小长度的两块木板相加,再把这些和累加到总费用中即可。为了提高效率,使用优先队列优化,并且还要注意使用long long int保存结果。
AC代码:
#include<queue>
#include<cstdio>
#include<iostream>
using namespace std;
int main()
{
long long int sum;
int i,n,t,a,b;
while(~scanf("%d",&n))
{
priority_queue<int,vector<int>,greater<int> >q;
for(i=0;i<n;i++)
{
scanf("%d",&t);
q.push(t);
}
sum=0;
if(q.size()==1)
{
a=q.top();
sum+=a;
q.pop();
}
while(q.size()>1)
{
a=q.top();
q.pop();
b=q.top();
q.pop();
t=a+b;
sum+=t;
q.push(t);
}
printf("%lld\n",sum);
}
9.Dijkstra算法
10.最小生成树算法