算法笔记_007:猜底牌问题【贪婪法】

目录

1 问题描述

2 解决方案

2.1 贪婪法原理简介

2.2 哈夫曼树及编码简介

2.3 具体编码

2.4 运行结果


1 问题描述

设计一种策略,使在下面的游戏中,期望提问的次数达到最小。有一副纸牌,是由1张A,2张2,3张3,...9张9组成的,一共包含45张牌。有人从这副牌洗过的牌中抽出一张牌,问一连串可以回答是或否的问题来确定这副牌的点数。


2 解决方案

2.1 贪婪法原理简介

贪婪法的核心是,所做的每一步选择都必须满足以下条件:

(1)可行的:即它必须满足问题的约束。

(2)局部最优:它是当前步骤中所有可行选择中最佳的局部选择。

(3)不可取消:即选择一旦做出,在算法的后面步骤中就无法改变了。

这些条件即要求:在每一步中,它要求“贪婪”地选择最佳操作,并希望通过一系列局部的最优选择,能够产生一个整个问题的(全局的)最优解。

2.2 哈夫曼树及编码简介

哈夫曼树,树中所有的左向边都标记为0,而所有的右向边都标记为1.可以通过记录从根到字符叶子的简单路径上的标记来获得一个字符的哈夫曼编码。

示例:

要了解哈夫曼树,首先了解一下哈夫曼算法,哈夫曼算法满足以下两步:

第一步:初始化n个单节点的树,并为它们标上字母表中的字符。把每个字符的概率记在树的根节点中,用来指出树的权重(更一般地来说,树的权重等于树中所有叶子节点的概率之和)。

第二步:重复下面的步骤,直到只剩下一棵单独的树。找到两棵权重最小的树(如果权重相同,则任意选取其一)。把它们作为新树中的左右子树,并把其权重之和作为新的权重记录在新树的根节点中。

示例:

树的带权路径长度:如果树中每个叶子上都带有一个权值,则把树中所有叶子的带权路径长度之和称为树的带权路径长度。

设某二叉树有n个带权值的叶子结点,则该二叉树的带权路径长度记为:

公式中,Wk为第k个叶子结点的权值;Lk为该结点的路径长度。

示例:

2.3 具体编码

针对猜底牌问题,此处需要构建哈夫曼树,通过哈夫曼编码的长度来确定具体回答是或否的个数。此处把哈夫曼编码中的0代表回答否,1代表回答是,根据哈夫曼编码中的0或者1的总个数来确定提问的次数。

首先,创建一个树的节点类:

package com.liuzhen.huffman;

/*定义一个Node类,其类型为T(PS:即在初始化时自行选择相关数据类型),该类用于定义哈夫曼树中一个节点的类型
 * 该类并实现Compareble接口
 * Compareble接口强行对实现它的每个类的对象进行整体排序。此排序被称为该类的自然排序 ,类的 compareTo 方法被称为它的自然比较方法 。
 * 实现此接口的对象列表(和数组)可以通过 Collections.sort (或Arrays.sort )进行自动排序。
 */
public class Node<T> implements Comparable<Node<T>> {
    private T data;         //二叉树节点的名称
    private int weight;     //二叉树节点的权重
    private Node<T> left;   //节点的左孩子
    private Node<T> right;  //节点的右孩子
    private String code = "";   //二叉树节点的哈夫曼编码,初始化为空

    //构造函数
    public Node(T data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    /*
     * toString的好处是在碰到“println”之类的输出方法时会自动调用,不用显式打出来
     * 它是Object里面已经有了的方法,此处将该方法进行重写
     * 它通常只是为了方便输出,比如System.out.println(xx),括号里面的“xx”如果不是String类型的话,
     * 就自动调用xx的toString()方法
     * 此处就可以输出一个Node类对象时,直接输出toString()方法体中返回的字符串
     */
    @Override
    public String toString() {

        return "data:" + this.data + ",weight:" + this.weight +",code:"+this.code+ ";   ";
    }

    @Override
    public int compareTo(Node<T> o) {
        //此处实现Compareble接口中方法compaerto,对Node类对象进行从大到小排序
        if (o.weight > this.weight) {
            return 1;
        } else if (o.weight < this.weight) {
            return -1;
        }
        return 0;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public Node<T> getLeft() {
        return left;
    }

    public void setLeft(Node<T> left) {
        this.left = left;
    }

    public Node<T> getRight() {
        return right;
    }

    public void setRight(Node<T> right) {
        this.right = right;
    }

}

其次,创建一个用于构建哈夫曼树和输出哈夫曼树的类:

package com.liuzhen.huffman;

import java.util.*;

public class HuffmanTree<T> {
    //构建哈夫曼树方法,其返回类型为Node<T>
     public static <T> Node<T> createTree(List<Node<T>> nodes) {
            while (nodes.size() > 1) {
                Collections.sort(nodes);   //调用Node<T>类中实现接口的排序方法,对节点对象进行从大到小排序
                Node<T> left = nodes.get(nodes.size() - 1);    //获取链表中最小的元素,作为左孩子
                Node<T> right = nodes.get(nodes.size() - 2);   //获取链表中第二小的元素,作为右孩子
                Node<T> parent = new Node<T>(null, left.getWeight()
                        + right.getWeight());    //左孩子和右孩子权重相加的和,作为其父节点
                parent.setLeft(left);
                parent.setRight(right);
                nodes.remove(left);
                nodes.remove(right);
                nodes.add(parent);            //将父节点加入链表中
            }
            return nodes.get(0);
        }

     //使用广度优先遍历法,返回哈夫曼树的结果队列
        public static <T> List<Node<T>> breath(Node<T> root) {
            //定义最终返回的结果队列
            List<Node<T>> list = new ArrayList<Node<T>>();
            /*Queue(队列)特性是先进先出,队尾插入,队首删除
             * Queue使用时要尽量避免Collection的add()和remove()方法,而是要
             * 使用offer()来加入元素,使用poll()来获取并移除元素
             * LinkedList类实现了Queue接口,因此,可以把LinkedList当成Queue来使用
             */
            Queue<Node<T>> queue = new LinkedList<>();
           // root.setCode("");            //哈夫曼树根节点的哈夫曼编码设定为空字符串“”
            queue.offer(root);
            while (!queue.isEmpty()) {
                Node<T> pNode = queue.poll();   //使用poll()来获取并移除元素

                list.add(pNode);
                if (pNode.getLeft() != null) {
                    String code = pNode.getCode();  //获取父节点的哈夫曼编码
                    code += "0";               //左孩子的哈夫曼编码加字符串“0”
                    pNode.getLeft().setCode(code);
                    queue.offer(pNode.getLeft());  //使用offer()来加入元素
                }
                if (pNode.getRight() != null) {
                    String code = pNode.getCode();   //获取父节点的哈夫曼编码
                    code += "1";                //右孩子的哈夫曼编码加字符串“1”
                    pNode.getRight().setCode(code);
                    queue.offer(pNode.getRight());
                }
            }
            return list;
        }
}

最后,输入题目中数据(对于题目的A,1,...,9,我分别取代号为字母A,B,C,...,其张数的大小即为哈夫曼树中相应节点的权重),得到输出最终结果:

package com.liuzhen.huffman;

import java.util.*;

public class HuffmanTreeTest {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        List<Node<String>> nodes = new ArrayList<Node<String>>();
        nodes.add(new Node<String>("A", 1));
        nodes.add(new Node<String>("B", 2));
        nodes.add(new Node<String>("C", 3));
        nodes.add(new Node<String>("D", 4));
        nodes.add(new Node<String>("E", 5));
        nodes.add(new Node<String>("F", 6));
        nodes.add(new Node<String>("G", 7));
        nodes.add(new Node<String>("H", 8));
        nodes.add(new Node<String>("I", 9));
        Node<String> root = HuffmanTree.createTree(nodes);
        System.out.println(HuffmanTree.breath(root));
    }
}

2.4 运行结果

[data:null,weight:45,code:;   , data:null,weight:18,code:0;   , data:null,weight:27,code:1;   , data:null,weight:9,code:00;   , data:I,weight:9,code:01;   , data:null,weight:12,code:10;   , data:null,weight:15,code:11;   , data:D,weight:4,code:000;   , data:E,weight:5,code:001;   , data:null,weight:6,code:100;   , data:F,weight:6,code:101;   , data:G,weight:7,code:110;   , data:H,weight:8,code:111;   , data:null,weight:3,code:1000;   , data:C,weight:3,code:1001;   , data:A,weight:1,code:10000;   , data:B,weight:2,code:10001;   ]

参考资料:

1、哈夫曼树

2、Comparable接口的实现和使用

3、(哈夫曼树)HuffmanTree的java实现

时间: 2024-10-06 00:30:50

算法笔记_007:猜底牌问题【贪婪法】的相关文章

算法笔记(二)抽牌法产生随机全排列

上一章的算法笔记,并不算一个算法系列的一个合适的开始.而本章将会介绍一种产生随机全排列的方法,下一章开始,就正式开始我们的排序算法了. 在我们的排序算法演示器中,我们需要一组随机的数据来作为排序的开始,而本章,就会产生这么一组随机数据. (一)实现分析 我们需要一组随机的数据,而且似乎产生的方法不会太难.我们只需要通过rand()函数获得一个随机数,让其对count(排序的规模)取模,结果作为数组的索引,其值是当前已产生的随机数的个数,如果已经存在了,继续查找,直到产生随机数的个数与总数相同.

算法笔记_220:猜算式(Java)

目录 1 问题描述 2 解决方案   1 问题描述 看下面的算式: □□ x □□ = □□ x □□□ 它表示:两个两位数相乘等于一个两位数乘以一个 三位数. 如果没有限定条件,这样的例子很多. 但目前的限定是:这9个方块,表示1~9的9个数字 ,不包含0.该算式中1至9的每个数字出现且只出现一次! 比如:46 x 79 = 23 x 15854 x 69 = 27 x 13854 x 93 = 27 x 186..... 请编程,输出所有可能的情况! 注意:左边的两个乘数交换算同一方案,不要

贪婪法——————贪心算法

华信清明节放假,所以不用去上课,而我又不想出去,所以就用了一点时间去研究算法. 我今天开始看王晓华写的<算法的乐趣>,把它当做教材. 看到贪心算法,因为大一的时候C语言没学好,所以作者写的C实现代码不是看得很懂,但是基本思想还是能够掌握的. 接下来我总结一下我今天学到的贪心算法: 贪心算法是寻找最优解问题的常用方法. 基本思想是分三个步骤: 1.建立对问题精确描述的数学模型,包货定义最优解的模型. 2.将问题分成一系列的子问题,同时定义子问题的最优解结构. 3.应用贪心算法原则可以确定每个子问

10算法策略之贪婪法

贪婪算法 贪婪法又叫登山法, 它的根本思想是逐步到达山顶,即逐步获得最优解.贪婪算法没有固定的算法框架,算法设计的关键是贪婪策略的选择.一定要注意,选择的贪婪策略要具有无后向性.某状态以后的过程和不会影响以前的状态,只与当前状态或以前的状态有关,称这种特性为无后效性. 可绝对贪婪问题 [例1]键盘输入一个高精度的正整数N,去掉其中任意S个数字后剩下的数字按原左右次序将组成一个新的正整数.编程对给定的N和S,寻找一种方案使得剩下的数字组成的新数最小. 输入数据均不需判错.输出应包括所去掉的数字的位

小算法笔记

素数: 除 1 外只能被 1 和自身整除的数. 方法一: #include <stdio.h> #define N 1000 int num = 0; int prime(int n) { int i; if(n % 2 == 0) return (n == 2); if(n % 3 == 0) return (n == 3); if(n % 5 == 0) return (n == 5); for(i = 7; i*i <= n; ++i) if(n % i == 0) return

算法笔记_018:旅行商问题(Java)

目录 1 问题描述 2 解决方案 2.1 蛮力法   1 问题描述 何为旅行商问题?按照非专业的说法,这个问题要求找出一条n个给定的城市间的最短路径,使我们在回到触发的城市之前,对每个城市都只访问一次.这样该问题就可以表述为求一个图的最短哈密顿回路的问题.(哈密顿回路:定义为一个对图的每个顶点都只穿越一次的回路) 很容易看出来,哈密顿回路也可以定义为n+1个相邻顶点v1,v2,v3,...,vn,v1的一个序列.其中,序列的第一个顶点和最后一个顶点是相同的,而其它n-1个顶点都是互不相同的.并且

算法笔记之排序

最近在看<算法笔记>,如果单从算法来说,这本书真正做到了短小精悍,首先以排序入题,那么我们今天也来说说排序. 排序 将一堆杂乱无章的元素按照某种规则有序排列的过程就叫"排序".排序是一种非常基础的算法,有着广泛的理论和实践基础.对一个排序算法来说,一般从如下3个方面衡量算法的优劣: 时间复杂度:主要是分析关键字的比较次数和记录的移动次数. 空间复杂度:分析排序算法中需要多少辅助内存 稳定性:若两个记录A和B的关键字值相等,但排序后A.B的先后次序保持不变,则称这种算法是稳定

我的算法笔记---上

数据结构和算法读书笔记 1.疑惑:数据结构和算法是什么啊?有毛用啊? 数据结构:数组.链表.栈.二叉树.哈希表 算法:对结构中数据的处理 算法和数据结构的作用: 现实世界数据存储(家庭地址的存储)  现实世界建模(图)   程序员工具 算法概述:增删查 遍历 排序 递归 专业术语:数据库  记录   字段   关键字(限定词带出记录,记录中包含所有字段,全部记录构成数据库) 过程性语言的问题: 程序与现实世界缺乏对应关系 程序内部结构出现问题, 重视方法,不重视数据,粗糙的数据组织结构, 对象的

算法大神之路----排序(冒泡排序法)

冒泡排序法 冒泡排序法又称为交换排序法,是由观察水中冒泡变化构思而成,气泡随着水深压力而改变.气泡在水底时,水压最大,气泡最小,而气泡慢慢浮上水面时,气泡所受压力最小,体积慢慢变大. 冒泡排序比较方式是从第一个元素开始,比较相邻的元素大小,如果大小顺序有误,则对调后进行下一个元素比较.直到所有元素满足关系为止. 冒泡排序法分析 冒泡排序法平均情况下,需要比较(n-1)/2次,时间复杂度为O(n2),最好的情况只需要扫描一次,不用操作,即作n-1次比较,时间复杂度为O(n). 由于冒泡排序为相邻两