理工大算法实验报告


实验一  递归与分治算法

1.1  实验目的与要求

1.进一步熟悉C/C++语言的集成开发环境;

2.通过本实验加深对递归与分治策略的理解和运用。

1.2  实验课时

2学时

1.3  实验原理

分治(Divide-and-Conquer)的思想:一个规模为n的复杂问题的求解,可以划分成若干个规模小于n的子问题,再将子问题的解合并成原问题的解。

需要注意的是,分治法使用递归的思想。划分后的每一个子问题与原问题的性质相同,可用相同的求解方法。最后,当子问题规模足够小时,可以直接求解,然后逆求原问题的解。

1.4  实验题目

1.上机题目:格雷码构造问题

Gray码是一个长度为2n的序列。序列无相同元素,每个元素都是长度为n的串,相邻元素恰好只有一位不同。试设计一个算法对任意n构造相应的Gray码(分治、减治、变治皆可)。

对于给定的正整数n,格雷码为满足如下条件的一个编码序列。

(1)序列由2n个编码组成,每个编码都是长度为n的二进制位串。

(2)序列中无相同的编码。

(3)序列中位置相邻的两个编码恰有一位不同。

   2.设计思想:

除了最高位,格雷码的位元完全上下对称,在实现的时候我们可以利用递归在每一层前面加上0或者1,递归的边界是1位的格雷码就是0或者1,然后n位格雷码是在n-1位格雷码的基础上加上字符0或者1构成的。

3.代码设计:#include<iostream> 

#include<string> 

#include<cmath>

using namespacestd; 

void GrayCode(intn,string *data) 

    if(n==1) 

    { 

        data[0]="0"; 

        data[1]="1"; 

        return; 

    } 

    GrayCode(n-1,data); 

    int len=(int)pow(2,n); 

 

    for(int i=len/2;i<len;i++) 

    { 

       data[i]="1"+data[len-i-1]; 

    } 

    for(int i=0;i<len/2;i++) 

    { 

        data[i]="0"+data[i]; 

    } 

 

int main() 

cout<<pow(4,3)<<endl;//计算4的3次方

    int n; 

    cin>>n; 

    string *data=newstring[(int)pow(2,n)]; 

    GrayCode(n,data); 

    for(int i=0;i<(int)pow(2,n);i++) 

    { 

        cout<<data[i]<<endl; 

    } 

1.5  思考题

(1)递归的关键问题在哪里?

先弄清楚递归的顺序。在递归的实现中,往往需要假设后续的调用已经完成,在此基础之上,才实现递归的逻辑。

分析清楚递归体的逻辑,然后写出来

考虑递归退出的边界条件。return语句该放在哪儿。

(2)递归与非递归之间如何实现程序的转换?

递归问题非递归实现的基本思想:将原问题分解成若干结构与原问题相同,但规模较小的子问题,并建立原问题与子问题解之间的递推关系,然后定义若干变量用于记录递推关系的每个子问题的解;程序的执行便是根据递推关系,不断修改这些变量的值,使之成为更大子问题的解的过程;当得到原问题解时,递推过程便可结束了。

(3)分析二分查找和快速排序中使用的分治思想。

二分搜索每次都要舍弃一半,从留下的一半中寻找目标;而分治法把一个大问题分成两个或多个小问题,递归地求这些小问题的解,最后再把它们小心谨慎的合并起来,并且要仔细考虑合并时产生的新的情况。这当然没有错,但你也马上会从这里意识到两者的巨大联系。就拿选取数组中第k个最小的数的算法来说,有一个版本便是从快速排序中修改而来:划分后,舍弃掉不存在的区间,对剩余部分迭代,而快速排序是分治法的典型代表。

快速排序是找出一个元素(理论上可以随便找一个)作为基准(pivot),然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。递归快速排序,将其他n-1个元素也调整到排序后的正确位置。最后每个元素都是在排序后的正 确位置,排序完成。所以快速排序算法的核心算法是分区操作,即如何调整基准的位置以及调整返回基准的最终位置以便分治递归。

(4)分析二次取中法和锦标赛算法中的分治思想。

在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。然后根据枢纽值进行分割。在一趟排序中将待排序的序列分割成两组,其中一部分记录的关键字均小于另一部分。然后分别对这两组继续进行排序,以使整个序列有序。在分割的过程中,枢纽值的选择至关重要,本文采取了三位取中法,可以很大程度上避免分组"一边倒"的情况。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

实验二  贪心算法

2.1  实验目的与要求

1.理解贪心算法的基本思想;

2.运用贪心算法解决实际问题,加深对贪心算法的理解和运用。

2.2  实验课时

4学时(课内2学时+课外2学时)

2.3  实验原理

贪心算法的思想:

(1)贪心算法(GreedyApproach)能得到问题的最优解,要证明我们所做的第一步选择一定包含着一个最优解,即存在一个最优解的第一步是从我们的贪心选择开始。

(2)在做出第一步贪心选择后,剩下的子问题应该是和原问题类似的规模较小的子问题,为此我们可以用数学归纳法来证明贪心选择能得到问题的最优解。

2.4实验题目

1.上机题目:最小延迟调度问题

给定等待服务的客户集合A={1,2,…,n},预计对客户i的服务时长为ti>0,T=(t1,t2,…,tn),客户i希望的服务完成时刻为di>0,D=(d1,d2,…,dn);一个调度f:ANf(i)为客户i的开始时刻。如果对客户i的服务在di之前结束,那么对客户i的服务没有延迟,即如果在di之后结束,那么这个服务就被延迟了,延迟的时间等于该服务的实际完成时刻f(i)+ti减去预期结束时刻di。一个调度f的最大延迟是所有客户延迟时长的最大值maxiA{f(i)+ti-di}。附图2所示是不同调度下的最大延迟。使用贪心策略找出一个调度使得最大延迟达到最小。

2.设计思想:按照客户希望的时间从小到大排序,如果希望时间相同,按照服务时长排序,进行贪心。

3.代码设计:

#include<iostream>

#include<algorithm>

const int N=101;

using namespace std;

struct value2{

         intid;

         intt;

         intd;

         booloperator<(const value2&rhs)const{

           if(d!=rhs.d) return d<rhs.d;

            else return t<rhs.t;

         }

};

int main(){

         intn;cout<<"客户数:";cin>>n;

          cout<<"输入 a t d"<<endl;

          struct value2 expert[N];

        

         for(inti=0;i<n;++i)

         {

                   cin>>expert[i].id>>expert[i].t;

                    cin>>expert[i].d;

         }

         sort(expert,expert+n);

          

          int index[N];

           int maxn=0;

           int cur=0;

           int j=0;

           for(int i=0;i<n;++i){

                  index[j++]=expert[i].id;//记录编号

                  cur+=expert[i].t;//记录总共的时间

                           

                    maxn=max(maxn,-expert[i].d+cur);

               

           }

          cout<<"最后的结果是:"<<maxn<<endl;

          for(int i=0;i<j;++i)

           cout<<index[i]<<" ";

          /*

           5

           1 5 10

           2 8 12

           3 4 15

           4 10 11

           5 3 20

          

          */

}

2.5思考题

(1)哈夫曼编码问题的编程如何实现?

#include<stdio.h> 

#include<stdlib.h> 

#include<string.h> 

#include<queue> 

using namespace std; 

 

typedef struct node{ 

    char ch;                          //存储该节点表示的字符,只有叶子节点用的到 

   int val;                         //记录该节点的权值 

   struct node *self,*left,*right;  //三个指针,分别用于记录自己的地址,左孩子的地址和右孩子的地址 

   friend bool operator <(const node &a,const node &b) //运算符重载,定义优先队列的比较结构 

   { 

       return a.val>b.val;          //这里是权值小的优先出队列 

   } 

}node; 

 

priority_queue<node> p;                //定义优先队列 

char res[30];                          //用于记录哈夫曼编码 

void dfs(node *root,int level)         //打印字符和对应的哈夫曼编码 

   if(root->left==root->right)       //叶子节点的左孩子地址一定等于右孩子地址,且一定都为NULL;叶子节点记录有字符 

   { 

       if(level==0)                   //“AAAAA”这种只有一字符的情况 

       { 

           res[0]='0'; 

           level++; 

       } 

       res[level]='\0';               //字符数组以'\0'结束 

       printf("%c=>%s\n",root->ch,res); 

    } 

    else 

    { 

        res[level]='0';                //左分支为0 

       dfs(root->left,level+1); 

        res[level]='1';                //右分支为1 

       dfs(root->right,level+1); 

    } 

void huffman(int *hash)               //构建哈夫曼树 

    node *root,fir,sec; 

    for(int i=0;i<26;i++)              //程序只能处理全为大写英文字符的信息串,故哈希也只有26个 

    { 

        if(!hash[i])                   //对应字母在text中未出现 

            continue; 

        root=(node*)malloc(sizeof(node));          //开辟节点 

        root->self=root;                            //记录自己的地址,方便父节点连接自己 

       root->left=root->right=NULL;                //该节点是叶子节点,左右孩子地址均为NULL 

        root->ch='A'+i;                             //记录该节点表示的字符 

        root->val=hash[i];                          //记录该字符的权值 

       p.push(*root);                              //将该节点压入优先队列 

   } 

    //下面循环模拟建树过程,每次取出两个最小的节点合并后重新压入队列 

    //当队列中剩余节点数量为1时,哈夫曼树构建完成 

   while(p.size()>1) 

   { 

       fir=p.top();p.pop();      //取出最小的节点 

       sec=p.top();p.pop();      //取出次小的节点 

       root=(node *)malloc(sizeof(node));          //构建新节点,将其作为fir,sec的父节点 

       root->self=root;                            //记录自己的地址,方便该节点的父节点连接 

       root->left=fir.self;      //记录左孩子节点地址 

       root->right=sec.self;     //记录右孩子节点地址 

       root->val=fir.val+sec.val;//该节点权值为两孩子权值之和 

       p.push(*root);            //将新节点压入队列 

   } 

   fir=p.top();p.pop();          //弹出哈夫曼树的根节点 

   dfs(fir.self,0);              //输出叶子节点记录的字符和对应的哈夫曼编码 

int main() 

    char text[100]; 

    int hash[30]; 

   memset(hash,0,sizeof(hash));  //哈希数组初始化全为0 

   scanf("%s",text);            //读入信息串text 

    for(inti=0;text[i]!='\0';i++)//通过哈希求每个字符的出现次数 

    { 

        hash[text[i]-'A']++;      //程序假设运行的全为英文大写字母 

    } 

    huffman(hash); 

    return 0; 

(2)使用贪心策略求解背包问题。

贪心算法解决背包问题的几种策略:

(i) 第一种贪婪准则为:从剩余的物品中,选出可以装入背包的价值最大的物品,利用这种规则,价值最大的物品首先被装入(假设有足够容量),然后是下一个价值最大的物品,如此继续下去。这种策略不能保证得到最优解。

(ii) 第二种方案是重量贪婪准则:从剩下的物品中选择可装入背包的重量最小的物品。虽然这种规则对于前面的例子能产生最优解,但在一般情况下则不一定能得到最优解。考虑n= 2 ,w=[10,20], p=[5,100], c= 25。当利用重量贪婪策略时,获得的解为x =[1,0], 比最优解[ 0 , 1 ]要差。

(iii) 第三种贪婪准则,每一项计算=/,即该项值和大小的比,再按比值的降序来排序,从第一项开始装背包,然后是第二项,依次类推,尽可能的多放,直到装满背包。

有的参考资料也称为价值密度/贪婪算法。这种策略也不能保证得到最优解。虽然按/非递(增)减的次序装入物品不能保证得到最优解,但它是一个直觉上近似的解。 而且这是解决普通背包问题的最优解,因为在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包,1≤i≤n。

(3)分析普里姆算法和克鲁斯卡尔算法中的贪心策略。

克鲁斯卡尔算法:找出最短的边,再以这条边构成的整体去寻找与之相邻的边,直至连接所有顶点,生成最小生成树。

普里姆算法:选择一个点作为源点,此时生成树只包含一个源点,后从未加入生成树的点中选择一条到生成树中的点的最小边加入生成树,将边的另一端点也加入生成树,所选的边(u,v)都是u点在生成树上v点在生成树外,由于这个性质所以不会产生回路。

(4)思考如何证明贪心策略的正确性。

步骤:

Step0: 给出贪心算法A的描述

Step1: 假设O是和A最相似(假设O和A的前k个步骤都相同,第k+1个开始不同,通常这个临界的元素最重要)的最优算法

Step2: [Key] 修改算法O(用ExchangeArgument,交换A和O中的一个元素),得到新的算法O’

Step3: 证明O’ 是feasible的,也就是O’是对的

Step4: 证明O’至少和O一样,即O’也是最优的

Step5: 得到矛盾,因为O’ 比O 更和A 相似。

证毕。

当然上面的步骤还有一个变种,如下:

Step0: 给出贪心算法A的描述

Step1: 假设O是一个最优算法(随便选,arbitrary)

Step2: 找出O和A中的一个不同。(当然这里面的不同可以是一个元素在O不再A,或者是一个pair的顺序在A的和在O的不一样。这个要根据具体题目)

Step3:Exchange这个不同的东西,然后argue现在得到的算法O'不必O差。

Step4: Argue 这样的不同一共有Polynomial个,然后我exchangePolynomial次就可以消除所有的不同,同时保证了算法的质量不比O差。这也就是说A是asgood as 一个O的。因为O是arbitrary选的,所以A是optimal的。

证毕

(5)使用贪心策略求解多机调度问题。

// 多机调度问题 

// @pre: job_time按递减序排列 

// @post: 

// @return: 最短时间 

int multi_dispatch(int job_time[], int jobs, int machines) 

    // 如果jobs数量<=machines数量, 那么耗时为jobs最长时间 

    if (jobs <= machines) 

    { 

        int max =job_time[0]; 

        int i; 

        for (i=1; i<jobs;i++) 

        { 

            if (max <job_time[i]) 

            { 

                max =job_time[i]; 

            } 

        } 

        return max; 

    } 

 

    //否则采用最长处理时间优先的贪心选择策略 

    int *machine_time =(int*)calloc(sizeof(int), machines); 

    int i=0;  

    for (i=0; i<jobs; i++) 

    { 

        // 选择最空闲的机器 

        int machine_no =get_min_index(machine_time, machines); 

        printf("将job[%d](耗时%3d)分配给机器machine[%d](预计完成时间:%d+%d)\n", 

            i, job_time[i],machine_no, machine_time[machine_no], job_time[i]); 

        machine_time[machine_no]+= job_time[i]; 

    } 

     

    int max=0; 

    for (i=0; i<machines;i++) 

    { 

        if (max <machine_time[i]) 

        { 

            max =machine_time[i]; 

        } 

    } 

    free(machine_time); 

    return max; 

 

实验三  动态规划算法

3.1  实验目的与要求

1.理解动态规划算法的基本思想;

2.运用动态规划算法解决实际问题,加深对贪心算法的理解和运用。

3.2  实验课时

4学时(课内2学时+课外2学时)

3.3  实验原理

动态规划(Dynamic Programming)算法思想:把待求解问题分解成若干个子问题,先求解子问题,然后由这些子问题的解得到原问题的解。动态规划求解过的子问题的结果会被保留下来,不像递归那样每个子问题的求解都要从头开始反复求解。动态规划求解问题的关键在于获得各个阶段子问题的递推关系式:

(1)分析原问题的最优解性质,刻画其结构特征;

(2)递归定义最优值;

(3)自底向上(由后向前)的方式计算最优值;

(4)根据计算最优值时得到的信息,构造一个最优解。

3.4  实验题目

上机题目:最大子段和问题

给定n个整数(可以为负数)组成的序列(a1,a2,…,an),使用动态规划思想求该序列的子段和的最大值。注:当所有整数均为负整数时,其最大子段和为0。

例如,对于六元组(-2, 11, -4, 13, -5,-2),其最大字段和为:a2 + a3 + a4= 20。

除了动态规划,该问题可以使用顺序求和+比较(蛮力法)和分治法求解,思考其求解过程。

2.设计思想:

先保存当前的最大子段和,如果小于0,则从下一个序号开始更新,max记录最终的最大值,每次和当前的最大子段和比较更新即可。

3.代码设计:

#include<iostream>

using namespace std;

#include<cmath>

/*

6

-2 11 -4 13 -5 -2

*/

const int N=10001;

int value[N];

int temp[N];

void solve(int n){

    int sum=value[0];

    temp[0]=value[0];

    for(int i=1;i<n;++i){

   

    if(temp[i-1]>0){

              temp[i]=temp[i-1]+value[i];

             

         }

         else temp[i]=value[i];

           sum=max(temp[i],sum);

          

}

cout<<sum<<endl;

}

int main(){

//最大子段和

int n;cin>>n;

 for(int i=0;i<n;++i)

  cin>>value[i];

  solve(n);

}

3.5  思考题

(1)深刻理解动态规划与递归求解问题的区别是什么?

递归算法就是通过解决同一问题的一个或多个更小的实例来最终解决一个大问题的算法。为了在C语言中实现递归算法,常常使用递归函数,也就是说能调用自身的函数。递归程序的基本特征:它调用自身(参数的值更小),具有终止条件,可以直接计算其结果。

      在使用递归程序时,我们需要考虑编程环境必须能够保持一个其大小与递归深度成正比例的下推栈。对于大型问题,这个栈需要的空间可能妨碍我们使用递归的方法。

     一个递归模型为分治法,最本质的特征就是:把一个问题分解成独立的子问题。如果子问题并不独立,问题就会复杂的多,主要原因是即使是这种最简单算法的直接递归实现,也可能需要难以想象的时间,使用动态规划技术就可以避免这个缺陷。

(2)动态规划思想解题的步骤是什么?

第一步:确定子问题。 在这一步重点是分析那些变量是随着问题规模的变小而变小的, 那些变量与问题的规模无关。

第二步:确定状态:根据上面找到的子问题来给你分割的子问题限定状态

第三步:推到出状态转移方程:这里要注意你的状态转移方程是不是满足所有的条件, 注意不要遗漏。

第四步:确定边界条件:先根据题目的限制条件来确定题目中给出的边界条件是否能直接推导出, 如果不行也可以尝试从边界条件反推(举个例子:a(n)→a(2)有递推关系, 但是a(2)→a(1)不符合上述递推关系, 我们就可以考虑用a(1)来倒推出a(2), 然后将递推的终点设置为a(2));

第五步:确定实现方式:这个依照个人习惯 就像是01背包的两层for循环的顺序

第六步:确定优化方法:很多时候你会发现走到这里步的时候你需要返回第1步重来。首先考虑降维问题(优化内存), 优先队列、四边形不等式(优化时间)等等。

(3)动态规划思想和贪心算法在求解问题时的区别是什么?

两种算法的应用背景很相近,针对具体问题,有两个性质是与算法选择直接相关的,上一次我们也提到了,那就是最优子结构性质和贪心选择性质。

最优子结构性质是选择类最优解都具有的性质,即全优一定包含局优,上一次选择最短路线的例子已经对此作了说。

当时我们也提到了贪心选择性质,满足贪心选择性质的问题可用贪心算法解决,不满足贪心选择性质的问题只能用动态规划解决。可见能用贪心算法解决的问题理论上都可以利用动态规划解决,而一旦证明贪心选择性质,用贪心算法解决问题比动态规划具有更低的时间复杂度和空间复杂度。

(4)使用动态规划算法求解最长公共子序列(LCS)问题。

#include<bits/stdc++.h> 

using namespacestd; 

const int MAXSTRLEN =1000; 

 

char a[MAXSTRLEN],b[MAXSTRLEN]; 

intdp[MAXSTRLEN][MAXSTRLEN], path[MAXSTRLEN][MAXSTRLEN]; 

 

///求序列x和y的LCS,path保存路径指向,以方便打印公共子序列 

int Lcs(char x[], chary[]) 

    int i, j, len1 = strlen(x + 1), len2 =strlen(y + 1); 

    memset(dp, 0, sizeof(dp)); 

    for (i = 1; i <= len1; ++i) 

        for (j = 1; j <= len2; ++j) 

        { 

            if (x[i] == y[j]) 

                dp[i][j] = dp[i - 1][j - 1] +1, path[i][j] = 1; 

            else if (dp[i - 1][j] >= dp[i][j- 1]) 

                dp[i][j] = dp[i - 1][j],path[i][j] = 2; 

            else 

                dp[i][j] = dp[i][j - 1], path[i][j]= 3; 

        } 

    return dp[len1][len2]; 

 

///打印LCS 

void PrintLcs(int i,int j) 

    if (i == 0 || j == 0) return; 

    if (path[i][j] == 1) 

    { 

        PrintLcs(i - 1, j - 1); 

        putchar(a[i]); 

    } 

    else if (path[i][j] == 2) PrintLcs(i - 1,j); 

    else PrintLcs(i, j - 1); 

 

int main() 

    while (gets(a + 1)) 

    { 

        gets(b + 1); 

        printf("%d\n", Lcs(a,b)); 

        PrintLcs(strlen(a + 1), strlen(b +1)); 

        putchar(10); 

    } 

    return 0; 

(5)使用动态规划算法求解最长最大字段和问题。

#include<iostream>

using namespace std;

#define M 100

int maxadd(int s[],intn){

int max[M];  //存放j从1到n时前面字段中最大的和值

max[1]=s[1];  //当只有一个字段是,最大的当然是第一个

int add=max[1];

//存放整个序列中最大的和值

for(int j=1;j<=n;j++){

/

if(max[j-1]>0)max[j]=max[j-1]+s[j];

else max[j]=s[j];

}

for(intz=2;z<=j;z++){

if(max[z]>add)add=max[z];

}

return add;

}

void main(){

int s[M];  //存放序列

int x,i=1,k;

int n;

//输入

cout<<"请输入序列(以00结束):";

while(cin>>x,x!=00){

s[i]=x;

i++;

}

//输出

for(k=1;k<i;k++){

cout<<s[k]<<"";

}

cout<<endl;

n=i-1;

int add=maxadd(s,n);

cout<<"最大子段和为:"<<add<<endl;

}

实验四  回溯算法

4.1  实验目的与要求

1.通过回溯算法的示例程序理解回溯算法的基本思想;

2.运用回溯算法解决实际问题,进一步加深对回溯算法的理解和运用。

4.2  实验课时

4学时(课内2学时+课外2学时)。

4.3  实验原理

回溯算法(Backtrack)的基本做法是搜索,或是一种组织得井井有条的、能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。

回溯算法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解:如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。

回溯算法的基本步骤:

(1)针对所给问题,定义问题的解空间;

(2)确定易于搜索的解空间结构;

(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

常用剪枝函数:

(1)用约束函数在扩展结点处剪去不满足约束的子树;

(2)用限界函数剪去得不到最优解的子树。

4.4  实验题目

排兵布阵问题

某游戏中,不同的兵种处于不同的地形上时,其攻击能力也一样,现有n个不同兵种的角色(1, 2, ..., n),需安排在某战区n个点上,角色ij点上的攻击力为Aij,使用回溯法设计一个布阵方案,使总的攻击力最大。注:个人决定A矩阵的初始化工作。该问题求解算法的输入数据形如附图4所示。

2.设计思想:

生成组合数,回溯各种情况然后找出最大值。

3.代码设计:#include<iostream>

#include<cstring>

using namespace std;

const int N=15;

int pos[N];

bool flag[N];

int value[N][N];

int maxn=0;

int ans[N];

void solve(int cur,int n){

         if(cur>n)

           {

                intsum=0;

                for(inti=1;i<=n;++i)

                  sum+=value[i][pos[i]];

                  if(maxn<sum){

                          maxn=sum;

                            for(int i=1;i<=n;++i)

                              ans[i]=pos[i];

                          }

                     return;

           }

          else {

                 for(inti=1;i<=n;++i){

                          if(!flag[i]){

                                  pos[cur]=i;

                                  flag[i]=true;

                                  solve(cur+1,n);

                                  flag[i]=false;

                           }

                   }

          }

}

int main(){

   int n;

  cout<<"请输入n:";

  cin>>n;     

  for(inti=1;i<=n;++i)

  for(int j=1;j<=n;++j)

  cin>>value[i][j];

  

         memset(flag,false,sizeof(flag));

         solve(1,n);

         cout<<"选择的位置:";

  for(int i=1;i<=n;++i)

   cout<<"("<<i<<","<<ans[i]<<")"<<"";

   cout<<"\n";

   cout<<"最大攻击力为:"<<maxn<<endl;

}

/*

6 4 8 5 6

9 6 8 7 2

3 5 4 5 8

9 4 3 7 9

6 8 9 6 5

70 30 50

80 40 60

40 50 30

*/

附图4  排兵布阵问题的初始状态

4.5 思考题

(1)什么是启发式搜索问题?

启发式搜索(Heuristically Search)又称为有信息搜索(InformedSearch),它是利用问题拥有的启发信息来引导搜索,达到减少搜索范围、降低问题复杂度的目的,这种利用启发信息的搜索过程称为启发式搜索。

(2)搜索算法的解空间树的如何定义?

问题的解往往需要经过一系列操作之后才能得到,而在这一系列的操作中,每一步的操作都会得到一个状态,当最终这个状态与目标状态相同时,此时也就是得到了结果,所以在搜索的过程中,着重需要处理的就是操作和状态。只有考虑了所有可能的操作,才会得到所有可能的状态。

(3)0-1背包问题的动态规划算法如何求解?

声明一个 大小为 m[n][c] 的二维数组,m[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值,那么我们可以很容易分析得出 m[i][j] 的计算方法,

(1). j < w[i] 的情况,这时候背包容量不足以放下第i 件物品,只能选择不拿

m[ i ][ j ] = m[ i-1 ][ j ]

(2). j>=w[i] 的情况,这时背包容量可以放下第i 件物品,我们就要考虑拿这件物品是否能获取更大的价值。

如果拿取,m[ i ][ j ]=m[ i-1 ][ j-w[ i ] ] +v[ i ]。 这里的m[ i-1 ][j-w[ i ] ]指的就是考虑了i-1件物品,背包容量为j-w[i]时的最大价值,也是相当于为第i件物品腾出了w[i]的空间。

如果不拿,m[ i ][ j ] = m[ i-1 ][ j ] , 同(1)

究竟是拿还是不拿,自然是比较这两种情况那种价值最大。

(4)n皇后问题使用回溯法如何求解?

#include<stdio.h>    

#include<math.h> 

using namespace std; 

#define N 60    

 

int sum = 0; //可行解个数    

int x[N]; //皇后放置的列数  (预留的空间)  

   

   

bool isPlaceAble(int queenAtRow)    

{    

    int i;    

   for(i=1;i<queenAtRow;i++)    

    { 

       if(abs(queenAtRow-i)==abs(x[queenAtRow]-x[i]) || x[queenAtRow] ==x[i])    

        { 

            return false;    

        } 

    } 

    return true;    

}    

   

   

int queen(int queenAtRow,int n)    

{    

    if( queenAtRow>n &&n>0) //当放置的皇后超过n时,可行解个数加1,此时n必须大于0    

    { 

          sum++;    

    } 

    else   

    { 

      for(inti=1;i<=n;i++)    

      {    

          x[queenAtRow] = i; //标明第t个皇后放在第i列    

         if(isPlaceAble(queenAtRow)) //如果可以放在某一位置,则继续放下一皇后    

           { 

               queen(queenAtRow+1,n);     

          } 

      }    

    } 

    return sum;    

}    

/*

功能: 求解放置N皇后方案的个数。

输入:

    皇后个数   

返回:

   int:放置N皇后方案的个数

*/ 

 

int NQueen(int n) 

    if( n==0 ) 

    { 

        return 0; 

    } 

 

    int methodNum = queen(1,n); 

 

    return methodNum; 

int main() 

{  

      

    int  sum; 

 

    sum = NQueen(5); 

     

   cout<<sum<<endl; 

 

 

 

  system("pause"); 

   return 0; 

(5)使用回溯法求解装载问题。

用回溯法解装载问题时,用子集树表示其解空间显然是最合适的。用可行性约束函数可剪去不满足约束条件的子树。在子集树的第j+1层的结点z处,用cw记当前的装载重量,即cw=,则当cw>c1时,以结点z为根的子树中所有结点都不满足约束条件,因而该子树中的解均为不可行解,故可将该子树剪去。(该约束函数去除不可行解,得到所有可行解)。

     可以引入一个上界函数,用于剪去不含最优解的子树,从而改进算法在平均情况下的运行效率。设z是解空间树第i层上的当前扩展结点。cw是当前载重量;bestw是当前最优载重量;r是剩余集装箱的重量,即r=。定义上界函数为cw+r。在以z为根的子树中任一叶结点所相应的载重量均不超过cw+r。因此,当cw+r<=bestw时,可将z的右子树剪去。

    


猜你喜欢

转载自blog.csdn.net/qq_37437892/article/details/80984446