数据结构和算法

DSA 主页 DSA 概述 DSA 环境设置 DSA 算法基础 DSA 渐近分析

数据结构

DSA 数据结构基础 DSA 数据结构和类型 DSA 数组数据结构

链接列表

DSA 链接列表数据结构 DSA 双向链接列表数据结构 DSA 循环链表数据结构

堆栈 &队列

DSA 堆栈数据结构 DSA 表达式解析 DSA 队列数据结构

搜索算法

DSA 搜索算法 DSA 线性搜索算法 DSA 二分搜索算法 DSA 插值搜索 DSA 跳跃搜索算法 DSA 指数搜索 DSA 斐波那契搜索 DSA 子列表搜索 DSA 哈希表

排序算法

DSA 排序算法 DSA 冒泡排序算法 DSA 插入排序算法 DSA 选择排序算法 DSA 归并排序算法 DSA 希尔排序算法 DSA 堆排序 DSA 桶排序算法 DSA 计数排序算法 DSA 基数排序算法 DSA 快速排序算法

图形数据结构

DSA 图形数据结构 DSA 深度优先遍历 DSA 广度优先遍历 DSA 生成树

树数据结构

DSA 树数据结构 DSA 树遍历 DSA 二叉搜索树 DSA AVL 树 DSA 红黑树 DSA B树 DSA B+ 树 DSA 伸展树 DSA 尝试 DSA 堆数据结构

递归

DSA 递归算法 DSA 使用递归的汉诺塔 DSA 使用递归的斐波那契数列

分而治之

DSA 分而治之 DSA 最大最小问题 DSA 施特拉森矩阵乘法 DSA Karatsuba 算法

贪婪算法

DSA 贪婪算法 DSA 旅行商问题(贪婪方法) DSA Prim 最小生成树 DSA Kruskal 最小生成树 DSA Dijkstra 最短路径算法 DSA 地图着色算法 DSA 分数背包问题 DSA 作业排序截止日期 DSA 最佳合并模式算法

动态规划

DSA 动态规划 DSA 矩阵链乘法 DSA Floyd Warshall 算法 DSA 0-1 背包问题 DSA 最长公共子序列算法 DSA 旅行商问题(动态方法)

近似算法

DSA 近似算法 DSA 顶点覆盖算法 DSA 集合覆盖问题 DSA 旅行商问题(近似方法)

随机算法

DSA 随机算法 DSA 随机快速排序算法 DSA Karger 最小割算法 DSA Fisher-Yates 洗牌算法

DSA 有用资源

DSA 问答 DSA 快速指南


集合覆盖问题


集合覆盖算法为许多现实世界的资源分配问题提供了解决方案。例如,假设一家航空公司为每架飞机分配机组人员,以确保有足够的人员满足旅程的需求。他们会考虑航班时间、飞行时长、中途停留次数以及机组人员的空闲时间,从而将他们分配到各个航班。这时,集合覆盖算法就派上用场了。

给定一个通用集合 U,其中包含少量元素,这些元素都被划分为子集。假设这些子集的集合为 S = {S1, S2, S3, S4... Sn},集合覆盖算法会找到最小数量的子集,使得它们覆盖全集中存在的所有元素。

universal_set

如上图所示,点表示全集 U 中存在的元素,这些元素被划分到不同的集合中,S = {S1, S2, S3, S4, S5, S6}。需要选择的最小集合数来覆盖所有元素,即最优输出 = {S1, S2, S3}。

集合覆盖算法

集合覆盖算法以集合集合作为输入,返回包含所有通用元素所需的最小集合数。

集合覆盖算法是一个 NP-Hard 问题,也是一个 2 近似贪婪算法。

算法

步骤 1 − 初始化 Output = {},其中 Output 表示元素的输出集合。

步骤 2 −如果输出集未包含全集的所有元素,请执行以下操作 −

  • 使用公式 $\frac{Cost\left ( S_{i} ight )}{S_{i}-Output}$ 计算全集中每个子集的成本效益

  • 在每次迭代中,找到成本效益最低的子集。将该子集添加到输出集。

步骤 3 − 重复步骤 2,直到全集中没有剩余元素。获得的输出即为最终的输出集。

伪代码

APPROX-GREEDY-SET_COVER(X, S)
   U = X
   OUTPUT = ф
   while U ≠ ф
      select Si Є S which has maximum |Si∩U|
   U = U – S
   OUTPUT = OUTPUT∪ {Si}
return OUTPUT

分析

假设元素总数等于集合总数 (|X| = |S|),则代码运行时间为 O(|X|3)

示例

Set_Cover_Algorithm

让我们看一个示例,更详细地描述集合覆盖问题的近似算法

S1 = {1, 2, 3, 4}                cost(S1) = 5
S2 = {2, 4, 5, 8, 10}            cost(S2) = 10
S3 = {1, 3, 5, 7, 9, 11, 13}     cost(S3) = 20
S4 = {4, 8, 12, 16, 20}          cost(S4) = 12
S5 = {5, 6, 7, 8, 9}             cost(S5) = 15

步骤 1

输出集,Output = ф

当输出集中没有元素时,计算每个集合的成本效益

S1 = cost(S1) / (S1 – Output) = 5 / (4 – 0)
S2 = cost(S2) / (S2 – Output) = 10 / (5 – 0)
S3 = cost(S3) / (S3 – Output) = 20 / (7 – 0)
S4 = cost(S4) / (S4 – Output) = 12 / (5 – 0)
S5 = cost(S5) / (S5 – Output) = 15 / (5 – 0)

本次迭代的最小成本效益在 S1 处实现,因此,添加到输出集的子集 Output = {S1},其元素为 {1, 2, 3, 4}。

步骤 2

计算输出集中新元素对每个集合的成本效益。

S2 = cost(S2) / (S2 – Output) = 10 / (5 – 4)
S3 = cost(S3) / (S3 – Output) = 20 / (7 – 4)
S4 = cost(S4) / (S4 – Output) = 12 / (5 – 4)
S5 = cost(S5) / (S5 – Output) = 15 / (5 – 4)

本次迭代的最小成本效益在 S3 处实现,因此,将子集添加到输出集,输出 = {S1, S3},其元素为 {1, 2, 3, 4, 5, 7, 9, 11, 13}。

步骤 3

计算输出集中新元素对每个集合的成本效益,

S2 = cost(S2) / (S2 – 输出) = 10 / |(5 – 9)|
S4 = cost(S4) / (S4 – 输出) = 12 / |(5 – 9)|
S5 = cost(S5) / (S5 – Output) = 15 / |(5 – 9)|

本次迭代的最小成本效益在 S2 处实现,因此,添加到输出集的子集 Output = {S1, S3, S2},其元素为 {1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 13}。

步骤 4

计算输出集中新元素对每个集合的成本效益。

S4 = cost(S4) / (S4 – Output) = 12 / |(5 – 11)|
S5 = cost(S5) / (S5 – Output) = 15 / |(5 – 11)|

本次迭代的最小成本效益在 S4 处实现,因此,添加到输出集的子集输出 = {S1, S3, S2, S4},其元素为 {1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 16, 20

步骤 5

计算输出集中新元素对每个集合的成本效益

S5 = cost(S5) / (S5 – Output) = 15 / |(5 – 14)|

本次迭代的最小成本效益在 S5 处实现,因此,添加到输出集的子集 Output = {S1, S3, S2, S4, S5},其元素为 {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 16, 20

覆盖通用有限集中所有元素的最终输出为 Output = {S1, S3, S2, S4, S5}。

实现

以下是实现上述方法在各种编程语言中的应用 −

#include <stdio.h>
#define MAX_SETS 100
#define MAX_ELEMENTS 1000
int setCover(int X[], int S[][MAX_ELEMENTS], int numSets, int numElements, int output[]) {
   int U[MAX_ELEMENTS];
   for (int i = 0; i < numElements; i++) {
      U[i] = X[i];
   }
   int selectedSets[MAX_SETS];
   for (int i = 0; i < MAX_SETS; i++) {
      selectedSets[i] = 0; // 全部初始化为0(未选择)
   }
   int outputIdx = 0;
   while (outputIdx < numSets) {  // 确保不超过最大组数
      int maxIntersectionSize = 0;
      int selectedSetIdx = -1;
      // 找到与 U 有最大交集的集合 Si
      for (int i = 0; i < numSets; i++) {
         if (selectedSets[i] == 0) { // 检查集合是否尚未被选择
            int intersectionSize = 0;
            for (int j = 0; j < numElements; j++) {
               if (U[j] && S[i][j]) {
                  intersectionSize++;
               }
            }
            if (intersectionSize > maxIntersectionSize) {
               maxIntersectionSize = intersectionSize;
               selectedSetIdx = i;
            }
         }
      }
      // 如果没有找到集合,则中断循环
      if (selectedSetIdx == -1) {
          break;
      }
      // 在数组中将选定的集合标记为“选定”
      selectedSets[selectedSetIdx] = 1;
      // 从 U 中删除选定集合所覆盖的元素
      for (int j = 0; j < numElements; j++) {
          U[j] = U[j] - S[selectedSetIdx][j];
      }
      // 将选定的集合添加到输出
      output[outputIdx++] = selectedSetIdx;
   }
   return outputIdx;
}
int main() {
   int X[MAX_ELEMENTS] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
   int S[MAX_SETS][MAX_ELEMENTS] = {
      {1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 1, 1, 1, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 1, 1, 1, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 1, 1, 1, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 1, 1, 1}
   };
   int numSets = 5;
   int numElements = 10;
   int output[MAX_SETS];
   int numSelectedSets = setCover(X, S, numSets, numElements, output);
   printf("Selected Sets: ");
   for (int i = 0; i < numSelectedSets; i++) {
      printf("%d ", output[i]);
   }
   printf("
");
   return 0;
}

输出

Selected Sets: 1 2 3 4 0
#include <iostream>
#include <vector>
using namespace std;
#define MAX_SETS 100
#define MAX_ELEMENTS 1000
// 使用近似贪婪集合覆盖算法查找集合覆盖的函数
int setCover(int X[], int S[][MAX_ELEMENTS], int numSets, int numElements, int output[])
{
   int U[MAX_ELEMENTS];
   for (int i = 0; i < numElements; i++) {
      U[i] = X[i];
   }
   int selectedSets[MAX_SETS];
   for (int i = 0; i < MAX_SETS; i++) {
      selectedSets[i] = 0; // 全部初始化为0(未选择)
   }
   int outputIdx = 0;
   while (outputIdx < numSets) {  // 确保不超过最大组数
      int maxIntersectionSize = 0;
      int selectedSetIdx = -1;
      // 找到与 U 有最大交集的集合 Si
      for (int i = 0; i < numSets; i++) {
         if (selectedSets[i] == 0) { // 检查集合是否尚未被选择
            int intersectionSize = 0;
            for (int j = 0; j < numElements; j++) {
               if (U[j] && S[i][j]) {
                  intersectionSize++;
               }
            }
            if (intersectionSize > maxIntersectionSize) {
               maxIntersectionSize = intersectionSize;
               selectedSetIdx = i;
            }
         }
      }
      // 如果没有找到集合,则中断循环
      if (selectedSetIdx == -1) {
         break;
      }
      // 在数组中将选定的集合标记为“选定”
      selectedSets[selectedSetIdx] = 1;
      // 从 U 中删除选定集合所覆盖的元素
      for (int j = 0; j < numElements; j++) {
         U[j] = U[j] - S[selectedSetIdx][j];
      }
      // 将选定的集合添加到输出
      output[outputIdx++] = selectedSetIdx;
   }
   return outputIdx;
}
int main()
{
   int X[MAX_ELEMENTS] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
   int S[MAX_SETS][MAX_ELEMENTS] = {
      {1, 1, 0, 0, 0, 0, 0, 0, 0, 0},
      {0, 1, 1, 1, 0, 0, 0, 0, 0, 0},
      {0, 0, 0, 1, 1, 1, 0, 0, 0, 0},
      {0, 0, 0, 0, 0, 1, 1, 1, 0, 0},
      {0, 0, 0, 0, 0, 0, 0, 1, 1, 1}
   };
   int numSets = 5;
   int numElements = 10;
   int output[MAX_SETS];
   int numSelectedSets = setCover(X, S, numSets, numElements, output);
   cout << "Selected Sets: ";
   for (int i = 0; i < numSelectedSets; i++) {
       cout << output[i] << " ";
   }
   cout << endl;
   return 0;
}

输出

Selected Sets: 1 2 3 4 0 
import java.util.*;
public class SetCover {
   public static List<Integer> setCover(int[] X, int[][] S) {
      Set<Integer> U = new HashSet<>();
      for (int x : X) {
         U.add(x);
      }
      List<Integer> output = new ArrayList<>();
      while (!U.isEmpty()) {
         int maxIntersectionSize = 0;
         int selectedSetIdx = -1;
         for (int i = 0; i < S.length; i++) {
            int intersectionSize = 0;
            for (int j = 0; j < S[i].length; j++) {
               if (U.contains(S[i][j])) {
                  intersectionSize++;
               }
            }
            if (intersectionSize > maxIntersectionSize) {
               maxIntersectionSize = intersectionSize;
               selectedSetIdx = i;
            }
         }
         if (selectedSetIdx == -1) {
            break;
         }
         for (int j = 0; j < S[selectedSetIdx].length; j++) {
            U.remove(S[selectedSetIdx][j]);
         }
         output.add(selectedSetIdx);
      }
      return output;
   }
public static void main(String[] args) {
   int[] X = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
   int[][] S = {
      {1, 2},
      {2, 3, 4},
      {4, 5, 6},
      {6, 7, 8},
      {8, 9, 10}
   };
   List<Integer> selectedSets = setCover(X, S);
   System.out.print("Selected Sets: ");
   for (int idx : selectedSets) {
      System.out.print(idx + " ");
   }
   System.out.println();
   }
}

输出

Selected Sets: 1 3 4 0 2 
def set_cover(X, S):
    U = set(X)
    output = []
    while U:
        max_intersection_size = 0
        selected_set_idx = -1
        for i, s in enumerate(S):
            intersection_size = len(U.intersection(s))
            if intersection_size > max_intersection_size:
                max_intersection_size = intersection_size
                selected_set_idx = i
        if selected_set_idx == -1:
            break
        U = U - set(S[selected_set_idx])
        output.append(selected_set_idx)
    return output
if __name__ == "__main__":
    X = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    S = [
        {1, 2},
        {2, 3, 4},
        {4, 5, 6},
        {6, 7, 8},
        {8, 9, 10}
    ]
    selected_sets = set_cover(X, S)
    print("Selected Sets:", selected_sets)

输出

Selected Sets: 1 3 4 0 2