【戀上數據結構】貪心(最優裝載、零錢兌換、0-1揹包)、分治(最大連續子序列和、大數乘法)

貪心(Greedy)

貪心策略,也稱爲貪婪策略

  • 每一步都採取當前狀態下最優的選擇(局部最優解),從而希望推導出全局最優解

貪心的應用

  • 哈夫曼樹
  • 最小生成樹算法:Prim、Kruskal
  • 最短路徑算法:Dijkstra

問題1:最優裝載(加勒比海盜)

在這裏插入圖片描述
貪心策略:每一次都優先選擇重量最小的古董

  • ① 選擇重量爲 2 的古董,剩重量 28
    ② 選擇重量爲 3 的古董,剩重量 25
    ③ 選擇重量爲 4 的古董,剩重量 21
    ④ 選擇重量爲 5 的古董,剩重量 16
    ⑤ 選擇重量爲 7 的古董,剩重量 9
  • 根據上面的選擇,最多能裝載 5 個古董
import java.util.Arrays;

/**
 * 有一天,海盜們截獲了一艘裝滿各種各樣古董的貨船,每一件古董都價值連城,一旦打碎就失去了它的價值
 * 海盜船的載重量爲 W,每件古董的重量爲 𝑤i,海盜們該如何把儘可能多數量的古董裝上海盜船?
 * 比如 W 爲 30,Wi 分別爲 3、5、4、10、7、14、2、11
 */
public class Pirate {
	public static void main(String[] args) {
		int[] weights = {3, 5, 4, 10, 7, 14, 2, 11};
		Arrays.sort(weights); // 排序, 默認從小到大

		int capacity = 30; // 最大容量
		int weight = 0, count = 0;
		// 每次優先選擇重量最小的古董
		for (int i = 0; i < weights.length && weight < capacity; i++) {
			int newWeight = weights[i] + weight;
			if (newWeight <= capacity) {
				weight = newWeight;
				count++;
				// System.out.println(weights[i]);
			}
		}
		// System.out.println("裝了 " + count + " 件古董。");
	}
	
}
2
3
4
5
7
裝了 5 件古董。

問題2:零錢兌換

假設有 25 分、10 分、5 分、1 分的硬幣,現要找給客戶 41 分的零錢,如何辦到硬幣個數最少

貪心策略:每一次都優先選擇面值最大的硬幣

  • ① 選擇 25 分的硬幣,剩 16 分
    ② 選擇 10 分的硬幣,剩 6 分
    ③ 選擇 5 分的硬幣,剩 1 分
    ④ 選擇 1 分的硬幣
  • 最終的解是共 4 枚硬幣
    25 分、10 分、5 分、1 分硬幣各一枚
import java.util.Arrays;
/**
 * 假設有 25 分、10 分、5 分、1 分的硬幣,
 * 現要找給客戶 41 分的零錢,如何辦到硬幣個數最少?
 */
public class CoinChange {
	public static void main(String[] args) {
		// 三種寫法, 結果是一樣的
		coinChange1(new Integer[] {25, 10, 5, 1}, 41);
		coinChange2(new Integer[] {25, 10, 5, 1}, 41);
		coinChange3(new Integer[] {25, 10, 5, 1}, 41);
	}
	
	// 寫法1
	static void coinChange1(Integer[] faces, int money) {
		Arrays.sort(faces); // 排序, 默認從小到大
		
		int coins = 0;
		// 貪心策略, 選擇面值最大的硬幣, 由於順序小->大, 從後往前放
		for (int i = faces.length - 1; i >= 0; i--) {
			// 如果面值比我要的錢大, 進行下一輪
			if (money < faces[i]) continue;
			// System.out.println(faces[i]);
			money -= faces[i];
			coins++;
			i = faces.length;
		}
		// System.out.println("使用了" + coins + "個硬幣。");
	}
	
	// 寫法2
	static void coinChange2(Integer[] faces, int money) {
		// 排序, 傳入了比較器, 所以是從大到小排序
		Arrays.sort(faces, (Integer f1, Integer f2) -> f2 - f1);
		
		// 貪心策略, 選擇面值最大的硬幣, 由於順序大->小, 從前往後放
		int coins = 0, i = 0;
		while (i < faces.length) {
			if (money < faces[i]) {
				i++;
				continue;
			}
			// System.out.println(faces[i]);
			money -= faces[i];
			coins++;
			// i = 0; // 這步是不需要的
		}
		// System.out.println("使用了" + coins + "個硬幣。");
	}

	// 寫法3
		static void coinChange3(Integer[] faces, int money) {
		Arrays.sort(faces);
		// 貪心策略, 選擇面值最大的硬幣, 由於順序小->大, 從後往前放
		int coins = 0, idx = faces.length - 1;
		while (idx >= 0) {
			while (money >= faces[idx]){
				// System.out.println(faces[idx]);
				money -= faces[idx];
				coins++;
			}
			idx--;
		}
		// System.out.println("使用了" + coins + "個硬幣。");
	}
	
}
25
10
5
1
使用了4個硬幣。

零錢兌換的另一個例子

在這裏插入圖片描述
將之前的代碼的輸入修改一下,可以得出結果:發現確實沒有得到最優解

public static void main(String[] args) {
	coinChange1(new Integer[] {25, 10, 5, 1}, 41);
	coinChange2(new Integer[] {25, 10, 5, 1}, 41);
	coinChange3(new Integer[] {25, 20, 5, 1}, 41);
}
25
5
5
5
1
使用了5個硬幣。

貪心注意點

貪心策略並不一定能得到全局最優解

  • 因爲一般沒有測試所有可能的解,容易過早做決定,所以沒法達到最佳解
  • 貪圖眼前局部的利益最大化,看不到長遠未來,走一步看一步

優點:簡單、高效、不需要窮舉所有可能,通常作爲其他算法的輔助算法來使用

缺點:鼠目寸光,不從整體上考慮其他可能每次採取局部最優解,不會再回溯,因此很少情況會得到最優解

問題3:0-1揹包

在這裏插入圖片描述

0-1 揹包 - 實例

在這裏插入圖片描述
Article類 模擬物品

package com.mj.ks;

public class Article {
	int weight; // 重量
	int value;  // 價值
	double valueDensity; // 價值密度
	
	public Article(int weight, int value) {
		this.weight = weight;
		this.value = value;
		valueDensity = value * 1.0 / weight;
	}
	@Override
	public String toString() {
		return "Article [weight=" + weight + ", value=" + value + ", ValueDensity=" + valueDensity + "]";
	}

}

0-1 揹包問題:分別按照價值主導重量主導價值密度主導解決。

package com.mj.ks;

import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
/**
 * 0-1 揹包問題
 * @author yusael
 */
public class Knapsack {
	public static void main(String[] args) {
		select("價值主導", (Article a1, Article a2) -> {
			// 價值大的優先
			return a2.value - a1.value;
		});
		select("重量主導", (Article a1, Article a2) -> {
			// 重量小的優先
			return a1.weight - a2.weight; 
		});
		select("價值密度主導", (Article a1, Article a2) -> {
			// 價值密度大的優先
			return Double.compare(a2.valueDensity, a1.valueDensity);
		});
	}
	
	/**
	 * 以一個屬性爲主導實現貪心策略
	 * @param title 顯示標題
	 * @param cmp 比較器決定主導屬性, [價值、重量、價值密度]
	 */
	static void select(String title, Comparator<Article> cmp) {
		// 模擬題意的物品
		Article[] articles = new Article[] {
			new Article(35, 10), new Article(30, 40),
			new Article(60, 30), new Article(50, 50),
			new Article(40, 35), new Article(10, 40),
			new Article(25, 30)
		};
		
		// 通過比較器, 按某個主導屬性進行排序
		Arrays.sort(articles, cmp);
		
		// 以某個屬性爲主導, 實現貪心策略
		int capacity = 150, weight = 0, value = 0;
		List<Article> selectedArticles = new LinkedList<Article>(); // 選擇的物品集合
		for (int i = 0; i < articles.length && weight < capacity; i++) {
			int newWeight = weight + articles[i].weight;
			if (newWeight <= capacity) {
				weight = newWeight;
				value += articles[i].value;
				selectedArticles.add(articles[i]);
			}
		}
		System.out.println("-----------------------------");
		System.out.println("【" + title + "】");
		System.out.println("總價值: " + value);
		for (Article article : selectedArticles) {
			System.out.println(article);
		}
	}
}
-----------------------------
【價值主導】
總價值: 165
Article [weight=50, value=50, ValueDensity=1.0]
Article [weight=30, value=40, ValueDensity=1.3333333333333333]
Article [weight=10, value=40, ValueDensity=4.0]
Article [weight=40, value=35, ValueDensity=0.875]
-----------------------------
【重量主導】
總價值: 155
Article [weight=10, value=40, ValueDensity=4.0]
Article [weight=25, value=30, ValueDensity=1.2]
Article [weight=30, value=40, ValueDensity=1.3333333333333333]
Article [weight=35, value=10, ValueDensity=0.2857142857142857]
Article [weight=40, value=35, ValueDensity=0.875]
-----------------------------
【價值密度主導】
總價值: 170
Article [weight=10, value=40, ValueDensity=4.0]
Article [weight=30, value=40, ValueDensity=1.3333333333333333]
Article [weight=25, value=30, ValueDensity=1.2]
Article [weight=50, value=50, ValueDensity=1.0]
Article [weight=35, value=10, ValueDensity=0.2857142857142857]

一些習題

分發餅乾
用最少數量的箭引爆氣球
買賣股票的最佳時機 II
種花問題
分發糖果

分治(Divide And Conquer)

分治,也就是分而治之。它的一般步驟是:

  • ① 將原問題分解成若干個規模較小的子問題(子問題和原問題的結構一樣,只是規模不一樣)
    ② 子問題又不斷分解成規模更小的子問題,直到不能再分解(直到可以輕易計算出子問題的解)
    ③ 利用子問題的解推導出原問題的解
    在這裏插入圖片描述

因此,分治策略非常適合用遞歸

需要注意的是:子問題之間是相互獨立的

分治的應用:

  • 快速排序
  • 歸併排序
  • Karatsuba 算法(大數乘法)

主定理(Master Theorem)

在這裏插入圖片描述

問題1:最大連續子序列和

題目:leetcode_53_最大子序和

給定一個長度爲 n 的整數序列,求它的最大連續子序列和

  • 比如 –2、1、–3、4–121、–5、4 的最大連續子序列和是 4 + (–1) + 2 + 1 = 6

這道題也屬於最大切片問題(最大區段,Greatest Slice)

概念區分

  • 子串子數組子區間:必須是連續的
  • 子序列:可以不連續

解法1 – 暴力出奇跡

窮舉出所有可能的連續子序列,分別計算出它們的和,最後取它們中的最大值。

時間複雜度:O(n3),空間複雜度:O(1)

/**
 * 暴力
 */
static int maxSubArray(int [] nums) {
	if (nums == null || nums.length == 0) return 0;
	// 這裏注意, 容易寫成 int max = 0, 可能會出錯, max 默認值必須是最小的值
	int max = Integer.MIN_VALUE; 
	// 窮舉, 列出所有可能的連續子序列, 分別計算它們的和, 最後取出最大值
	for (int begin = 0; begin < nums.length; begin++) {
		for (int end = begin; end < nums.length; end++) {
			int sum = 0; // sum是[begin, end]的和
			// nums[begin] 到 nums[end] 求和
			for (int i = begin; i <= end; i++) {
				sum += nums[i];
			}
			max = Math.max(max, sum); // 取最大值
		}
	}
	return max;
}

這個結果應該不意外吧…
在這裏插入圖片描述

暴力出奇跡 – 優化

重複利用前面計算過的結果

時間複雜度:O(n2),空間複雜度:O(1)

/**
 * 暴力 - 優化
 */
static int maxSubArray(int [] nums) {
	if (nums == null || nums.length == 0) return 0;
	// 這裏注意, 容易寫成 int max = 0, 可能會出錯, max 默認值必須是最小的值
	int max = Integer.MIN_VALUE;
	// 窮舉, 列出所有可能的連續子序列, 分別計算它們的和, 最後取出最大值
	for (int begin = 0; begin < nums.length; begin++) {
		// 重複利用sum, 只有當begin修改纔會重置
		int sum = 0;
		// begin不動, end修改的話, 子序列的和是疊加的, 無需每次都重新計算
		for (int end = begin; end < nums.length; end++) {
			sum += nums[end]; // sum是[begin, end]的和
			max = Math.max(max, sum); // 取最大值
		}
	}
	return max;
}

至少不超時了。。。
在這裏插入圖片描述

解法2 – 分治

在這裏插入圖片描述

空間複雜度:O(logn)
時間複雜度:O(nlogn),跟歸併排序、快速排序一樣,利用主定理計算:T(n) = 2T(n/2) + O(n)

/**
 * 分治
 */
static int maxSubArray(int [] nums) {
	if (nums == null || nums.length == 0) return 0;
	return maxSubArray(nums, 0, nums.length);
}
static int maxSubArray(int[] nums, int begin, int end) {
	// 遞歸基: end - begin < 2, 說明只有一個元素, nums[begin] == nums[end]
	if (end - begin < 2) return nums[begin];
	
	int mid = (begin + end) >> 1;

	// 最長子序列是 [i, mid) + [mid, j) 的情況
	int leftMax = Integer.MIN_VALUE;
	int leftSum = 0;
	for (int i = mid - 1; i >= begin; i--) { // [i,mid)
		leftSum += nums[i];
		leftMax = Math.max(leftSum, leftMax);
	}
	int rightMax = Integer.MIN_VALUE;
	int rightSum = 0;
	for (int i = mid; i < end; i++) { // [mid, end)
		rightSum += nums[i];
		rightMax = Math.max(rightSum, rightMax);
	}
	
	// 最長子序列在 left部分, right部分的情況
	return Math.max(leftMax + rightMax,
		Math.max(
			maxSubArray(nums, begin, mid), 	// 最長子串在[begin, mid)的情況 
			maxSubArray(nums, mid, end) 	// 最長子串在[mid, end)的情況
		));
}

可以看到這種解法特別快。。。
在這裏插入圖片描述
挖個坑,這題其實可以用動態規劃解決。

問題2:大數乘法

在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章