一、实验目的
1.加深学生对分治法算法设计方法的基本思想、基本步骤、基本方法的理解与掌握;
2.提高学生利用课堂所学知识解决实际问题的能力;
3.提高学生综合应用所学知识解决实际问题的能力。
二、实验任务
1、 编写一个生命游戏:
规则如下:(或者网上找到更详细的规则)
一个人可以有8个邻居;
一个人若只有一个邻居,在下一代会孤独的死去;
若有2或3个邻居,在下一代依然活着;
若有4个或以上邻居,在下一代会因拥挤而死;
死去的人若有3个邻居,在下一代会复活;
所有的死去或复活都在下一代变化时同时发生。
2、 带锁的门:
在走廊上有n个带锁的门,从1到n依次编号。最初所有的门都是关着的。我们从门前经过n次,每次都从1号门开始。在第i次经过时(i = 1,2,…, n)我们改变i的整数倍号锁的状态;如果门是关的,就打开它;如果门是打开的,就关上它。在最后一次经过后,哪些门是打开的,哪些门是关上的?有多少打开的门?
3、三壶谜题:
有一个充满水的8品脱的水壶和两个空水壶(容积分别是5品脱和3品脱)。通过将水壶完全倒满水和将水壶的水完全倒空这两种方式,在其中的一个水壶中得到4品脱的水。
4、串匹配问题
给定一段文本,在该文本中查找并定位任意给定字符串。
要求:
(1)实现BF算法;(必做)
(2) 实现BF算法的改进算法:KMP算法和BM算法;(选做)
5、交替放置的碟子
我们有数量为2n的一排碟子,n黑n白交替放置:黑,白,黑,白…
现在要把黑碟子都放在右边,白碟子都放在左边,但只允许通过互换相邻碟子的位置来实现。为该谜题写个算法,并确定该算法需要执行的换位次数。
三、实验设备及编程开发工具
实验设备:Win8电脑
开发工具:Microsoft Visual c++ 6.0
四、实验过程设计(算法设计过程)
(1)生命游戏
规则可简化为以下,并使用case比对即可使用程式操作:
1.邻居个数为0,1,4,5,6,7,8时则该细胞下次的状态为死亡。
2.邻居个数为2时,则该细胞下次状态为复活。
3.邻居个数为3时,则该细胞下次状态为稳定代码是不断生成细胞存活的状态图,首先细胞默认都是死的,活细胞需要自己一个一个生成。
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <conio.h>
#include <iostream>
#define ROWLEN 10//二维空间行数;
#define COLLEN 10//二维空间列数;
#define DEAD 0//死细胞;
#define ALIVE 1//活细胞;
using namespace std;
int cell[ROWLEN][COLLEN];//当前生命细胞的状态;
int celltemp[ROWLEN][COLLEN];//用于判断当前细胞的下一个状态
void initcell() {
int row,col;
for(row=0; row<ROWLEN; row++) {
for(col=0; col<COLLEN; col++) {
cell[row][col]=DEAD;
}
}
printf("请先输入一组活细胞的坐标位置,请输入(-1,1)结束\n");
while(1) {
printf("请输入一个活细胞的坐标位置: ");
// scanf("%d%d",&row,&col);
cin>>row>>col;
if(0<=row&&row<ROWLEN&&0<=col&&col<COLLEN) {
cell[row][col]=ALIVE;
} else if(row==-1||col==-1) {
break;
} else {
printf("输入坐标超过范围。\n");
}
}
}
int LinSum(int row,int col) {
int count=0,c,r;
for(r=row-1; r<=row+1; r++) {
for(c=col-1; c<=col+1; c++) {
if(r<0||r>=ROWLEN||c<0||c>=COLLEN) {
continue;
}
if(cell[r][c]==ALIVE) {
count++;
}
}
}
if(cell[row][col]==ALIVE) {
count--;
}
return count;
}
void OutCell() {
int row,col;
printf("\n细胞状态\n");
// 这种奇怪的符号在制表符里有哦~
// printf("┍-");
for(col=0; col<COLLEN-1; col++) {
// printf("━━┳");
}
// printf("━━┓\n") ;
cout<<endl;
for(row=0; row<ROWLEN; row++) {
// printf("|");
for(col=0; col<COLLEN; col++) {
switch(cell[row][col]) {
case ALIVE:
// printf("●|");//活细胞;
printf("●");//活细胞;
break;
case DEAD:
// printf("○|");//死细胞;
printf("○");//死细胞;
break;
default:
;
}
}
printf("\n");
if(row<ROWLEN-1) {
// printf("├");
for(col=0; col<COLLEN-1; col++) {//输出一行;
// printf("━━┼");
}
// printf("━━┫");
}
}
// printf("┗");
for(col=0; col<COLLEN-1; col++) { //最后一行的横线;
// printf("━━┻");
}
// printf("━━┛\n");
cout<<endl;
}
void cellfun() {
int row,col,sum;
int count=0;
for(row=0; row<ROWLEN; row++) {
for(col=0; col<COLLEN; col++) {
switch(LinSum(row,col)) {//四周活细胞适量;
case 2:
celltemp[row][col]=cell[row][col];//保持细胞原样;
break;
case 3:
celltemp[row][col]=ALIVE;//复活;
break;
default://死了;
celltemp[row][col]=DEAD;
}
}
}
for(row=0; row<ROWLEN; row++) {
for(col=0; col<COLLEN; col++) {
cell[row][col]=celltemp[row][col];
}
}
for(row=0; row<ROWLEN; row++) {
for(col=0; col<COLLEN; col++) {
if(cell[row][col]==ALIVE) {//如果是活细胞;
count++;//累计或细胞数量;
}
}
}
sum=count;
OutCell() ;//显示当前细胞状态;
printf("当前状态下,一共有%d个活细胞。\n",sum);
}
int main() {
char again;
printf("生命游戏!\n") ;
initcell(); //初始化
OutCell(); //输出初始细胞状态;
printf("按任意键开始游戏,进行细胞转换。\n");
getch() ;
S1:
cellfun();
S2:
printf("\n继续生成下一次细胞状态(y/n)?");
fflush(stdin);
cin>>again;
if(again=='y'||again=='Y') {
goto S1;
} else if(again=='n'||again=='N') {
goto S3;
} else {
printf("输入错误,请重新输入!\n");
goto S2;
}
S3:
printf("游戏结束!\n");
return 0;
}
(2)带锁的门
门的状态只有两 种,每经过一次,状态就会发生变化。如果一道门经过奇数次,那么结果状态和原始状态就会不一样,而经过偶数次则不会发生变化。因此问题的关键就是找出那些 经过奇数次的门有多少道。很幸运,那些门的编号正好是整数i的完全平方数即1,4,9,16,…,因此只需要找出这样的数字有多少个即可。
如:假设n = 5
0表示门是关着的,1表示门打开的
一 二 三 四 五
一 1 1 1 1 1
二 1 0 1 0 1
三 1 0 0 0 1
四 1 0 0 1 1
五 1 0 0 0 0
可以发现对角线上的数字就是最后门打开的情况,正好是i的平方数
#include <stdio.h>
#define N 1000
int main()
{
int L[N];
int i,j,k;
int n;
printf("请输入小于1000的整数代表门的总数:");
while(1)
{
scanf("%d",&n);
if(n<0||n>1000)
printf("输入错误,请重新输入");
else break;
}
for(i=0;i<n;i++)
L[i]=0;
for(j=1;j<=n;j++)
for(k=1;k<=n;k++)
if(k%j==0)
L[k-1]=(L[k-1]+1)%2;
for(i=0;i<n;i++)
{
if(L[i]==1)
printf("第%d号门开着\n",i+1);
}
printf("\n");
return 0;
}
(3)三壶谜题
可以把每次三个水壶中水的量组成一个状态,比如初始状态为008,对应第一个水壶0品脱水,第二个水壶0品脱水,第三个水壶8品脱水。对题目的状态空间图进行广度优先遍历。当表示状态的数字中出现4时,即求出答案。
1、为了打印出倒水的过程,需要声明一个前置状态保存当前状态由哪个状态转换而来,然后就可以回溯到初始状态,打印出倒水过程。相当于树中的父结点。
2、可以声明一个map表,保存已有的状态,对已有的状态,就不再向下继续遍历,这样可以节省求解时间。
3、因为是广度优先遍历,所以第一次解得的答案所需的倒水的次数最少,解为最优解。
#include <iostream>
#include <vector>
#include <map>
#define MaxFirst 3
#define MaxSecond 5
#define MaxThird 8
using namespace std;
class State
{
public:
int second;
int num[3];
State* preState;
static map<int,int> mapping;
public:
State(int first,int second,int third)
{
num[0]=first;
num[1]=second;
num[2]=third;
}
void init()
{
mapping[0]=MaxFirst;
mapping[1]=MaxSecond;
mapping[2]=MaxThird;
}
bool canPour(int from,int to)//判断是否可以从from水壶中倒水到to水壶中
{
if(num[from]==0)
{
return false;
}
if(num[to]==mapping[to])
{
return false;
}
else
{
return true;
}
}
void pour(int from,int to)//倒水过程
{
if(num[from]+num[to]>mapping[to])
{
num[from]=num[from]-(mapping[to]-num[to]);
num[to]=mapping[to];
}
else
{
num[to]=num[to]+num[from];
num[from]=0;
}
}
};
map<int,int> State::mapping;
int main()
{
map<int,int> states;
State *start=new State(0,0,8);
start->init();
State *state=start;
State *endState=new State(8,8,8);//只有获得解endState才会改变,赋值全为8为了方便判断是否获得最终解
vector<State> action;//保存所有状态对象
action.push_back(*start);//把初始状态先加入队列中
int n=0;
do{
for(int i=0;i<3;i++)//双层循环为从i水壶中倒水入j水壶中
{
for(int j=0;j<3;j++)
{
if(i!=j)
{
if(state->canPour(i,j))
{
state->pour(i,j);
if(states[state->num[0]*100+state->num[1]*10+state->num[2]]==0)//如果该状态不在hash表中,即为第一次出现该状态
{
states[state->num[0]*100+state->num[1]*10+state->num[2]]++;
(state->preState)=new State(action[n]);
action.push_back(*state);
if(state->num[0]==4||state->num[1]==4||state->num[2]==4)//获得解
{
endState=state;
i=4;
break;
}
}
}
}
*state=action[n];
}
}
n++;
}while(endState->num[0]==8&&endState->num[1]==8&& n<action.size());
cout<<endState->num[0]<<" "<<endState->num[1]<<" "<<endState->num[2]<<endl;
state=endState;
do
{
state=state->preState;
cout<<state->num[0]<<" "<<state->num[1]<<" "<<state->num[2]<<endl;
}while(state->num[2]!=8);
return 0;
}
(4)串匹配问题
BF算法是将所有目标字符串中的子串与需查找的字符串进行比较,成功则返回该子串在目标字符串中的位置,失败则继续比较直至成功或所有子串全部比较,如果都失败,则返回一个成功是不可能出现的值。
在实现时有一个难点:如何获得全部子串。其实事实是并不用获取全部子串,只需要遍历整个目标字符串,分别让以目标字符串中的每个字符为开始位置的子串与需查找字符串对比,一旦对比失败,立即放弃所有以此字符开头的字符串,重新在目标字符串中以下一字符为开始的所有子串中查找需查找字符串。如果对比需查找字符串成功,直接返回该字符位置在目标字符串中的位置。在这里对比失败后需要定位目标字符串中下一字符的位置,这里使用双层循环跳出第二次循环的方法,也可以只用一层循环,使用当前目标字符串位置减去需查找字符串位置再加1。
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
/*
BF算法--串的匹配
*/
int BF(char s[],char t[])
{
int i=0,j=0;
while(i<strlen(s) && j<strlen(t))
{
if(s[i]==t[j])
{
i++;
j++;
}
else
{
i = i-j+1;
j = 0;
}
}
if(j>=strlen(t))
{
return (i-strlen(t));
}
else
{
return -1;
}
}
int main()
{
char s[]="";
char t[]="";
cin>>s;
cin>>t;
int cnt = BF(s,t);
printf("%d\n",cnt);
return 0;
}
(5)交替放置的碟子
首先把问题转化一下,用1表示黑碟子,0表示白碟子,那么目前的顺序是:
1010…1010
结果要求1都放在右边,0都放在左边。这个题目看起来很眼熟。看关键字:交换相邻的碟子,排好顺序。嗯,就是经常出现在面试中的冒泡排序了。
为便于观察,假设目前有6个碟子:101010。使用冒泡排序,第一次迭代,碟子序列变为:010101,交换3次。在进行第二次迭代之前,观察一下。
现在,不仅第一个碟子就位,最后一个也是了,因此第二次迭代只需要对第2到第5个进行排序,巧合的是,碟子[2->5]仍然是10交替出现,不过比上一次少了两个,这样就简单了,可以得到结论:对于2n个碟子,可以使用n次迭代完成,交换的次数分别是:n+(n-1)+…+2+1,即n(n+1)/2。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n,num=0;
printf("输入碟子的总数量:");
scanf("%d",&n);
printf("请输入碟子的数量2n,其中黑白碟子各n个:\n");
int sum[100];
// 将所有碟子存放在一个数组里,设白碟子值为1,黑碟子值为2,
//初始排序为:21212121……
//换位后排序为 11112222……
for(int i=0;i<=(n-2)/2;i++)
{
sum[2*i]=2;
sum[2*i+1]=1;
}
//输出碟子的初始排序
printf("一开始的顺序为:");
for(i=0;i<n;i++)
{
printf(sum[i]+" ");
if(sum[i]==1)
{
printf("白 ");
}
else
{
printf("黑 ");
}
}
printf("\n");
//进行排序
for(i=0;i<n-1;i++)
{
for(int j=0;j<(n-1-i);j++)
{
if(sum[j+1]<sum[j])
{
int t=sum[j];
sum[j]=sum[j+1];
sum[j+1]=t;
num++;
}
}
}
printf("排序后的顺序为:");
for(i=0;i<n;i++)
{
printf(sum[i]+" ");
if(sum[i]==1)
{
printf("白 ");
}
else
{
printf("黑 ");
}
}
printf("\n一共换位了%d次",num);
printf("\n");
return 0;
}
五、实验结果及算法复杂度分析
(1)
算法复杂度分析
时间复杂度:O(4n^2)
(2)
法复杂度分析
时间复杂度:O(n^2+2n)
(3)
算法复杂度分析
时间复杂度:O(n^2)
(4)
算法复杂度分析
最坏时间复杂度:O(m*n)
(5)
算法复杂度分析
时间复杂度:O(n(n+1)/2)
实验小结(包括问题和解决方法、心得体会等)
通过本次的实验,对分治算法有了一定的了解,分治就是把一个任务,分为形式和原任务相同,但是规模更小的几个部分任务,分别完成,或只需要选一部分完成,然后再处理完成后的这一个或几部分的结果,实现整个任务的完成。而且本次实验使得我又巩固了一遍c/c++的语言代码,在以后的学习中,还要努力去提高编程能力。