回溯和贪心
回溯
意义:编程解决问题时,常遇到需要例遍所有可能性来求解问题的情况。此时,回溯将是不错的选择。
代码示例:
#include <stdio.h> #include <stdlib.h> #include <math.h> /*这个程序用来测试回溯算法在解决问题中的应用*/ /* 八皇后问题: 经典的八皇后问题,即在一个8*8的棋盘上放8个皇后, 使得这8个皇后无法互相攻击( 任意2个皇后不能处于同一行,同一列或是对角线上), 输出所有可能的摆放情况。 */ /* 将以上问题引申成在n*n的棋盘上摆放n个皇后的问题。 以下代码就使用了回溯法解决n皇后问题。 */ int place(int*,int);//判断当前位置摆放皇后是否可行 void nQueens(int*,int);//n皇后问题算法核心 void printSolution(int*,int);//用于输出已求出的解决方案 int main() { int n; int *x;//指针x作为动态数组使用 scanf("%d",&n); //为指针x动态分配n+1个int的空间。用于代替数组模拟棋盘 x=(int*)malloc(sizeof(int)*(n+1)); nQueens(x,n);//将数组和皇后数量传入,开始求解 return 0; } int place(int *x,int k)//形参x为模拟棋盘的数组,k为当前放置皇后的行数 { int i; //循环例遍数组中每一个已经确定位置的皇后 for(i=1;i<k;i++) { /* 以下判断条件将在以下另外解析 */ if(x[i]==x[k]||fabs(x[i]-x[k])==fabs(i-k)) return 0;//如果第k行的皇后和第i行的皇后冲突,返回0 } return 1;//如果没有出现任何冲突,返回1 } void nQueens(int *x,int n) { int k;//定义k作为行标 k=1;//从第一行开始摆放皇后 x[k]=0;//初始化第k行 while(k>0) { x[k]++;//首先在第k行第1列摆放皇后 while(x[k]<=n&&!place(x,k))//判断是否可行 x[k]++;//否,则在下一列摆放 if(x[k]<=n)//判断以上循环结束之后,列数是否超标 { if(k==n)//判断是否摆放完最后一个皇后 printSolution(x,n);//是,则输出此时的解 else { k++;//否则,使行标加一 x[k]=0;//将第k行清空 } } else//如果列数超标,说明当前行无法摆放皇后。 k--;//使k减一,下次循环时将重新摆放第k行的皇后 } } void printSolution(int *x,int n)//用于输出结果的函数 { int i,j; /*使用双重循环输出平面图像*/ for(i=1;i<=n;i++) { for(j=1;j<=n;j++) { if(j==x[i]) printf("Q "); else printf("* "); } printf("\n"); } printf("\n"); }
结果:
解析:
1.对于place函数中两个判断条件的解析:
第一个判断条件很好理解:两个皇后不能在同一列上.
第二个判断条件是通过线的表达式推断的。将棋盘视为平面直角坐标系,每个皇后作为坐标系上一个特殊的点。则能以每个皇后的坐标画出两条斜率分别为1和-1的直线。要保证两个皇后的位置不冲突,只要保证两个皇后衍生出的所有直线上都不出现另一个皇后即可。(例如:x[i]和x[k]两个皇后的坐标分别是:(i,x[i]),(k,x[k])。则如果两个皇后出现在同一直线上,必有:y=x+(x[i]-i)和y=x+(x[k]-k)是同一条直线。或y=-x+(x[i]+i)和y=-x+(x[k]+k)是同一条直线。则可得:x[i]-x[k]=i-k或x[i]-x[k]=k-i。即:|x[i]-x[k]|=|i-k|
2.以上代码中的x数组第0个元素不使用,数组下标n代表棋盘的第n行,其中的数组值m代表第n行第m列放置一个皇后。这种设计保证每行有且只有一个皇后,规避了大量重复判断的同时也比二维数组更节省资源。
3.以上代码中nQueens函数使用了循环加回溯保证例遍每一种皇后摆放情况。如无法直接理解,可使用单步调试观察。
贪心算法
定义:通过局部最优达到全局最优
代码示例1:
#include <stdio.h> #include <stdlib.h> /*这个程序用来测试贪心算法*/ /* 对于以下问题: 有3种硬币,面值分别为1元、5角和1角,各自的数量不限。 设计自动售货机时,需要为顾客找零钱,请设计程序进行计算, 要求给顾客的硬币枚数最少 */ int main() { int money[10]={100,50,10,0};//存储着以分为单位的各种货币 int x;//x用于存储需要找零的金钱总数 int i=0,n=0,m;//n用于存储零钱总枚数 scanf("%d",&x); /*只要需要找零的金钱数还未归零,并且还存在面值更小的零钱,就继续循环*/ while(x>0&&money[i]!=0) { m=x/money[i];//使m为当前货币可使用的最大量 n+=m;//货币总枚数增加 x-=m*money[i];//剩余需要找零的金钱减少 i++;//i++,使用数组中存储的下一种零钱继续找零 } if(x==0)//最终,如果成功找零 printf("%d\n",n);//输出零钱枚数 else printf("fail\n");//否则,输出失败 return 0; }结果:
解析:
1.以上程序的核心是:首先用面值较大的零钱进行找零。直到无法找零,再用面值较小的找零。
2.贪心思路:不从整体最优考虑,而是保证所做的每一步选择都是局部最优的方案,最终将所有的局部最优糅合为整体最优。
代码示例2:
#include <stdio.h> #include <stdlib.h> /* 货船装箱问题 已知一批集装箱的重量,要将它们装入一个载重量为c的货船中, 在货船装载体积不限的前提下,怎样才能将最多的集装箱装上船? */ /*使用贪心法能解决此问题*/ /* 设货船重量c为13,共5个集装箱重量依次为5,7,6,3,2 只要优先将重量轻的装上船即可。 */ #define N 50 int main() { /*变量定义及初始化*/ int w[N];//用于存储各个集装箱的重量 int x[N];//用于记录已装入货船的箱子 int t[N];//用于存储各个集装箱的编号 int c;//最大载重量 int n;//箱子总数 int i,j,tmpw,tmpt; printf("输入货船的最大载重量:"); scanf("%d",&c); printf("输入集装箱个数:"); scanf("%d",&n); printf("输入每个集装箱的质量:\n"); for(i=0;i<n;i++) scanf("%d",&w[i]); for(i=0;i<n;i++) t[i]=i; /*冒泡法为输入的集装箱按重量从小到大排序*/ for(i=0;i<n-1;i++) for(j=0;j<n-i-1;j++) if(w[j]>w[j+1]) { tmpw=w[j]; w[j]=w[j+1]; w[j+1]=tmpw; tmpt=t[j]; t[j]=t[j+1]; t[j+1]=tmpt; } /*从小到大将集装箱装入货船,直到无法再装*/ for(i=0;i<n&&w[i]<=c;i++) { x[t[i]]=1; c=c-w[i]; } /*输出装入的箱子列表*/ for(i=0;i<n;i++) { if(x[i]==1) printf("BOX:%d\t",i); } return 0; }结果:
解析:
同样是很容易理解的贪心思路:要装入的箱子数目最多,只要将重量小的箱子优先装入即可。
贪心算法的核心是:
每次抉择时,都“贪便宜”。无视这个选择对未来的抉择的影响,只选择当前最优的选项。这种算法在大多数情况下并不能使最终结果“最优”,但常常能得到“ 较优”的结果。在实际应用时,难点不在于贪心的实现,而在于判断题目是否可以用贪心来解。以及怎样的选择才是“当前最优”的。