1. 贪心(Greedy)
贪心策略,也称贪婪策略。 每一步都采取当前状态下最优的选择(局部最优解),从而推导出全局最优解。 应用:
- 哈夫曼树;
Prim 、Kruskal ;Dijkstra 。
1.1 加勒比海盗
海盗们打劫了古董船,里面的古董都加载连城,打碎就没有加载。 设船的载重量为W,没件古董的重量为wi,如何尽可能的多装古董? 例如: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;
int count = 0;
for (int i = 0; i < weights.length; i++) {
int newWeight = weight + weights[i];
if (newWeight > capacity){
break;
}else {
count ++;
weight = newWeight;
System.out.println(weights[i]);
}
}
System.out.println(count);
}
}
1.2 零钱兑换
假设有25分、10分、5分、1分的硬币(可以重复使用),现要找给客户41分零钱,如何用最少的硬币数量? 贪心: 每次都选择面值最大的硬币。
public class CoinChange {
public static void main(String[] args) {
Integer[] faces = {25, 10, 5, 1};
Arrays.sort(faces, (Integer f1, Integer f2) -> f2 - f1);
int money = 41;
int coins = 0;
int i = 0;
while (i < faces.length){
if (money < faces[i]){
i++;
continue;
}
money -= faces[i];
coins++;
System.out.println(faces[i]);
}
}
}
1.3零钱兑换Ⅱ
假设有25分、20分、5分、1分的硬币(可以重复使用),现要找给客户41分零钱,如何用最少的硬币数量? 贪心: 每次都选择面值最大的硬币。
使用贪心的解法:25、5、5、5、1;共五枚硬币。 但明显可以看出应该使用:40、40、1;共三枚硬币。
1.4 贪心的注意点
- 特点并不一定能够达到最优解:没有测试所有可能,过早的做决定;贪图眼前的利益最大化。
- 优点:简单、高效、不需要穷举所有可能,一般辅助其它算法。
- 缺点:不从整体考虑,每次都是局部最优,很少可以得到最优解。
1.5 0-1 背包
有n件物品和一个最大承重为W的背包,每件物品重量是wi,价值是vi; 在保证重量不超过W的前提下,如果装使得背包价值最大。 每个物品只有一件,只能选择0或1,因此称为0 - 1问题。
- 价值主导:优先选择价值最高的物品放入背包;
- 重量主导:优先选择重量最轻的物品放入背包;
- 价值密度主导:优先选择性价比最高的物品放入背包(价值密度 = 价值 ÷ 重量)。
假设背包总重量150,物品如下:
- 价值主导: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) {
Article[] values = select((Article a1, Article a2) -> a2.value - a1.value);
Info art = getArt(values, 150);
System.out.println(art);
Article[] weights = select((Article a1, Article a2) -> a1.weight - a2.weight);
Info art1 = getArt(weights, 150);
System.out.println(art1);
Article[] density = select((Article a1, Article a2) -> Double.compare(a2.valDensity,a1.valDensity));
Info art2 = getArt(density, 150);
System.out.println(art2);
}
static Article[] select(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);
return articles;
}
static Info getArt(Article[] articles, int capacity){
List<Article> selectArticles = new LinkedList<>();
int weight = 0;
int value = 0;
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;
selectArticles.add(articles[i]);
}
}
return new Info(selectArticles,weight,value);
}
private static class Article{
int weight;
int value;
double valDensity;
public Article(int weight, int value) {
this.weight = weight;
this.value = value;
this.valDensity = value * 1.0 / weight;
}
@Override
public String toString() {
return "Article{" +
"weight=" + weight +
", value=" + value +
", valDensity=" + valDensity +
'}';
}
}
private static class Info{
int weight = 0;
int value = 0;
List<Article> articles;
public Info(List<Article> articles, int weight, int value) {
this.articles = articles;
this.weight = weight;
this.value = value;
}
@Override
public String toString() {
return "Info{" +
"weight=" + weight +
", value=" + value +
", articles=" + articles +
'}';
}
}
}
2. 分治(Divide And Conquer)
即分而治之,子问题之间是相互独立的。
- 将原问题分解成若干个规模较小的子问题(子问题和原问题的结构一样,只是规模不同);
- 子问题不断分解成更小的子问题,直到不能分解(轻易算出子问题的解)。
- 利用子问题推导原问题的解。
- 归并排序;
- 快速排序;
- 大数乘法。
2.1 主定理(Master Theorem)
分治遵守的通用模式:
2.2 最大连续子序列和
- 子序列:一个序列中的部分序列,不一定非连续。
例如:-2、-1、-3、4、-1、2、1、-5、4子序列可以是:-2 、-1;-2、-3等。 - 子串、子数组、子区间:必须连续。
给定一个长度为n的子序列和,求它的最长子序列和。 例如:-2、-1、-3、4、-1、2、1、-5、4的最大连续子序列为:4、-1、2、1,和为:6。
2.2.1 暴力出奇迹
穷举出所有可能的连续子序列,计算出和,取最大值。
- 不断获取从
[0,0] 到[n,n] 之间获取[begin,end] 区间的值,并计算当前区间的和。
static int maxSubarray(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 = 0; end < nums.length; end++) {
int sum = 0;
for (int i = begin; i < end; i++) {
sum += nums[i];
}
max = Math.max(max,sum);
}
}
return max;
}
2.2.2 暴力 - 优化
上方代码中重复计算了很多次;这里使用上次计算得出的和与本次的值进行计算。
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;
}
2.2.3 分治
将序列均与的分割成2个子序列。 [begin,end) = [begin,mid) + [mid,end);mid = (begin + end ) >> 1;
如果问题的解是[i, j) ,那么存在3种可能:
-
[i, j) 存在于[begin, mid) 中; -
[i, j) 存在于[mid, end) 中; -
[i, j) 部分存在于[begin, mid) 中,另一部分存在于 [mid, end) 中;也就是[i, j) = [i,mid) + [mid, j) ;
static int maxSubarray3(int[] nums){
if (nums == null || nums.length == 0) return 0;
return maxSubarray3(nums, 0, nums.length);
}
static int maxSubarray3(int[] nums, int begin, int end){
if (end - begin < 2) return nums[begin];
int mid = (begin + end) >> 1;
int maxLeft = Integer.MIN_VALUE;
int sumLeft = 0;
for (int i = mid - 1; i >= begin; i--) {
sumLeft += nums[i];
maxLeft = Math.max(maxLeft,sumLeft);
}
int maxRight = Integer.MIN_VALUE;
int sunRight = 0;
for (int i = mid; i < end; i++) {
sunRight += nums[i];
maxRight = Math.max(maxRight,sunRight);
}
int max = maxLeft + maxRight;
return Math.max(max,
Math.max(
maxSubarray3(nums, begin, mid),
maxSubarray3(nums, begin, mid)
)
);
}
- 空间复杂度:O(logn);
- 时间复杂度:O(nlogn)。
2.3 大数乘法
两个超大的数字进行乘法运算,如何才能进行计算? 依据小时候学习的乘法方式,每个位分别计算,大概需要计算**n2**次。
时间复杂度为O(n1.585)。
|