就这!!就这!!就这!!哈哈哈哈。
(一)就自己对与这几个算法的一些总结。
1.动态规划法: 基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解,以自底向上的方式解各子问题。
2.分治法问题: 将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
注: 你会发现,其实这两个没啥区别,都是大问题分解成小问题,然后找最优解的问题。不同的是分治法分解后的子问题是相互独立的,不相同。而动态规划法分解后的子问题有相同的,保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。
3.贪心算法问题: 采用自顶向下,以迭代的方法做出相继的贪心选择,每做一次贪心选择,就将所求问题简化为一个规模更小的子问题,通过每一步贪心选择,可得到问题的一个最优解。虽然每一步上都要保证能获得局部最优解,但由此产生的全局解有时不一定是最优的。
4.回溯法问题: 一般都是问题的解空间转化成了图或者树的结构表示,然后使用深度优先搜索策略进行遍历,遍历的过程中记录和寻找所有可行解或者最优解。
(二)各个方法的详解
1.动态规划法:
(1)在解决动态规划问题时一般分为四步:
1、定义一个状态,这是一个最优解的结构特征
2、进行状态递推,得到递推公式
3、进行初始化
4、返回结果
(2)适用性
1、最优子结构性质
2、无后效性
3、子问题的重叠性
2.分治法:
(1)在每一层的递归上的步骤
1、分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题。
2、解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题。
3、合并:将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,...,yk) △ 合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。
ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法
ADHOC(P)求解。算法MERGE(y1,y2,...,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,...,Pk的相应的解
y1,y2,...,yk合并为P的解。
(2)适用性
1、该问题的规模缩小到一定的程度就可以容易地解决。
2、该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3、利用该问题分解出的子问题的解可以合并为该问题的解。
4、该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。
3.贪心算法:
(1)算法的步骤
1、建立数学模型来描述问题。
2、把求解的问题分成若干个子问题。
3、对每个子问题求解,得到子问题的局部最优解。
4、把子问题的解局部最优解合成原来解问题的一个解。
(2)适用性(基本要素)
1、贪心选择性质
2、最优子结构性质
该算法存在问题:
1. 不能保证求得的最后解是最佳的;
2. 不能用来求最大或最小解问题;
3. 只能求满足某些约束条件的可行解的范围。
4.回溯法问题:
(1)关键要素
1、针对给定的问题,定义问题的解空间。
2、确定易于搜索的解空间结构。
3、以深度优先方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
(1)有两种实现回溯法(递归法和迭代法)
1、递归:
函数模板如下:
void BackTrace(int t) {
if(t>n)
Output(x);
else
for(int i = f (n, t); i <= g (n, t); i++ ) {
x[t] = h(i);
if(Constraint(t) && Bound (t))
BackTrace(t+1);
}
}
2、迭代:
函数模板如下:
void IterativeBackTrace(void) {
int t = 1;
while(t>0) {
if(f(n, t) <= g( n, t))
for(int i = f(n, t); i <= g(n, t); i++ ) {
x[t] = h(i);
if(Constraint(t) && Bound(t)) {
if ( Solution(t))
Output(x);
else
t++;
}
}
else
t− −;
}
}
(三)各个算法的具体例子
1.动态规划
使用动态规划法解决最大子段和问题:
题目: 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和,全为负数时,返回0。
思路方法: 用一个MaxSum来保存前面得到的子串的数值,当遍历到array[i]且MaxSum<0的时候,前面的子串对当前的加和是没有贡献的,所以舍弃掉这样的子串,让MaxSum =array[i],重新开始子串的累积,而如果MaxSum>0,那么前面的子串对加和是有贡献的,所以继续追加子串,MaxSum= array[i] + MaxSum;这样每次循环还检验更新最大值就可以了。
空间复杂度O(1).
下面为实现的代码:
#include<iostream>
using namespace std;
int min,max; //最大子段和区间
//动态规划法
int MaxSubSum3(int array[],int n){
int MaxSum=0;
int t=0;
for(int i=0;i<n;i++){
if(t+array[i]>array[i]) {
t=t+array[i];
}else{
t=array[i]; //重新记录起点
}
//记录字段和的下标
if(t==array[i]){ //新的起点
min=i;
}else{
max=i; //终点
}
if(MaxSum<t){
MaxSum=t;
}
}
return MaxSum;
}
int main(){
int array[]={7,2,-6,4,9,8};
cout<<MaxSubSum3(array,6)<<endl;
cout<<"区间为:["<<min<<","<<max<<"]"<<endl;
return 0;
}
运行结果:
2.分治法问题
使用分治法来实现二路归并算法:
题目: 二路归并算法
思路方法:
将目标数组a[ ]分成左右两个数组,然后用两个指针分别记录左右两个数组,一一比较放入temp[ ]数组中。如果发现左边数组没排完,就加入到temp[ ]中,右边同理。再将数组赋值到目标数组a[ ]。
使用递归来实现二路归并排序。
下面为实现代码:
//归并排序
#include<iostream>
using namespace std;
//将数组a[left,mid]与数组a[mid,right]合并
void MergeArr(int a[],int left,int mid,int right,int temp[]){
int k=0,rmid=mid+1;
int i=left,j=right; //i和j来指向左右两个数组操作
while(i<=mid && rmid<=j){
if(a[i] <= a[rmid]){
temp[k++]=a[i++];
}else{
temp[k++]=a[rmid++];
}
}
while(i <= mid){
temp[k++] = a[i++];
}
while(rmid <= j){
temp[k++] = a[rmid++];
}
for(i=0;i<k;i++){
a[i+left]=temp[i]; //两两合并
cout<<temp[i]<<",";
}
cout<<endl;
}
//递归实现二路归并
void MergeSort(int a[],int left,int right,int temp[]){
if(left<right){
int mid=(left+right)/2;
MergeSort(a,left,mid,temp);
MergeSort(a,mid+1,right,temp);
MergeArr(a,left,mid,right,temp);
}
}
int main(){
//cin
int array[]={3,-5,7,1,2,-6,4};
int t[100]; //缓冲数组
MergeSort(array,0,6,t);
for(int i=0;i<7;i++){
cout<<array[i]<<",";
}
cout<<endl;
return 0;
}
运行结果:
注: 最后一行为运行结果,前面几行都是他的二路归并过程。
3.贪心算法
使用贪心算法实现钱币问题
题目: 指定币值和相应的数量,用最少的数量凑齐某金额。
思路方法: 我们就直接优先选择面值大的钱币,以此类推,直到凑齐总金额。(贪心不一定最优)
下面为实现代码:
//题目: 指定币值和相应的数量,用最少的数量凑齐某金额。
#include<iostream>
using namespace std;
int main(){
int money[] = { 1, 2, 5, 10, 20, 50, 100 };//拥有的面额
int counts[] = { 3, 3, 2, 1, 1, 3, 3 }; //所对应面额的张数
int sum=246; //所需要换的钱数;
int result[7];
int add=0; //当前凑的金额
for(int i=6;i>=0;i--)
{
int num = ((sum-add)/money[i]);
if(num>counts[i])
{
num=counts[i];
}
add=add+num*money[i];
result[i]=num;
}
cout<<"各币值的数量:";
for(int j=0;j<7;j++){
cout<<result[j]<<",";
}
cout<<endl;
return 0;
}
运行结果:
4.回溯法问题
使用回溯法解决n皇后问题
题目: 在n×n格的国际象棋上摆放n个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
思路方法: 使用回溯法,放一个皇后,判断他的八个方向是否有皇后,用数组来存储。
下面是实现代码:
#include <iostream>
using namespace std;
int count = 0; //摆法个数
const int nqueen=8;//设置皇后的个数
//判断当前位置是否能放皇后
bool IsSet(int i, int j, int (*Q)[nqueen])
{
int s,t;
//判断某一行上是否能放皇后
for(s=i,t=0;t<nqueen;t++){
if (Q[s][t]==1 && t!=j)
return false;
}
//判断某一列上是否能放皇后
for(t=j,s=0;s<nqueen;s++){
if(Q[s][t]==1 && s!=i)
return false;
}
//判断左上是否能放皇后
for(s=i-1,t=j-1;s>=0&&t>=0;s--,t--){
if(Q[s][t]==1)
return false;
}
//判断右下是否能放皇后
for(s=i+1,t=j+1;s<nqueen&&t<nqueen;s++,t++){
if(Q[s][t]==1)
return false;
}
//判断右上是否能放皇后
for(s=i-1,t=j+1;s>=0&&t<nqueen;s--,t++){
if(Q[s][t]==1)
return false;
}
//判断左下是否能放皇后
for(s=i+1,t=j-1;s<nqueen&&t>=0;s++,t--){
if(Q[s][t]==1)
return false;
}
//其它情况
return true;
}
//放置皇后
void Queen(int j, int (*Q)[nqueen])
{
int i,k;
if(j==nqueen) //如果8个皇后全部放置完毕
{
for(i=0;i<nqueen;i++){
for(k=0;k<nqueen;k++){
cout<<" "<<Q[i][k];
}
cout<<endl;
}
cout<<endl;
count++;
return;
}
for(i=0;i<nqueen;i++){
if(IsSet(i,j,Q))
{
Q[i][j]=1;
Queen(j+1,Q);
Q[i][j]=0;
}
}
}
int main()
{
int Q[nqueen][nqueen];
int i,j;
for(i=0;i<nqueen;i++){
for(j=0;j<nqueen;j++){
Q[i][j]=0;
}
}
Queen(0,Q);
cout<<count<<endl;
return 0;
}
运行结果:
注: 这92种都打印出来了,没截出来。