贪心算法和分治算法

本文是数据结构与算法之美的学习笔记

贪心算法的概念

贪心算法是指在解决问题的时候,总是选择当前最好的,并希望通过一系列的最优选择,能够产生一个问题的全局最优解。

比如我们有一个可以容纳100kg物品的背包,我们有5种豆子吗,每种豆子的总量和总价值都不一样,如何能让背包中的物品总价值最大呢?

豆子 总量(kg) 总价值(元)
黄豆 100 100
绿豆 30 90
红豆 60 120
黑豆 20 80
青豆 50 75

很简单:

  • 先算每个豆子的单价,黄豆1元,绿豆3元,红豆2元,黑豆4元,青豆1.5元
  • 按照单价由高到底依次往背包里装就可以了,结果就是20kg黑豆,30kg绿豆,50kg红豆

贪心算法的思路

  1. 满足约束条件,比如上面的例子限制值就是100kg,期望值是总价值最大
  2. 局部最优,每次选出当前步骤中的最优解,比如上面的例子中从单价最高的开始往下一个一个的加
  3. 前面的选择会影响后面的选择,选择之后就没法改变,上面的例子使用贪心思想可以找到最优解,有时候使用贪心思想不能找到最优解,比如下面的例子

贪心算法和分治算法_第1张图片

上面是一个有权图,从顶点S开始找一条道T的最短路径。如果使用贪心算法的思想,每次都是找权重最小的那么结果是S->A->E->T,长度是1+4+4=9

但是如果我们仔细看最短路径并不是这个,而是S->B->D->T长度是2+2+2=6

这就是前面的选择会影响后面的选择,一旦做出了选择,就不能改变了,上面的例子,我们一开始选择了最小权重1到达了A,那么A后面不管权重多大都得捏着鼻子往下走。

贪心算法实战

第一题:钱币找零

假如我们有1元,2元,5元,10元,20元,50元,100元面额的纸币,他们的张数分别为c1,c2,c5,c10,c20,c50,c100。现在需要支付K元,最少需要多少张纸币

很简单,使用贪心思想,先从最大的面值开始支付,如果不够就继续使用更小一点的面值,以此类推直到支付完成。

第二题:最大整数

有n个整数,把他们连成一排,组成一个最大的多位数
比如:n=3 整数位 5,16,876 连成的最大整数就是:876516

先把整数换成字符串,比较a+b和b+a,如果a+b>=b+a,就把a放在b的前面,反之把a放在b的后面

第三题:区间覆盖

给定一个长度为m的区间,在给出n条线段的起点和终点,从中选出尽量多的线段,要求每个线段都是独立的,不跟其他的线段有交集。

比如:m=10

线段:[6,8] [2,4] [3,5] [1,5] [5,9] [8,10]

结果:[2,4] [6,8] [8,10]

  1. 首先按照起始点排序为[1,5] [2,4] [3,5] [5,9] [6,8] [8,10]
  2. 然后选取左端点跟前面的已经覆盖的区间不重合,并且右端点尽量小这样可以让剩下的未覆盖的区间尽可能的大,也就可以放置更多的区间。

贪心算法不一定会给出最优解,但是它简单好理解,如果一个问题可以使用多种方法解决,那么使用贪心算法也是最好的选择之一。

分治算法

分治算法顾名思义就是分而治之,就是把一个复杂的问题分成若干个相同或者相似的子问题,在把小问题分成更小的问题,直到最后的小问题可以很简单的求解,然后把各个子问题的解合并起来形成原问题的解。

现在比较火的大数据,实际上就是使用了分治算法的思想,当数据大到一定的程度的时候,一台机器处理不了,那好就用很多台机器一起处理,把大数据分成若干个小数据分到每个机器上分别处理,然后把处理结果合并。当在处理不了的时候就加机器继续分。

还有比较常用的快速排序算法,归并排序算法都用了分治的思想。

分治算法比较适合使用递归来实现,每一层的递归一般涉及到3个操作

  1. 把问题分解成一系列的子问题
  2. 递归的求解各个子问题
  3. 将子问题的结果合并成原问题

根据上面的分治算法的思想,我们可以想象一下可以使用分治思想解决的问题的特点

  1. 原问题和分解成的小问题具有相同的模式
  2. 原问题分解成的子问题可以独立的求解,子问题之间没有相关性
  3. 具有分解终止的条件,条件就是子问题可以直接求解
  4. 可以将子问题的解合并成原问题,合并的操作复杂度不能太高,否则就不能起到见效算法总体复杂度的效果了。

例子:求一组数据的逆序对个数

有序对就是左边小于右边,逆序对就是左边大于右边,比如一组数2,4,1,5,6。逆序对就是(2,1) (4,3) (4,1) (3,1)

我们可以通过归并排序来实现,归并排序中有个操作是把两个有序的小组合并成一个有序数组,在合并的过程中我们就可以计算着两个小组的逆序对数了。

比如一组数据 1,5,6,2,3,4。

首先将其分成两部分分别排序 : 1,5,6和2,3,4,

这里分组已经排序完成,下面就是两组合并成一组

  1. 准备一个空数组
  2. 1和2比较,1比2小,1放入数组
  3. 5和2比较5比2大,2放入数组中,5和5后面的都比2大,说明可以组成逆序对,所以这里逆序对个数+2
  4. 5和3比较,5比3大,3放入数组中,5和5后面的都比2大,说明可以组成逆序对,所以这里逆序对个数+2
  5. 5和4比较,5比4大,4放入数组中,5和5后面的都比2大,说明可以组成逆序对,所以这里逆序对个数+2
  6. 最后5和6放入数组中排序完成
  7. 结果逆序对的个数就是2+2+2=6

代码:

private int num = 0; // 全局变量或者成员变量

public int count(int[] a, int n) {
  num = 0;
  mergeSortCounting(a, 0, n-1);
  return num;
}

private void mergeSortCounting(int[] a, int p, int r) {
  if (p >= r) return;
  int q = (p+r)/2;
  mergeSortCounting(a, p, q);
  mergeSortCounting(a, q+1, r);
  merge(a, p, q, r);
}

private void merge(int[] a, int p, int q, int r) {
  int i = p, j = q+1, k = 0;
  int[] tmp = new int[r-p+1];
  while (i<=q && j<=r) {
    if (a[i] <= a[j]) {
      tmp[k++] = a[i++];
    } else {
      num += (q-i+1); // 统计 p-q 之间,比 a[j] 大的元素个数
      tmp[k++] = a[j++];
    }
  }
  while (i <= q) { // 处理剩下的
    tmp[k++] = a[i++];
  }
  while (j <= r) { // 处理剩下的
    tmp[k++] = a[j++];
  }
  for (i = 0; i <= r-p; ++i) { // 从 tmp 拷贝回 a
    a[p+i] = tmp[i];
  }
}

你可能感兴趣的:(数据结构与算法)