学习恋上数据结构与算法的记录,本篇主要内容是贪心与分治
贪心(Greedy)
贪心策略,也称为贪婪策略
每一步都采取当前状态下最优的选择(局部最优解),从而希望推导出全局最优解
贪心的应用:哈夫曼树、最小生成树算法:Prim、Kruskal、最短路径算法:Dijkstra
练习1 –最优装载问题(加勒比海盗)
有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值海盗船的载重量为W,每件古董的重量为𝑤i,海盗们该如何把尽可能多数量的古董装上海盗船?
比如W 为30,𝑤i分别为3、5、4、10、7、14、2、11
●贪心策略:每一次都优先选择重量最小的古董
①选择重量为2 的古董,剩重量28
②选择重量为3 的古董,剩重量25
③选择重量为4 的古董,剩重量21
④选择重量为5 的古董,剩重量16
⑤选择重量为7 的古董,剩重量9
最多能装载5 个古董
public static void main(String[] args) {
int[] weights = {3, 5, 4, 10, 7, 14, 2, 11};
Arrays.sort(weights);
int capacity = 30, weight = 0,count=0;
for (int i = 0; i < weights.length && weight<capacity; i++) {
int newWeight = weight + weights[i];
if(newWeight <= capacity) {//小于装船,装满为止
weight = newWeight;
count++;
System.out.println(weights[i]);
}
}
System.out.println(count+"件");
}
练习2 –零钱兑换
假设有25 分、10 分、5 分、1 分的硬币,现要找给客户41 分的零钱,如何办到硬币个数最少?
贪心策略:每一次都优先选择面值最大的硬币
①选择25 分的硬币,剩16 分
②选择10 分的硬币,剩6 分
③选择5 分的硬币,剩1 分
④选择1 分的硬币
最终的解是共4 枚硬币✓25 分、10 分、5 分、1 分硬币各一枚
static void coinChange(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+"个");
}
实际上本题的最优解是:2 枚20 分、1 枚1 分的硬币,共3 枚硬币
●贪心策略并不一定能得到全局最优解
因为一般没有测试所有可能的解,容易过早做决定,所以没法达到最佳解
贪图眼前局部的利益最大化,看不到长远未来,走一步看一步
●优点:简单、高效、不需要穷举所有可能,通常作为其他算法的辅助算法来使用
●缺点:鼠目寸光,不从整体上考虑其他可能,每次采取局部最优解,不会再回溯,因此很少情况会得到最优解
练习3 –0-1背包
有n 件物品和一个最大承重为W 的背包,每件物品的重量是𝑤i、价值是𝑣i,在保证总重量不超过W 的前提下,将哪几件物品装入背包,可以使得背包的总价值最大?
注意:每个物品只有1 件,也就是每个物品只能选择0 件或者1 件,因此称为0-1背包问题
如果采取贪心策略,有3个方案
①价值主导:优先选择价值最高的物品放进背包
②重量主导:优先选择重量最轻的物品放进背包
③价值密度主导:优先选择价值密度最高的物品放进背包(价值密度= 价值÷重量)
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);
});
}
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<>();
for (int i = 0; i < articles.length && weight < capacity; i++) {
int newWight = weight + articles[i].weight;
if(newWight <= capacity) {
weight = newWight;
value += articles[i].value;
selectedArticles.add(articles[i]);
}
}
System.out.println("【" + title + "】");
System.out.println("总价值:" + value);
for(int i=0; i<selectedArticles.size();i++) {
System.out.println(selectedArticles.get(i));
}
System.out.println("-----------------------------");
}
/**
* 0-1背包实体类
* @author hee
*
*/
public class Article {
public int weight;
public int value;
public 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 + "]";
}
}
分治(Divide And Conquer)
分治,也就是分而治之。它的一般步骤是
①将原问题分解成若干个规模较小的子问题(子问题和原问题的结构一样,只是规模不一样)
②子问题又不断分解成规模更小的子问题,直到不能再分解(直到可以轻易计算出子问题的解)
③利用子问题的解推导出原问题的解
因此,分治策略非常适合用递归
需要注意的是:子问题之间是相互独立的
分治的应用:快速排序、归并排序、Karatsuba算法(大数乘法)
练习1 –最大连续子序列和
leetcode_53_最大子序和:https://leetcode-cn.com/problems/maximum-subarray/
给定一个长度为n 的整数序列,求它的最大连续子序列和
比如–2、1、–3、4、–1、2、1、–5、4 的最大连续子序列和是4 + (–1) + 2 + 1 = 6
这道题也属于最大切片问题(最大区段,Greatest Slice)
●概念区分:子串、子数组、子区间必须是连续的,子序列是可以不连续的
解法1 –暴力出奇迹
穷举出所有可能的连续子序列,并计算出它们的和,最后取它们中的最大值
static int maxSubarray1(int[] nums) {
if(nums == null || nums.length ==0) return 0;
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]的和
for(int i=begin;i<=end;i++) {
sum +=nums[i];
}
max=Math.max(max, sum);
}
}
return max;
}
解法1 –暴力出奇迹–优化
重复利用前面计算过的结果
static int maxSubarray2(int[] nums) {
if(nums == null || nums.length ==0) return 0;
int max = Integer.MIN_VALUE;
for(int begin = 0;begin<nums.length;begin++) {
int sum = 0;// sum是[begin, end]的和
for (int end = begin; end < nums.length; end++) {
sum +=nums[end];
max=Math.max(max, sum);
}
}
return max;
}
解法2 –分治
将序列均匀地分割成2 个子序列
[begin , end) = [begin , mid) + [mid , end),mid= (begin+ end) >> 1
假设[begin , end) 的最大连续子序列和是S[i , j),那么它有3 种可能
●[i , j) 存在于[begin , mid) 中,同时S[i , j)也是[begin , mid) 的最大连续子序列和
●[i , j) 存在于[mid , end) 中,同时S[i , j)也是[mid , end) 的最大连续子序列和
●[i , j) 一部分存在于[begin , mid) 中,另一部分存在于[mid , end) 中
✓[i , j) = [i , mid) + [mid, j)
✓S[i , mid) = max { S[k , mid) },begin≤ k <mid
✓S[mid, j) = max { S[mid, k) },mid<k ≤ end
static int maxSubArray(int[] nums, int begin, int end) {
/*
if (end - begin < 2) return nums[begin];
int leftmax = Integer.MIN_VALUE;
int mid = (begin + end) >> 1;
int leftSum = 0;
for (int i = mid - 1; i >= begin; i--) {
leftSum += nums[i];
leftmax = Math.max(leftmax, leftSum);
}
int rightmax = Integer.MIN_VALUE;
int rightSum = 0;
for (int i = mid; i < end; i++) {
rightSum += nums[i];
rightmax = Math.max(rightmax, rightSum);
}
*/
if (end - begin < 2) return nums[begin];
int mid = (begin + end) >>1;
int leftmax = nums[mid -1];
int leftsum = leftmax;
for(int i = mid-2;i>=begin;i--) {
leftsum +=nums[i];
leftmax = Math.max(leftmax, leftsum);
}
int rightmax = nums[mid];
int rightsum = rightmax;
for (int i = mid +1 ; i < end; i++) {
rightsum +=nums[i];
rightmax = Math.max(rightmax, rightsum);
}
return Math.max(leftmax+rightmax, Math.max(maxSubArray(nums,begin,mid), maxSubArray(nums,mid,end)));
}
练习2 –大数乘法
2个超大的数(比如2个100位的数),如何进行乘法?
按照小学时学习的乘法运算,在进行n位数之间的相乘时,需要大约进行n2次个位数的相乘
比如计算36 x 54
1960 年Anatolii Alexeevitch Karatsuba 提出了Karatsuba 算法,提高了大数乘法的效率