算法--贪心与分治(十八)

学习恋上数据结构与算法的记录,本篇主要内容是贪心与分治

贪心(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 算法,提高了大数乘法的效率
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值