C++算法——贪心算法

一、贪心算法概述

1. 定义

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优(即最有利)的选择,从而希望导致结果是全局最优的算法策略。

2. 基本思想

贪心算法的核心是"局部最优导致全局最优"。它不像动态规划那样考虑所有可能的子问题,而是通过一系列局部最优选择来构建问题的解。

3. 适用条件

贪心算法适用于满足以下两个条件的问题:

  • 贪心选择性质:局部最优选择能导致全局最优解
  • 最优子结构:问题的最优解包含其子问题的最优解

二、贪心算法的基本框架

// 贪心算法伪代码框架
GreedyAlgorithm(problem) {
    solution = empty;  // 初始化解
    
    while (!problem.isSolved()) {
        // 选择当前最优的局部解
        bestChoice = selectBestOption(problem.options);
        
        // 应用这个选择
        solution.add(bestChoice);
        problem.update(bestChoice);
    }
    
    return solution;
}

三、贪心算法的典型应用场景

1. 简单问题示例:找零钱问题

问题描述:用最少数量的硬币凑出指定金额。

#include 
#include 
#include 

using namespace std;

/**
 * 贪心算法解决找零钱问题
 * @param amount 需要找零的金额
 * @param coins 可用的硬币面值数组
 * @return 包含所用硬币的vector,如果无法正好找零则返回空vector
 */
vector<int> coinChange(int amount, vector<int>& coins) {
    // 将硬币按面值从大到小排序(贪心选择:优先使用大面值硬币)
    sort(coins.rbegin(), coins.rend());
    
    vector<int> result; // 存储结果的vector
    
    // 遍历所有硬币面值
    for (int coin : coins) {
        // 当当前硬币面值小于等于剩余金额时,尽可能多地使用该硬币
        while (amount >= coin) {
            amount -= coin;       // 扣除已使用的硬币金额
            result.push_back(coin); // 记录使用的硬币
        }
    }
    
    // 检查是否正好找零
    if (amount != 0) {
        cout << "无法正好找零,剩余金额: " << amount << endl;
        return {}; // 返回空vector表示无法正好找零
    }
    
    return result;
}

int main() {
    vector<int> coins = {1, 5, 10, 25}; // 美国常用硬币面值
    int amount = 63; // 需要找零63美分
    
    vector<int> result = coinChange(amount, coins);
    
    // 输出结果
    if (!result.empty()) {
        cout << "找零" << amount << "美分需要的硬币: ";
        for (int coin : result) {
            cout << coin << " ";
        }
        cout << "\n总共需要" << result.size() << "枚硬币" << endl;
    }
    
    return 0;
}

2. 中等难度问题:区间调度问题

问题描述:选择最多的不重叠区间。

#include 
#include 
#include 

using namespace std;

/**
 * 贪心算法解决区间调度问题(选择最多不重叠区间)
 * @param intervals 区间数组,每个区间表示为[start, end]
 * @return 可以选择的最大不重叠区间数量
 */
int intervalScheduling(vector<vector<int>>& intervals) {
    // 如果区间为空,直接返回0
    if (intervals.empty()) return 0;
    
    // 按结束时间升序排序(贪心选择:优先选择结束早的区间)
    sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    });
    
    int count = 1; // 至少可以选择第一个区间
    int end = intervals[0][1]; // 第一个区间的结束时间
    
    // 遍历所有区间
    for (int i = 1; i < intervals.size(); ++i) {
        // 如果当前区间的开始时间 >= 上一个选择区间的结束时间
        if (intervals[i][0] >= end) {
            count++; // 选择该区间
            end = intervals[i][1]; // 更新结束时间
        }
        // 否则跳过该区间(因为会与已选区间重叠)
    }
    
    return count;
}

int main() {
    // 测试数据:每个子数组表示一个活动的开始和结束时间
    vector<vector<int>> intervals = {
        {1, 3}, {2, 4}, {3, 6}, 
        {5, 7}, {8, 9}
    };
    
    int maxActivities = intervalScheduling(intervals);
    cout << "最多可以安排" << maxActivities << "个不重叠活动" << endl;
    
    return 0;
}

3. 较难问题:霍夫曼编码

问题描述:构建最优前缀编码。

#include 
#include 
#include 
#include 

using namespace std;

// 定义霍夫曼树的节点结构
struct Node {
    char ch;        // 字符
    int freq;       // 频率
    Node *left;     // 左子节点
    Node *right;    // 右子节点
    
    // 构造函数
    Node(char c, int f) : ch(c), freq(f), left(nullptr), right(nullptr) {}
};

// 优先队列的比较函数(按频率从小到大排序)
struct compare {
    bool operator()(Node* l, Node* r) {
        return l->freq > r->freq;
    }
};

/**
 * 递归生成霍夫曼编码
 * @param root 当前节点
 * @param str 当前路径的编码字符串
 * @param huffmanCode 存储编码结果的哈希表
 */
void encode(Node* root, string str, unordered_map<char, string>& huffmanCode) {
    if (!root) return; // 空节点直接返回
    
    // 如果是叶子节点(存储字符的节点)
    if (!root->left && !root->right) {
        huffmanCode[root->ch] = str; // 保存字符对应的编码
    }
    
    // 递归处理左子树(路径添加"0")
    encode(root->left, str + "0", huffmanCode);
    // 递归处理右子树(路径添加"1")
    encode(root->right, str + "1", huffmanCode);
}

/**
 * 构建霍夫曼树并生成编码
 * @param text 输入文本
 */
void buildHuffmanTree(string text) {
    // 1. 统计字符频率
    unordered_map<char, int> freq;
    for (char ch : text) {
        freq[ch]++;
    }
    
    // 2. 创建优先队列(最小堆)
    priority_queue<Node*, vector<Node*>, compare> pq;
    
    // 为每个字符创建节点并加入优先队列
    for (auto pair : freq) {
        pq.push(new Node(pair.first, pair.second));
    }
    
    // 3. 构建霍夫曼树(贪心选择:每次合并频率最小的两个节点)
    while (pq.size() != 1) {
        // 取出频率最小的两个节点
        Node* left = pq.top(); pq.pop();
        Node* right = pq.top(); pq.pop();
        
        // 创建新节点,频率为两个子节点频率之和
        int sum = left->freq + right->freq;
        Node* newNode = new Node('\0', sum); // 内部节点字符设为空
        newNode->left = left;
        newNode->right = right;
        
        // 将新节点加入优先队列
        pq.push(newNode);
    }
    
    // 4. 获取霍夫曼树的根节点
    Node* root = pq.top();
    
    // 5. 生成霍夫曼编码
    unordered_map<char, string> huffmanCode;
    encode(root, "", huffmanCode);
    
    // 6. 输出编码表
    cout << "霍夫曼编码表:" << endl;
    for (auto pair : huffmanCode) {
        cout << pair.first << " : " << pair.second << endl;
    }
    
    // 7. 编码原始文本
    string encodedStr;
    for (char ch : text) {
        encodedStr += huffmanCode[ch];
    }
    
    cout << "\n原始文本: " << text << endl;
    cout << "编码后的字符串: " << encodedStr << endl;
    
    // 计算压缩率
    double originalSize = text.length() * 8; // 假设原始是ASCII编码,每个字符8位
    double compressedSize = encodedStr.length();
    double ratio = (compressedSize / originalSize) * 100;
    
    cout << "\n压缩率: " << ratio << "%" << endl;
}

int main() {
    string text = "hello world";
    cout << "构建霍夫曼编码示例:\n" << endl;
    buildHuffmanTree(text);
    return 0;
}

四、贪心算法的优缺点

优点:

  1. 实现简单,易于理解
  2. 通常时间复杂度较低
  3. 适用于某些特定问题能得到最优解

缺点:

  1. 不能保证对所有问题都得到最优解
  2. 需要证明贪心选择的正确性
  3. 对某些问题可能需要复杂的预处理

五、贪心算法与动态规划的比较

特性 贪心算法 动态规划
决策依据 当前最优选择 所有可能的子问题
时间复杂度 通常较低 通常较高
空间复杂度 通常较低 通常较高
适用范围 满足贪心选择性质的问题 具有最优子结构的问题
解的正确性 不一定全局最优 保证全局最优

六、如何判断是否使用贪心算法

  1. 尝试构建反例:尝试找出贪心选择不能得到最优解的情况
  2. 验证贪心选择性质:证明每一步的贪心选择能导致全局最优
  3. 考虑问题特性:问题是否具有"无后效性"(当前选择不影响后续选择)

七、贪心算法的经典问题

  1. 背包问题(分数背包)
  2. 最小生成树(Prim和Kruskal算法)
  3. 最短路径问题(Dijkstra算法)
  4. 任务调度问题
  5. 加油站问题

你可能感兴趣的:(C++算法,算法,c++,贪心算法)