【计算机算法的构建策略】

计算机算法的构建策略

1. 分治策略Divide-and-Conquer

就是将复杂的问题分解为多个简单的子问题,然后再将每一个子问题分解为更简单的子子问题。最后对子子问题求解,合并,得到原本复杂问题的解。当然复杂的问题规模比较大的时候,计算就起来就很慢了。并且分解出来的子问题之间要互相独立,且与原问题形式相同。

适用条件:
第一步:判断问题缩小到一定程度之后是否很容易解决;
第二步:分解成的子问题是最优子问题,子问题是独立的,且子问题之间不包含公共子子问题;
第三步:分解之后的子问题可以合并为该问题的解;

Divide-and-Conquer(P)

1. if |P|≤n0  // 当问题的规模|P|小于一定阈值n0时

2. then return(ADHOC(P))  //那么表示这个问题很简单,不需要分解,直接用ADHOC求解

3.// 将P分解为较小的子问题 P1 ,P2 ,...,Pk

4. for i←1 to k

5. do yi ← Divide-and-Conquer(Pi) //递归解决Pi

6. T ← MERGE(y1,y2,...,yk) // 合并子问题

7. return(T)

一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1.
适用问题:
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔

2. 动态规划算法Dynamic Programming

当分治法中的子问题不独立,有公共的子子问题,那么可以使用动态规划算法。
动态规划算法:
step1:将问题分解为多个子问题(子阶段),划分后的子阶段一定要是有序的或者是可排序的,否则问题就无法求解。
step2:按顺序求解子阶段,前一个子阶段的解为后一子阶段求解提供有用信息。
step3: 在求解任一子阶段时,列出各种可能的局部解,通过决策保留那些局部最优解,丢弃其它局部解
step4:依次解决各个子阶段,最后一个子阶段就是初始问题的解。

for(j=1; j<=m; j=j+1) // 第一个阶段
   xn[j] = 初始值;

 for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段
   for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式
     xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};

t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案

print(x1[j1]);

for(i=2; i<=n-1; i=i+1{  
     t = t-xi-1[ji];

     for(j=1; j>=f(i); j=j+1)
        if(t=xi[ji])
             break;
}

(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3) 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)。

3.贪心算法Greedy Algorithm

如果分治法无法子问题的解无法合并为原问题的解,那么需要适用贪心算法。
贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
step1:从某个初始解出发
step2:采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一部分解,缩小问题规模;
step3:将所有解综合起来
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。
事例:找零钱问题
找给顾客41分钱,现在的货币有 25 分、20分、10 分、5 分和 1 分5种类型硬币;如何保证给顾客找的硬币个数最少,钱数正确?

按照贪心算法的三个步骤:

1.41分,局部最优化原则,先找给顾客25分;
2.此时,41-25=16分,还需要找给顾客10分,然后5分,然后1分;
3.最终,找给顾客一个25分,一个10分,一个5分,一个1分,共四枚硬币。
是不是觉得哪里不太对,如果给他2个20分,加一个1分,三枚硬币就可以了。
此时结果就不是全局最优
但是当问题变成了货币有 25 分、10 分、5 分和 1 分四种硬币,要找给客户 41 分钱的硬币,如何安排才能找给客人的钱既正确且硬币的个数又最少?
那么上面那种方法就变成了全局最优。

4. 回溯法Back Tracking

回溯法可以理解为通过选择不同的岔路口寻找目的地,一个岔路口一个岔路口的去尝试找到目的地。如果走错了路,继续返回来找到岔路口的另一条路,直到找到目的地。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
step1:首先明确问题的解空间,并且问题的解空间至少包含问题的一个最优解
step2:确定节点的扩展搜索规则
step3:以深度优先的搜索方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,……,n)之间满足某种条件,记为f(ai)。

  1. 非递归回溯框架
int a[n],i;
初始化数组a[];
i = 1;
while (i>0(有路可走)   and  (未达到目标))  // 还未回溯到头
{
    if(i > n)                                              // 搜索到叶结点
    {   
          搜索到一个解,输出;
    }
    else                                                   // 处理第i个元素
    { 
          a[i]第一个可能的值;
          while(a[i]在不满足约束条件且在搜索空间内)
          {
              a[i]下一个可能的值;
          }
          if(a[i]在搜索空间内)
         {
              标识占用的资源;
              i = i+1;                              // 扩展下一个结点
         }
         else 
        {
              清理所占的状态空间;            // 回溯
              i = i –1; 
         }
}

回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:

int a[n];
try(int i)
{
    if(i>n)
       输出结果;
     else
    {
       for(j = 下界; j <= 上界; j=j+1)  // 枚举i所有可能的路径
       {
           if(fun(j))                 // 满足限界函数和约束条件
             {
                a[i] = j;
              ...                         // 其他操作
                try(i+1);
              回溯前的清理工作(如a[i]置空值等);
              }
         }
     }
}

八皇后问题:
八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后(棋子),使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
step1
尝试先放置第一枚皇后,被涂黑的地方是不能放皇后
在这里插入图片描述
step2
第二行的皇后只能放在第三格或第四格,比方我们放第三格,则:
在这里插入图片描述
step3
可以看到再难以放下第三个皇后,此时我们就要用到回溯算法了。我们把第二个皇后更改位置,此时我们能放下第三枚皇后了。
在这里插入图片描述
step4
虽然是能放置第三个皇后,但是第四个皇后又无路可走了。返回上层调用(3号皇后),而3号也别无可去,继续回溯上层调用(2号),2号已然无路可去,继续回溯上层(1号),于是1号皇后改变位置如下,继续回溯。

在这里插入图片描述

5. 分支限界Branch and bound

类似于回溯法,也是一种在问题的解空间树T上搜索问题解的算法。回溯法的求解目标是找出T中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
集装箱问题:
现在有一个集装箱, 能装30的容量, 有分别为10, 15, 20的货物, 问如何能最大化集装箱的载重货物??
我们知道答案为30

#include <iostream>
#include <list>
using namespace std;
#define MAX 3//一共三个货物
const int CAP = 30;//最大容量
const int box[MAX] = {10,15,20};//三箱货物
int main()
{
	int temp = 0, level = -1, best = 0;//best是最优解
	int curVal = 0, parentVal = 0, expectVal = 0;//parent是队列种pop出来的父节点,他能产生两个子节点。
	//curVal是左子树的目标值,expectVal是右子树的目标值,表示预期目标值。curVal和expectValue都能用来剪枝。
	list<int> queue; //队列
	queue.push_back(-1);//-1 表示一层分层用
	queue.push_back( parentVal ); //当前点,父节点
	do{
		parentVal = queue.front();//先进先出,当前分的点
		queue.pop_front();//根据先进先出原则取第一元素,并且删除。
		if( parentVal != -1){//表示在层间, 每一个parentVal可以分两个点left child 和right child
			//left child,若加入该box[level], 加入box[level]为true
			curVal = parentVal + box[level];//根据约束条件1(最大容量)剪枝, 可以不满足约束条件的没有再进行分支
			if( curVal > best && curVal <= CAP ){//如果当前最优值比全局最优值好,并且满足约束条件,则更新全局最优解
				best = curVal;
				std::cout <<"Current BestValue:\t" << best << endl;
				if(level < MAX - 1){//如果不是最后节点,更新后的子节点加入队列,可以继续进行分支
					queue.push_back(curVal);
				}//end if level
			}//end if CurVal 左节点结束,左子树的值为CurVal
			//right child
			temp = 0;
			curVal = parentVal;
			for(int i = level + 1; i < MAX; i++){//不包括 i = level,级不加入box[level],即加入box[level为 false
				temp += box[i];
			}
			expectVal = curVal + temp;//期望值为不加上当前 level的值,剩下的可能最大值, 计算右子树的值 expectVal
			std::cout << "Expect Value:\t" << expectVal << endl;
			//预计最大值若大于当前最佳值,则加入队列;否则不加入。即剪枝
			if(expectVal > best && level < MAX - 1){
				queue.push_back(curVal);
			}
			for(list<int>::iterator ite = queue.begin(); ite != queue.end(); ite++){//打印当前列
				std::cout<<"\t"<<*ite;
			}
			std::cout << endl;
		}//end if parentVal
		else{//表示层结束,加入层标志。层标志主要是用来区分left child 和 right child
			if(level < MAX-1)
			{
				queue.push_back(-1);
			}
			level++;  	    
		}
	}while(level != MAX && queue.empty() != true);//到最后一层或者是队列为空结束//end do 
	std::cout <<"Final BestValue:\t" << best << endl;
	system("pause");
	return 0;
}

链接: 五大常用算法
链接: 贪心算法
链接: 分支定界算法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值