第四章 动态规划

动态规划的基本思想:
动态规划算法通常用于求解具有某种最优性质的问题。
基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。

动态规划问题的特征:
1.最优子结构
当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
2.重叠子问题算法设计2
动态规划的基本概念:
1 阶段和阶段变量:
2. 状态和状态变量:
3. 决策、决策变量和决策允许集合:
4.策略和最优策略:
5. 状态转移方程

设计动态规划法的步骤
1.找出最优解的性质,并刻画其结构特征;
2.递归地定义最优值(写出动态规划方程);
3.以自底向上的方式计算出最优值;
4.根据计算最优值时得到的信息,构造一个最优解。

给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。
解析:
设这个计算次序在矩阵Ak和Ak+1之间将矩阵链断开,1≤k<n
则其相应完全加括号方式为(A1A2…Ak)(Ak+1Ak+2…An)
计算量(乘法计算的次数)=:
A[1:k]的计算量+
A[k+1:n]的计算量+
A[1:k]和A[k+1:n]相乘的计算量
设计算A[i: j],1≤i≤j≤n,所需要的最少乘法次数m[i][j]
原问题的最优值为m[1][n]
当i=j时,A[i: j]=Ai,因此,m[i][i]=0,i=1,2,…,n
当i<j 时,
m[i][j]=m[i][k+m[k+1][j]+Pi-1PkPj
Ai的维数是Pi-1×Pi
m[i][j]=0 i=j
m[i][j]=m[i][k+m[k+1][j]+Pi-1
PkPj i<j

算法设计2
#define NUM 51
int p[NUM];
int m[NUM][NUM];
int s[NUM][NUM];
void MatrixChain (int n){
  for (int i=1;  i<=n;  i++) m[i][i] = 0;
  for (int r=2; r<=n;  r++)
for (int i=1; i<=n-r+1;  i++) {
  int j=i+r-1; 
  m[i][j] = m[i+1][j]+p[i-1]*p[i]*p[j];
  s[i][j] = i; 
for (int k=i+1;k<j;k++) {
int t = m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
if (t < m[i][j]) {
m[i][j] = t;
s[i][j] = k;}
  }
}
}

算法设计2
void TraceBack(int i, int j) {
if(i==j) cout<<“A “<< i;
else
{
cout<<”(”;
TraceBack(i,s[i][j]);
TraceBack(s[i][j]+1,j);
cout<<")";
}
}

算法设计2
int Recurve(int i, int j){
  if (i == j) return 0;
  int u = Recurve(i, i)+Recurve(i+1,j)+p[i-1]*p[i]*p[j];
  s[i][j] = i;
  for (int k = i+1; k<j; k++) {
int t = Recurve(i, k) + Recurve(k+1,j)+p[i-1]*p[k]*p[j];
if (t<u) { u = t; s[i][j] = k;}
  }
  m[i][j] = u;
  return u;
}

算法设计2
int LookupChai (int i, int j){
  if (m[i][j]>0) return m[i][j];
  if (i==j) return 0;
  int u = LookupChain(i,i)+LookupChain(i+1,j)+p[i-1]*p[i]*p[j];
  s[i][j] = i;
  for (int k = i+1; k<j; k++) {
int t = LookupChain(i,k)+LookupChain(k+1,j)+p[i-1]*p[k]*p[j];
if (t < u) { u = t; s[i][j] = k;}
  }
  m[i][j] = u;
  return u;
}

计算最优值
算法设计2
#define NUM 100
int c[NUM][NUM];
int b[NUM][NUM];
void LCSLength (int m, int n, const char x[],char y[]){
  int i,j;
  for (i = 1; i <= m; i++) c[i][0] = 0;
  for (i = 1; i <= n; i++) c[0][i] = 0;
    for (i = 1; i <= m; i++)
   for (j = 1; j <= n; j++){
if (x[i]==y[j])
  {c[i][j]=c[i-1][j-1]+1; b[i][j]=1; } //↖
else if (c[i-1][j]>=c[i][j-1])
{c[i][j]=c[i-1][j]; b[i][j]=2; } //↑
else { c[i][j]=c[i][j-1]; b[i][j]=3; } //←
  }
}
编辑距离问题
设A和B是2个字符串。要用最少的字符操作将字符串A转换为字符串B。这里所说的字符操作包括 (1)删除一个字符; (2)插入一个字符; (3)将一个字符改为另一个字符。
将字符串A变换为字符串B所用的最少字符操作数称为字符串A到 B的编辑距离。
输入: word1 = “horse”, word2 = “ros”
输出: 3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
1.修改ai,此时编辑距离为 Ai-1 和Bj-1的编辑距离+1
2.在A中增加一个符号bi ,此时编辑距离为 Ai 和Bj-1的编辑距离+1
3.或删除A中的一个符号ai,此时编辑距离为 Ai-1 和Bj的编辑距离+1
如果i=0||j=0 dp[i][0]=i; dp[0][j]=j;
如果A[i]=B[j] dp[i][j]=dp[i-1][j-1];
如果 A[i]!=B[j] dp[i][j]=min
dp[i-1][j-1]+1;//替换A中的符号
dp[i-1][j]+1 //在A中删除一个符号a[i]
dp[i][j-1]+1 //在A[i]增加一个符号[j]

完全背包问题
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。
【样例输入】
10 4
2 1
3 3
4 5
7 9
【样例输出】
  max=12
 
#include
using namespace std;
const int maxm = 201, maxn = 31;int m, n,w[maxn], c[maxn], f[maxn][maxm];
int main(){
scanf("%d%d",&m, &n); //背包容量m和物品数量n
for (int i = 1; i <= n; i++)
scanf(“%d%d”,&w[i],&c[i]); //每个物品的重量和价值
for (int i = 1; i <= n; i++) //f[i][v]表示前i件物品,总重量不超过v的最优价值
for (int v = 1; v <= m; v++)
if (v < w[i]) f[i][v] = f[i-1][v];
else
    if (f[i-1][v] > f[i][v-w[i]]+c[i]) f[i][v] = f[i-1][v]; //不装入第i件物品
else f[i][v] = f[i][v-w[i]]+c[i]; //多次装入第i件物品
printf(“max=%d”,f[n][m]); // f[n][m]为最优解
return 0;}
以上伪代码
for i=1…N
   for v=0…V
     f[v]=max{f[v],f[v-w[i]]+c[i]};

数字三角形问题
数字三角形,从顶部出发,在每一结点可以选择向左走或者向右走,一直走到底层。
试设计一个算法,计算出从三角形的顶至底的一条路径,使该路径经过的数字总和最大。
算法设计2
假设tri[1][i1],tri[2][i2]…, tri[n-1][in-1],tri[n][in]是数字和最大的一条路,
则tri[2][i2]…, tri[n-1][in-1] tri[n][in]是倒数n-1层的数字三角形中数字和最大的一条路
#define NUM 100
int tri[NUM][NUM];
int triangle(int n){
 int i, j;
 for (i=n-2; i>=0; i- -)
  for (j=0; j<=i; j++)
   if( tri[i+1][j] >tri[i+1][j+1])
    tri[i][j] = tri[i][j] +tri[i+1][j];//向左走
  else
tri[i][j] =tri[i][j]+ tri[i+1][j+1]; //向右走
 return tri[0][0];
}
void TraceBack(int **tri, int **cost, int n){
int sum=tri[0][0],row=0,col=0;
cout<<"("<<row<<","<<col<<")"<<endl;
for( row=1;row<n;row++) {
if(sum-cost[row-1][col]==tri[row][col])
sum=tri[row][col];
else{
sum=tri[row][col+1];
col=col+1;
}
cout<<"("<<row<<","<<col<<")"<<endl;
}
return ;
}

最大k乘积问题
设I是一个n位十进制整数。如果将I划分为k段,则可得到k个整数。这k个整数的乘积称为I的一个k乘积。试设计一个算法,对于给定的I和k,求出I的最大k乘积。编程任务:对于给定的I 和k,编程计算I 的最大k 乘积。输入:
2 1 (n,k)
15 ( x )
输出
15
最优解结构分析
用data[i][j]:表示从i开始的到j位数组成的一个数字,
m[i][j]表示从第1位数到第i位数的j乘积,即将前i位数分成j段,各段积的最大值。则根据k乘积的定义:
对于1<w<=j(w为第j段的起始位置)
m[i][j]=m[w-1][j-1]*data[w][i]
建立动态转移方程
m[i][j]表示前i个数的j乘积,则原问题的最优值为m[n][k]
当k=1时,m[n][1]=data[1][n]
当k!=1时,利用最优子结构的性质计算
m[n][k]=max(m[w-1][k-1]*data[w][n])(k<=w<n)
w表示第k段断开的位置(第k段开始的位置)

#include
using namespace std;
void compute(int n, int k,int x){
int i,j,*a, **data,*m;
a=new int [n+1];
data=new int
[n+1];
m=new int *[n];
for(i=n;i>0;i–) {
data[i]=new int[n+1];
//最大是k乘积,为符合计数习惯,分配k+1单元,m[i][k]就表示前i个数的k乘积。
m[i]=new int[k+1];
a[i]=x%10;
x=x/10;
}
for(i=1;i<=n;i++){
data[i][i]=a[i];
for(j=i+1;j<=n;j++){data[i][j]=data[i][j-1]*10+a[j];}
}
for(i=1;i<=n;i++){
m[i][1]=data[1][i];//从1开始的i个数分解为1段的乘积等于data[1][i]
}
for(i=2;i<=n;i++) //依次计算m[i][j]的值,最终求解m[n][k]
for(j=2;j<=k;j++)//分解为j段,j得最大值为k
{
int max=0;
for(int d=j;d<=i;d++)//前i个数,分成j段,d是第j段的分割点.计算m[i][j]的值
{
int temp=m[d-1][j-1]*data[d][i];
if(temp>max)
max=temp;
}
m[i][j]=max;
}
cout<<m[n][k]<<endl;
}
int main()
{
int n,k,x;
cin>>n>>k>>x;
compute(n,k,x);
return 0;
}

沿着高速公路,有很多家McBurger快餐连锁店。最近,他们决定沿着公路建几个仓库,每一个仓库都建在一家快餐店边上,以便给快餐店提供配料。这些仓库所在的位置,应使得快餐店与其指定的仓库之间的平均距离是最短的。
请编写程序,计算建造仓库的最优的位置和最优数量。

快餐店的数量是n,仓库的数量是k。
沿高速公路n个快餐店的位置,定义为数组:
int d[202];
在任意两个快餐店之间建一个仓库时,这两个快餐店之间所有的快餐店与该仓库的距离,数组:
int cost[202][202]; //行号、列号分别代表两个快餐店的编号
由一个仓库推算到k个仓库时,所有快餐店与这些仓库的距离和,数组:
int dis[i][j]; //i: 仓库的个数 ,j:快餐店的编号 。在快餐店1~j之间,建i个仓库的最小运输距离。最优值为dist[k][n]
int number =1;          //测试例编号
while(scanf("%d %d",&n, &k) && (n||k)){
  for(i = 1; i <= n; i ++)
    scanf("%d",&d[i]);
  //计算数组cost:在任意两个快餐店中间建一个仓库时,
  //该两个快餐店之间的所有快餐店与该仓库的距离和
  for( i = 1; i <= n; i++)
    for( j = 1; j <= n; j++){
      int temp = (i+j) / 2;
      cost[i][j] = 0;
      for( m = i; m <= j; m++)
        cost[i][j] += abs(d[m] - d[temp]);
    }
  //4个字节为1,即0x01010101,是16843009,作为∞
  memset(dis, 1, sizeof(dis));
  //初始化,快餐店1与快餐店i(1≤i≤n)之间建一个仓库时的距离和
  for( i = 1; i <= n; i++)
    dis[1][i] = cost[1][i];
      //由一个仓库推算到k个仓库时,所有快餐店与这些仓库的距离和
  for( i = 2; i <= k; i++)//i表示要建的仓库个数
    for( j = i; j <= n; j++) //j表示快餐店的编号
      for( m = i-1; m < j; m++)
      {
        temp = dis[i-1][m] + cost[m+1][j];
        if( temp < dis[i][j])
          dis[i][j] = temp;
      }
  //最优值在dis[k][n]中
  printf(“Chain %d\nTotal distance sum = %d\n\n”,
        number++, dis[k][n]);
}

第五章 贪心算法

贪心算法总是作出在当前看来最好的选择。
贪心算法并不从整体最优考虑,它所作出的选择只是在某种意义上的局部最优选择。当然,希望贪心算法得到的最终结果也是整体最优的。

贪心算法和动态规划算法的比较:
贪心算法的选择策略即贪心选择策略,通过对候选解按照一定的规则进行排序,然后就可以按照这个排好的顺序进行选择了,选择过程中仅需确定当前元素是否要选取,与后面的元素是什么没有关系。
动态规划的选择策略是试探性的,每一步要试探所有的可行解并将结果保存起来,最后通过回溯的方法确定最优解,其试探策略称为决策过程。
共同点:
最优子结构性质是选择类最优解都具有的性质,即全优一定包含局优
不同之处:
贪心算法具有贪心选择特性。贪心算法求得局部最优解(局部最优,不一定是全局最优)
动态规划算法从全局最优考虑问题

给定一个载重量为M的背包,考虑n个物品,其中第i个物品的重量 wi ,价值vi (1≤i≤n),要求把物品装满背包,且使背包内的物品价值最大。
有两类背包问题(根据物品是否可以分割),如果物品不可以分割,称为0-1背包问题(动态规划);如果物品可以分割,则称为背包问题(贪心算法)。
有3种方法来选取物品:
1)当作0-1背包问题,用动态规划算法,获得最优值220;
(2)当作0-1背包问题,用贪心算法,按性价比从高到底顺序选取物品,获得最优值160。由于物品不可分割,剩下的空间白白浪费。
(3)当作背包问题,用贪心算法,按性价比从高到底的顺序选取物品,获得最优值240。由于物品可以分割,剩下的空间装入物品3的一部分,而获得了更好的性能。
贪心算法:
double knapsack(int n, bag a[], double c){
  double cleft = c;
  int i = 0;
  double b = 0;
  while(i<n && a[i].w<=cleft)  {
    cleft -= a[i].w;
    b += a[i].v;
     //物品原先的序号是a[i].index,全部装入背包
    a[a[i].index].x = 1.0; //因为形参数组a已经排序
    i++;
  }
  if (i<n) {
    a[a[i].index].x = 1.0cleft/a[i].w;
    b += a[a[i].index].x
a[i].v;
  }
  return b;
}

Kruskal算法
最小生成树
算法设计2
Kruskal算法构造G的最小生成树的基本思想:
将G的n个顶点看成n个孤立的连通分量,将所有的边按权从小到大排序。
从第一条边开始,依边权递增的顺序查看每一条边,并按下述方法连接两个不同的连通分量:
当查看到第i条边(u,v)时,如果端点u和v分别是当前两个不同的连通分量T1和T2中的顶点时,就用边(u,v)将T1和T2连接成一个连通分量,然后继续查看第i+1条边;
如果端点u和v在当前的同一个连通分量中,就直接再查看第i+1条边。
这个过程一直进行到只剩下一个连通分量时为止,该连通分量就是G的一棵最小生成树。
算法思想

  1. 初始化:U=V; TE={ };
  2. 循环直到T中的连通分量个数为1
    2.1 在E中寻找最短边(u,v);
    2.2 如果顶点u、v位于T的两个不同连通分量,则
    2.2.1 将边(u,v)并入TE;
    2.2.2 将这两个连通分量合为一个;
    2.3 在E中标记边(u,v),使得(u,v)不参加后续最短边的选取;

删数问题
给定n位正整数a,去掉其中任意k≤n个数字后,剩下的数字按原次序排列组成一个新的正整数。对于给定的n位正整数a和正整数k,设计一个算法找出剩下数字组成的新数最小的删数方案(顺序不改变)。
#include
#include
using namespace std;
int main(){
string a; //n位数a
int k; //删除数字的个数
cin>>a>>k;
if (k >= a.size()) a.erase(); //如果k≥n,所有数字均被删除
else while(k > 0){ //寻找最近下降点,逐个删除
int i;
for (i=0; (i<a.size()-1) && (a[i] <= a[i+1]); ++i);
a.erase(i, 1);//删除xi
k–;
}
while(a.size() > 1 && a[0] == ‘0’) //删除前导数字0
a.erase(0, 1);
cout<<a<<endl;
}

有 N 堆纸牌,编号分别为 1,2,…, N。每堆上有若干张,但纸牌总数必为 N 的倍数。可以在任一堆上取若干张纸牌,然后移动。
移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 N 的堆上取的纸牌,只能移到编号为 N-1 的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
贪心策略:每堆牌最多移动一次
cin>>n;
  ave=0;step=0;
  for (i=1;i<=n;++i)   {
    cin>>a[i]; ave+=a[i]; //读入各堆牌张数,求总张数ave
   }
  ave/=n; //求牌的平均张数ave
  for (i=1;i<=n;++i) a[i]-=ave; //每堆牌的张数减去平均数
  i=1;j=n;
  while (a[i]==0&&i<n) ++i; //过滤左边的0
  while (a[j]==0&&j>1) --j; //过滤右边的0
  while (i<j)   {
   a[i+1]+=a[i]; //将第i堆牌移到第i+1堆中去
   a[i]=0; //第i堆牌移走后变为0
   ++step; //移牌步数计数
   ++i; //对下一堆牌进行循环操作
   while (a[i]==0&&i<j) ++i; //过滤移牌过程中产生的0
   }
  cout<<step<<endl;

有n个人要过一条河,每个人过河都需要一个时间,有一艘船,每次过河只能最多装两个人。两个人划船过河所需的时间都取决于过河时间长的那个人。比如,A,B两人过河所需时间分别为a,b,那么,他们成一条船过河所需的时间为:max{a,b}。现在让你安排一个过河方案,让所有人用最短的时间全部过河。
分析:
设其中四人为a、b、c、d,并且所需时间a<b<c<d
那么,现在想让c、d过河,然后再让船回到过河前的位置,准备好继续送其他的人过河。有两种运载方式:
1.过河顺序为:ac、a、ad、a 时间消耗:t 1 =2a+c+d
2.过河顺序为:ab、a(b)、cd、b(a) 时间消耗:t 2 =a+2b+d
t 1 −t 2 =a+c−2b
选择两种方案的哪一种,和a+c−2b 的值有关。
设定n个人过河的贪心策略为:
在要过河人数n≥4的时候,先用上述两种方法中较好的一个,把最大的两个送过河(用过河时间最少的人作为上述方法的a,第二少的作为上述方法的b)。
然后该问题就变成了:寻找把剩下的n-2个人送过河的最优策略。
反复执行此策略,直到
n=2时,显然两个人直接过去就行了,
n=3时,用最小的分别把两个送过去为最优(三个人过河,显然就是:过去两个人,回来一个,在过去两个,两次过去两个的花费分别为:b、c,那这个回来的人,应该是a才能最快,也就是让a分别送b、c。)
#include
#include
using namespace std;
int x[1100];
int min(int a, int b ){
if(a>=b)return b;
return a;
}
int main(){
int N;
cin>>N;
for(int i=1;i<=N;++i)
cin>>x[i];
sort(x+1,x+N+1);
int num=0;
while(N>3){
num+=min((x[1]+x[2]+x[N]+x[2]),(x[1]+x[N]+x[1]+x[N-1]));
N-=2;
}
if(N2)
num+=x[2];
else if(N
3)
num+=x[1]+x[2]+x[3];
else if(N==1)
num+=x[1];
cout<<num;
return 0;
}

第六章 回溯算法

以深度优先的方式系统地搜索问题的解的方法称为回溯法。
可以系统地搜索一个问题的所有解或任意解。
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。
问题解空间:
应用回溯法求解时,需要明确定义问题的解空间。
问题的解空间应至少包含问题的一个(最优)解。

在生成解空间树时,定义以下几个相关概念:
活结点:
如果已生成一个结点而它的所有儿子结点还没有全部生成,则这个结点叫做活结点。
扩展结点:
当前正在生成其儿子结点的活结点叫扩展结点(正扩展的结点)。
死结点:
不能再进一步扩展或者其儿子结点已全部生成的结点就是死结点。

在回溯法搜索解空间树时,通常采用两种策略(剪枝函数)避免无效搜索以提高回溯法的搜索效率:
用约束函数在扩展结点处剪去不满足约束条件的子树;
用限界函数剪去不能得到最优解的子树。

素数环:从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。
算法分析
从1开始,每个空位有20种可能,只要填进去的数合法:
与前面的数不相同;
与左边相邻的数的和是一个素数。
第20个数还要判断和第1个数的和是否素数。
#include
#include
#include
#include
using namespace std;
bool b[21]={0}; //判断i是否出现在素数环中
int total=0,a[21]={0}; //a记录素数环中的每一个数
int search(int t); //回溯过程。形参表示素数环中的数的编号
int print(); //输出方案
bool pd(int,int); //判断素数
int search(int t){ //寻找所有解
int i;
for (i=1;i<=20;i++) //有20个数可选
if (pd(a[t-1],i)&&(!b[i])){ //判断与前一个数是否构成素数及该数是否可用
a[t]=i; //素数环中的第t个数
b[i]=1; //i进入素数环
if (t==20) { //一个解
if (pd(a[20],a[1])) print();}
else
search(t+1);
b[i]=0;
}
}int main(){
search(1);
cout<<total<<endl; //输出总方案数
}
int print(){
total++;
cout<<"<"<<total<<">";
for (int j=1;j<=20;j++)
cout<<a[j]<<" ";
cout<<endl;
}
bool pd(int x,int y){
int k=2,i=x+y;
while (k<=sqrt(i)&&i%k!=0) k++;
if (k>sqrt(i)) return 1;
else return 0;
}

装载问题
给定n个集装箱要装上一艘载重量为c的轮船,其中集装箱i的重量为wi。集装箱装载问题要求确定在不超过轮船载重量的前提下,将尽可能多的集装箱装上轮船
#include
using namespace std;
class goods{
int weight;
public:
goods(int w=0):weight(w)
{}
int get_w(){
return weight;
}
void set(int w){
weight=w;
}

};
void load(goods *g, int *x, int t, int n,int cw, int &bestcw ,int *best,int r,int c){
if(t>n) { //已经遍历的到叶子结点,得到了一个解决方案
if(cw>bestcw) {
for(int i=0;i<n;i++)
best[i]=x[i];
bestcw=cw;
}
}
else{ //每个结点可以有两个分支,分别利用约束规则和限界规则进行剪枝
r=r-g[t].get_w();//剩余未处理的物品的重量和,与是否选取当前物品无关
if(cw+g[t].get_w()<=c){ // 根据题意中的约束条件进行剪枝
x[t]=1;
cw=cw+g[t].get_w(); //当前装入的物品的重量和
load(g,x,t+1,n,cw,bestcw,best,r,c);
cw=cw-g[t].get_w(); //回溯的需要
}
if(cw+r>bestcw) { //限界规则
x[t]=0;
load(g,x,t+1,n,cw,bestcw,best,r,c);
}
r=r+g[t].get_w(); //回溯的需要
}
}
int main(){
int n,c,bestcw=0;
int *x,*best, r=0;
cout<<“请输入物品的件数和轮船的装载重量:";
cin>>n>>c;
goods *g;
g=new goods[n];
x=new int [n];
best=new int[n];
cout<<“请输入每件物品的重量:”;
for(int i=0;i<n;i++) {
int w; cin>>w; g[i].set(w);r=r+w;
}
load(g,x,0,n,0,bestcw,best,r,c);
cout<<bestcw<<endl;
for(i=0;i<n;i++)
cout<<best[i]<<" ";
cout<<endl;
return 0;
}

表达式问题回溯算法的数据结构
#define LEFT -1 //左括号
#define RIGHT -2 //右括号
#define MUL -3 //×号
#define ADD -4 //+号
#define SUB -5 //—号
#define OP -6 //有运算符
#define NONE -10
char a[100]; //原始的等式数据
int b[100]; //伪表达式,对原始等式的改造
int best[100]; //答案
int op[30]; //运算符在数组b中的位置
int bn; //数组b的项数
int iLeft; //等式左边的数
int possible; //是否有解
int apos; //数组a的位置指针
int bpos; //数组b的位置指针
int opos; //操作符数组的位置指针
表达式问题中的函数介绍
void create(); //构造伪表达式。将字符串形式的表达式变成整数形式的表达式
void space(); //跳过表达式中的空格
void backtrack(int dep); //在伪表达式的基础上,递归完成表达式的构造。即在伪表达式中填入运算符
int compute();//计算伪表达式的值
int bracket(); //括号的判断与数值计算。取出表达式中的一个整数
void create(); //构造伪表达式。将字符串形式的表达式变成整数形式的表达式。同时统计出表达式中运算符的个数。
void space(); //跳过表达式中的空格
void backtrack(int dep); //在伪表达式的基础上,递归完成表达式的构造。即在伪表达式中填入运算符
int compute();//计算为表达式的值
int bracket(); //括号的判断与数值计算。取出表达式中的一个整数

处理流程
调用create(); 构造伪表达式
调用 backtrack(int dep); 即在伪表达式中填入运算符
在填好运算符之后时,调用compute()判断是否是一个有效解
在 compute()函数中,调用bracket(), 取出表达式中的一个整数进行计算

第七章分支限界算法

分支限界法类似于回溯法,是一种在问题的解空间树上搜索问题解的算法。
分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出使某一目标函数值达到极大或极小的解,即在某种意义下的最优解。
分支限界法常以广度优先的方式搜索问题的解空间树。
在分支限界法中,每一个活结点只有一次机会成为扩展结点。
活结点一旦成为扩展结点,就一次性产生其所有儿子结点。在这些儿子结点中,导致不可行解或导致非最优解的儿子结点被舍弃,其余儿子结点被加入活结点表中。
此后,从活结点表中取下一结点成为当前扩展结点,并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。
从活结点表中选择下一个活结点作为新的扩展结点,分支限界算法通常可以分为两种形式:
FIFO(First In First Out)分支限界算法
按照先进先出(FIFO)原则选择下一个活结点作为扩展结点,即从活结点表中取出结点的顺序与加入结点的顺序相同。
优先队列分支限界算法
在这种情况下,每个结点都有一个耗费或收益。
根据问题的需要,可能是要查找一个具有最小耗费的解,或者是查找一个具有最大收益的解。

最少步数问题
在各种棋中,棋子的走法总是一定的,如中国象棋中马走“日”。有一位小学生就想如果马能有两种走法将增加其趣味性,因此,他规定马既能按“日”走,也能如象一样走“田”字。他的同桌平时喜欢下围棋,知道这件事后觉得很有趣,就想试一试,在一个(100*100)的围棋盘上任选两点A、B,A点放上黑子,B点放上白子,代表两匹马。棋子可以按“日”字走,也可以按“田”字走,俩人一个走黑马,一个走白马。谁用最少的步数走到左上角坐标为(1,1)的点时,谁获胜。现在他请你帮忙,给你A、B两点的坐标,想知道两个位置到(1,1)点可能的最少步数。
#include
#include
#include
using namespace std;
int dx[12]={-2,-2,-1,1,2,2,2,2,1,-1,-2,-2},
dy[12]={-1,-2,-2,-2,-2,-1,1,2,2,2,2,1};
int main(){
int s[101][101],que[10000][4]={0},x1,y1,x2,y2;
memset(s,0xff,sizeof(s)); //s数组的初始化
int head=1,tail=1; //初始位置入队
que[1][1]=1;que[1][2]=1;que[1][3]=0;
cin>>x1>>y1>>x2>>y2; //读入黑马和白马的出发位置
while(head<=tail) { //若队列非空,则扩展队首结点
for(int d=0;d<=11;d++){ //枚举12个扩展方向
int x=que[head][1]+dx[d]; //计算马按d方向跳跃后的位置
int y=que[head][2]+dy[d];
if(x>0&&y>0&&x<=100&&y<=100)
if(s[x][y]-1) { //若(x,y)满足约束条件
s[x][y]=que[head][3]+1; //计算(1,1)到(x,y)的最少步数
tail++; //(1,1)至(x,y)的最少步数入队
que[tail][1]=x;
que[tail][2]=y;
que[tail][3]=s[x][y];
if(s[x1][y1]>0&&s[x2][y2]>0){ //输出问题的解
cout<<s[x1][y1]<<endl;
cout<<s[x2][y2]<<endl;
system(“pause”);
return 0;
}
}
}
head++;
} }
给定n个集装箱要装上一艘载重量为c的轮船,其中集装箱i的重量为wi。集装箱装载问题要求确定在不超过轮船载重量的前提下,将尽可能多的集装箱装上轮船。
由于集装箱问题是从n个集装箱里选择一部分集装箱,假设解向量为X(x1, x2, …, xn),其中xi∈{0, 1}, xi =1表示集装箱i装上轮船, xi =0表示集装箱i不装上轮船。
int MaxLoading(){
 queue Q;
 Q.push(-1);
 int i = 0; //层号
 int Ew = 0; //当前的装载重量
 int bestw = 0;//最优装载重量
 int r = 0; //剩余货物的总重量
 for(int j=1; j<n; j++)
  r += w[j];
 while (true) {//搜索子空间树
  //检查左子树
  int wt = Ew+w[i];
  if (wt<=c){  //检查约束条件
   if (wt>bestw) bestw = wt;
      if (i<n-1) Q.push(wt); //加入活结点队列
  }
  if (Ew+r>bestw && i<n-1) //检查右子树, 检查上界条件
     Q.push(Ew);
  Ew = Q.front(); //从队列中取出活结点
  Q.pop();
  if (Ew
-1) { //判断同层的尾部
   if (Q.empty())
return bestw;
   Q.push(-1); //同层结点尾部标志
   Ew = Q.front(); //从队列中取出活结点
   Q.pop();
   i++;
   r -= w[i];
  }
 }
 return bestw;
}

旅行商问题
是指一销售商从n个城市中的某一城市出发,不重复地走完其余n—1个城市并回到原出发点,在所有可能的路径中求出路径长度最短的一条。本题假定该旅行商从第1个城市出发。
#include
#include
using namespace std;

class Traveling{
int n;//图中顶点的个数
int e;//边的数目
int **a;//邻接矩阵(无向图的邻接矩阵)
int cc;//当前路径(子路径,非完整路径)的长度
int bestc;//当前最小费用
int NoEdge;
public:
int BBTSP();
Traveling();
};
struct node{
  //优先队列以lcost为优先级参数
  friend bool operator < (const node& a,const node& b){
    if(a.lcost > b.lcost) return true;
    else return false;
  }
  int lcost; //子树费用的下界
  int rcost; //从x[s]~x[n-1]顶点的最小出边和
  int cc;   //当前费用。截止到当前城市的路径长度
  int s;   //当前结点的编号。结点的层次编号。已经经过的城市的数目
  int *x;  //需进一步搜索的路径x[s+1:n]。全排列
};
定义优先队列
priority_queue H;
int minOut[NUM];    //各个顶点的最小出边费用
int minSum = 0;     //最小出边费用之和
//计算各个顶点的最小出边费用
int i, j;
for(i=1; i<=n; i++)
{
  int Min = NoEdge;
  for(j=1; j<=n; j++)
    if( a[i][j]!=NoEdge && (a[i][j]<Min || MinNoEdge))
      Min = a[i][j];
  if (Min
NoEdge) return NoEdge; //无回路
  minOut[i] = Min;
  minSum += Min;
}

相关文章:

  • 2022-12-23
  • 2021-05-03
  • 2021-12-21
  • 2021-08-27
  • 2021-12-23
猜你喜欢
  • 2021-10-17
  • 2021-05-30
  • 2022-02-06
  • 2021-05-05
  • 2021-11-11
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案