1. 程式人生 > 其它 >貪心演算法(集合覆蓋問題)

貪心演算法(集合覆蓋問題)

技術標籤:演算法與資料結構貪心演算法集合覆蓋問題java演算法

一、貪心演算法概述

貪心演算法的核心思想可以總結為:貪心演算法總是做出在當前看來最好的選擇。

也就是說貪心演算法並不從整體最優考慮,它所做出的選擇只是在某種意義上的區域性最優選擇。當然,希望貪心演算法得到的最終結果也是整體最優的。

雖然貪心演算法不能對所有問題都得到整體最優解,但對許多問題它能產生整體最優解,如單源最短路經問題,最小生成樹問題等。雖然在一些情況下,即使貪心演算法不能得到整體最優解,但其最終結果卻是最優解的很好近似。

二、集合覆蓋問題

2.1 問題描述

假設你辦了個廣播節目,要讓國內的 8 個重要城市的聽眾都收聽得到。為此,你需要決定在哪些廣播臺播出。在每個廣播臺播出都需要支付費用,因此你力圖在儘可能少的廣播臺播出。現有廣播臺名單如下。

廣播臺覆蓋地區
K1北京、上海、天津
K2廣州、北京、深圳
K3成都、上海、杭州
K4上海、天津
K5杭州、大連

如何選擇最少的廣播臺,讓所有的城市都可以接收到訊號?

2.2 集合覆蓋問題的貪心演算法

每個廣播臺都覆蓋特定的區域,不通過的廣播覆蓋的區域可能是重疊的。如何找出覆蓋 8 個城市的最小廣播臺集合呢?

首先我們最容易想到的辦法就是窮舉法:

  1. 列出每個可能的廣播臺的集合,可能的集合有 2ⁿ 個;
  2. 在這個集合中,選出能夠覆蓋 8 個城市的最小集合。

上面這個方法確實可以求得最終結果,但是問題在於 n 個廣播臺可能的集合有 2ⁿ 個,因此執行時間為 O(2ⁿ)。如果廣播臺不多,比如只有 5~10 個,這個方法倒還可行。但是一旦問題的規模變大,廣播臺的數量增多,需要的時間將激增。假設每秒可以計算出 10 個集合,那麼所需的時間如下表所示:

廣播臺數量需要的時間
53.2 秒
10102.4 秒
3213.6 年
1004 x 10²¹ 年

顯然,使用窮舉所有集合的方法不是一個明智的選擇。

針對這類集合覆蓋問題,貪心演算法是一個比較合適的演算法,雖然貪心演算法不一定能夠得到最優解,但是它可以得到非常接近的解。

對於本題,貪心演算法的基本思想如下:

  1. 優先選擇出覆蓋了最多未被覆蓋的城市的廣播臺。即使這個廣播臺覆蓋了一些已覆蓋的城市也沒關係;
  2. 重複第一步,直到選出的廣播臺覆蓋了所有的城市。

可以看到,使用貪心演算法解決集合覆蓋問題的思路貫徹著 “選擇當下看來最好的選擇” 這樣的思想。簡單來說就是走好每一步路,最終得到的結果必定不會太差。

貪心演算法不僅簡單,而且執行速度也很快。對於本題,貪心演算法的執行時間為 O(n²),其中 n 為廣播臺的數量。

2.3 程式碼實現

首先來說一下程式碼實現的思路:

  1. 建立一個集合 cities,存放所有的城市;
  2. 建立一個散列表 broadcast,廣播站作為鍵,對應的城市作為值;
  3. 建立一個集合 selectedList,用於存放已選擇的廣播站;
  4. 遍歷散列表,找到覆蓋了最多未覆蓋的城市的廣播站,將其加入到已選擇廣播站集合 selectedList中;
  5. 將上一步選擇的廣播站所覆蓋的城市從城市集合 cities 中移除,同時將廣播站從散列表中移除;
  6. 重複執行 4、5 步,直至城市集合為空。

完整的程式碼實現如下 :

public static void main(String[] args) {
    HashMap<String, Set> broadcast = new HashMap<>();   // 用於存放廣播和覆蓋的城市
    HashMap<String, Set> selectedList = new HashMap<>();    // 存放被選擇的廣播
    HashSet<String> cities = new HashSet<>();   // 城市表,存放的是未被廣播覆蓋的城市

    HashSet<String> k1_set = new HashSet<>();   // 廣播 K1
    k1_set.add("北京");
    k1_set.add("上海");
    k1_set.add("天津");
    broadcast.put("K1", k1_set);

    HashSet<String> k2_set = new HashSet<>();   // 廣播 K2
    k2_set.add("廣州");
    k2_set.add("北京");
    k2_set.add("深圳");
    broadcast.put("K2", k2_set);

    HashSet<String> k3_set = new HashSet<>();   // 廣播 K3
    k3_set.add("成都");
    k3_set.add("上海");
    k3_set.add("杭州");
    broadcast.put("K3", k3_set);

    HashSet<String> k4_set = new HashSet<>();   // 廣播 K4
    k4_set.add("上海");
    k4_set.add("天津");
    broadcast.put("K4", k4_set);

    HashSet<String> k5_set = new HashSet<>();   // 廣播 K5
    k5_set.add("杭州");
    k5_set.add("大連");
    broadcast.put("K5", k5_set);

    cities.addAll(k1_set);		// 在未選擇廣播站的狀態下,所有的城市都未被覆蓋
    cities.addAll(k2_set);
    cities.addAll(k3_set);
    cities.addAll(k4_set);
    cities.addAll(k5_set);
    System.out.println(cities);	// 列印城市集合

    while (cities.size() > 0){	// 如果城市表不為空,說明還有城市未被覆蓋
        String maxKey = null;	// 記錄覆蓋了最多的未覆蓋城市的廣播
        for (String item : broadcast.keySet()){
            Set set = broadcast.get(item);  // 獲取當前廣播覆蓋的城市
            set.retainAll(cities);  // 求當前廣播覆蓋的城市和城市列表中的交集,也就是找到廣播中未覆蓋的城市
            int num = set.size();   // 獲取交集的個數
            if (maxKey == null && num > 0){ // 如果 maxKey 為空且 num > 0
                maxKey = item;
            }else if (maxKey != null){  // 如果 maxKey 不為空,那就和當前的廣播比較一下誰和未覆蓋城市列表的交集多
                Set maxSet = broadcast.get(maxKey); // 獲取之前記錄下來的最多未覆蓋城市的廣播的城市集合
                maxSet.retainAll(cities);	// 找到原來的 maxKey 覆蓋了哪些未覆蓋的城市
                int maxNum = maxSet.size();
                if (maxNum < num){
                    maxKey = item;
                }
            }
        }
        // 一輪 for 迴圈之後,將會得到覆蓋城市最多的廣播
        selectedList.put(maxKey, broadcast.get(maxKey));    // 新增廣播到被選中列表
        cities.removeAll(broadcast.get(maxKey));    // 城市已經覆蓋了,移除掉
        broadcast.remove(maxKey);   // 廣播使用過了,移除掉
    }
    // 列印被選中的廣播
    for (String item : selectedList.keySet()){
        System.out.print(item + " ");
    }
}

執行結果如下:

在這裡插入圖片描述