贪心法

概述

贪心法把一个复杂问题分解为一系列较为简单的局部最优选择,每一步选择都是对当前解的一个扩展,直至获得问题的完整解。

求解过程

应用贪心法的关键是确定贪心选择策略,这种贪心策略只是根据当前信息作出最好的选择,不去考虑在后面看来这种选择是否合理

贪心法即使不能得到整体最优解,通常也是最优解的很好近似

例题

付款问题

问题

假设有面值为 5元、2元、1元、5 角、2 角、1 角的货币,需要找给顾客4 元 6 角现金,付款问题要求找到一个付款方案,使得付出的货币张数最少

分析

贪心选择策略:每次都找面值最大的货币,直到找不出更大的面值为止

值得注意的是,不一定能找出最优解,比如找给顾客 4 元 6 角,贪心法会找出 5 元、1 元、1 角、1 角、1 角,但是最优解是 2 元、2 元、1 角、1 角

实现

int payMoney(double sum){
    int money[6]={50, 20, 10, 5, 2, 1};
    int count=0,n=sum*10;
    int i=0;
    while(n>0){
        if(n>=money[i]*10){
            n-=money[i]*10;
            count++;
        }
        else
            i++;
    }
}

TSP问题

问题

给定一个有 n 个城市的地图,每个城市之间都有一条道路相连,求从某个城市出发,经过每个城市一次,最后回到出发城市的最短路径

分析

从任意城市出发,每次在没有到过的城市中选择最近的一个,直至经过了所有城市,最后回到出发城市

实现

int TSP(int *arc[],int n,int w){
    bool flag[n]={false};
    int edgeCount=0;
    int TSPLength=0;
    int u=w;
    flag[u]=true;
    while(edgeCount<n-1){
        int min=9999;
        int v;
        for(int i=0;i<n;i++){
            if(arc[u][i]<min && flag[i]==false){
                min=arc[u][i];
                v=i;
            }
        }
        TSPLength+=arc[u][v];
        cout<<u<<"->"<<v<<endl;
        u=v;
        flag[u]=true;
        edgeCount++;
    }
    cout<<u<<"->"<<w<<endl;
    TSPLength+=arc[u][w];
    return TSPLength;
}

图着色问题

问题

求无向连通图G=(V, E)的最小色数 k,使得用 k 种颜色对 G 中的顶点着色,可使任意两个相邻顶点着不同颜色

分析

贪心法解决着色问题

  • 选择一个颜色,依次对所有自身未着色且邻接点未着色的顶点进行着色
  • 选择下一个颜色,依次对所有自身未着色且邻接点着色不冲突的顶点进行着色
  • 依次进行,直至所有顶点都被着色

这种贪心策略是不一定能得到最优解的,比如对于下图,贪考虑顶点顺序为1、2、3、4、5得到最优解
img
考虑顶点顺序为1、5、2、3、4得到近似解
img

实现

int colorGraph(int *arc[],int n,int color[]){
    int m=n;
    int k=0;
    while(m>0){
        k++;
        for(int i=0;i<n;i++){
            if(color[i]!=0)
                continue;
            //找到第一个没有着色的点,着新色
            color[i]=k;
            m--;
            //找到所有与它相邻的点,跳过着色
            int j;
            for(j=0;j<n;j++){
                if(arc[i][j]==1&&color[j]==k)
                    break;
            }
            if(j==n)
                continue;
            //如果有相邻的点着了新色,那么这个点就不能着新色,跳过
            else{
                color[i]=0;
                m++;
            }
        }
    }
    return k;
}

最小生成树

问题

给定一个连通图,求生成树中边的权值之和最小的生成树

分析

贪心法求解最小生成树:prim算法

  • 从图中任意选取一个顶点作为初始顶点,将其加入到生成树中
  • 找到与初始顶点相邻的最小权值的边,将该边的另一个顶点加入到生成树中
  • 循环寻找与生成树相邻的最小权值的边,将该边的另一个顶点加入到生成树中

实现

int prim(int *r[],int n,int w){
    int lowcost[n];
    int closest[n];
    int minDist=0;
    for(int i=0;i<n;i++){
        lowcost[i]=r[w][i];
        closest[i]=w;
    }
    lowcost[w]=0;
    int m=n-1;
    while(m--){
        int k;
        int min=999;
        for(int i=0;i<n;i++){
            if(lowcost[i]<min&&lowcost[i]!=0){
                min=lowcost[i];
                k=i;
            }
        }
        cout<<"k="<<k<<endl;
        //k点放入树中
        minDist+=lowcost[k];
        lowcost[k]=0;
        cout<<closest[k]<<"---"<<k<<endl;
        //更新lowcost和closest
        for(int i=0;i<n;i++){
            if(lowcost[i]!=0&&r[k][i]<lowcost[i]){
                lowcost[i]=r[k][i];
                closest[i]=k;
            }
        }
    }
    return minDist;
}

背包问题

问题

给定 n 个物品和一个容量为 C 的背包,物品\((i1 ≤ i ≤ n)\)的重量是 \(w_i\),其价值为 \(v_i\),背包问题是如何选择装入背包的物品,使得装入背包中物品的总价值最大

分析

选择单位重量价值最大的物品(与0/1背包问题不同,背包内物品可以切分)

实现

double knapSack(int w[],int v[],int n,int C){
    double x[n]={0};
    int maxV=0;
    int i=0;
    for(i=0;w[i]<=C;i++){
        x[i]=1;
        maxV+=v[i];
        C-=w[i];
    }
    if(C>0){
        x[i]=(double)C/w[i];
        maxV+=v[i]*x[i];
    }
    return maxV;
}

活动安排问题

问题

设有 \(n\) 个活动的集合 \(E=\{1, 2, …, n\}\),其中每个活动都要求使用同一资源,而在同一时间只有一个活动能使用这个资源。每个活动 \(i(1 ≤ i ≤ n)\)都有一个要求使用该资源的起始时间 \(s_i\) 和一个结束时间 \(f_i\),且 \(s_i < f_i\) 。如果选择了活动\(i\),则它在半开时间区间\([s_i, f_i )\)内占用资源。若区间\([s_i, f_i )\)与区间\([s_j, f_j )\)不相交,则称活动 \(i\) 与活动 \(j\) 是相容的。

活动安排问题要求在所给的活动集合中选出个数最多的相容活动

分析

贪心法求解活动安排问题

  • 最早开始时间:增大活动利用率
  • 最早结束时间:使下一个活动尽早开始

后者更合适,因为活动安排问题的目标是使得活动的总数最大,而不是使得活动的总时间最长,贪心策略为选择最早结束的活动

实现

int activityManage(int s[],int f[],int B[],int n){
    //设s[]和f[]已经按照f[]的非递减顺序排列
    
    int count=0;
    B[0]=0;
    count++;
    int k=0;
    for(int i=1;i<n;i++){
        if(s[i]>=f[k]){
            B[count]=i;
            count++;
            k=i;
        }
    }
    return count;
}

埃及分数

问题

古埃及人只用分子为 1 的分数,在表示一个真分数时,将其分解为若干个埃及分数之和,例如:7/8 表示为 1/2 + 1/3 + 1/24。埃及分数问题要求把一个真分数表示为最少的埃及分数之和的形式

分析

贪心法求解埃及分数问题:
选择小于此真分数的最大埃及分数

实现

int egyptFraction(int a,int b){
    while(a!=1){
        int c=b/a+1;
        cout<<1<<"/"<<c<<" + ";
        a=a*c-b;
        b=b*c;
        //通分
        for(int i=2;i<=a;i++){
            if(a%i==0&&b%i==0){
                a=a/i;
                b=b/i;
            }
        }
    }
    cout<<1<<"/"<<b<<endl;
}

田忌赛马

问题

田忌和齐王赛马,他们各有 n 匹马,每次双方各派出一匹马进行赛跑,获胜的一方记 1 分,失败的一方记 -1 分,平局不计分,假设每匹马只能出场一次,每匹马有个速度值,比赛中速度快的马一定会获胜。田忌知道所有马的速度值,且田忌可以安排每轮赛跑双方出场的马,问田忌如何安排马的出场次序,使得最后获胜的比分最大?

分析

  1. 田忌最快的马比齐王最快的马快,则直接记1分
  2. 田忌最快的马比齐王最快的马慢,则用田忌最慢的马比齐王最快的马,记-1分
  3. 田忌最快的马和齐王最快的马速度相同
    1. 田忌最慢的马比齐王最慢的马快,则直接记1分(齐王最慢的马肯定会输,则消耗最差的和他比)
    2. 田忌最慢的马比齐王最慢的马慢,则用田忌最慢的马比齐王最快的马,记-1分

实现

int TianjiHorse(int t[],int q[],int n){
    int s=0;
    int t1=0,q1=0,t2=n-1,q2=n-1;
    while(t1<=t2){
        //1
        if(t[t1]>q[q1]){
            s++;
            t1++;
            q1++;
        }
        //2
        else if(t[t1]<q[q1]){
            s--;
            t2--;
            q1++;
        }
        //3
        else{
            //3.1
            if(t[t2]>q[q2]){
                s++;
                t2--;
                q2--;
            }
            //3.2
            else if(t[t2]<q[q2]){
                s--;
                t1++;
                q2--;
            }
        }
    }
    return s;
}

哈夫曼算法

问题

给出各元素权重,生成哈夫曼树,计算最小带权路径长度

分析

利用贪心算法,每次选择权重最小的两个元素,生成一个新的元素,权重为两个元素权重之和,重复此过程,直到所有元素都被合并为一个元素

实现

int huffman(int w[],int n){
    int s=0;
    while(n>1){
        int min1=0,min2=1;
        if(w[min1]>w[min2]){
            int temp=min1;
            min1=min2;
            min2=temp;
        }
        for(int i=2;i<n;i++){
            if(w[i]<w[min1]){
                min2=min1;
                min1=i;
            }
            else if(w[i]<w[min2]){
                min2=i;
            }
        }
        s+=w[min1]+w[min2];
        w[min1]=w[min1]+w[min2];
        w[min2]=w[n-1];
        n--;
    }
    return s;
}

练习

    • 题目:对于给定n位正整数a,删掉其中任意的k位数字,使得剩下的数字按原次序组成的新整数仍然是一个正整数,且新整数尽可能小。例如,当n=5,k=2,a=32547时,应输出247
    • 分析:贪心法,从高位开始,如果当前位比后一位大,则删除当前位,否则不删除,直到删除k位
    • 实现:
        void deleteK(int a[],int n,int k){
            int i=0;
            while(k>0){
                if(a[i]>a[i+1]){
                    for(int j=i;j<n-1;j++){
                        a[j]=a[j+1];
                    }
                    k--;
                    if(i>0){
                        i--;
                    }
                }
                else{
                    i++;
                }
            }
            for(int i=0;i<n-k;i++){
                cout<<a[i];
            }
        }
        ```
    
    • 题目:有n个顾客,顾客\(i\)需要的时间为\(t_i\),如何安排这n个顾客的服务顺序,使得所有顾客的等待时间之和最小?
    • 分析:贪心法,每次选择等待时间最短的顾客
    • 实现:
        void customer(int t[],int n){
            int s=0;
            for(int i=0;i<n;i++){
                int min=i;
                for(int j=i+1;j<n;j++){
                    if(t[j]<t[min]){
                        min=j;
                    }
                }
                int temp=t[i];
                t[i]=t[min];
                t[min]=temp;
                s+=t[i]*(n-i-1);
            }
            cout<<s<<endl;
        }
    
    • 问题:一辆汽车加满油后可以行驶m公里,途中有若干加油站,距离由A[m]给出,其中A[i]表示第i-1个加油站到i加油站的距离,起点终点各有一个加油站,计算加油最少次数
    • 分析:贪心法,每次选择能到达的最远的加油站
    • 实现:
        int car(int a[],int n,int m){
        int i=0;
        //加满
        int last=n;
        int sum=0;
        for(i=0;i<m;i++){
            last=last-a[i];
            if (last<0) break;
            
            if(a[i]>last){
                sum++;
                last=n;
            }
        }
        if(i==m) return sum;
        else return -1;
    }
    
    • 问题:TSP采用最短链接策略,每次选择最短的边加入解集合,保证最终形成哈密顿回路,设计算法求解TSP问题
    • 分析:首先按照城市之间距离远近进行排列,从距离最近的两个城市开始,如果这两个城市不在一个联通分量中并且度数均小于等于2,那么记录二者之间的路径,将它们划分到一个联通分量并将度数增加1;然后从距离第二小的两个城市开始,重复上述操作直到记录的路径有n-1条,最后找到度数为1的两个城市,作为最后一条路径。
    • 实现:
       //最短链接策略TSP问题
        int tsp(int **a,int n){
            //保存连接图中每个点的度
            int d[n]={0};
            int k=n;
            int totalLenth=0;
            while(k>0){
                //找到权重最小的边
                int min=10000;
                int minI,minJ;
                for(int i=0;i<n;i++){
                    for(int j=0;j<n;j++){
                        if(a[i][j]<min&&a[i][j]!=0){
                            min=a[i][j];
                            minI=i;
                            minJ=j;
                        }
                    }
                }
                //如果这条边的两个顶点的度都小于2,则加入到最短路径中
                if(d[minI]<2&&d[minJ]<2){
                    totalLenth+=a[minI][minJ];
                    d[minI]++;
                    d[minJ]++;
                    a[minI][minJ]=0;
                    cout<<minI<<"->"<<minJ<<endl;
                    k--;
                }
                else
                    a[minI][minJ]=10000;
            }
            return totalLenth;
            
        } 
    
    • 问题:Kruskal算法生成最小生成树
    • 分析:首先将所有边按照权重从小到大排序,然后从权重最小的边开始,如果这条边的两个顶点不在一个联通分量中,那么将这条边加入到最小生成树中,否则不加入,直到最小生成树中有n-1条边为止
    • 实现:
        //Kruskal算法生成最小生成树
        int kruskal(int **a,int n){
            int totalLenth=0;
            int k=n;
            while(k>0){
                //找到权重最小的边
                int min=10000;
                int minI,minJ;
                for(int i=0;i<n;i++){
                    for(int j=0;j<n;j++){
                        if(a[i][j]<min&&a[i][j]!=0){
                            min=a[i][j];
                            minI=i;
                            minJ=j;
                        }
                    }
                }
                //如果这条边的两个顶点不在一个联通分量中,那么将这条边加入到最小生成树中
                if(!isConnected(a,minI,minJ,n)){
                    totalLenth+=a[minI][minJ];
                    a[minI][minJ]=0;
                    cout<<minI<<"->"<<minJ<<endl;
                    k--;
                }
                else
                    a[minI][minJ]=10000;
            }
            return totalLenth;
        }
    
    • 问题:Dijkstra算法求解最短路径
    • 分析:首先将所有顶点的距离初始化为无穷大,然后将起点的距离初始化为0,然后从起点开始,找到与起点相连的所有顶点,更新这些顶点的距离,然后从这些顶点中找到距离最小的顶点,重复上述操作直到所有顶点的距离都被更新
    • 实现:
        //Dijkstra算法求解最短路径
        void dijkstra(int **a,int n,int start){
            int *d=new int[n];
            int *p=new int[n];
            int *s=new int[n];
            for(int i=0;i<n;i++){
                d[i]=10000;
                p[i]=-1;
                s[i]=0;
            }
            d[start]=0;
            s[start]=1;
            for(int i=0;i<n;i++){
                if(a[start][i]!=0&&a[start][i]!=10000){
                    d[i]=a[start][i];
                    p[i]=start;
                }
            }
            for(int i=0;i<n;i++){
                int min=10000;
                int minI;
                for(int j=0;j<n;j++){
                    if(d[j]<min&&s[j]==0){
                        min=d[j];
                        minI=j;
                    }
                }
                s[minI]=1;
                for(int j=0;j<n;j++){
                    if(a[minI][j]!=0&&a[minI][j]!=10000&&s[j]==0){
                        if(d[j]>d[minI]+a[minI][j]){
                            d[j]=d[minI]+a[minI][j];
                            p[j]=minI;
                        }
                    }
                }
            }
            for(int i=0;i<n;i++){
                cout<<d[i]<<" ";
            }
            cout<<endl;
            for(int i=0;i<n;i++){
                cout<<p[i]<<" ";
            }
            cout<<endl;
        }
    
posted @ 2023-03-26 16:58  asdio  阅读(324)  评论(0)    收藏  举报