博主联系方式:
QQ:1540984562
QQ交流群:892023501
群里会有往届的smarters和电赛选手,群里也会不时分享一些有用的资料,有问题可以在群里多问问。
1、穷举法
基本思想:
在可能的解空间中穷举出每一种可能的解,并对每一个可能解进行判断,从中筛选出问题的答案。
关键步骤:划定问题的解空间,并在该解空间中一一枚举每一种可能的解。
注意点:
1、解空间的划定必须保证覆盖问题的全部解:只有问题的解集属于解空间集合才能使用穷举法求解。
2、解空间集合以及问题的解集一定是离散的集合。即可列的、有限的
例1:寻找给定区间的素数
分析:最简便的方法就是穷举法,在1-100中对每个整数进行判断。
code:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include "malloc.h"
#include "conio.h"
int IsPrime(int n)
{
//判断n是否为素数,如果是返回1,如果不是返回0
int i;
for (i=2;i<n;i++)
{
if (n % i == 0) return 0;
}
return 1;
}
void GetPrime(int low, int high)
{
//寻找【low,high】之间的素数
int i;
for (i=low;i<=high;i++)
{
if (IsPrime(i))
{
printf("%d ",i);
}
}
}
//测试函数
int main()
{
int low, high;
printf("输入搜索范围:\n");
printf("搜索起点:");
scanf("%d",&low);
printf("搜索终点:");
scanf("%d", &high);
printf("区域中的素数为:\n");
GetPrime(low,high);
_getche();
return 0;
}
例2:TOM的借书方案:
TOM共有5本新书,要借给A、B、C三个同学,每人只能借1本书,则TOM可以有多少种借书方法?
分析:设5本书的序号为{1,2,3,4,5},每个同学借的书范围限定于此。方案总数不可能超过5 * 5 * 5=125种。由于1本书一次只能借给1位同学,因此在每一种借书方案中,元素有重复的排列组合一定不会是问题的解。所以可以描述如下:
//测试函数
int main()
{
int i, j, k;
int print_times = 0;
printf("有如下的几种方案:\n");
for(i=1;i<=5;i++)
for(j=1;j<=5;j++)
for(k=1;k<=5;k++)
if (i != j && j != k && i != k)
{
print_times++;
printf(" (%d,%d,%d) ",i,j,k); //输出借书方案
if (print_times % 10 == 0) printf("\n");
}
_getche();
return 0;
}
效果:
2、贪心算法
贪心算法不从整体最优上加以考虑,所做的仅是在某种意义上的局部最优解。而局部最优解叠加到一起便是该问题整体上的最优解,或者近似最优解。
严格意义上讲,要使用贪心算法求解问题,该问题应当具备如下性质:
1、贪心选择性质:指求解的问题的整体最优解可以通过一系列的局部最优解得到。所谓局部最优解,就是指在当前的状态下做出的最好的选择。
2、最优子结构性质:
一个问题的最优解包含着它的子问题的最优解
例题1:最优装船问题
有一批集装箱要装入一个载重量为C的货船中,每个集装箱的质量由用户自己输入指定,在货船的装载体积不限制的前提下,如何装载集装箱才能尽可能多地将集装箱装入货船中?
算法设计:
1、数组w[]存放每个集装箱的质量
2、数组x[]存放每个集装箱的取舍状态
3、变量c存放货船的剩余载重量,初始值为C
4、为了使装载可能多的集装箱,每次都选出数组w[]中当前w[i]值最小的集装箱,并将x[i]置1,同时c=c-w[i]
5、循环执行4操作,直道w[i]>c,表明货船装货已经达到最大载重量,输出x[]中所有x[i]=1的下标i
代码描述
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include "malloc.h"
#include "conio.h"
void swap(int& a, int& b)
{
//方法一:
int tmp = 0;
tmp = b;
b = a;
a = tmp;
//方法二:
//a = a+b;
//b = a-b;
//a = a -b;
//方法三:
//a ^= b ^= a ^= b;
//方法四:
//a = a+b-(b=a);
}
void sort(int w[], int t[], int n)
{
int i, j;
//动态开辟一个临时数组,存放w[]中的内容,用于排序
int* w_tmp = (int*)malloc(sizeof(int) * n);
for (i = 0;i < n;i++)
t[i] = i; //初始化t[]
for (i = 0;i < n;i++)
w_tmp[i] = w[i]; //复制数组w[]到w_tmp[]中
//将w_tmp[]与t[]共同排序,当w_tmp实现从小到大排列后,t[]中存放的就是w_tmp[]元素在w[]中对应的下标
for (i = 0;i < n - 1;i++)
for (j = 0;j < n - i - 1;j++)
if (w_tmp[j] > w_tmp[j + 1])
{
swap(w_tmp[j], w_tmp[j + 1]);
swap(t[j], t[j + 1]);
}
}
//输入: x[]集装箱取舍状态 w[]每个集装箱的质量 c船的剩余载重量 n一共几个货物
void Loading(int x[],int w[],int c,int n)
{
int i, s = 0;
//动态开辟出一个临时数组,存放w[]的下标,如果t[i],t[j],i<j,则有w[t[i]] <= w[t[j]];
int* t = (int*)malloc(sizeof(int)*n);
sort(w, t, n); //排序,用数组t[]存放w[]的下标
for (i = 0;i < n;i++)
x[i] = 0; //初始化取舍状态数组
for (i = 0;i < n && w[t[i]] <= c;i++)
{
x[t[i]] = 1;
c = c - w[t[i]];
}
}
//t存放数组w[]的下标,使得如果i<j,则w[t[i]]<=w[t[j]];
//例如w[3]={5,3,2} 则t[3]={2,1,0},即将w[]从小到大排序,然后记录下w[i]的原本下标i在新的顺序中的位置。
//原本5,3,2三个元素排列顺序为:5,3,2,对应下标0,1,2,从小到大排之后5,3,2三个元素排列顺序为:2,3,5,下标对应原来的(2,1,0)
int main()
{
int x[5], w[5], c, i;
printf("输入船最大载重量:\n");
scanf("%d",&c);
printf("输入5个载货箱的重量:\n");
for (i = 0;i < 5;i++)
scanf("%d",&w[i]);
//进行最优装载
Loading(x,w,c,5);
printf("这些箱子将被装入:\n");
for (i = 0;i < 5;i++)
{
if (x[i] == 1) printf("BOX:%d ",i);
}
_getche();
return 0;
}
3、递归与分治算法
基本思想:
将规模较大的问题分割为规模较小的同类问题,然后将这些子问题逐个加以解决,最终也就将整个大的问题解决了。
分治思想:举例,折半查找就是利用序列之间元素的顺序关系,不断缩小问题的规模,每次都将问题缩小为原来的1/2,采用顺序的方法查找需要O(n),而折半只需要O(long2n)
递归思想:一种直接或间接地调用原算法本身的一种算法。
设计递归算法需要注意的几点:
1、每个递归函数都必须有一个非递归定义得初始值,作为递归结束标志,或递归结束的出口
2、递归算法的运行较低,时间和空间复杂度都比较高,对于一些对时间和空间要求较高的程序,建议使用非递归算法设计。
例题:
1、计算整数的划分数
将一个正整数n表示为一系列的正整数之和:
n=n1+n2+n3+n4;(n1>=n2>=n3>=nk>=1,k>=1)
被称为正整数n的一次划分。一个正整数n可能存在不同的划分,例如正整数6的全部的划分为;
6=6
6=5+1
6=4+2、6=4+1+1
6=3+3、6=3+2+1、6=3+1+1+1
6=2+2+2、6=2+2+1+1、6=2+1+1+1+1
6=1+1+1+1+1+1
正整数n的不同的划分的个数被称为该正整数n的划分数。
编写一个程序,计算输入的正整数n的划分数
分析:设标记p(n,m)表示正整数n的所有不同的划分中,最大加数不大于m的划分个数。例如P(6,2)=4,因为在正整数6的全部划分中,最大加数不大于2的只有:6=2+2+2、6=2+2+1+1、6=2+1+1+1+1
6=1+1+1+1+1+1,四个划分。
建立如下4个递归关系:
1、P(n,1) =1.n>=1
任何正整数n的划分中,加数不大于1的划分有且仅有1种,即n=1+1+1+…+1(n个1)
2、P(n,m)=P(n,n),m>=n
最大加数等于n,不存在最大加数大于n的情况
3、P(n,n)=P(n,n-1)+1
最大加数为n的划分只有1种,即n=n,因此最大加数不大于n的划分数就是P(n,n-1)+1
4、P(n,m)=P(n,m-1)+P(n-m,m),n>m>1
正整数n的最大加数不大于m的划分数=n的最大加数不大于m-1的划分数+n的最大加数为m的划分数
例:
根据这四个式子书写code:
int P(int n, int m)
{
if (m == 1 || n == 1) return 1;
if (m > n) return P(n,n);
if (m == n) return 1 + P(n, m - 1);
return P(n, m - 1) + P(n - m, m);
}
int main()
{
int n, s;
printf("请输入一个整数\n");
scanf("%d",&n);
s = P(n, n);
printf("整数的最大划分数为 %d \n",s);
_getche();
return 0;
}
2、递归折半查找
原本的折半查找code:
int bin_search(keytype key[], int n, keytype k)
{
int low = 0, high = n - 1, mid;
while (low <= high)
{
mid = (low + high) / 2;
if (key[mid] == k)
return mid;
if (k > key[mid])
low = mid + 1; //在后半序列中查找
else
high = mid - 1;
}
return -1; //查找失败,返回-1
}
修改为递归形式:
typedef int keytype;
int bin_search(keytype key[], int low,int high,keytype k)
{
int mid;
if (low > high) return -1;
else
{
mid = (low+high) / 2;
if (key[mid] == k)
return mid;
else if (k > key[mid])
return bin_search(key, mid + 1,high,k);
else
return bin_search(key,low,mid-1,k);
}
}
int main()
{
int n, i, addr;
int A[10] = { 2,3,5,7,8,10,12,15,19,21 };
printf("初始序列为:\n");
for (i = 0;i < 10;i++)
printf("%d ",A[i]);
printf("\n输入待查找的元素:\n");
scanf("%d",&n);
addr = bin_search(A,0,9,n);
if (addr != -1)
{
printf("%d 下标为%d",n,addr);
}
else
{
printf("元素不在序列中");
}
_getche();
return 0;
}
4、回溯算法
基本思想:
在包含问题的所有解的解空间中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去;如果该结点不包含问题的解,那就说明以该结点为根结点的子树一定不包含问题的最终解,因此要跳过对以该结点为根的子树的系统探索,逐层向其祖先结点回溯。这个过程叫做解空间树的“剪枝”操作。
如果应用回溯法求解问题的所有解,要回溯到解空间树的树根,这样才能保证根结点的所有子树都被探索到才结束。如果只要求得问题的一个解,那么在探索解空间树时,只要搜索到问题的一个解就可以结束了。
1、四皇后问题求解
问题描述:求解如何在一个NxN的棋盘上无冲突地摆放N个皇后棋子,在任意一个皇后所在位置的水平、数值、以及45度斜线上都不能出现其他皇后的棋子,否则视为冲突。
以四皇后问题为例,构建出一棵空间树,通过探索这棵解空间树,可以得到四皇后的一个解或者几个解。
根结点派生出4个子结点,每个结点都示意出前两个皇后可能摆放的位置。每个结点又可以派生出4个子结点,每个结点都示意出前3个皇后可能摆放的位置,整个解空间树为一棵4叉的满树,共包含4x4x4+4x4+4+1=85个结点.
应用回溯法解决问题:从根结点出发,深度优先搜索整个解空间树。当访问到根结点的第一个孩子时,观察结点,如果该结点不包含问题的解,那么由该结点作为根结点派生出来的子树中也肯定不会包含四皇后问题的解,停止向下搜索,回溯到根结点,继续太多根结点的下一个孩子结点。这就是”剪枝“操作,可以减少搜索的次数
在解决出四皇后问题时,并不一定要真的构建出这样的一棵解空间树,它完全可以通过一个递归回溯来模拟。所谓解空间树只是一个逻辑上的抽象。当然也可以用树结构真实地创建出一棵解空间树,不过比较浪费空间资源。
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include "malloc.h"
#include "conio.h"
//四皇后问题求解
int count = 0; //记录四皇后问题解的个数
//判断该位置的8邻域是否存在皇后,如果有返回0 如果没有返回1
int IsCorrect(int i,int j,int (*Q)[4])
{
int s, t;
for (s = i, t = 0;t < 4;t++)
if (Q[s][t] == 1 && t != j) return 0; //判断是否在同一行
for (t = j, s = 0;s < 4;s++)
if (Q[s][t] == 1 && s != i) return 0; //判断是否在在同一列
for (s = i - 1, t = j - 1;s >= 0 && t >= 0;s--, t--)
if (Q[s][t] == 1) return 0; //判断是否在左上方
for (s = i + 1, t = j + 1;s < 4 && t < 4;s++, t++)
if (Q[s][t] == 1) return 0; //判断是否在右下方
for (s = i - 1, t = j + 1;s >= 0 && t <4;s--, t++)
if (Q[s][t] == 1) return 0; //判断是否在右上方
for (s = i + 1, t = j - 1;s < 4 && t >=0;s++, t--)
if (Q[s][t] == 1) return 0; //判断是否在左下方
//否则返回1
return 1;
}
//输入参数: j:二维数组Q的列号(0-3),最开始调用时,j=0,表明第一个皇后摆放在棋盘上的第一列。(*Q)[4]:指向二维数组每一行的指针
//中间变量:i:二维数组Q的行号(0-3),通过对变量i的4次循环,可以分别探索四皇后问题的4棵解空间树(以Q[0][0]、Q[1][0]、Q[2][0]、Q[3][0]为解空间树的根结点)
//当j=4时,表明Q第0-3列都已经放置好棋子了,说明此时得到了一个四皇后的解,因此程序返回上一层
void FourQueen(int j, int(*Q)[4])
{
int i, k;
//如果得到一个解
if (j == 4)
{
//打印出结果
for (i = 0;i < 4;i++)
{
for (k = 0;k < 4;k++)
printf("%d ", Q[i][k]);
printf("\n");
}
printf("\n");
//解个数+1
//返回到上一级
_getche();
count++;
return;
}
//否则
for (i = 0;i < 4;i++)
{
if (IsCorrect(i, j, Q)) //如果Q[i][j]可以放置皇后,表明这一列不需要再探索了
{
Q[i][j] = 1;
FourQueen(j+1,Q); //探索下一列,递归深度优先搜索
//Q[i][j]=0,以便循环,试探Q[i+1][j](下一行)是否可以摆放皇后,因为如果这一行如果没有找到解,需要清除这一行痕迹,然后到下一行继续探索
Q[i][j] = 0;
}
}
}
//测试函数
int main()
{
int Q[4][4];
int i, j;
//初始化数组Q
for (i = 0;i < 4;i++)
for (j = 0;j < 4;j++)\
Q[i][j] = 0;
FourQueen(0,Q); //执行四皇后求解
printf("四皇后解法有%d种\n",count);
_getche();
}
效果:
2、上楼梯问题
已知楼梯有20阶,上楼可以一步上1阶,也可以一步上2阶。编写一个程序计算共有多少种不同的上楼梯方法。
分析:采用递归回溯,用一棵空间二叉树描述
从根结点出发向下搜索,每经过一个结点,则表示这一步登上的台阶数。每次都将访问结点中的数字相加,记录为当前已经走过的台阶数,当这个数字等于20,就表示寻找出了一种上楼梯的方案。那么该结点以下的结点也就没必要再访问了,而是向其父结点回溯并继续探索下一分支。
代码:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include "malloc.h"
#include "conio.h"
#define MAX_STEPS 20 //定义20个台阶的楼梯
int Steps[MAX_STEPS] = { 0 }; //Steps[i]等于1或者等于2,记录第i步登上的台阶数
int cnt = 0; //记录上楼梯的方案的数目
void Upstairs(int footstep,int count,int step)
{
//footsetp:当前要登的台阶数、count:已经走过的阶数、step:走过的步数
int i;
if (count + footstep == MAX_STEPS)
{
//得到一种上楼梯方案
Steps[step] = footstep;
cnt++; //方案数+1
//打印出方案
for (i = 0;i < step;i++)
{
printf("%d ",Steps[i]);
}
printf("\n");
//返回到父结点,不必再向下搜索
}
else if (count + footstep > MAX_STEPS)
{
//超过了楼梯的阶数,不必再向下搜索,返回到父结点
return;
}
else
{
//
Steps[step] = footstep; //记录当前上楼梯的台阶数
step++; //步数+1
count = count + footstep; //记录目前已经走过的台阶数
//递归探索后续的分支
Upstairs(1,count,step); //走1步的分支
Upstairs(2, count, step); //走2步的分支
}
}
void UpstairsALL()
{
Upstairs(1,0,0); //从第一步上1个台阶开始探索解空间树
Upstairs(2, 0, 0); //从第一步上2个台阶开始探索解空间树
}
int main()
{
UpstairsALL();
printf("一共有%d种解法\n",cnt);
_getche();
}
效果:
5、数值概率算法
概率算法允许在执行过程中随机地选择下一步的计算步骤,因此使用概率算法有时会大大降低复杂度,提高算法的效率,但有时也可能得不到问题的全部答案。
概率算法可以分为4类:
1、数值概率算法
2、蒙特卡洛算法
3、拉斯维加斯算法
4、舍伍德算法
这里只介绍最基础的数值概率算法。
数值概率算法常用于解决数值计算的问题。应用数值概率算法往往只能得到问题的近似解,并且该近似解的精度一般随着计算时间的增加而不断提高。
例:f(x)=1-x^2;计算定积分:
该积分的几何意义如下:
如果随机地向图中虚线与x,y坐标轴所围成的正方形中投点,那么根据几何概率知识可知,随机点落入阴影区域的概率即为阴影部分的面积与虚线与x,y轴所围成的正方形的面积之比。
所以只要求出随机点落入阴影区域的概率的概率就求出了定积分的近似值。
算法描述:
#include<iostream>
#include <stdio.h>
#include <stdlib.h>
#include "malloc.h"
#include "conio.h"
#include "time.h"
double Darts(int n) //n为试验投点的个数,决定了计算概率的精度
{
double x, y;
time_t t;
int i, count = 0;
srand((unsigned)time(&t));
for (i = 0;i < n;i++)
{
//随机初始化投点的坐标
x = rand() % 100 / 100.0;
y = rand() % 100 / 100.0;
if (y <= 1 - pow(x, 2))
count++; //如果随机点落入阴影区域,则用count++记录个数
}
return (double)count / (double)n; //返回概率
}
//测试函数
int main()
{
int n;
char c;
printf("请输入试验精度(越大精度越好):\n");
scanf("%d", &n);
printf("结果为:\n");
printf("%f\n", Darts(n));
_getche();
return 0;
}