K元归并树——贪心算法

问题介绍

        两个分别包含n个和m个记录的已分类文件可以在O(n+m)时间内归并在一起而得到一个分类文件。当要把两个以上的已分类文件归并在一起时,可以通过成对地重复归并已分类的文件来完成。例如:假定X1,X2,X3,X4是要归并的文件,则可以首先把X1和X2归并成文件Y1,然后将Y1和X3归并成Y2,最后将Y2和X4归并,从而得到想要的分类文件;也可以先把X1和X2归并成Y1,然后将X3和X4归并成Y2,最后归并Y1和Y2而得到想要的分类文件。给出n个文件,则有许多种把这些文件成对地归并成一个单一分类文件的方法。不同的配对法要求不同的计算时间。
         问题是确定一个把n个已分类文件归并在一起的最优方法(即需要最少比较的方法)
像刚才所描述的归并模式称为二路归并模式(每一个归并步包含两个文件的归并)。二路归并模式可以用二元归并树来表示。叶结点被画成方块形,表示已知的文件。这些叶结点称为外部结点。剩下的结点被画成圆圈,称为内部结点。每个内部结点恰好有两个儿子,表示把它的两个儿子所表示的文件归并而得到的文件。每个结点中的数字都是那个结点所表示的文件的长度(记录数)。一个i级结点在距离根为i-1的地方,在i级结点上的文件的记录都要移动i-1次。如果di是由根到代表文件Fi的外部结点的距离,qi是Fi的长度,则这颗二元归并树的记录移动总量是 。这个和数叫做这颗树的带权外部路径长度。一个最优二路归并模式与一颗具有最小权外部路径的二元树相对应。
        生成归并树的贪心方法也适用于K路归并的情况。相应的归并树是一颗K元树。所有的内部结点的度数必须为K,所以对于n的某些值,就不与K元归并树相对应。例如:当K=3时,就不存在具有n=2个外部结点的K元归并树。所以有必要引进一定量的“虚”外部结点。每一个虚外部结点被赋以0值的qi。这个虚值不会影响所产生的K元树的带权外部路径长度。

问题分析

算法思路分析

问题简述为:“确定一个把n个已分类文件归并在一起的最优方法”
即:最优K路归并模式问题,也就是如何构建一棵具有最短带权外部路径的K叉树
而为了构造具有最短带权外部路径的树,感性的、直观的想法是将复杂的(即文件Fi的长度qi长的) 置于上层,简单的置于下层
即,先解决K元短文件之间的归并,再考虑剩下的以及已经归并了的文件中的K元“简单文件”的归并。

这个思路,以及构造具有最短带权外部路径的树,毫不意外想到使用哈夫曼算法来构建哈夫曼树

所以问题可以等价替换为:
用所给的n个文件(Fi)的长度值(qi)来构建K叉哈夫曼树,从而得到最优值(最少比较次数)和最优解(K路归并的分配策略)

证明:K叉哈夫曼树是最优树

设:T(n) 为带权节点   q 1 ⩽ q 2 ⩽ … ⩽ q n − 1 ⩽ q n ( n > 2 ) \ q_1 \leqslant q_2 \leqslant … \leqslant q_{n-1} \leqslant q_n(n > 2)  q1q2qn1qn(n>2)的最优树的带权外部路径长度。
    证明 哈夫曼树是最优树 等价于:
        证明:
             { Q 1 : 带 权 q 1 , q 2 , … , q K 为 兄 弟 节 点 Q 2 : 最 优 树 收 缩 与 展 开 同 为 最 优 树 Q 3 : 最 优 树 的 合 并 仍 为 最 优 树 \begin{cases} \mathbf{Q_1}:带权q_1,q_2,…,q_K为兄弟节点 \\ \mathbf{Q_2}:最优树收缩与展开同为最优树 \\ \mathbf{Q_3}:最优树的合并仍为最优树 \end{cases} Q1q1,q2,,qKQ2Q3

证明 Q 1 : 带 权 q 1 , q 2 , … , q K 为 兄 弟 节 点 \mathbf{Q_1}:带权q_1,q_2,…,q_K为兄弟节点 Q1:q1,q2,,qK

1、设: N o d e m a x Node_{max} Nodemax为最优树中通路长度最长的内节点,则其一定有K个叶子节点。
    由于K叉哈夫曼算法的性质决定,内节点一定包含K个叶子节点。
2、设: N o d e m a x Node_{max} Nodemax为最优树中通路长度最长的内节点,则其K个叶子节点的权一定为 q 1 , q 2 , … , q K q_1,q_2,…,q_K q1,q2,,qK
    若 L x L_x Lx N o d e m a x Node_{max} Nodemax的一叶子节点,但是其权重 q x ∉ { q 1 , q 2 , … , q K } q_x\notin\{q_1,q_2,…,q_K\} qx/{q1,q2,,qK}
        L y L_y Ly为上层的某叶子节点 q x ∈ { q 1 , q 2 , … , q K } q_x\in\{q_1,q_2,…,q_K\} qx{q1,q2,,qK}, 即, q x > q y q_x > q_y qx>qy
       则,可以计算其 T ( n ) T(n) T(n):
               T ( n ) = g ( n ) + L ( x ) ∗ q x + L ( y ) ∗ L y T(n)=g(n)+L(x)*q_x+L(y)*L_y T(n)=g(n)+L(x)qx+L(y)Ly… … … … … …(1)
       其中, g ( n ) g(n) g(n)为除去 L x L_x Lx L y L_y Ly的加权路径和。 L ( x ) ∗ q x L(x)*q_x L(x)qx L x L_x Lx的加权路径, L y L_y Ly同理。

       若,交换节点 L x L_x Lx L y L_y Ly
       则,可以计算其 T ∗ ( n ) : T^*(n): T(n):
               T ∗ ( n ) = g ( n ) + L ( y ) ∗ q x + L ( x ) ∗ q y T^*(n)=g(n)+L(y)*q_x+L(x)*q_y T(n)=g(n)+L(y)qx+L(x)qy… … … … … …(2)
       (1) - (2)得:
               T ( n ) − T ∗ ( n ) = T(n)-T^*(n)= T(n)T(n)= ( L ( x ) − L ( y ) ) ∗ q x (L(x)-L(y))*q_x (L(x)L(y))qx+ ( L ( y ) − L ( x ) ) ∗ q y (L(y)-L(x))*q_y (L(y)L(x))qy
        ∵ L ( x ) > L ( y ) \because L(x)>L(y) L(x)>L(y) q x ⩾ q y q_x \geqslant q_y qxqy
        ∴ T ( n ) − T ∗ ( n ) ⩾ 0 \therefore T(n)-T^*(n) \geqslant 0 T(n)T(n)0
       即, T ( n ) ⩾ T ∗ ( n ) T(n) \geqslant T^*(n) T(n)T(n),当且仅当 q y = q k , q x = q k + 1 , q k = q k + 1 q_y = q_k, q_x = q_{k+1}, q_k = q_{k+1} qy=qk,qx=qk+1,qk=qk+1时,取等。

∴ \therefore 得证,带权 q 1 , q 2 , . . . , q k q_1,q_2,...,q_k q1,q2,...,qk为兄弟节点


证明 Q 2 : 最 优 树 收 缩 与 展 开 同 为 最 优 树 \mathbf{Q_2}:最优树收缩与展开同为最优树 Q2

设:将带权 q 1 , q 2 , . . . , q k q_1,q_2,...,q_k q1,q2,...,qk的K个叶子节点收缩至 N o d e m a x Node_{max} Nodemax(其权重为: ∑ i = 1 k q i \sum_{i=1}^{k}q_i i=1kqi)
       得到新树 T ∗ ( n − k ) : T^*(n-k): T(nk):
               T ( n ) = T ∗ ( n − k ) + ∑ i = 1 k q i T(n)=T^*(n-k)+\displaystyle \sum_{i=1}^k q_i T(n)=T(nk)+i=1kqi … … … … … … (3)
       必有与之对应的最优树 T ( n − k ) T(n-k) T(nk),将其中 N o d e m a x Node_{max} Nodemax展开,
       得,
               T ( n − k ) = T ∗ ( n ) − ∑ i = 1 k q i T(n-k)=T^*(n)-\displaystyle \sum_{i=1}^k q_i T(nk)=T(n)i=1kqi … … … … … … (4)
       两式作和:
               T ( n ) + T ( n − k ) = T ∗ ( n − k ) + T ∗ ( n ) T(n)+T(n-k)=T^*(n-k)+T^*(n) T(n)+T(nk)=T(nk)+T(n)
              当且仅当,
                             { T ( n ) = T ∗ ( n ) T ( n − k ) = T ∗ ( n − k ) \begin{cases} T(n) = T^*(n) \\ T(n-k) = T^*(n-k) \\ \end{cases} {T(n)=T(n)T(nk)=T(nk)        时,成立

∴ \therefore 得证,最优树收缩与展开同为最优树


证明 Q 3 : 最 优 树 的 合 并 仍 为 最 优 树 \mathbf{Q_3}:最优树的合并仍为最优树 Q3

∵ Q 2 : \because Q_2: Q2:最优树收缩与展开同为最优树,
∴ \therefore 当子树 T ( x ) , T ( y ) , . . . , T ( k ) T(x),T(y),...,T(k) T(x),T(y),...,T(k)K个子树分别收缩于 T ( n ) T(n) T(n)的子节点,它们再次收缩,直至只剩下 T ( n ) T(n) T(n)节点,然后反向展开,得到的树仍为最优树

∴ \therefore 得证,最优树的合并仍为最优树

综上所述,哈夫曼树(K叉)是最优树,是一种带权路径长度最短的树



代码实现(内含详尽注释)

/**
* @author LWJ
* @date 2021/11/12
* @brief K Way Merge Problem
* Copyright (C), 2021 Algorithms Analysis and Design
*/
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <stack>
#include <utility>
#include <assert.h>

using namespace std;

 基本数据结构定义 
/**
 * @brief 树节点类
 * 使用vector存储指针域,使得支持K路归并
*/
class __TreeNode
{
    public:
        /**
         * @brief 虚节点构造器
        */
        __TreeNode()
        {
            m_file_name = "";
            m_file_size = 0;
        }

        /**
         * @brief           叶子节点构造器
         * @param file_name 文件名
         * @param file_size 文件大小
        */
        __TreeNode(string file_name, unsigned file_size)
        {
            m_file_name = file_name;
            m_file_size = file_size;
        }

        /**
         * @brief           内节点构造器
         * @param file_name 文件名
         * @param file_size 文件大小
         * @param ptr_vec   K元指针域
        */
        __TreeNode(string file_name, unsigned file_size,
            vector<__TreeNode *>ptr_vec)
        {
            m_file_name = file_name;
            m_file_size = file_size;
            m_ptr_vec = ptr_vec;
        }

        /**
         * @brief           析构器
         * 当删除根节点时,释放其自身的指针域指向的指针
         * 而且指针域指针即为指向子节点的指针,
         * 从而删除根节点,即可删除整棵树
        */
        ~__TreeNode()
        {
            for (auto ptr : m_ptr_vec)
            {
                if (ptr)
                {
                    delete ptr;
                }
            }
        }

        /**
         * @brief           增加内节点表示的文件长度(用于内节点构造阶段)
         * @param increase  增量
        */
        inline void inc_size(unsigned increase) { m_file_size += increase; }

        /**
         * @brief       添加子节点(用户内节点构造阶段)
         * @param child 待添加的子节点
        */
        inline void push_child(__TreeNode *child) { m_ptr_vec.push_back(child); }

        /**
         * @brief 获取文件名
         * @return 文件名
        */
        inline const string get_name() const { return m_file_name; }

        /**
         * @brief   获取文件大小
         * @return  文件大小
        */
        inline const unsigned get_size() const { return m_file_size; }

        /**
         * @brief   获取子节点序列
         * @return  子节点序列
        */
        inline const auto &get_children() const { return m_ptr_vec; }

private:
    string m_file_name;	    // 文件名
    unsigned m_file_size;   // 文件大小
    vector<__TreeNode*> m_ptr_vec; // 指针域
};

 K路归并问题所需基本数据结构定义 
// 文件基本信息(文件名,文件长度)
using File_info = pair<string, unsigned>;
// 文件序列
using File_list = vector<File_info>;
// 最优值
using OPT_value = size_t;

 算法所需比较规则定义 
// 树节点指针的自定义升序比较(函数对象)
class tree_node_com_greater
{
public:
    inline bool operator()(__TreeNode *a, __TreeNode *b)
    {
        return a->get_size() > b->get_size();
    }
};


 K叉树(最优解)打印所需基本缩进常量(层级符号) 

// 例如:某种2叉树
// root
// |___F1           (2)
// |   |___F2       (22)
// |   \___F3       (21)
// \___F4           (1)
//     |___F5       (12)
//     |   |___F6   (122)
//     |   \___F7   (121)
//     \___F8       (11)
// 易发现层级符号具有规律,
// 由此可以快速构建可视化多叉树
// 类似于(Linux中的tree命令)
//
constexpr auto node = "|___";
constexpr auto next_level = "|   ";
constexpr auto last_node = "\\___";
constexpr auto last_branch = "    ";

/**
 * @brief       K叉树可视化(最优解可视化)
 *              使用 前序遍历 的方式实现可视化
 * @param root  K叉树根节点
*/
void __visual_tree(const __TreeNode *root)
{
    // 初始化栈
    stack<__TreeNode *> preorder_stack;
    for (auto child_node = root->get_children().rbegin();
        child_node != root->get_children().rend(); ++child_node)
    {
        preorder_stack.push(*child_node);
    }

    // 初始化层索引(用于确定层级相对位置,输出相应的层级符号)
    vector<size_t> index_each_level;
    index_each_level.push_back(root->get_children().size());
    // 打印根节点
    cout << "Final File" << endl;
    // 开始打印K叉树
    while (!preorder_stack.empty())
    {
        // 打印层级符号
        for (auto itr = index_each_level.begin();
            itr != index_each_level.end() - 1; ++itr)
        {
            switch (*itr)
            {
            case 1:
                cout << last_branch;
                break;
            default:
                cout << next_level;
                break;
            }
        }
        __TreeNode *top = preorder_stack.top();
        preorder_stack.pop();
        if (index_each_level.back() == 1)
        {
            cout << last_node << top->get_name();
            if (top->get_children().empty())
            {
                while (!index_each_level.empty()
                        && index_each_level.back() == 1)
                {
                    index_each_level.pop_back();
                }
                if (!index_each_level.empty())
                {
                    --index_each_level.back();
                }
             
            }
            else
            {
                index_each_level.push_back(top->get_children().size());
            }
        }
        else
        {
            cout << node << top->get_name();
            if (top->get_children().empty())
            {
                --index_each_level.back();
            }
            else
            {
                index_each_level.push_back(top->get_children().size());
            }
        }
        cout << endl;

        // 前序遍历
        for (auto child_node = top->get_children().rbegin();
            child_node != top->get_children().rend(); ++child_node)
        {
            preorder_stack.push(*child_node);
        }
    }
}


/**
 * @brief           K路归并(算法入口)
 * @param files_vec 待归并文件序列
 * @param K         归并路数
 * @return          最优值
*/
OPT_value k_way_merge(File_list &files_vec, const unsigned K)
{
    // 校验输入合法性
    assert(files_vec.size() > 2 && K >= 2);

    // 构造优先队列实现贪心策略
    priority_queue<__TreeNode *, vector<__TreeNode *>, tree_node_com_greater> pro_K_queue;

    // 构建K叉哈夫曼树叶子节点
    for (auto &file : files_vec)
    {
        __TreeNode *temp = new __TreeNode(file.first, file.second);
        pro_K_queue.push(temp);
    }

    // 贪心策略构建K叉哈夫曼树,求取最优解
    while (pro_K_queue.size() != 1)
    {
        // 局部权重(局部K份文件最小长度和)
        __TreeNode *temp = new __TreeNode("Package", 0);
        for (unsigned i = 0; i < K && !pro_K_queue.empty(); ++i)
        {
            temp->inc_size(pro_K_queue.top()->get_size());
            temp->push_child(pro_K_queue.top());
            pro_K_queue.pop();
        }
        pro_K_queue.push(temp);
    }
    // 获取根节点
    __TreeNode *root = pro_K_queue.top();
    // 清空优先队列
    pro_K_queue.pop();

    // 层序遍历K叉哈夫曼树,求取最优值
    // 初始化层序遍历队列
    queue<__TreeNode *> level_queue;
    for (auto &itm : root->get_children())
    {
        level_queue.push(itm);
    }
    // 记录1层节点个数
    size_t length = level_queue.size();
    // 初始化最优值
    OPT_value result = 0;
    // 开始层序遍历计算最优值
    for (size_t i = 1, level = 1; !level_queue.empty(); ++i)
    {
        // 遍历第level层的i个节点
        // 计算叶节点的加权最短路径长度(最优值)
        if (level_queue.front()->get_name() != "Package")
        {
            result += level_queue.front()->get_size() * level;
        }
        // 下一层节点入队
        for (auto &itm : level_queue.front()->get_children())
        {
            level_queue.push(itm);
        }
        // 弹出当前节点
        level_queue.pop();
        // 当前层遍历完成
        if (i == length)
        {
            // 获取下一层节点个数
            length = level_queue.size();
            // 重置i为0
            i = 0;
            // 层数递增
            ++level;
        }
    }
    // 树型结构最优解可视化
    __visual_tree(root);

    // 堆内存释放
    delete root;

    return result;
}


感谢阅读!
有疑问或者认为有错误请留言,谢谢!
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值