第四章 贪心算法思维导图

基本概念

所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。

基本要素

1.最优子结构性质

2.贪心选择性质

基本思路

1.建立数学模型来描述问题。
2.把求解的问题分成若干个子问题。
3.对每一子问题求解,得到子问题的局部最优解。
4.把子问题的解局部最优解合成原来解问题的一个解。

适用的前提

局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。

经典案例分析

活动安排问题

基本思路

先选择活动1,活动1具有最早的完成时间。下面证明该问题具有贪心选择性质,即该问题有一个最优解以贪心选择开始,即该最优解中包含活动1。

证明:

    设子集A是该问题的一个最优解,且A中的活动按结束时间排序,其第一个活动为k:

     (1)若k=1,则A就是以贪心选择开始的最优解。

     (2)若k>1,则设子集B=A-{k}U{1}。由于finish[1]≤finish[k],且A中的活动是相容的,所以B中的活动也是相容的。又由于B中活动的个数与A中相同,且A是最优的,所以B也是最优的。所以B是以贪心选择1开始的最优解。

     综上所述,总是存在以贪心选择开始的最优解。该问题具有贪心选择性质。

最优子结构性质:

   在做出了贪心选择,即选择了活动1之后,原问题简化为对E中所有与1相容的活动进行活动安排的子问题。也就是说,如果A是原问题的最优解,则其子集C=A-{1}是该子问题的最优解(由反证法易得,假设该子问题存在一个活动数比C更多的解D,则{1}+D是原问题的一个最优解,且其活动数比A多,得出矛盾)。所以该问题具有最优子结构性质。

子问题:选择了活动1之后,C=A-{1}活动安排的最优解

代码

#include<iostream>
#include<cstdlib>
#include<algorithm>
using namespace std;
struct Act{
	int start,end;
};
bool cmp(struct Act A1,struct Act A2){
	return A1.end<A2.end;
}
int main(){
	int N;
	cin>>N;
	struct Act A[N];
	for(int i=0;i<N;i++){
		cin>>A[i].start>>A[i].end;
	}
	sort(A,A+N,cmp);
	struct Act B;
	B=A[0];
	int cnt=1;
	for(int j=1;j<N;j++){
		if(B.end<A[j].start){
		  B=A[j];  cnt++;
		} 
	}
	cout<<cnt<<endl;
} 

最优装载问题

基本思路

首先用数组承载每个集装箱的重量

为了方便,给数组排个序

循环小于集装箱数量
 

方法一:每个重量累加判断是否超过载重量

方法二:定义剩余变量,剩余变量减每个的重量,然后判断每个的重量是否大于剩余重量

哈夫曼编码

贪心算法:

       要求平均码长最小的前缀码方案。

贪心选择:频率最小的两棵树合成一棵树,频率变为和,然后在继续频率最小的两棵树,直到只剩一棵树。

      编码字符集中每个字符c的频率是f(c),以f为键值的优先队列Q用在做贪心选择时有效地确定算法当前要合并的两个具有最小频率的树。一旦两棵具有最小频率的树合并后,产生一颗新的树,其频率为合并的两棵树的频率之和。

      经过n-1次合并后,优先队列只剩下一棵树,即所要求的树T。

#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
struct Hcode
{
    char ch;
    deque<int> huffcode;
};
struct Hnode
{
    int weight;
    Hnode *parent;
    Hnode *lchild;
    Hnode *rchild;
};
struct cmp
{
    bool operator()(const Hnode * a,const Hnode * b)
    {
        return a->weight > b->weight;
    }
};
void huff_init(priority_queue<Hnode*,vector<Hnode*>,cmp> &pq,int n)
{
    for(int i=0;i<n-1;i++)
    {
        if(pq.size()==1)
            break;
        Hnode *pnode=new Hnode;
        Hnode *lnode=pq.top();
        pq.pop();
        Hnode *rnode=pq.top();
        pq.pop();
        pnode->parent=NULL;
        pnode->weight=lnode->weight+rnode->weight;
        pnode->lchild=lnode;
        pnode->rchild=rnode;
        lnode->parent=pnode;
        rnode->parent=pnode;
        pq.push(pnode);
    }
}
void huff_code(Hnode *node,Hcode *code,int n)
{
    for(int i=0;i<n;i++)
    {
        Hnode *curnode=&node[i];
        while(1)
        {
            if(curnode->parent==NULL)
                break;
            else
            {
                if(curnode->parent->rchild==curnode)
                    code[i].huffcode.push_front(1);
                else
                    code[i].huffcode.push_front(0);
            }
            curnode=curnode->parent;
        }
    }
}
void huff_print(Hcode *code,int n)
{
	cout<<"哈夫曼编码:"<<endl;
    for(int i=0;i<n;i++)
    {
        cout<<code[i].ch<<":";
        deque<int>::iterator it;
        for(it=code[i].huffcode.begin();it!=code[i].huffcode.end();it++)
            cout<<*it;
        cout<<endl;
    }
}
int main(){
        int n;
        cin>>n;
        Hcode code[1005];
        Hnode node[1005];
        for(int i=0;i<n;i++)
        {
        	cin>>code[i].ch;
            cin>>node[i].weight;
            node[i].parent=NULL;
            node[i].lchild=NULL;
            node[i].rchild=NULL;
        }
        priority_queue<Hnode*,vector<Hnode*>,cmp> pq;
        for(int i=0;i<n;i++)
            pq.push(node+i);
        huff_init(pq,n);
        huff_code(node,code,n);
        huff_print(code,n);
        return 0;
}

单源最短路径

思路:

贪心算法

贪心选择:从V-S中选择具有最短特殊路径的顶点u,从而确定从源到u的最短路径长度dist[u].

设置顶点集合S,不断做贪心选择来扩充这个集合。

每次从V-S中取出具有最短特殊路径长度的顶点u,将u添加到S中,同时对数组dist做必要的修改。一旦S包含了所有V中的顶点,dist就记录了从源到其它顶点间的最短路径长度。

#include<iostream>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
struct node
{
    int x;
    int s;
    bool operator<(const node p)const
    {
        return p.s<s;
    }
};
bool book[100];
int dis[100];
const int inf = 2100000000;
int main()
{
    vector<int>u[100];
    vector<int>w[100];
    int n,m;
    cin>>n>>m;
    int x,y,z;
    for(int i=0;i<m;i++){
        cin>>x>>y>>z;
        u[x].push_back(y);
        w[x].push_back(z);
    }
    fill(dis,dis+n+1,inf);
    dis[1]=0;
    priority_queue<node>q;
    node exa;
    exa.x=1;
    exa.s=0;
    q.push(exa);
    while(!q.empty()){
        exa=q.top();q.pop();
        if(book[exa.x]){continue;}
        book[exa.x]=true;
        int t=exa.x;
        for(int i=0;i<u[t].size();i++){
            if(dis[u[t][i]]>dis[t]+w[t][i]){
                dis[u[t][i]]=dis[t]+w[t][i];
                exa.s=dis[u[t][i]];
                exa.x=u[t][i];
                q.push(exa);
            }
        }
    }
    cout<<"从源点到其它顶点的最短路径长度:"<<endl;
    for(int i=1;i<=n;i++){
        cout<<"dis["<<i<<"]:"<<dis[i]<<" ";
		cout<<endl;
    }
    
}

最小生成树

基本思路

任何只由G的边构成,并包含G的所有顶点的树称为G的生成树(G连通).加权无向图G的生成树的代价是该生成树的所有边的代码(权)的和.最小代价生成树是其所有生成树中代价最小的生成树。

实现最小生成树的算法常用的是Prim,Kruskal学校数据结构的书上讲解了这两大算法的思路及用C++实现,但关于其合理性的证明却略过去了,这里主要加上我自己的一些总结,证明一下,最后写个模版用。

Prim

基本思想:

1.在图G=(V, E)(V表示顶点,E表示边)中,从集合V中任取一个顶点(例如取顶点v0)放入集合 U中,这时 U={v0},集合T(E)为空。
2. 从v0出发寻找与U中顶点相邻(另一顶点在V中)权值最小的边的另一顶点v1,并使v1加入U。即U={v0,v1 },同时将该边加入集合T(E)中。
3. 重复2,直到U=V为止。
这时T(E)中有n-1条边,T = (U, T(E))就是一棵最小生成树。

关键是每一步选取的边起点是已加入集合U中的点,终点是未加入集合U中的点,在所有这样的点中选取权值最小的一条,把未加入U的点加入U,这一条边加入树T上.

对于这一贪心策略的证明:

首先,一定有一个最优解包含了权值最小的边e_0(prim的第一步),因为如果不是这样,那么最优的解不包含e_0,把e_0加进去会形成一个环,任意去掉环里比e_0权值大的一条边,这样就构造了更优的一个解,矛盾.
用归纳法,假设prim的前k步选出来的边e_0,…, e_k-1是最优解的一部分,用类似的方法证明prim的方法选出的e_k一定也能构造出最优解。

Kruskal

基本思想:

假设WN=(V,{E})是一个含有n个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:先构造一个只含n个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有n棵树的一个森林。之后,从网的边集E中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,也就是说,将这两个顶点分别所在的两棵树合成一棵树;反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。依次类推,直至森林中只有一棵树,也即子图中含有n-1条边为止。

贪心策略的证明:

如果按Kruskal算法加入的边(u,v)在某一最优解T中不包含,那么T+(u,v)一定有且只有一个环,而且至少有一条边(u'.v')的权值大于等于(u,v).删除该边后,得到新树T'=T+(u,v)-(u',v')不会比T差,所以按Kruskal算法加入的边是最优的.

多机调度问题

基本思路:

本题目可以分为两种情况进行考虑:

   (1)机器数大于作业数:即用作业数数量的机器同时进行工作,而作业时间最长的那个即为处理机所需要的最短作业时间。

   (2)机器数小于作业数:用for循环总是找出空闲的机器,然后将机器上的作业时间进行累加,最后累加的时间最长的即为所求处理机所需最短作业时间

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值