贪心算法介绍及典型题目详解
贪心算法是一种在每个步骤中都做出局部最优选择的算法设计策略,目的是希望这些局部最优选择能够导致全局最优解。贪心算法并不总是能得到全局最优解,但在许多问题中,它能提供一个相对简单且高效的解决方案。
核心思想
贪心算法的核心在于每一步决策时都选择当前状态下最好的或最优的选择,而不考虑整体的未来影响。这种“短视”的方法使得贪心算法在某些特定情况下非常有效,尤其是在那些可以通过局部最优解逐步构建出全局最优解的问题中。
适用场景
贪心算法适用于具有以下性质的问题:
- 贪心选择性质:可以在不考虑未来后果的情况下,通过局部最优选择来构建全局最优解。
- 最优子结构性质:一个问题的最优解包含其子问题的最优解。
这类问题通常可以通过动态规划解决,但贪心算法往往更简单且计算效率更高。
基本步骤
贪心算法使用基本步骤:
1.从问题的某个初始解出发
2.采用循环语句,当可以向求解目标前进一步时,就根据局部最优策略,得到一个部分解,缩小问题的范围或规模。
3.将所有的部分解综合起来,得到问题的最终解。
常见应用实例
1. 活动选择问题:
- 给定一组活动,每个活动都有开始时间和结束时间,目标是选择尽可能多的互不冲突的活动。
- 策略:按照活动结束时间排序,然后依次选择最早结束的活动,并排除与之冲突的其他活动。
题目描述:
给定一组活动,每个活动都有一个开始时间和结束时间。要求从这些活动中选择尽可能多的互不冲突的活动(即没有两个活动的时间段重叠)。目标是找到一种方法,使得可以选择最多的活动数量。
输入:
- 一个包含多个活动的列表,每个活动由一对整数
(start, end)表示,分别表示该活动的开始时间和结束时间。
输出:
- 返回可以安排的最大活动数量,并列出这些活动的索引或标识符。
C语言解答
#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
int start_a = ((int *)a)[0];
int end_a = ((int *)a)[1];
int start_b = ((int *)b)[0];
int end_b = ((int *)b)[1];
return (end_a - end_b); // 按结束时间升序排列
}
void activitySelection(int activities[][2], int n) {
// 先对活动按结束时间进行升序排序
qsort(activities, n, sizeof(int) * 2, compare);
int i, count = 1; // 第一个活动总是被选中
printf("选择的活动索引为: 0");
// 选择第一个活动
int last_selected_end = activities[0][1];
// 遍历剩余的活动
for (i = 1; i < n; i++) {
if (activities[i][0] >= last_selected_end) {
printf(", %d", i);
count++;
last_selected_end = activities[i][1];
}
}
printf("\n最多可以安排的活动数量为 %d\n", count);
}
int main() {
int activities[][2] = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 8}, {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 13}, {12, 14}};
int n = sizeof(activities) / sizeof(activities[0]);
activitySelection(activities, n);
return 0;
}
C语言代码解释
-
比较函数:
int compare(const void *a, const void *b) { int start_a = ((int *)a)[0]; int end_a = ((int *)a)[1]; int start_b = ((int *)b)[0]; int end_b = ((int *)b)[1]; return (end_a - end_b); // 按结束时间升序排列 }定义了一个比较函数,用于
qsort函数对活动数组按结束时间升序排序。 -
活动选择函数:
void activitySelection(int activities[][2], int n) { qsort(activities, n, sizeof(int) * 2, compare); ... }使用
qsort对活动数组进行排序,并初始化第一个选择的活动。 -
选择活动:
int last_selected_end = activities[0][1]; for (i = 1; i < n; i++) { if (activities[i][0] >= last_selected_end) { printf(", %d", i); count++; last_selected_end = activities[i][1]; } }遍历剩余的活动,选择符合条件的活动,并更新
last_selected_end。 -
主函数:
int main() { int activities[][2] = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 8}, {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 13}, {12, 14}}; int n = sizeof(activities) / sizeof(activities[0]); activitySelection(activities, n); return 0; }定义了活动数组并调用
activitySelection函数。
Python解答
def activity_selection(activities):
# 按结束时间升序排序
activities.sort(key=lambda x: x[1])
# 初始化选择的第一个活动
selected_activities = [activities[0]]
print(f"选择的活动索引为: 0")
# 记录最后一个选择的活动的结束时间
last_selected_end = activities[0][1]
# 遍历剩余的活动
for i in range(1, len(activities)):
if activities[i][0] >= last_selected_end:
selected_activities.append(activities[i])
print(f", {i}", end="")
last_selected_end = activities[i][1]
print(f"\n最多可以安排的活动数量为 {len(selected_activities)}")
# 示例
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 8), (5, 9), (6, 10), (8, 11), (8, 12), (2, 13), (12, 14)]
activity_selection(activities)
Python代码解释
-
排序活动:
activities.sort(key=lambda x: x[1])使用
sort方法对活动数组按结束时间升序排序。 -
选择活动:
selected_activities = [activities[0]] print(f"选择的活动索引为: 0") last_selected_end = activities[0][1] for i in range(1, len(activities)): if activities[i][0] >= last_selected_end: selected_activities.append(activities[i]) print(f", {i}", end="") last_selected_end = activities[i][1]遍历剩余的活动,选择符合条件的活动,并更新
last_selected_end。 -
输出结果:
print(f"\n最多可以安排的活动数量为 {len(selected_activities)}")输出选择的活动数量。
2. 最小生成树:
- 在加权无向图中找到一棵生成树,使得所有边的权重和最小。
- Kruskal算法和Prim算法都是基于贪心策略的经典算法。
题目描述:
给定一个连通的无向图,其中每条边都有一个权重。请编写一个函数来找到该图的最小生成树(MST),即连接所有顶点且总权重最小的子图。
输入:
- 一个整数
n表示图中顶点的数量。 - 一个二维数组
edges,每个元素是一个三元组[u, v, w],表示从顶点u到顶点v的一条边,权重为w。
输出:
- 返回最小生成树的总权重。如果无法形成最小生成树(例如图不连通),则返回
-1。
解答程序
Kruskal算法简介
Kruskal算法是一种用于解决最小生成树问题的经典贪心算法。其基本思想是按边的权重从小到大排序,然后依次将边加入生成树中,但要确保不会形成环。具体步骤如下:
- 初始化:创建一个并查集(Union-Find)数据结构来管理各个顶点的连通性。
- 排序边:将所有边按权重从小到大排序。
- 选择边:依次选择权重最小的边,并检查这条边是否会形成环。如果不形成环,则将其加入生成树。
- 终止条件:当生成树包含
n-1条边时,停止选择边。
C语言实现
#include <stdio.h>
#include <stdlib.h>
// 并查集结构体
typedef struct {
int *parent;
int *rank;
} UnionFind;
// 初始化并查集
UnionFind* initUnionFind(int n) {
UnionFind *uf = (UnionFind *)malloc(sizeof(UnionFind));
uf->parent = (int *)malloc(n * sizeof(int));
uf->rank = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
uf->parent[i] = i;
uf->rank[i] = 0;
}
return uf;
}
// 查找操作(带路径压缩)
int find(UnionFind *uf, int x) {
if (uf->parent[x] != x) {
uf->parent[x] = find(uf, uf->parent[x]);
}
return uf->parent[x];
}
// 合并操作(按秩合并)
void unionSets(UnionFind *uf, int x, int y) {
int rootX = find(uf, x);
int rootY = find(uf, y);
if (rootX != rootY) {
if (uf->rank[rootX] > uf->rank[rootY]) {
uf->parent[rootY] = rootX;
} else if (uf->rank[rootX] < uf->rank[rootY]) {
uf->parent[rootX] = rootY;
} else {
uf->parent[rootY] = rootX;
uf->rank[rootX]++;
}
}
}
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
int *edgeA = *(int **)a;
int *edgeB = *(int **)b;
return edgeA[2] - edgeB[2];
}
// Kruskal算法
int kruskal(int n, int edges[][3], int numEdges) {
// 对边进行排序
qsort(edges, numEdges, sizeof(int *), compare);
// 初始化并查集
UnionFind *uf = initUnionFind(n);
int totalWeight = 0;
int numEdgesInMST = 0;
// 遍历每条边
for (int i = 0; i < numEdges && numEdgesInMST < n - 1; i++) {
int u = edges[i][0];
int v = edges[i][1];
int weight = edges[i][2];
// 如果两个顶点不在同一个集合中,合并它们
if (find(uf, u) != find(uf, v)) {
unionSets(uf, u, v);
totalWeight += weight;
numEdgesInMST++;
}
}
// 释放并查集资源
free(uf->parent);
free(uf->rank);
free(uf);
// 如果生成树包含 n-1 条边,返回总权重;否则返回 -1
return numEdgesInMST == n - 1 ? totalWeight : -1;
}
int main() {
int n = 4;
int edges[][3] = {{0, 1, 1}, {1, 2, 2}, {2, 3, 3}, {0, 3, 4}};
int numEdges = sizeof(edges) / sizeof(edges[0]);
int result = kruskal(n, edges, numEdges);
if (result == -1) {
printf("无法形成最小生成树\n");
} else {
printf("最小生成树的总权重为 %d\n", result);
}
return 0;
}
Python实现
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
rootX = self.find(x)
rootY = self.find(y)
if rootX != rootY:
if self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
elif self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
def kruskal(n, edges):
# 对边进行排序
edges.sort(key=lambda edge: edge[2])
# 初始化并查集
uf = UnionFind(n)
total_weight = 0
num_edges_in_mst = 0
# 遍历每条边
for u, v, weight in edges:
if uf.find(u) != uf.find(v):
uf.union(u, v)
total_weight += weight
num_edges_in_mst += 1
# 如果生成树包含 n-1 条边,返回总权重;否则返回 -1
return total_weight if num_edges_in_mst == n - 1 else -1
# 示例
n = 4
edges = [[0, 1, 1], [1, 2, 2], [2, 3, 3], [0, 3, 4]]
result = kruskal(n, edges)
if result == -1:
print("无法形成最小生成树")
else:
print(f"最小生成树的总权重为 {result}")
详细解释
-
并查集(Union-Find):
- 并查集用于管理顶点的连通性,支持高效的查找和合并操作。
find方法用于查找某个顶点所属的集合根节点,并通过路径压缩优化查询效率。union方法用于合并两个顶点所属的集合,并通过按秩合并优化合并效率。
-
排序边:
- 使用
qsort(C语言)或sort(Python)对所有边按权重从小到大排序。这是贪心策略的核心,优先选择权重较小的边。
- 使用
-
选择边:
- 遍历排序后的边列表,对于每条边
(u, v, weight):- 使用并查集检查顶点
u和v是否属于同一集合。如果是,则跳过该边以避免形成环。 - 如果不是,则合并这两个顶点所在的集合,并将该边的权重累加到总权重中。
- 使用并查集检查顶点
- 遍历排序后的边列表,对于每条边
-
终止条件:
- 当生成树包含
n-1条边时,说明已经找到了最小生成树,返回总权重。 - 如果遍历完所有边后仍未达到
n-1条边,说明图不连通,返回-1。
- 当生成树包含
3. 哈夫曼编码:
- 一种用于数据压缩的编码方式,通过构建最优前缀码来减少信息存储空间。
- 策略:每次合并频率最低的两个节点,直到构建出完整的二叉树。
题目描述:
给定一个字符及其出现频率的列表,请编写一个函数来生成这些字符的哈夫曼编码。哈夫曼编码是一种用于数据压缩的前缀编码方式,通过为每个字符分配不同长度的二进制编码来减少总编码长度。
输入:
- 一个包含字符及其频率的字典
char_freq,其中键是字符,值是该字符在文本中出现的频率。
输出:
- 返回一个字典,键是字符,值是该字符对应的哈夫曼编码。
哈夫曼编码简介
哈夫曼编码是一种贪心算法,用于构建最优前缀码。其基本思想是根据字符出现的频率构建一棵二叉树,频率越高的字符离根节点越近,从而使得高频字符的编码更短。具体步骤如下:
- 初始化:将每个字符及其频率作为叶子节点插入优先队列(最小堆)。
- 合并节点:从优先队列中取出两个频率最小的节点,创建一个新的内部节点作为它们的父节点,并将新节点的频率设为这两个节点频率之和。将新节点重新插入优先队列。
- 重复合并:重复上述步骤,直到优先队列中只剩下一个节点,即哈夫曼树的根节点。
- 生成编码:从根节点开始遍历哈夫曼树,左分支标记为’0’,右分支标记为’1’,记录每个字符到达叶子节点的路径即为其哈夫曼编码。
C语言实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Node {
char ch;
int freq;
struct Node *left, *right;
} Node;
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
return ((Node *)a)->freq - ((Node *)b)->freq;
}
// 创建新节点
Node* createNode(char ch, int freq, Node* left, Node* right) {
Node* node = (Node*)malloc(sizeof(Node));
node->ch = ch;
node->freq = freq;
node->left = left;
node->right = right;
return node;
}
// 构建哈夫曼树
Node* buildHuffmanTree(Node nodes[], int n) {
while (n > 1) {
// 对节点按频率排序
qsort(nodes, n, sizeof(Node), compare);
// 取出两个频率最小的节点
Node *left = &nodes[n - 2];
Node *right = &nodes[n - 1];
// 创建新的内部节点
Node *parent = createNode('\0', left->freq + right->freq, left, right);
// 替换最后一个节点,并减少节点数量
nodes[n - 2] = *parent;
n--;
}
return &nodes[0];
}
// 递归生成哈夫曼编码
void generateCodes(Node* root, char* code, int top, char codes[][20]) {
if (root->left) {
code[top] = '0';
generateCodes(root->left, code, top + 1, codes);
}
if (root->right) {
code[top] = '1';
generateCodes(root->right, code, top + 1, codes);
}
if (!root->left && !root->right) {
code[top] = '\0';
strcpy(codes[root->ch], code);
}
}
// 打印哈夫曼编码
void printCodes(Node* root, char codes[][20]) {
char code[20];
generateCodes(root, code, 0, codes);
for (int i = 0; i < 256; i++) {
if (codes[i][0] != '\0') {
printf("'%c': %s\n", i, codes[i]);
}
}
}
int main() {
// 示例输入
Node nodes[] = {
{'A', 5, NULL, NULL},
{'B', 9, NULL, NULL},
{'C', 12, NULL, NULL},
{'D', 13, NULL, NULL},
{'E', 16, NULL, NULL},
{'F', 45, NULL, NULL}
};
int n = sizeof(nodes) / sizeof(nodes[0]);
// 构建哈夫曼树
Node* root = buildHuffmanTree(nodes, n);
// 初始化编码数组
char codes[256][20];
for (int i = 0; i < 256; i++) {
codes[i][0] = '\0';
}
// 生成并打印哈夫曼编码
printCodes(root, codes);
return 0;
}
Python实现
import heapq
from collections import defaultdict
class Node:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
# 定义小于操作符,以便于堆排序
def __lt__(self, other):
return self.freq < other.freq
def build_huffman_tree(char_freq):
heap = [Node(ch, freq) for ch, freq in char_freq.items()]
heapq.heapify(heap)
while len(heap) > 1:
# 弹出两个最小频率的节点
left = heapq.heappop(heap)
right = heapq.heappop(heap)
# 创建新的内部节点
merged = Node(None, left.freq + right.freq)
merged.left = left
merged.right = right
# 将新节点加入堆
heapq.heappush(heap, merged)
return heap[0]
def generate_codes(node, prefix="", codebook=None):
if codebook is None:
codebook = {}
if node is not None:
if node.char is not None:
codebook[node.char] = prefix
generate_codes(node.left, prefix + "0", codebook)
generate_codes(node.right, prefix + "1", codebook)
return codebook
def huffman_encoding(char_freq):
root = build_huffman_tree(char_freq)
codebook = generate_codes(root)
return codebook
# 示例
char_freq = {'A': 5, 'B': 9, 'C': 12, 'D': 13, 'E': 16, 'F': 45}
huffman_codes = huffman_encoding(char_freq)
print("哈夫曼编码:")
for char, code in sorted(huffman_codes.items()):
print(f"'{char}': {code}")
详细解释
-
节点类定义:
- 在C语言中,定义了一个结构体
Node来表示哈夫曼树中的节点。 - 在Python中,定义了一个类
Node来表示哈夫曼树中的节点,并实现了比较方法__lt__,以便于堆排序。
- 在C语言中,定义了一个结构体
-
构建哈夫曼树:
- 在C语言中,手动维护一个数组
nodes并使用qsort进行排序,逐步合并频率最小的两个节点。 - 在Python中,使用
heapq模块来管理优先队列,每次从堆中弹出两个频率最小的节点,合并后重新插入堆中,直到堆中只剩下一个节点。
- 在C语言中,手动维护一个数组
-
生成哈夫曼编码:
- 使用递归的方式遍历哈夫曼树,左分支标记为
'0',右分支标记为'1',记录每个字符到达叶子节点的路径即为其哈夫曼编码。 - 在C语言中,使用一个二维数组
codes来存储每个字符的编码。 - 在Python中,使用一个字典
codebook来存储每个字符的编码。
- 使用递归的方式遍历哈夫曼树,左分支标记为
-
打印结果:
- 输出每个字符及其对应的哈夫曼编码。
4. 硬币找零问题:
- 给定一些不同面值的硬币,找出用最少数量的硬币凑成某一金额的方法。
- 对于某些特定的硬币体系(如美国硬币),贪心算法可以得到最优解。
找零问题是贪心算法的经典应用之一,它涉及到使用最少数量的硬币来凑出某个特定金额。具体来说,给定一个总金额和几种不同面值的硬币,目标是找出用这些硬币凑出该金额所需的最少硬币数。
问题描述
假设你有一个无限数量的不同面值的硬币集合(如1分、5分、10分、25分),你需要找到一种方法,使得使用最少数量的硬币凑出指定的金额。例如,如果你需要找零30分,你可以选择使用三个10分硬币,或者两个10分硬币加两个5分硬币等,但最优解是使用一个25分硬币和一个5分硬币,总共只需要两枚硬币。
贪心算法步骤
在找零问题中,贪心算法的基本思想是每次尽可能选择当前可用的最大面值的硬币,逐步减少所需找零的金额,直到找零完成。以下是具体的步骤:
- 初始化:设定一个变量
amount表示需要找零的总金额,并准备一个列表或数组coins存储所有可用的硬币面值。 - 排序硬币面值:将硬币面值按降序排列(从大到小)。
- 循环选择硬币:
- 对于每一个硬币面值
coin,如果coin小于等于当前的amount,则选择这个硬币,并从amount中减去相应的面值。 - 记录所选硬币的数量。
- 对于每一个硬币面值
- 终止条件:当
amount减至0时,说明已经找到了满足条件的最少硬币组合;如果遍历完所有硬币后amount仍然大于0,则说明无法用现有硬币面值组合凑出该金额。
示例代码
题目描述:
给定一个总金额 amount 和若干种不同面值的硬币 coins[],请编写一个函数来计算出凑成该金额所需的最少硬币数量。如果无法通过这些硬币凑出该金额,则返回 -1。
输入:
- 一个整数
amount表示需要找零的金额。 - 一个整数数组
coins[]表示可用的硬币面值。
输出:
- 返回凑成该金额所需的最少硬币数量。如果无法凑出该金额,则返回
-1。
python代码实现
def min_coins_greedy(amount, coins):
# 对硬币面值进行降序排列
coins.sort(reverse=True)
# 初始化使用的硬币数量为0
num_coins = 0
# 遍历每个硬币面值
for coin in coins:
# 尽可能多地使用当前面值的硬币
while amount >= coin:
amount -= coin # 减去当前硬币面值
num_coins += 1 # 增加使用的硬币数量
# 如果最终金额为0,返回使用的硬币数量;否则返回-1表示无法凑出该金额
return num_coins if amount == 0 else -1
python代码详细步骤解析
-
函数定义:
def min_coins_greedy(amount, coins):定义一个名为
min_coins_greedy的函数,接受两个参数:amount:需要凑出的总金额。coins:一个包含不同面值硬币的列表。
-
排序硬币面值:
coins.sort(reverse=True)对硬币面值列表
coins进行降序排列(从大到小)。这是为了确保在每次循环中优先选择最大面值的硬币,从而符合贪心策略的核心思想。 -
初始化计数器:
num_coins = 0初始化一个变量
num_coins,用来记录已经使用的硬币总数。 -
遍历硬币面值:
for coin in coins:使用
for循环遍历每一个硬币面值coin。由于之前对coins进行了降序排列,因此会首先处理较大面值的硬币。 -
尽可能多地使用当前面值的硬币:
while amount >= coin: amount -= coin num_coins += 1使用
while循环尽可能多地使用当前面值的硬币:- 检查当前金额
amount是否大于等于当前硬币面值coin。 - 如果是,则从
amount中减去当前硬币面值coin。 - 同时增加已使用的硬币计数
num_coins。
- 检查当前金额
-
返回结果:
return num_coins if amount == 0 else -1在所有硬币面值都处理完后,检查剩余的金额
amount是否为0:- 如果
amount为0,说明成功凑出了所需金额,返回使用的硬币总数num_coins。 - 如果
amount不为0,说明无法通过现有硬币面值凑出该金额,返回-1。
- 如果
C语言代码实现
如果你确认特定的硬币系统适用贪心策略,那么可以编写一个基于贪心算法的函数来解决硬币找零问题。以下是一个使用贪心算法的C语言实现示例:
C语言实现
#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
return (*(int *)b - *(int *)a); // 按降序排列
}
// 贪心算法函数:计算最少硬币数量
int minCoinsGreedy(int amount, int coins[], int n) {
// 先对硬币面值进行降序排序
qsort(coins, n, sizeof(int), compare);
int num_coins = 0;
for (int i = 0; i < n; i++) {
// 尽可能多地使用当前面值的硬币
while (amount >= coins[i]) {
amount -= coins[i];
num_coins++;
}
}
// 如果金额没有完全凑齐,则返回-1
if (amount != 0) {
return -1;
} else {
return num_coins;
}
}
int main() {
int coins[] = {1, 5, 10, 25}; // 示例硬币系统
int n = sizeof(coins) / sizeof(coins[0]);
int amount = 49; // 需要找零的总金额
int result = minCoinsGreedy(amount, coins, n);
if (result == -1) {
printf("无法用这些硬币凑出该金额\n");
} else {
printf("最少需要 %d 枚硬币来找零 %d 分\n", result, amount);
}
return 0;
}
C语言代码解释
-
排序硬币面值:
- 使用
qsort函数对硬币面值数组coins[]进行降序排序。这是因为贪心算法每次都会选择尽可能大的面值硬币。
- 使用
-
初始化计数器:
- 定义一个变量
num_coins来记录使用的硬币总数。
- 定义一个变量
-
贪心选择:
- 对于每一个硬币面值
coins[i],在amount大于等于当前硬币面值时,尽可能多地使用该硬币,并减少相应的amount。 - 每次使用一个硬币后,增加
num_coins的计数。
- 对于每一个硬币面值
-
检查剩余金额:
- 如果最终
amount不为0,说明无法通过现有硬币组合凑出该金额,返回-1。 - 否则,返回使用的硬币总数
num_coins。
- 如果最终
局限性
虽然贪心算法在某些硬币系统下可以得到最优解,但它并不总是能得到全局最优解。例如,在某些特殊的硬币系统中,贪心算法可能会失败。考虑以下硬币系统 {1, 7, 10} 和需要找零的金额为14的情况:
- 贪心算法会选择一枚10分硬币和四枚1分硬币,共五枚硬币。
- 然而,最优解应该是两枚7分硬币,只需要两枚硬币。
因此,在使用贪心算法解决找零问题之前,确保你的硬币系统符合贪心选择性质即每次都选择最大面值硬币能够保证最终结果是最优的。如果不满足这一性质,可能需要采用动态规划等其他方法来求解。
5. 背包问题
- 给定一组物品和背包容量,如何选择物品,使得背包中物品总价值最大。
- 策略:按照物品的价值排序,然后依次选择价值最大的物品,直到背包容量不足。
题目描述:
给定一个容量为 capacity 的背包和一组物品,每个物品有一个重量 weight[i] 和价值 value[i]。请编写一个函数来计算在不超过背包容量的前提下,能够装入背包的最大总价值。
输入:
- 一个整数
capacity表示背包的容量。 - 两个数组
weights[]和values[],分别表示每个物品的重量和价值。 - 一个整数
n表示物品的数量。
输出:
- 返回能够装入背包的最大总价值。
注意:
由于0/1背包问题是NP完全问题,通常使用动态规划解决。但是,我们可以使用贪心算法得到一个近似解,即优先选择单位重量价值最高的物品。
C语言实现
#include <stdio.h>
#include <stdlib.h>
// 定义一个结构体来存储物品的信息
typedef struct {
int value;
int weight;
float ratio; // 单位重量价值
} Item;
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
return ((Item *)b)->ratio - ((Item *)a)->ratio;
}
float fractionalKnapsack(int capacity, Item items[], int n) {
// 对物品按单位重量价值进行降序排序
qsort(items, n, sizeof(Item), compare);
float totalValue = 0.0;
for (int i = 0; i < n; i++) {
if (items[i].weight <= capacity) {
// 如果当前物品能完全装入背包
totalValue += items[i].value;
capacity -= items[i].weight;
} else {
// 如果当前物品只能部分装入背包
totalValue += items[i].ratio * capacity;
break;
}
}
return totalValue;
}
int main() {
int capacity = 50;
Item items[] = {{60, 10, 0}, {100, 20, 0}, {120, 30, 0}};
int n = sizeof(items) / sizeof(items[0]);
// 计算单位重量价值
for (int i = 0; i < n; i++) {
items[i].ratio = (float)items[i].value / items[i].weight;
}
float result = fractionalKnapsack(capacity, items, n);
printf("最大总价值为 %.2f\n", result);
return 0;
}
Python实现
class Item:
def __init__(self, value, weight):
self.value = value
self.weight = weight
self.ratio = value / weight
def fractional_knapsack(capacity, items):
# 按单位重量价值降序排序
items.sort(key=lambda x: x.ratio, reverse=True)
total_value = 0.0
for item in items:
if item.weight <= capacity:
# 如果当前物品能完全装入背包
total_value += item.value
capacity -= item.weight
else:
# 如果当前物品只能部分装入背包
total_value += item.ratio * capacity
break
return total_value
# 示例
capacity = 50
items = [Item(60, 10), Item(100, 20), Item(120, 30)]
result = fractional_knapsack(capacity, items)
print(f"最大总价值为 {result:.2f}")
详细解释
-
定义物品结构体:
- 在C语言中,定义一个
Item结构体来存储每个物品的重量、价值和单位重量价值(即value / weight)。 - 在Python中,定义一个
Item类,并在初始化时计算单位重量价值。
- 在C语言中,定义一个
-
计算单位重量价值:
- 对于每个物品,计算其单位重量价值
ratio = value / weight。
- 对于每个物品,计算其单位重量价值
-
排序物品:
- 使用
qsort(C语言)或sort(Python)对所有物品按单位重量价值从高到低排序。这是贪心策略的核心,优先选择单位重量价值最高的物品。
- 使用
-
选择物品:
- 遍历排序后的物品列表,对于每个物品:
- 如果该物品的重量小于等于剩余背包容量,则将该物品完全装入背包,并更新总价值和剩余容量。
- 如果该物品的重量大于剩余背包容量,则只能部分装入该物品,计算并累加这部分物品的价值后终止循环。
- 遍历排序后的物品列表,对于每个物品:
-
返回结果:
- 返回累计的总价值。
6. 最短路径问题
- 给定一个带权有向图,找到从源点到所有其他点的最短路径。
- 策略:按照边的权重排序,然后依次选择权值最小的边,直到所有顶点都被访问到。
题目描述:
给定一个加权有向图,其中每条边都有一个非负权重。请编写一个函数来找到从指定起点到其他所有顶点的最短路径。
输入:
- 一个整数
n表示图中顶点的数量。 - 一个二维数组
edges,每个元素是一个三元组[u, v, w],表示从顶点u到顶点v的一条边,权重为w。 - 一个整数
start表示起点顶点。
输出:
- 返回一个数组
dist,其中dist[i]表示从起点start到顶点i的最短路径长度。如果某个顶点不可达,则对应的dist[i]应为-1。
Dijkstra算法简介
Dijkstra算法是一种用于解决单源最短路径问题的经典贪心算法。其基本思想是从起点开始,逐步扩展到距离最近的未访问顶点,并更新这些顶点的最短路径估计值。具体步骤如下:
- 初始化:将起点的最短路径设为0,其他顶点的最短路径设为无穷大。
- 优先队列:使用最小堆(或优先队列)来存储和选择当前距离最近的顶点。
- 松弛操作:对于当前顶点的所有邻接顶点,尝试通过当前顶点更新它们的最短路径估计值。
- 终止条件:当所有顶点都被处理完时,算法结束。
C语言实现
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
// 定义一个优先队列节点结构体
typedef struct {
int vertex;
int distance;
} PriorityQueueNode;
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
PriorityQueueNode *nodeA = (PriorityQueueNode *)a;
PriorityQueueNode *nodeB = (PriorityQueueNode *)b;
return nodeA->distance - nodeB->distance;
}
// 更新优先队列中的距离
void updatePriorityQueue(PriorityQueueNode pq[], int size, int index, int newDistance) {
pq[index].distance = newDistance;
// 重新排序以保持最小堆属性
qsort(pq, size, sizeof(PriorityQueueNode), compare);
}
// Dijkstra算法
void dijkstra(int n, int edges[][3], int numEdges, int start, int dist[]) {
// 初始化距离数组,所有距离设为无穷大
for (int i = 0; i < n; i++) {
dist[i] = INT_MAX;
}
dist[start] = 0;
// 创建优先队列并初始化
PriorityQueueNode pq[n];
for (int i = 0; i < n; i++) {
pq[i].vertex = i;
pq[i].distance = dist[i];
}
while (1) {
// 找到距离最小的顶点
int minDist = INT_MAX;
int minIndex = -1;
for (int i = 0; i < n; i++) {
if (pq[i].distance < minDist && pq[i].distance != -1) {
minDist = pq[i].distance;
minIndex = i;
}
}
// 如果没有可处理的顶点,退出循环
if (minIndex == -1) break;
int u = pq[minIndex].vertex;
pq[minIndex].distance = -1; // 标记为已处理
// 松弛操作
for (int i = 0; i < numEdges; i++) {
int v = edges[i][1];
int weight = edges[i][2];
if (edges[i][0] == u && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
updatePriorityQueue(pq, n, minIndex, dist[v]);
}
}
}
// 将不可达的顶点距离设为-1
for (int i = 0; i < n; i++) {
if (dist[i] == INT_MAX) {
dist[i] = -1;
}
}
}
int main() {
int n = 5;
int edges[][3] = {{0, 1, 1}, {1, 2, 2}, {2, 3, 3}, {0, 3, 4}};
int numEdges = sizeof(edges) / sizeof(edges[0]);
int start = 0;
int dist[n];
dijkstra(n, edges, numEdges, start, dist);
printf("从顶点 %d 到其他顶点的最短路径长度:\n", start);
for (int i = 0; i < n; i++) {
printf("%d -> %d: %d\n", start, i, dist[i]);
}
return 0;
}
Python实现
import heapq
def dijkstra(n, edges, start):
# 构建邻接表
graph = [[] for _ in range(n)]
for u, v, weight in edges:
graph[u].append((v, weight))
# 初始化距离数组,所有距离设为无穷大
dist = [float('inf')] * n
dist[start] = 0
# 使用优先队列(最小堆)
pq = [(0, start)]
while pq:
current_distance, current_vertex = heapq.heappop(pq)
# 如果当前距离大于记录的距离,跳过
if current_distance > dist[current_vertex]:
continue
# 松弛操作
for neighbor, weight in graph[current_vertex]:
distance = current_distance + weight
if distance < dist[neighbor]:
dist[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
# 将不可达的顶点距离设为-1
for i in range(n):
if dist[i] == float('inf'):
dist[i] = -1
return dist
# 示例
n = 5
edges = [(0, 1, 1), (1, 2, 2), (2, 3, 3), (0, 3, 4)]
start = 0
result = dijkstra(n, edges, start)
print(f"从顶点 {start} 到其他顶点的最短路径长度:")
for i in range(n):
print(f"{start} -> {i}: {result[i]}")
详细解释
-
构建邻接表:
- 在Python实现中,我们首先根据输入的边列表构建一个邻接表
graph,其中graph[u]存储从顶点u出发的所有边及其权重。
- 在Python实现中,我们首先根据输入的边列表构建一个邻接表
-
初始化距离数组:
- 初始化一个距离数组
dist,所有元素初始值设为无穷大(在C语言中用INT_MAX,在Python中用float('inf')),起点的距离设为0。
- 初始化一个距离数组
-
优先队列(最小堆):
- 使用优先队列(最小堆)来存储和选择当前距离最近的顶点。在Python中使用
heapq模块,在C语言中手动维护一个最小堆并通过qsort排序。
- 使用优先队列(最小堆)来存储和选择当前距离最近的顶点。在Python中使用
-
松弛操作:
- 对于当前顶点的所有邻接顶点,尝试通过当前顶点更新它们的最短路径估计值。如果新的路径更短,则更新距离数组并将该顶点加入优先队列。
-
终止条件:
- 当优先队列为空时,说明所有顶点都已处理完毕,算法结束。此时,距离数组
dist中存储了从起点到各个顶点的最短路径长度。
- 当优先队列为空时,说明所有顶点都已处理完毕,算法结束。此时,距离数组
-
处理不可达顶点:
- 最后,检查距离数组中的无穷大值,并将其替换为
-1表示不可达。
- 最后,检查距离数组中的无穷大值,并将其替换为
贪心算法的局限性
尽管贪心算法在很多情况下表现良好,但它并不适用于所有问题。例如,在某些复杂问题中,贪心选择可能会导致次优解。因此,在使用贪心算法之前,必须仔细分析问题是否满足贪心选择性质和最优子结构特性。
- 不能保证求得的最后解是最佳的;
- 不能用来求最大或最小解问题;
- 只能求满足某些约束条件的可行解的范围。
总结
贪心算法是一种简单直观的算法设计策略,适用于具有特定性质的问题。它通过每一步做出局部最优选择来尝试构建全局最优解。虽然不是所有问题都能通过贪心算法得到最优解,但对于适合的问题,这种方法可以提供高效且易于实现的解决方案。
5890

被折叠的 条评论
为什么被折叠?



