贪心
贪心策略, 也叫作贪婪策略
每一步都采取当前状态下最优解, 从而推导出全局最优解
应用, 哈夫曼树, 最小生成树, 最短路径
例, 最优装载问题 加勒比海盗
海盗截获一搜装满各种各样古董的船, 船的载重为W, 每件古董重量为wi, 海盗们该如何把尽可能多数量的古董装上船
W 为30, wi 分别为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 + "件古董");
}
例 零钱兑换
假设有25 分, 10 分, 5 分, 1 分硬币, 找给客户41 分的零钱, 如何办到硬币个数最少?
贪心策略, 每一次都优先选在面值最大的硬币
- 选在25 分硬币, 剩余16 分
- 选在10 分硬币, 剩余6 分
- 选在5 分硬币, 剩余1 分
- 选在1 分硬币, 剩余0 分
- 最终选择了4 枚硬币, 25, 10, 5, 1
// 解法一
static void coinChange1() {
int[] faces = {25, 5, 10, 1};
Arrays.sort(faces);
int money = 41, 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);
}
// 解法二
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++;
}
System.out.println(coins);
}
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, 20, 5, 1 硬币, 找41 的零钱, 最优解为两枚20, 1 枚1 分, 共3 枚
如果贪心, 则为, 一枚25, 3 枚5, 一枚1, 共5 枚
例, 0-1 背包问题
假设背包最大承重150, 7 个物品如下表所示
编号 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
重量 | 35 | 30 | 60 | 50 | 40 | 10 | 25 |
价值 | 10 | 40 | 30 | 50 | 35 | 40 | 30 |
价值密度 | 0.29 | 1.33 | 0.5 | 1.0 | 0.88 | 4.0 | 1.2 |
- 价值主导, 放入背包的物品编号, 4, 2, 6, 5, 总重量130, 总价值165
- 重量主导, 放入背包的物品编号, 6, 7, 2, 1, 5, 总重量140, 总价值155
- 价值主导, 放入背包的物品编号, 6, 2, 7, 4, 1, 总重量150, 总价值170
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);
});
}
static void select(String title, Comparator 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 selectedArticles = new LinkedList<>();
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("[" + title + "]");
System.out.println("总价值" + value);
for (int i = 0; i < selectedArticles.size(); i++) {
System.out.println(selectedArticles.get(i));
}
System.out.println("--------------------------------");
}
}
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 + "]";
}
}
分治
分治步骤
- 将原问题分解成若干规模较小的问题, 子问题结构和原问题结构一样, 知识规模不同
- 子问题不断分解成规模更小的子问题, 直到不能分解, 可以轻易计算出子问题解
- 利用子问题的解推导出原问题解
分治适合使用递归, 快速排序, 归并排序都用到
分治策略通常遵守一种通用模式
解决规模为n 的问题, 分解成a 个规模为n/b 的子问题, 然后再O(n^d) 时间内将子问题的解合并起来
算法运行时间为T(n) = aT(n/b) + O(n^d), a > 0, b > 1, d >= 0
- d > logb(a), T(n) = O(n^d)
- d = logb(a), T(n) = O((n^d(logn))
- d < logb(a), T(n) = O(n^(logb(a)))
例如, 归并排序运行时间: T(n) = 2T(n/2) + O(n), a = 2, b = 2, d = 1, 所有T(n) = O(nlogn)
例, 最大连续子序列和
规定一个长度为n 的整数序列, 求他的最大连续子序列和
-2, 1, -3, 4, -1, 2, 1, -5, 4
暴力法
穷举出所有可能, 并计算出他们的和, 取最大值
/**
* 最大连续子序列和
* @param nums
* @return
*/
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++) {
// sum 是[begin, end] 的和
int sum = 0;
for (int i = begin; i <= end; i++) {
sum += nums[i];
}
max = Math.max(max, sum);
}
}
return max;
}
// 优化
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;
for (int end = begin; end < nums.length; end++) {
sum += nums[end];
max = Math.max(max, sum);
}
}
return max;
}
空间复杂度O(1), 时间复杂度O(n^3), 优化后为O(n^2)
分治法
-
将序列均匀分为2 个子序列
- [begin, end) = [begin, mid) + [mid, end), mid = (begin + end) >> 1
-
假设[begin, end) 的最大连续子序列和是S[i, j), 那么有三种可能
[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
/**
* 分治
* @param nums
* @return
*/
static int maxSubarray(Integer[] nums) {
if (nums == null || nums.length == 0) return 0;
Integers.println(nums);
return maxSubarray(nums, 0, nums.length);
}
static int maxSubarray(Integer[] nums, int begin, int end) {
System.out.println();
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);
System.out.print("_letf_: "+ nums[i]);
}
System.out.println();
int rightMax = nums[mid];
int rightSum = rightMax;
for (int i = mid + 1; i < end; i++) {
rightSum += nums[i];
rightMax = Math.max(rightMax, rightSum);
System.out.print("_right_: "+ nums[i]);
}
int maxM = leftMax + rightMax;
int maxL = maxSubarray(nums, begin, mid);
int maxR = maxSubarray(nums, mid, end);
int maxLOR = Math.max(maxL, maxR);
int max = Math.max(maxM, maxLOR);
return max;
}
空间复杂度O(logn)
时间复杂度T(n) = 2T(n/2) + O(n), 所以O(nlogn)