C语言:贪心算法

贪心算法(Greedy Algorithm)是一种常用的算法策略,用于在每个阶段都选择当前状态下的最佳选择,以希望最终得到全局最优解。贪心算法的核心思想是每次都选择局部最优解,并相信通过这种选择方式可以达到全局最优解。

以下是贪心算法的一般步骤:

  1. 确定问题的最优子结构:贪心算法通常应用于具有最优子结构性质的问题,即问题的最优解可以通过一系列局部最优解得到。

  2. 构建贪心选择:对于给定的问题,通过定义一种选择方式,在每个阶段都做出一个贪心选择,即选择当前状态下的局部最优解。

  3. 验证贪心选择的可行性:验证所做的贪心选择是否符合问题的约束条件,确保选择是可行的。

  4. 更新问题的状态:根据所做的贪心选择,更新问题的状态,进入下一个阶段。

  5. 判断是否达到结束条件:判断是否已经达到了问题的结束条件,如果满足结束条件,则得到了问题的最终解;否则,返回第2步继续执行。

贪心算法的优点在于简单、高效,并且在某些问题上可以得到最优解。然而,贪心算法并不能保证对所有问题都能得到全局最优解,因为贪心选择可能导致局部最优解并不一定是全局最优解。因此,在应用贪心算法时,需要仔细分析问题的特点,确保贪心选择的有效性。

贪心算法的应用广泛,例如:

  • 最小生成树问题(如Prim算法和Kruskal算法)
  1. Prim算法的步骤:

    • 选择任意一个顶点作为起始顶点,并将其加入最小生成树。
    • 在剩余的顶点中,选择与当前最小生成树距离最近的顶点,并将其加入最小生成树。
    • 重复上述步骤,直到所有顶点都加入最小生成树,形成一棵生成树。
  2. Kruskal算法的步骤:

    • 将所有边按照权重进行升序排序。
    • 从权重最小的边开始,逐个加入最小生成树中,但要确保加入的边不会形成环路。
    • 重复上述步骤,直到最小生成树中包含了所有顶点(边数为V-1)。

下面是使用Prim算法和Kruskal算法解决最小生成树问题的示例代码:

#include 
#include 

#define V 5  // 顶点数量

// 找到与当前最小生成树距离最近的顶点
int findMinKey(int key[], bool mstSet[]) {
    int min = INT_MAX, min_index;
 
    for (int v = 0; v < V; v++)
        if (mstSet[v] == false && key[v] < min)
            min = key[v], min_index = v;
 
    return min_index;
}

// 打印最小生成树
void printMST(int parent[], int graph[V][V]) {
    printf("Edge \tWeight\n");
    for (int i = 1; i < V; i++)
        printf("%d - %d \t%d \n", parent[i], i, graph[i][parent[i]]);
}

// 使用Prim算法解决最小生成树问题
void primMST(int graph[V][V]) {
    int parent[V]; // 最小生成树的父节点
    int key[V];    // 顶点到最小生成树的距离
    bool mstSet[V]; // 顶点是否包含在最小生成树中

    for (int i = 0; i < V; i++)
        key[i] = INT_MAX, mstSet[i] = false;

    key[0] = 0;     // 第一个顶点作为起始顶点
    parent[0] = -1; // 第一个顶点没有父节点

    for (int count = 0; count < V - 1; count++) {
        int u = findMinKey(key, mstSet);
        mstSet[u] = true;

        for (int v = 0; v < V; v++)
            if (graph[u][v] && mstSet[v] == false && graph[u][v] < key[v])
                parent[v] = u, key[v] = graph[u][v];
    }

    printMST(parent, graph);
}

// 使用Kruskal算法解决最小生成树问题
void kruskalMST(int graph[V][V]) {
    int parent[V];  // 最小生成树的父节点
    int minIndex[V];  // 存储每个顶点所属的最小生成树集合的代表节点
    int minCost[V];  // 存储每个顶点与最小生成树的最小边权重
    int minEdge = 0;  // 最小生成树的总边权重
    int edgeCount = 0;  // 已经加入最小生成树的边数量

    // 初始化并查集
    for (int i = 0; i < V; i++) {
        parent[i] = i;
        minIndex[i] = -1;
        minCost[i] = INT_MAX;
    }

    while (edgeCount < V - 1) {
        int minWeight = INT_MAX;
        int u, v;

        // 找到权重最小的边
        for (int i = 0; i < V; i++) {
            for (int j = 0; j < V; j++) {
                if (graph[i][j] && find(i, parent) != find(j, parent) && graph[i][j] < minWeight) {
                    minWeight = graph[i][j];
                    u = i;
                    v = j;
                }
            }
        }

        // 将边加入最小生成树
        unionSets(u, v, parent);
        minIndex[u] = find(u, parent);
        minIndex[v] = find(v, parent);
        minCost[minIndex[u]] = minWeight;
        minCost[minIndex[v]] = minWeight;
        minEdge += minWeight;
        edgeCount++;
    }

    // 打印最小生成树
    printf("Edge \tWeight\n");
    for (int i = 1; i < V; i++)
        printf("%d - %d \t%d \n", i, minIndex[i], minCost[i]);
    printf("Minimum Cost: %d\n", minEdge);
}

int main() {
    int graph[V][V] = {
        {0, 2, 0, 6, 0},
        {2, 0, 3, 8, 5},
        {0, 3, 0, 0, 7},
        {6, 8, 0, 0, 9},
        {0, 5, 7, 9, 0}
    };

    printf("Prim's Algorithm:\n");
    primMST(graph);

    printf("\nKruskal's Algorithm:\n");
    kruskalMST(graph);

    return 0;
}

这是一个使用邻接矩阵表示图的示例,图中的顶点数量为V=5。通过调用primMST函数和kruskalMST函数,可以分别使用Prim算法和Kruskal算法解决最小生成树问题,并输出最小生成树的边和权重。请注意,这是一个简化的示例,实际应用中可能需要根据具体问题进行相应的调整。

  • 最短路径问题(如Dijkstra算法)
  1. Dijkstra算法的步骤:

    • 创建一个数组dist用于存储从起始顶点到其他顶点的最短距离,初始化为无穷大(表示不可达)。
    • 将起始顶点的最短距离设置为0。
    • 创建一个集合visited用于存储已经访问过的顶点。
    • 重复以下步骤,直到所有顶点都被访问:
      • 选择距离起始顶点最近的未访问顶点u
      • 标记顶点u为已访问。
      • 遍历顶点u的邻居顶点v,更新从起始顶点到顶点v的最短距离dist[v]
        • 计算从起始顶点经过顶点u到达顶点v的距离newDist
        • 如果newDist小于dist[v],则更新dist[v]newDist
    • 完成后,dist数组中存储的即为从起始顶点到其他顶点的最短距离。
  2. 示例代码:

#include 
#include 
#include 

#define V 6  // 顶点数量

// 找到距离起始顶点最近的未访问顶点
int findMinDistance(int dist[], bool visited[]) {
    int min = INT_MAX, min_index;

    for (int v = 0; v < V; v++)
        if (visited[v] == false && dist[v] <= min)
            min = dist[v], min_index = v;

    return min_index;
}

// 打印最短路径
void printShortestPaths(int dist[]) {
    printf("Vertex \tDistance from Source\n");
    for (int i = 0; i < V; i++)
        printf("%d \t%d\n", i, dist[i]);
}

// 使用Dijkstra算法解决最短路径问题
void dijkstra(int graph[V][V], int source) {
    int dist[V];     // 距离起始顶点的最短距离
    bool visited[V]; // 顶点是否已访问

    for (int i = 0; i < V; i++) {
        dist[i] = INT_MAX;
        visited[i] = false;
    }

    dist[source] = 0; // 起始顶点到自身的距离为0

    for (int count = 0; count < V - 1; count++) {
        int u = findMinDistance(dist, visited);
        visited[u] = true;

        for (int v = 0; v < V; v++) {
            if (!visited[v] && graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v])
                dist[v] = dist[u] + graph[u][v];
        }
    }

    printShortestPaths(dist);
}

int main() {
    int graph[V][V] = {
        {0, 4, 0, 0, 0, 0},
        {4, 0, 8, 0, 0, 0},
        {0, 8, 0, 7, 0, 4},
        {0, 0, 7, 0, 9, 14},
        {0, 0, 0, 9, 0, 10},
        {0, 0, 4, 14, 10, 0}
    };

    int source = 0; // 起始顶点

    dijkstra(graph, source);

    return 0;
}

这是一个使用邻接矩阵表示图的示例,图中的顶点数量为V=6。通过调用dijkstra函数,可以使用Dijkstra算法解决最短路径问题,并输出从起始顶点到其他顶点的最短距离。请注意,这是一个简化的示例,实际应用中可能需要根据具体问题进行相应的调整。

  • 区间调度问题
  1. 区间调度问题的步骤:

    • 将所有区间按照结束时间进行升序排序。
    • 选择第一个区间作为选中的区间。
    • 从第二个区间开始,遍历每个区间:
      • 如果当前区间的开始时间大于等于前一个选中区间的结束时间,则将该区间选中。
    • 完成后,选中的区间即为最大数量的不相交区间集合。
  2. 示例代码:

#include 
#include 

typedef struct {
    int start;
    int end;
} Interval;

// 按照结束时间进行升序排序的比较函数
int compareIntervals(const void* a, const void* b) {
    Interval* intervalA = (Interval*)a;
    Interval* intervalB = (Interval*)b;
    return intervalA->end - intervalB->end;
}

// 使用贪心算法解决区间调度问题
void intervalScheduling(Interval intervals[], int n) {
    // 将区间按照结束时间进行升序排序
    qsort(intervals, n, sizeof(Interval), compareIntervals);

    // 选中的第一个区间为第一个区间
    int selectedCount = 1;
    Interval selectedIntervals[n];
    selectedIntervals[0] = intervals[0];

    // 遍历每个区间,选择不相交的区间
    int lastSelected = 0;
    for (int i = 1; i < n; i++) {
        if (intervals[i].start >= intervals[lastSelected].end) {
            selectedIntervals[selectedCount] = intervals[i];
            selectedCount++;
            lastSelected = i;
        }
    }

    // 打印选中的区间
    printf("Selected Intervals:\n");
    for (int i = 0; i < selectedCount; i++) {
        printf("[%d, %d]\n", selectedIntervals[i].start, selectedIntervals[i].end);
    }
}

int main() {
    // 区间示例
    Interval intervals[] = {
        {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(intervals) / sizeof(intervals[0]);

    intervalScheduling(intervals, n);

    return 0;
}

在上面的示例代码中,我们定义了一个结构体Interval表示区间的开始和结束时间。通过调用intervalScheduling函数,可以使用贪心算法解决区间调度问题,并输出选中的不相交区间集合。在示例中,我们使用了一个示例区间数组,但你可以根据具体问题自定义输入。

  • 零钱找零问题
  1. 零钱找零问题的步骤:

    • 创建一个可用零钱的数组,按面额降序排序。
    • 初始化一个结果数组,用于存储每种面额的零钱使用数量。
    • 遍历可用零钱数组,对于每个面额的零钱:
      • 将需要找零的金额除以当前面额,得到使用该面额的零钱数量。
      • 更新结果数组和需要找零的金额。
    • 完成后,结果数组即为每种面额的零钱使用数量。
  2. 示例代码:

#include 

// 使用贪心算法解决零钱找零问题
void makeChange(int coins[], int n, int amount) {
    int result[n];

    for (int i = 0; i < n; i++) {
        result[i] = 0;

        while (amount >= coins[i]) {
            amount -= coins[i];
            result[i]++;
        }
    }

    // 打印结果
    printf("Coin\tCount\n");
    for (int i = 0; i < n; i++) {
        printf("%d\t%d\n", coins[i], result[i]);
    }
}

int main() {
    int coins[] = {25, 10, 5, 1};
    int n = sizeof(coins) / sizeof(coins[0]);

    int amount = 96;

    makeChange(coins, n, amount);

    return 0;
}

在上面的示例代码中,我们定义了一个可用零钱的数组coins,并按面额降序排序。通过调用makeChange函数,可以使用贪心算法解决零钱找零问题,并输出每种面额的零钱使用数量。在示例中,我们假设需要找零的金额为96,你可以根据具体问题自定义输入。请注意,示例中的结果数组即为每种面额的零钱使用数量。

  • 背包问题的一些变体等。
  1. 部分背包问题的步骤:

    • 将物品按照单位重量的价值进行降序排序。
    • 初始化当前背包的总价值为0。
    • 从价值最高的物品开始,将尽可能多的物品放入背包中:
      • 如果当前物品的重量小于等于背包剩余的容量,将该物品完全放入背包,并更新当前背包的总价值和剩余容量。
      • 如果当前物品的重量大于背包剩余的容量,计算该物品能放入的部分并放入背包,并更新当前背包的总价值和剩余容量。
    • 完成后,当前背包的总价值即为最优解。
  2. 示例代码:

#include 
#include 

// 物品结构体
typedef struct {
    int weight;
    int value;
} Item;

// 按照单位重量的价值进行降序排序的比较函数
int compareItems(const void* a, const void* b) {
    Item* itemA = (Item*)a;
    Item* itemB = (Item*)b;
    double valuePerWeightA = (double)itemA->value / itemA->weight;
    double valuePerWeightB = (double)itemB->value / itemB->weight;
    if (valuePerWeightA > valuePerWeightB)
        return -1;
    else if (valuePerWeightA < valuePerWeightB)
        return 1;
    else
        return 0;
}
// 使用贪心算法解决部分背包问题
double fractionalKnapsack(Item items[], int n, int capacity) {
    // 按照单位重量的价值进行降序排序
    qsort(items, n, sizeof(Item), compareItems);

    double totalValue = 0.0;
    int currentCapacity = capacity;

    for (int i = 0; i < n; i++) {
        if (items[i].weight <= currentCapacity) {
            // 物品能完全放入背包
            totalValue += items[i].value;
            currentCapacity -= items[i].weight;
        } else {
            // 物品只能放入部分背包
            double fraction = (double)currentCapacity / items[i].weight;
            totalValue += items[i].value * fraction;
            break;
        }
    }

    return totalValue;
}

int main() {
    Item items[] = {
        {10, 60},
        {20, 100},
        {30, 120}
    };

    int n = sizeof(items) / sizeof(items[0]);
    int capacity = 50;

    double maxValue = fractionalKnapsack(items, n, capacity);

    printf("Max Value: %.2f\n", maxValue);

    return 0;
}

 

在上面的示例代码中,我们定义了一个物品的结构体Item,其中包括物品的重量和价值。通过调用fractionalKnapsack函数,可以使用贪心算法解决部分背包问题,并输出背包能装载的物品的最大价值。在示例中,我们假设背包的容量为50,你可以根据具体问题自定义输入。请注意,示例中的最大价值即为背包能装载的物品的最大价值。

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