贪心与回溯与DP

  一.贪心

  贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。

  它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯

  贪婪算法是一种改进了的分级处理方法。其核心是根据题意选取一种量度标准。然后将这多个输入排成这种量度标准所要求的顺序,按这种顺序一次输入一个量。如果这个输入和当前已构成在这种量度意义下的部分最佳解加在一起不能产生一个可行解,则不把此输入加到这部分解中。这种能够得到某种量度意义下最优解的分级处理方法称为贪婪算法。

  1.特性

  贪婪算法可解决的问题通常大部分都有如下的特性:
  ⑴ 有一个以最优方式来解决的问题。为了构造问题的解决方案,有一个候选的对象的集合:比如不同面值的硬币。
  ⑵ 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
  ⑶ 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
  ⑷ 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
  ⑸ 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
  ⑹ 最后,目标函数给出解的值。
  为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。

  2.步骤

  *从问题的某一初始解出发;
  *while 能朝给定总目标前进一步 do
    求出可行解的一个解元素;
  *由所有解元素组合成问题的一个可行解。
  贪心算法的关键在于贪心策略(怎么才算局部最优)的选取。

  3.例子

  活动安排问题

  问题表述:设有n个活动的集合E = {1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活i都有一个要求使用该资源的起始时间si和一个结束时间fi,si < fi 。如果选择了活动i,则它在半开时间区间[si, fi)内占用资源。若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的。也就是说,当si >= fjsj >= fi时,活动i与活动j相容。

  怎么样安排尽量多的活动?

  贪心解决:贪心策略是选取时间结束最早的事情做。根据相交把不可行的解排除掉。

  二.回溯

  回溯算法也叫试探法,它是一种系统地搜索问题的解的方法。
  用回溯算法解决问题的一般步骤:
  1 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
  2 确定易于搜索的解空间结构,使得能用回溯法方便地搜索整个解空间。
  3 以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
  回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。

  例子八皇后问题:

  在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

  int vis[3][16]; //因为是逐行放置的,则皇后肯定不会横向攻击,所以只需检测纵向和斜向攻击
        //vis[0]表示纵向是否有冲突
        //vis[1]表示左斜对角线是否有冲突
        //vis[2]表示右斜对角线是否有冲突
  int tot; //解个数
  int C[8];//解的放置方法

  void search(int cur){
    int i,j;
    if(cur==8) tot++; //递归边界
    else for (int i = 0; i < 8; i++)
    {
      if (!vis[0][i] && !vis[1][cur+i] && !vis[2][cur-i+8]) //剪枝,关键在于如何构建全局变量来剪枝!
      {
        C[cur]=i; //把cur行的皇后放在i列,如何不用打印解,此C可舍弃
        vis[0][i]=vis[1][cur+i]=vis[2][cur-i+8]=1;//修改列和对角线有冲突的位置
        search(cur+1);
        vis[0][i]=vis[1][cur+i]=vis[2][cur-i+8]=0;//切记!一定要改回来!!
      }
    }
  }

  例子二:数组中寻找和为定值的多个数

  

 1 /**  
 2  * 输入t, r, 尝试Wk 
 3  */  
 4 void sumofsub(int t, int k ,int r, int& M, bool& flag, bool* X)  
 5 {  
 6     X[k] = true;   // 选第k个数  
 7     if (t + k == M) // 若找到一个和为M,则设置解向量的标志位,输出解  
 8     {  
 9         flag = true;  
10         for (int i = 1; i <= k; ++i)  
11         {  
12             if (X[i] == 1)  
13             {  
14                 printf("%d ", i);  
15             }  
16         }  
17         printf("/n");  
18     }  
19     else  
20     {   // 若第k+1个数满足条件,则递归左子树  
21         if (t + k + (k+1) <= M)  
22         {  
23             sumofsub(t + k, k + 1, r - k, M, flag, X);  
24         }  
25         // 若不选第k个数,选第k+1个数满足条件,则递归右子树  
26         if ((t + r - k >= M) && (t + (k+1) <= M))   //r的作用就在这里!如果剩余的总和也不够,就剪枝掉
27         {  
28             X[k] = false;  
29             sumofsub(t, k + 1, r - k, M, flag, X);  
30         }  
31     }  
32 }  
33   
34 void search(int& N, int& M)  
35 {  
36     // 初始化解空间  
37     bool* X = (bool*)malloc(sizeof(bool) * (N+1));  
38     memset(X, false, sizeof(bool) * (N+1));  
39     int sum = (N + 1) * N * 0.5f;  
40     if (1 > M || sum < M) // 预先排除无解情况  
41     {  
42         printf("not found/n");  
43         return;  
44     }  
45     bool f = false;  
46     sumofsub(0, 1, sum, M, f, X);  
47     if (!f)  
48     {  
49         printf("not found/n");  
50     }  
51     free(X);  
52 }  
53   
54 int main()  
55 {  
56     int N, M;  
57     printf("请输入整数N和M/n");  
58     scanf("%d%d", &N, &M);  
59     search(N, M);  
60     return 0;  
61 }  

 

 

  三.DP动态规划

  把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

  如果问题是由交叠的子问题所构成,我们就可以用动态规划技术来解决它,一般来说,这样的子问题出现在对给定问题求解的递推关系中,这个递推关系包含了相同问题的更小子问题的解。动态规划法建议,与其对交叠子问题一次又一次的求解,不如把每个较小子问题只求解一次并把结果记录在表中(动态规划也是空间换时间的),这样就可以从表中得到原始问题的解。

  步骤:

  (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
  (2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
  (3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。
  (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

  例子:

  01背包是在M件物品取出若干件放在空间为W的背包里,每件物品的体积为W1,W2……Wn,与之相对应的价值为P1,P2……Pn。求出获得最大价值的方案。

  考虑用动态规划的方法来解决,这里的:
  阶段是:在前N件物品中,选取若干件物品放入背包中;
  状态是:在前N件物品中,选取若干件物品放入所剩空间为W的背包中的所能获得的最大价值;
  决策是:第N件物品放或者不放;
  由此可以写出动态转移方程
  我们用f[i,j]表示在前 i 件物品中选择若干件放在所剩空间为 j 的背包里所能获得的最大价值
  f[i,j]=max{f[i-1,j-Wi]+Pi (j>=Wi), f[i-1,j]}
  [1]这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c的背包中”,此时能获得的最大价值就是f[v-c]再加上通过放入第i件物品获得的价值w。
  这样,我们可以自底向上地得出在前M件物品中取出若干件放进背包能获得的最大价值,也就是f[m,w]

  装箱问题

  有一个箱子容量为V(正整数,0≤V≤20000),同时有n个物品(0小于n≤30),每个物品有一个体积(正整数)。要求从n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
  输入v,n,在输入n个物品。输出箱子的剩余空间为最小。
  转化为01背包,认为每个箱子的价值和空间相等,用01背包求出价值最大值,在用空间减去即可
  #include <cstdio>
  int v,n,i,j,k,a[31];
  bool f[20001]; //保存所有状态,f[k]表示装了k容量
  int main(){
      scanf("%d%d", &v, &n);
      for (int i = 1; i <= n; i ++ ) scanf("%d", a + i);
      f[0] = 1;
      for (int i = 1; i <= n; i ++ )
          for (int j = v; j >= a[i]; j -- )
              if (!f[j] && f[j - a[i]]) f[j] = 1;  //查看已有状态,是否含有f[j - a[i]]的状态
                           //有的话就可以到达此值
      k = v;
      for (; k > 1 && !f[k]; k -- );
      printf("%d\n", v - k);
  }

 

参考:百科

posted on 2013-11-19 11:13  依蓝jslee  阅读(1715)  评论(0编辑  收藏  举报

导航