贪心法(Huffman编码、模拟退火)

贪心法

    算法思想: 把整个问题分解成多个步骤,在每个步骤都选取当前步骤的最优方案,直到所有步骤结束;在每一步都不考虑对后续步骤的影响,在后续步骤中也不再回头改变前面的选择。简单地说,就是 “走一步看一步”
    求解的问题需满足以下特征:

  1. 最优子结构性质,即从局部最优能扩展到全局最优
  2. 贪心选择性质。问题的整体最优解可以通过一系列局部最优的选择来得到

常见问题
1、活动安排问题(区间调度)

    问题描述

    对最早结束时间进行贪心,算法步骤如下:
  1. 把 n 个活动按结束时间排序
  2. 选择第 1 个结束的活动,并删除(或跳过)与它时间相冲突的活动
  3. 重复步骤(2),直到活动为空。每次选择剩下的活动中最早结束的那个活动,并删除与它时间相冲突的活动

2、区间覆盖问题

    问题描述
    给定一个长度为 n 的区间,再给出 m 条线段的左端点(起点)和右端点(终点),问最少用多少条线段可以将整个区间完全覆盖

    贪心思路是尽量找出更长的线段,算法步骤如下:

  1. 把每个线段按照左端点递增排序
  2. 设已经覆盖的区间是 [L, R] ,在剩下的线段中找所有左端点小于等于 R 且右端点最大的线段,把这个线段加入到已覆盖区间里,并更新已覆盖区间的 [L, R] 值
  3. 重复步骤 2 ,直到区间全部覆盖

3、最优装载问题

    问题描述

    贪心思路是尽量找浓度小的药水。先对药水按浓度从小到大排序,药水的浓度不大于 w% 就加入,如果药水的浓度大于 w% ,计算混合后的总浓度,不大于 w% 就加入,否则判断结束

4、多机调度问题

    问题描述
    设有 n 个独立的作业,由 m 台相同的计算机进行加工。作业 i 的处理时间为 t ,每个作业可在任何一台计算机上加工处理,但不能间断、拆分。要求给出一种作业调度方案,在尽可能短的时间内,由 m 台计算机加工处理完成这 n 个作业
    贪心策略是最长处理时间的作业优先,即把处理时间最长的作业分配给最先空闲的计算机。让处理时间长的作业得到优先处理,从而在整体上获得尽可能短的处理时间

Huffman编码

    Huffman编码是前缀编码算法中的最优算法,方案如下:

字符ABCDE
频次3961519
编码11001111101100

其二叉树如下图:

    构造编码二叉树的步骤: 首先对所有字符按出现的频次排序,然后从出现频次最少的字符开始,用贪心思想安排在二叉树上。
    贪心的过程是按出现的频次从底层往顶层生成二叉树。注意:每一步都要按频次重新排序。这个过程保证出现频次最少的字符被放在树的底层,编码更长;出现多的字符被放在上层,编码更短。
    上例的编码二叉树的构造步骤如下:
    问题描述
    解题思路

    首先统计字符出现的频次,然后用 Huffman 算法编码,最后计算编码后的总长度。不过,由于只需要输出编码的总长度,而不要求输出每个自负的编码,所以可以跳过编码过程,利用上述的 Huffman 编码思想,直接计算出编码的总长度
    代码如下(Java)

import java.util.*;

public class Main
{
    String ss;
    PriorityQueue<Integer> Q = new PriorityQueue<Integer>();  //优先队列,最小的在队首
    
    void Entropy(){
        int t = 1;
        char[] s = ss.toCharArray();
        Arrays.sort(s);  //排序,便于后续统计各字符频次
        for(int i=1; i<s.length; i++){   //统计字符出现的频次,并放进优先队列
            if(s[i] != s[i-1]){
                Q.offer(t);
                t = 1;
            }
            else t++;
        }
        Q.offer(t);
        int ans = 0;
        while (Q.size() >1){
            int a = Q.poll();   
            int b = Q.poll();   //提取队列中最小的两个
            Q.offer(a+b);
            ans += a+b;   //直接计算编码的总长度:越靠近队首的元素被加的次数越多,相当于二叉树中处于底层的结点,而每个元素被累加的总次数就是对应结点的层数,即编码长度
        }
        Q.poll();
        System.out.println(ans);  //ans 就是编码后的总长度
    }
    
    public static void main(String args[]){
        Main m = new Main();
        Scanner sc = new Scanner(System.in);
        while (!sc.hasNext("0")){ //以 0 字符串结束输出
            m.ss = sc.nextLine();   //输入字符串
            m.Entropy();
        }
    }
}

模拟退火

    模拟退火算法基于这样一个物理原理:一个高温物体降温到常温,温度越高时降温的概率越大(降温更快),温度越低时降温的概率越小(降温更慢)。模拟退火算法基于这样一种思想进行搜索,即进行多次降温(迭代),直到获得一个可行解
    在迭代过程中,模拟退火算法随机选择下一个状态,有两种可能:(1)新状态比原状态更优,那么接受这个新状态;(2)新状态更差,那么以一定的概率接受该状态,不过这个概率应该随着时间的推移逐渐降低

模拟退火其实也是一种贪心算法,但是它的搜索过程引入了随机因素。在迭代更新可行解时,以一定的概率来接受一个比当前解要差的解,因此有可能会跳出这个局部的最优解,达到全局的最优解。
以下图为例,假定初始解为左边蓝色点A,模拟退火算法会快速搜索到局部最优解B,但在搜索到局部最优解后,不是就此结束,而是会以一定的概率接受到右边的移动。也许经过几次这样的不是局部最优的移动后会到达全局最优点D,于是就跳出了局部最小值。

    根据热力学的原理,在温度为T时,出现能量差为dE的降温的概率为p(dE),表示为:

    其中k是波尔兹曼常数,值为k=1.3806488(13)×(10^−23),exp表示自然指数,且dE<0。因此dE/kT<0,所以p(dE)函数的取值范围是(0,1)。满足概率密度函数的定义。其实这条公式更直观意思就是:温度越高,出现一次能量差为p(dE)的降温的概率就越大(大于随机数的概率也就越大,因此需更新状态);温度越低,则出现降温的概率就越小。
    在实际问题中,这里的“一定的概率”的计算参考了金属冶炼的退火过程。假定当前可行解为x,迭代更新后的解为x_new,那么对应的“能量差”定义为:

    其对应的“一定概率”为:

注:在实际问题中,可以设定k=1。因为kT可以等价于一个参数T。如设定k=2、T=1000,等于直接设定T=2000的效果。

    算法步骤如下:

  1. 设置一个初始温度T
  2. 温度下降,状态转移。从当前温度按降温系数下降到下一个温度,在新的温度计算当前状态
  3. 如果温度降到设定的温度下界,程序停止
    模拟退火的典型应用有:函数最值问题,TSP 旅行问题,最小圆覆盖问题,最小球覆盖问题

    例题描述

    代码如下(Java)

import java.util.*;

public class Exa {
    final double eps = 1e-8;   //终止温度
    double y;

    private double func(double x){   //计算函数值
        return 6*Math.pow(x,7.0)+ 8*Math.pow(x,6.0)+ 7*Math.pow(x,3.0)+ 5*Math.pow(x,2.0)- y*x;
    }

    private double solve(){   
        double T = 100;   //初始温度
        double delta = 0.98;   //降温系数
        double x = 50.0;   //x 的初始值
        double now = func(x);   //计算函数初始值
        double ans = now;   //返回值
        while (T > eps){
            int[] f = {1,-1};
            double newx = x + f[(int)(Math.random()*100)%2] * T;   //按概率改变 x,随 T的降温而减少
            if(newx >= 0 && newx <= 100){
                double next = func(newx);
                ans = Math.min(ans, next);
                if(now - next > eps){   //更新 x
                    x = newx;
                    now = next;
                }
            }
            T *= delta;
        }
        return ans;
    }

    public static void main(String[] args) {
        Exa m = new Exa();
        Scanner sc = new Scanner(System.in);
        int cas = sc.nextInt();
        while (cas-->0){
            m.y = sc.nextDouble();
            System.out.println(String.format("%.4f",m.solve()));
        }

    }
}

1

    模拟退火算法得到的只是一个可行解,而不是一个精确解。例如上面的例题,计算到4 位小数点的精度就停止了,实际上是一个可行解,所以算法的效率和要求的精度有关。一般情况下,模拟退火算法的复杂度会比其他精度算法差。应用时需仔细选择初始温度 T、降温系数 delta 、终止温度 eps 等
  1. 本人对于更新 x 值处的 if 判断不是特别理解,但算法确实如此,望有更佳的理解能告知 ↩︎

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值