1.分治
分治法将原问题划分成若干个规模较小而结构与原问题相同或相似的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到原问题的解。
子问题应该是相互独立的,没有交叉的。
2.递归
定义递归:”要理解递归,你要先理解递归,直到你能理解递归“
递归在于反复调用自身函数,但是每次都把问题的范围缩小,直到范围缩小到可以直接得到边界数据的结构,然后在返回的路上求出对应的解。
for循环里的递归,和双重递归
这是一个非常繁杂的概念,过程较为麻烦,在for循环里嵌套调用递归,最为典型的算法便是全排列算法
在此之前需要了解递归在程序中运行的顺序:
Void f1(){
f2();
f3();
}
Void f2()
{
……
}
Void f3()
{
……
}
这里我们知道,如果f2这个方法不返回的话,f3是不会被执行的,如果这个时候把f2()变成f1(),即和调用他的方法是一样的,即自己调用自己,这个时候就变成了递归,我们在分析递归程序的时候也要时刻注意这一点。
让我们看全排列问题
从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起叫做从n个不同元素中取出m个元素的一个排列,当m=n时所有的排列情况叫全排列。
如1,2,3三个元素的全排列为:
1,2,3
1,3,2
2,1,3
2,3,1
3,1,2
3,2,1
上图为转载@麦穗的守望者
对我接下来要举的例子仅作启发,并不是程序的执行图,但是上图所阐述的思想与程序一致
- 在for循环里面嵌套的递归语句,每次调用递归到边界条件后,进行返回,在返回到递归函数时,第一个返回值便已经完成 打印(1,2,3) ,注意,此时返回的递归函数,是返回的路径上,第一次可以进行for循环,即i=1的那条递归语句
- 到i = 1的那条语句后,i++,重复递归函数的调用过程,打印(1,3,2)
- . 此时,再次返回,返回到可以进行i++的那条递归语句,i++,再进行一次与上次递归基本一致的递归调用,此时具体分析函数的调用,执行递归语句下面那条语句,调用递归到边界条件,返回打印(2,1,3)
- 依次类推,打印所有的全排列
完整的递归全排列代码如下,摘自算法笔记
#include<iostream>
using namespace std;
const int maxn=11;
int n,P[maxn],hashTable[maxn]={false};
void generateP(int index){
if(index==n+1){//递归边界,已经处理完排列1~n位了
//可以输出了
for(int i=1;i<=n;i++){
cout<<P[i];
}
cout<<endl;
return;
}
for(int x=1;x<=n;x++){ //枚举1~n 试图将x填入P[index] 每次都从小到大枚举所有数字
if(hashTable[x]==false){//如果x不在P[0]~P[index-1]中(前index-1已经填好了)
P[index]=x; //将x填入P中 也即是index位
hashTable[x]=true; //记x在P中
generateP(index+1); //处理排列的第index+1号位
hashTable[x]=false; //已经处理完P[index]为x的子问题,还原状态,让第index位填其他数字前要先将此x放开,否则没机会了
}
}
}
int main(){
//千万不要二次定义变量n 否则全局变量变成局部变量 害死人
while(cin>>n){
generateP(1);//从1开始填
}
return 0;
}
回溯算法
八皇后问题:
八皇后问题,是指在8X8d的棋盘上放置八个皇后,使得她们不能互相攻击,皇后的攻击范围是同行同列,或是在一条对角线上,满足上列条件的摆法一共有多少种?
与回溯法相对应的是暴力法,也称为暴力检索法:采用组合数的方式来枚举每一种情况(即从n的平方个位置中选择n个位置),需要枚举量巨大,n = 8 时便有54502232次枚举,相当复杂。
经过改良:考虑到每行每列都只能放置一个皇后,那么如果把n列皇后的行号依次写出就是一个1-n的排列,因此所有放置的可能就是一个全排列算法,与上文相对应。n = 8时也只有92次,大大减少了复杂度
#include <cstdio>
#include <stdlib.h>
const int maxn = 11;
int n,P[maxn],hashTable[maxn] = {false};
int count = 0;
void generateP(int index)
{
if(index == n+1) //递归结束边界条件
{
bool flag = true; //flag为true时,表示当前排列为一个合法方案
for(int i = 1;i <= n;i++)
{
for(int j = i+1; j <=n ;j++)
{
if(abs(i-j) == abs(P[i]-P[j]))
flag = false; //不合法,因为在同一对角线上
}
}
if(flag) count++; //如果合法,记录合法的方案个数
return;
}
for(int x = 1; x <= n; x++)
{
if(hashTable[x] == false)
{
P[index] = x;
hashTable[x] = true;
generateP(index+1);
hashTable[x] = false;
}
}
}
int main()
{
n = 8;
generateP(1);
printf("%d",count);
return 0;
}
这种枚举所有做法,判断每一种情况的合法性的做法是十分朴素的(一般把不使用优化算法,直接用朴素算法来解决问题的做法称为暴力法),事实上,通过思考可以发现,当使用放置了一部分皇后时(对应生成了排列的一部分),前面的皇后已经违反了不能在同一条对角线上的规则,因此后面的皇后无论如何放置都不能满足要求,此时便没有必要往下递归了,直接返回上层即可,如此以来可减少很多计算量。
回溯法
一般来说,如果在到达递归边界前的某层,由于一些事实导致已经不需要往任何一个子问题递归,就可以直接返回上一层,一般把这种算法称为回溯法
下面代码在上面代码的基础上使用回溯法
#include <cstdio>
#include <stdlib.h>
const int maxn = 11;
int n,P[maxn],hashTable[maxn] = {false};
int count = 0;
void generateP(int index)
{
if(index == n+1) //递归结束边界条件]
{
count++;
return;
}
for(int x = 1; x <= n; x++)
{
if(hashTable[x]==false) //第x行还没有皇后
{
bool flag = true; //flag为true表示当前皇后不会和之前的皇后冲突
for(int pre = 1;pre < index;pre++) //遍历之前的皇后
{
if(abs(index-pre)==abs(x-P[pre]))
{
flag = false; //当前皇后与之前的皇后在一条对角线,冲突,不进行递归,返回
break;
}
}
if(flag) //如果满足上述条件,把皇后放在x行,进行排列保证行列不冲突
{
P[index] = x;
hashTable[x] = true;
generateP(index+1);
hashTable[x] = false;
}
}
}
}
int main()
{
n = 8;
generateP(1);
printf("%d",count);
return 0;
}
给出例题
问题 D: 八皇后
时间限制: 1 Sec 内存限制: 32 MB
提交: 1075 解决: 593
[提交][状态][讨论版][命题人:外部导入]
题目描述
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。
对于某个满足要求的8皇后的摆放方法,定义一个皇后串a与之对应,即a=b1b2…b8,其中bi为相应摆法中第i行皇后所处的列数。已经知道8皇后问题一共有92组解(即92个不同的皇后串)。
给出一个数b,要求输出第b个串。串的比较是这样的:皇后串x置于皇后串y之前,当且仅当将x视为整数时比y小。
输入
第1行是测试数据的组数n,后面跟着n行输入。每组测试数据占1行,包括一个正整数b(1 <= b <= 92)
输出
输出有n行,每行输出对应一个输入。输出应是一个正整数,是对应于b的皇后串。
样例输入
3
6
4
25
样例输出
25713864
17582463
36824175
Accept
#include <cstdio>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
const int maxn = 11;
int m,n,P[maxn],hashTable[maxn] = {false};
int count = 0;
int ans[100] = {0},k = 0;
void generateP(int index)
{
if(index == n+1)
{
for(int i = 0;i<9;i++)
{
ans[k] = ans[k]*10+P[i];
}
k++;
return;
/*for(int i = 1;i <= n;i++)
{
printf("%d",P[i]);
}
printf("\n");
return;*/
}
for(int x = 1; x <= n; x++)
{
if(hashTable[x]==false) //第x行还没有皇后
{
bool flag = true; //flag为true表示当前皇后不会和之前的皇后冲突
for(int pre = 1;pre < index;pre++) //遍历之前的皇后
{
if(abs(index-pre)==abs(x-P[pre]))
{
flag = false; //当前皇后与之前的皇后在一条对角线,冲突,不进行递归,返回
break;
}
}
if(flag) //如果满足上述条件,把皇后放在x行,进行排列保证行列不冲突
{
P[index] = x;
hashTable[x] = true;
generateP(index+1);
hashTable[x] = false;
}
}
}
}
int main()
{
int a,b;
n=8;
generateP(1);
while(scanf("%d",&a)!=EOF)
{
for(int i = 0;i < a;i++)
{
scanf("%d",&b);
printf("%d\n",ans[b-1]);
}
}
return 0;
}
思路
先把92中结果转换为对应的92个整数保存在ans数组中。
再进行main函数里的取值输出
整数转换算法
if(index == n+1)
{
for(int i = 0;i<9;i++)
{
ans[k] = ans[k]*10+P[i];
}
k++;
return;
}
递归经典问题:苹果的摆放
C - 放苹果 POJ - 1664
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
Input
第一行是测试数据的数目t(0 <= t <= 20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。
Output
对输入的每组数据M和N,用一行输出相应的K。
Sample Input
1
7 3
Sample Output
8
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int solve(int n,int m)
{
if(n == 1 || m == 1 || n == 0)
return 1;
if(n<m)
return solve(n,n);
else
return solve(n,m-1)+solve(n-m,m);
}
int main()
{
int t,n,m;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
printf("%d\n",solve(n,m));
}
return 0;
}
这道题可以采取递归的思想,分为两种状况,n为苹果数,m为盘子数
第一:当n=m时,那么
1.将至少其中一个盘不放,那么就是n个苹果放到m-1个盘的方法
2.每个盘放一个,然后就是n-m个放在m个盘的放法