目录
第1章摘要 2
第2章主题(介绍各类算法即用途) 2
2.1蛮力算法 2
2.2分治算法 2
2.3动态规划算法 2
2.4贪心算法 3
2.5回溯算法 3
2.6分支限界算法 3
第3章算法运用举例 4
3.1蛮力算法 PAGEREF _Toc18801 \h 4
3.2分治算法 4
3.3动态规划算法 4
3.4贪心算法 4
3.5回溯算法
3.6分支限界算法 5
第4章算法讨论 19
4.1 各类算法的优缺点 19
4.2算法的各种应用场景
第五章课程设计的体会即遇到的问题
算法设计技术主要应用于计算机科学中的经典问题,但也可以看成问题求解的一般性工具。它的应用不仅限于传统的计算问题和数学问题,也告诉人们如何应用一些特定的策略来解决问题,从而提高解决问题的能力。
算法+数据结构=程序清楚地解释了算法和数据结构这两个计算机科学的重要性和统一性。算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令标志或多个操作。
各种算法及其用途
蛮力算法思想
蛮力算法也称为穷举法或枚举法是一种简单、直接地解决问题的方法,常常直接基于问题的描述和所涉及的概念进行定义。所以,蛮力法也是最容易应用的方法。虽然,用蛮力法设计的算法时间特性往往也是最低的,但是许多问题我们一开始并没有很优化的算法,而蛮力法则可以帮助我们从低效的算法结构中剖析低效的缘由,进而提炼出更为优化的算法。
用途:(1)适用范围广,可能是唯一一种几乎什么问题都能解决的一般性方法;(2)常用于一些非常基本但又十分重要的算法(排序、查找、矩阵乘法和字符串匹配等),并不限制实例的规模;(3)解决一些规模小或能够接受的速度对实例求解的问题。
分治算法思想
分治算法,根据字面意思解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
分治法解题的一般步骤:
(1)分解,将要解决的问题划分成若干规模较小的同类问题;
(2)求解,当子问题划分得足够小时,用较简单的方法解决;
(3)合并,按原问题的要求,将子问题的解逐层合并构成原问题的解。
利用分治策略求解时,所需时间取决于分解后子问题的个数、子问题的规模大小等因素,而二分法,由于其划分的简单和均匀的特点,是经常采用的一种有效的方法,例如二分法检索、棋盘覆盖问题、求an。
动态规划算法
动态规划算法将带求解的问题成若干个相互重叠的子问题,每个子问题对应决策过程的一个阶段,一般来说,子问题的重叠关系表现在对给定问题求解的递推关系(也就是动态规划函数)中,将子问题的求解一次并填入表中,当需要再次求解此问题时,可以通过查表获得该子问题的解而不用再次求解,从而避免了大量重复计算。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
动态规划与其它算法相比,大大减少了计算量,丰富了计算结果,不仅求出了当前状态到目标状态的最优值,而且同时求出了到中间状态的最优值,这对于很多实际问题来说是很有用的。动态规划相比一般算法也存在一定缺点:空间占据过多,但对于空间需求量不大的题目来说,动态规划无疑是最佳方法!动态规划算法和贪婪算法都是构造最优解的常有方法。动态规划算法没有一个固定的解题模式,技巧性很强。
贪心算法
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择。
利用贪心法求解的问题应具备如下2个特征:
1、贪心选择性质
一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次的选择可以依赖以前作出的选择,但不依赖于后面要作出的选择。这就是贪心选择性质。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解 。
2、最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心法求解的关键所在。在实际应用中,至于什么问题具有什么样的贪心选择性质是不确定的,需要具体问题具体分析。
有很多经典的应用,比如霍夫曼编码,普利姆和克鲁斯卡尔最小生成树算法,还有迪杰斯特拉单源最短路径算法,都是使用了这种思维。
回溯算法
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
分支限界算法
分支限界法常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
在分支限界法中,每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
与贪婪算法一样,这种方法也是用来为组合优化问题设计求解算法的,所不同的是它在问题的整个可能解空间搜索,所设计出来的算法虽其时间复杂度比贪婪算法高,但它的优点是与穷举法类似,都能保证求出问题的最佳解,而且这种方法不是盲目的穷举搜索,而是在搜索过程中通过限界,可以中途停止对某些不可能得到最优解的子空间进一步搜索(类似于人工智能中的剪枝),故它比穷举法效率更高。
1.蛮力算法
冒泡排序是蛮力法的一个经典体现。
算法思想:比较列表中相邻的元素,如果是逆序的话,就交换他们的位置。重复多次之后,最大的元素就排到了最后一个位置。第二遍操作将第二个元素排到了倒数第二个位置上,这样一直依次比较下去,直到n-1遍之后,就排好了整个列表。
选择排序,顾名思义,这是一种有选择的排序方法。怎么选择,如何体现蛮力?
选择,就是将序列分为有序跟无序,这两部分,刚开始,有序部分的元素肯定为空集。接下来,就是所谓的蛮力的部分,开始不管三七二十一,就在无序的部分开始找啊,找到在无序中最小的数,将把最小的数与无序序列的第一个元素交换。到这一步后,又开始划有序与无序部分,这个时候,要把刚才找的最小的数放到有序部分中。这时,有序部分的元素就加1了,所谓排好一个了,还有剩下的元素在无序序列中。再开始蛮力部分。一直这样更新有序无序序列,蛮力;更新有序序列,蛮力。直至无序序列中没有元素。这样我们就实现了排序。
分治算法
在一个由2的k次方*2的k次方个方格组成的棋盘中,有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。要求使用不同形态的L型骨牌(四种)覆盖给定的棋盘上除特殊方格以外的所有方格,且骨牌之间不能重叠。
3.动态规划算法
0/1背包问题
给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问:应如何选择装入背包的物品,使得装入背包中物品的总价值最大?在选择装入背包的物品时,对每种物品i只有两种选择,即装入背包或者不装入背包。不能将物品i装入背包多次,也不能只装入部分物品i。
4.贪心算法
背包问题
背包问题是指给定n种物品和一个容量为C的背包,物品i的重量是w,,其价值为v,如何选择装人背包的物品,使得装入背包中物品的总价值最大?背包问题可以让物品放入一部分。
每次从物品集合中选择单位重量价值最大的物品,如果其重量小于背包容量,就可以把它装入,并将背包容量减去该物品的重量,然后我们就面临一个最优子问题-—同样是背包问题,只不过背包容量减少了,物品集合减少了。因此背包问题具有最优子结构性质。
5.回溯算法
1.问题描述
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。
问题分析
回溯法是求解皇后问题最经典的方法。算法的思想在于如果一个皇后选定了位置,那么下一个皇后的位置便被限制住了,下一个皇后需要一直找直到找到安全位置,如果没有找到,那么便要回溯到上一个皇后,那么上一个皇后的位置就要改变,这样一直递归直到所有的情况都被举出。
首先思考一下,输出的元组口中每一个分量,分开来看,在八皇后问题中,每一个分量都有八种可能,那么在每一层递归中我们都要穷举这八种可能,然后根据条件来判断该种结果是否合理。再者,因为不能在同一列,也为了避免重复,下一个皇后只能在上一个皇后的下面。这样思考问题就迎刃而解了。
6.分支限界算法
任务分配问题要求把n项任务分配给n个人,每个人完成每项任务的成本不同,要求分配总成本最小的最优分配方案。
各类算法的运行,需要截屏留证据
1.蛮力算法 冒泡排序
#include
int *bubble_sort(int arr[], int len);
int main() {
int *result, len;
int data[] = {12, 43, 23, 13, 65, 17, 98, 45, 67, 88};
len = (int)sizeof(data) / sizeof(*data);
printf("使用冒泡排序前的原始数据是:");
for (int i = 0; i < len; i++) {
printf("%3d", data[i]);
}
printf("\n");
result = bubble_sort(data, len);
printf("使用冒泡排序后的数据是:");
for (int j = 0; j < len; j++) {
printf("%3d", *(result + j));
}
return 0;
}
int *bubble_sort(int arr[], int len) {
int temp;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
printf("第%d次循环排序后结果:", i + 1);
for (int k = 0; k < len; k++) {
printf("%3d", arr[k]);
}
printf("\n");
}
return arr;
}
2.蛮力算法 选择排序
#include
int main()//主函数
{
int i,j,temp,array[11];//定义变量
printf("请输入10个数:\n");//提示语句
for(i=1;i<=10;i++)
{
scanf("%d",&array[i]);
}
for(i=1;i<=9;i++)//外层循环
{
for(j=i+1;j<=10;j++)//内层循环
{
if(array[i]>array[j]) //如果前一个数比后一个数大,则利用中间变量t实现两值互换
{
temp=array[i];
array[i]=array[j];
array[j]=temp;
}
}
}
printf("\n排序后顺序:\n");//提示语句
for(i=1;i<=10;i++)//输出排序后的数
{
printf("%5d",array[i]);
}
printf("\n");//换行
return 0;//函数返回值为0
}
3.分治算法
棋盘覆盖问题
#include
#include
int title=1;
int board[100][100]={0};
void chess (int h0 , int l0 ,int th ,int tl ,int size )
{
if (size==1) return;
int t=++title;
int s=size/2;
//判断左上
if(th<=h0+s-1&&tl<=l0+s-1)
//在左上就直接调函数求解
chess(h0,l0,th,tl,s);
else
//不再左上 就把左上的右下格置为t
{
board[h0+s-1][l0+s-1]=t;
//左上就有特殊方格啦 可以直接递归调用
chess(h0,l0,h0+s-1,l0+s-1,s);
}
//判断右上
if(th<=h0+s-1&&tl>=l0+s)
chess(h0,l0+s,th,tl,s);
else
{
board[h0+s-1][l0+s]=t;
//将右上添加一个特殊方格之后 右上部分的左上角就是再次调用函数的第一组参数
chess(h0,l0+s,h0+s-1,l0+s,s);
}
//判断左下
if(th>=h0+s&&tl<=l0+s-1)
chess(h0+s,l0,th,tl,s);
else
//将左下的右上置为t
{ board[h0+s][l0+s-1]=t;
chess(h0+s,l0,h0+s,l0+s-1,s);
}
//判断右下
if(th>=h0+s&&tl>=l0+s)
chess(h0+s,l0+s,th,tl,s);
else
{
board[h0+s][l0+s]=t;
chess(h0+s,l0+s,h0+s,l0+s,s);
}
}
int main()
{
int x,y,size1;
while(1)
{
printf("输入棋盘大小size= (size是2的n次方|n是整数):\n");
scanf("%d",&size1);
printf("输入特殊方格的坐标
scanf("%d %d",&x,&y);
chess(0,0,x,y,size1);
//输出棋盘
for(int i=0;i { for(int j=0;j { printf("%d\t",board[i][j]); } printf("\n"); } for(int i=0;i { for(int j=0;j { board[i][j]=0; } } } return 0; } 4.动态规划算法 0/1背包问题 #include const int N = 4; void Knapsack(int v[],int w[],int c,int n,int m[][10]); void Traceback(int m[][10],int w[],int c,int n,int x[]); int getMin(int a,int b); int getMax(int a,int b); int main() { int c=8; int v[]= {0,2,1,4,3},w[]= {0,1,4,2,3}; //下标从1开始 int x[N+1]; int m[10][10]; printf("待装物品重量分别为:\n"); for(int i=1; i<=N; i++) { printf("%d ",w[i]); } puts(""); printf("待装物品价值分别为:\n"); for(int i=1; i<=N; i++) { printf("%d ",v[i]); } puts(""); Knapsack(v,w,c,N,m); printf("背包能装的最大价值为:%d\n",m[1][c]); Traceback(m,w,c,N,x); printf("背包装下的物品编号为:\n"); for(int i=1; i<=N; i++) { if(x[i]==1) { printf("%d ",i); } } puts(""); return 0; } void Knapsack(int v[],int w[],int c,int n,int m[][10]) { int jMax = getMin(w[n]-1,c);//背包剩余容量上限 范围[0~w[n]-1] for(int j=0; j<=jMax; j++) { m[n][j]=0; } for(int j=w[n]; j<=c; j++)//限制范围[w[n]~c] { m[n][j] = v[n]; } for(int i=n-1; i>1; i--) { jMax = getMin(w[i]-1,c); for(int j=0; j<=jMax; j++)//背包不同剩余容量j<=jMax { m[i][j] = m[i+1][j];//没产生任何效益 } for(int j=w[i]; j<=c; j++) //背包不同剩余容量j-wi >c { m[i][j] = getMax(m[i+1][j],m[i+1][j-w[i]]+v[i]);//效益值增长vi } } m[1][c] = m[2][c]; if(c>=w[1]) { m[1][c] = getMax(m[1][c],m[2][c-w[1]]+v[1]); } } //x[]数组存储对应物品0-1向量,0不装入背包,1表示装入背包 void Traceback(int m[][10],int w[],int c,int n,int x[]) { for(int i=1; i { if(m[i][c] == m[i+1][c]) { x[i]=0; } else { x[i]=1; c-=w[i]; } } x[n]=(m[n][c])?1:0; } int getMin(int a,int b) { if(a>=b) return b; else return a; } int getMax(int a, int b) { if(a>=b) return a; else return b; } 5.贪心算法 背包问题 #include using namespace std; //插入排序 void Sort1(int n,double w[],double v[]){ int max1; double t[20]; for(int i=1;i<=n;i++){ t[i]=v[i]/w[i]; //cout< } for(int i=1;i<=n;i++){ max1=i; for(int j=i+1;j<=n;j++){ if(t[j]>t[max1]){ max1=j; } } swap(t[i],t[max1]); swap(w[i],w[max1]); swap(v[i],v[max1]); } for(int i=1;i<=n;i++){ cout< } } //n为物品数量,c为背包最大容量,w[]为物品重量,v[]为物品价值,b[]为背包存放 void knapsack_greedy(int n,double c,double w[],double v[],double b[]){ int j; Sort1(n,w,v); for(int i=1;i<=n;i++)//初始化背包b[] b[i]=0; for(j=1;j<=n;j++){ if(c b[j]=1; c=c-w[j]; } if(j<=n) b[j]=c/w[j]; cout<<"输出:"< for(int i=1;i<=n;i++){ cout<<"物品重量为:"< } } int main(){ int n,c; double b[20],w[20],v[20]; cout<<"输入物品数量和背包容量:"; cin>>n>>c; cout<<"输入物品重量(如:30 10 20):"; for(int i=1;i<=n;i++) cin>>w[i]; cout<<"输入物品的价值(如:120 60 100):"; for(int i=1;i<=n;i++) cin>>v[i]; knapsack_greedy(n,c,w,v,b); return 0; } 6.回溯算法 #include using namespace std; #define MAX 8; //回溯法 int chessboard[8][8] = { 0 }; int num = 0; bool check(int x, int y) // { for (int i = 0; i < x; i++) { //检查同列是否有皇后 if (chessboard[i][y] == 1) return false; if (x - 1 - i >= 0 &&y-1-i>=0&&chessboard[x - 1 - i][y - 1 - i] == 1) //检查左斜线 return false; if (x - 1 - i >= 0 &&y+1+i<=7&& chessboard[x-1-i][y+1+i] == 1) //检查右斜线 return false; } return true; } void show() { // ++num; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { cout << chessboard[i][j]; } cout << endl; } } void putQueen(int x) { int i; for (i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) chessboard[x][j] = 0; if (check(x,i)) { chessboard[x][i] = 1; if (x == 7) { cout << "第" << num + 1 << "种解法" << endl; show(); cout << endl; ++num; } else { putQueen(x + 1); } } } } int main() { putQueen(0); cout << num << endl; } 7.分支限界算法 #include #include #include using namespace std; #define N 4 int c[][N]={ 3, 8, 4, 12, 9, 12, 13, 5, 8, 7, 9, 3, 12, 7, 6, 8 }; int costBoundInK = 65535;//搜索深度为k时的花费下界,深度为k有N-k个节点的下界,取最小值给minCostInk int minCostInk = 65535;//搜索深度为k时的最小下界 int minCostLine = 0;//记录最小下界节点所在的行 int expanded[N];//存放被扩展节点的顺序 vector int min_exceptiInRow(int i,int row) { int min = 65535; for (int line=0; line { bool currExpanded =false; for (int m=0;m { if (expanded[m] == line)//line行的某一节点已经之前被扩展过,故这一行的节点不能再扩展 { currExpanded = true; break; } } //功能:除去当前行和之前被扩展的节点所在行之后,通过循环找出row这列的最小值 if (line!=i && !currExpanded && c[line][row] { min = c[line][row]; } } return min; } /************************************************************************ **功能:求出最小的花费 **参数:k为作业编号 mincost为最终返回的最小花费 ************************************************************************/ void branchBound(int k,int& mincost) { while(true) { if (k { bool currExpanded = false; minCostInk = 65535; for (int i=0; i { for (int m=0;m { if (expanded[m]==i)//查看当前行的某一节点是否已经被扩展过,若扩展过,则当前行的所有节点都不能再扩展 { currExpanded = true; break; } } if (!currExpanded)//当前行未被扩展 { costBoundInK = c[i][k];//costBoundInK表示在某个搜索深度k下,把作业k分配给工人i时的时间下界 for (int j=k+1;j { costBoundInK += min_exceptiInRow(i,j);//深度k下的花费下界,即在未扩展节点所在列均取花费的最小值,并累加 } if (costBoundInK < minCostInk) { minCostInk = costBoundInK; minCostLine = i; //记录要扩展节点的行号 expanded[k] = i; //expanded[k]记录花费矩阵中节点被扩展的顺序,k为列,代表作业编号,依次为0,1...N //对应的i为行,代表工人编号,即分配方案为工人i完成作业k } vectors.push_back(costBoundInK);//不同深度k下计算的花费下界存入vectors容器;深度为k时,需要计算N-k次下界,取最小值为扩展节点 } currExpanded = false; expanded[k]=-1;//同级节点扩展,恢复当前节点为未扩展 } expanded[k] = minCostLine;//选取当前搜索深度k下,花费最少的为扩展节点c[minCostLine][k] k++; } else//到达叶节点 { for(int i=0;i { mincost +=c[expanded[i]][i]; } break; } } } int main(int argc, char *argv[]) { memset(expanded, -1, N*sizeof(int));//给数组置-1 int mincost=0; branchBound(0,mincost);//分支限界算法,从根节点开始扩展,由mincost返回最少花费 //输出 cout<<"-------花费矩阵(行为工人,列为作业,从0开始)----------"< for (int i=0;i { for (int j=0;j { printf("%3d ",c[i][j]); } cout << endl; } cout<<"---------------------------------------------"< for(int i=0;i { cout<< "作业:" << i << " --> 由工人" << expanded[i] << "完成. "<< " 花费为:" << c[expanded[i]][i] << " " << endl; } cout << "总花费为:"<< mincost<< endl; return 0; } 4.1总结各种算法的优缺点 蛮力算法的优缺点,各种应用场景 蛮力法所具有的优点: (1)应用范围广,不受实列规模的限制 (2)当要解决的问题低频率出现,并且高效算法很难设计时可选用蛮力法 (3)对解决一些小规模的问题实列仍然有效 (4)可作为衡量其他算法的参照物 蛮力法所具有的缺点: 用蛮力法设计的算法其时间性能往往也是最低的,典型的指数时间算法一般都是通过蛮力搜索而得到的。 分治算法的优缺点,各种应用场景 分治算法主要用于解决规模较大的问题,通过将大问题“分而治之"将有效降低题目难度。又因为分解得到的子问题之间是相互独立且互不影响的,所以可以同时进行,以此提升解决问题的效率。 分治算法的缺陷在于,分治算法常常采用递归的方式实现,整个过程需要消耗大量的系统资源,严重时还会导致程序崩溃。 动态规划算法的优缺点,各种应用场景 动态规划模型相对于静态规划模型的优点: 能够得到全局最优解; 可以得到一族最优解; 3.由于动态规划方法反映了动态过程演变的联系和特征,在计算时可以利用实际知识和经验提高求解效率。 动态规划模型的缺点: 1.没有统一的标准模型; 2.数值方法求解时存在维数灾。 贪心算法的优缺点,各种应用场景 优点:用贪篓法改计算法的特点是一步一步地进仃,常以当前情况为基础根据某个优化测度作最优选择,而不考虑各种可能的整体情况,它省去了为找最优解要穷尽所有可能而必须耗费的大量时间。 缺点:它采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解,虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的,所以贪婪法不要回溯。 回溯算法的优缺点,各种应用场景 优点:回溯算法的思想非常简单,大部分情况下,都是用来解决广义的搜索问题,也就是从一组可能的解中,选择出一个满足要求的解。回溯算法非常适合用递归来实现,在实现的过程中剪枝操作是提高回溯效率的一种技巧。利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率。 劣势:效率相对于低(动态规划) 分支界限算法的优缺点,各种应用场景 优点:节省了空间;缺点:需要较多的分枝运算,耗费的时间较多。 4.2算法的各种应用场景 蛮力算法应用场景: 第一,可以应用蛮力法来解决广阔领域的各种问题。 第二,对于一些重要的问题来说(比如:排序、查找、矩阵乘法和字符串匹配),蛮力法可以产生一些合理的算法。 第三,如果要解决的问题实例不多,而且蛮力法可以用一种能够接受速度对实例求解。 第四,即使效率通常很低,仍然可以用蛮力法解决一些小规模的问题实例。 分治法应用场景: 该问题的规模缩小到一定的程度就可以容易地解决 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。 利用该问题分解出的子问题的解可以合并为该问题的解; 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加; 第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应阳;第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好 动态规划应用场景: 适用动态规划的问题必须满足最优化原理、无后效性和重叠性。 1.最优化原理(最优子结构性质)最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。 2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。 3.子问题的重叠性动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大干其它的算法。 贪心算法适用范围: 贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。 回溯算法适用场景: 回溯算法是个"万金油”。基本上能用的动态规划、贪心解决的问题,我们都可以用回溯算法解决。回溯算法相当于穷举搜索。穷举所有的情况,然后对比得到最优解。不过,回溯算法的时间复杂度非常高,是指数级别的,只能用来解决小规模数据的问题。对于大规模数据的问题,用回溯算法解决的执行效率会非常低。 分支限界法适用场景: 适用于求解最优化问题,并且是小型问题。 在调试程序是要先了解程序的意义,思考该怎样运行的。然后进行编译,查看程序是否出错,如果出错,检查出错的地方哪里出错了,然后进行修改。会遇到缺失符号,例如少写分号。或者少写参数。更严重的还有直接运行不了。要重新修改程序。程序输出了意料之外的结果,可能是数组没有赋初值;是访问了非法内存;循环次数是否远远超过或低于预期。程序输出的结果与推算的不一致,但差别不大。可能是把初值赋错。也可能是把“=="写成了“=”。或者弄混">"<"">="”<=”;或者是变量类型不对;输出类型对不对;检查数据类型是否正确。也可以熟练运用调试功能;不一定要打开调试面板调试,输出中间变量也是一个很好的方法。通过对比一个(或多个)关键变量在程序中的变化与预期变化的差距,往往能发现问题。在运行冒泡排序时,由于没有弄好循环的次数,导致排序出错。所以我重新了解程序的意义、运行过程。从而找到程序的错误。总而言之,运行程序一定要先了解程序、熟悉程序。才能调试程序。 回溯算法的形式模型 .中国知网 康桂花主编,计算概论,中国铁道出版社,2016.08,第123页 补录,计算机算法设计与分析研究,新华出版社,2015.09,第88页 宋荣兴,荆湘霞,李扶民.运筹学:东北财经大学出版社,2018-03 算法与数据结构;清华大学出版社;陈媛,卢玲,何波刘恒洋编著 第四章算法讨论
第五章课程的设计及遇到的问题
参考文献