貪心演算法(集合覆蓋問題)
技術標籤:演算法與資料結構貪心演算法集合覆蓋問題java演算法
一、貪心演算法概述
貪心演算法的核心思想可以總結為:貪心演算法總是做出在當前看來最好的選擇。
也就是說貪心演算法並不從整體最優考慮,它所做出的選擇只是在某種意義上的區域性最優選擇。當然,希望貪心演算法得到的最終結果也是整體最優的。
雖然貪心演算法不能對所有問題都得到整體最優解,但對許多問題它能產生整體最優解,如單源最短路經問題,最小生成樹問題等。雖然在一些情況下,即使貪心演算法不能得到整體最優解,但其最終結果卻是最優解的很好近似。
二、集合覆蓋問題
2.1 問題描述
假設你辦了個廣播節目,要讓國內的 8 個重要城市的聽眾都收聽得到。為此,你需要決定在哪些廣播臺播出。在每個廣播臺播出都需要支付費用,因此你力圖在儘可能少的廣播臺播出。現有廣播臺名單如下。
廣播臺 | 覆蓋地區 |
---|---|
K1 | 北京、上海、天津 |
K2 | 廣州、北京、深圳 |
K3 | 成都、上海、杭州 |
K4 | 上海、天津 |
K5 | 杭州、大連 |
如何選擇最少的廣播臺,讓所有的城市都可以接收到訊號?
2.2 集合覆蓋問題的貪心演算法
每個廣播臺都覆蓋特定的區域,不通過的廣播覆蓋的區域可能是重疊的。如何找出覆蓋 8 個城市的最小廣播臺集合呢?
首先我們最容易想到的辦法就是窮舉法:
- 列出每個可能的廣播臺的集合,可能的集合有 2ⁿ 個;
- 在這個集合中,選出能夠覆蓋 8 個城市的最小集合。
上面這個方法確實可以求得最終結果,但是問題在於 n 個廣播臺可能的集合有 2ⁿ 個,因此執行時間為 O(2ⁿ)。如果廣播臺不多,比如只有 5~10 個,這個方法倒還可行。但是一旦問題的規模變大,廣播臺的數量增多,需要的時間將激增。假設每秒可以計算出 10 個集合,那麼所需的時間如下表所示:
廣播臺數量 | 需要的時間 |
---|---|
5 | 3.2 秒 |
10 | 102.4 秒 |
32 | 13.6 年 |
100 | 4 x 10²¹ 年 |
顯然,使用窮舉所有集合的方法不是一個明智的選擇。
針對這類集合覆蓋問題,貪心演算法是一個比較合適的演算法,雖然貪心演算法不一定能夠得到最優解,但是它可以得到非常接近的解。
對於本題,貪心演算法的基本思想如下:
- 優先選擇出覆蓋了最多未被覆蓋的城市的廣播臺。即使這個廣播臺覆蓋了一些已覆蓋的城市也沒關係;
- 重複第一步,直到選出的廣播臺覆蓋了所有的城市。
可以看到,使用貪心演算法解決集合覆蓋問題的思路貫徹著 “選擇當下看來最好的選擇” 這樣的思想。簡單來說就是走好每一步路,最終得到的結果必定不會太差。
貪心演算法不僅簡單,而且執行速度也很快。對於本題,貪心演算法的執行時間為 O(n²),其中 n 為廣播臺的數量。
2.3 程式碼實現
首先來說一下程式碼實現的思路:
- 建立一個集合
cities
,存放所有的城市; - 建立一個散列表
broadcast
,廣播站作為鍵,對應的城市作為值; - 建立一個集合
selectedList
,用於存放已選擇的廣播站; - 遍歷散列表,找到覆蓋了最多未覆蓋的城市的廣播站,將其加入到已選擇廣播站集合
selectedList
中; - 將上一步選擇的廣播站所覆蓋的城市從城市集合
cities
中移除,同時將廣播站從散列表中移除; - 重複執行 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 + " ");
}
}
執行結果如下: