zzuli算法复习大三上

分值占比

一、(20 5 * 4)四道解递归方程

二、(40 5 * 2+10 * 3 )分析阅读代码和伪代码回答时间复杂度等问题,动态规划最长公共子序列,动态规划、分治的原理步骤以及改进策略。

三、(40 20+20)大题: 回溯/分支限界20分、 最大连续子序列求/货物装载(20分)

主定理法

我们要处理一个 规模为 n 的问题通过分治,得到 a 个规模为 \frac{n}{b} 的问题,分解子问题和合并子问题的时间是 f(n)image-20211215215849340

三种判断:image-20211215215930995

image-20211215220049084

分治法

  1. 分治法的设计思想:对于一个规模为n的问题,若该问题可以容易地解决(例如n的规模较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题相互独立且与原问题形式相同,递归地解决子问题,然后将各子问题的解合并得到原问题的解,这种算法的策略叫做分治法。

  2. 分治法的步骤

    1. 分解成若干个问题:将原问题分解成若干个规模较小,相互独立,与原问题相同的子问题
    2. 求解子问题:若子问题规模较小,容易被解决,则直接求解,否则递归地求解各个子问题
    3. 合并子问题:将各个子问题合并。
  3. 分治法的改进策略

    • 代数变换,减少子问题个数
    • 增加预处理,利用预处理减少递归内部的计算量
  4. 分治法的适用情况:1 该问题的规模缩小到一定的程度就可以容易地解决。2 该问题可以分解为若干个规模较小的相同问题。3 利用该问题分解出的子问题的解可以合并为该问题的解。4该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题。

  5. 循环赛日程表

    image-20211215202842644

    /*
       将n=2k问题划分为4部分:
       (1)左上角:左上角为2k-1个选手在前半程的比赛日程(k=1时直接给出,否则,上一轮求出的就是2k-1个选手的比赛日程)。
       (2)左下角:左下角为另2k-1个选手在前半程的比赛日程,由左上角加2k-1得到,例如22个选手比赛,左下角由左上角直接加2(2k-1)得到,23个选手比赛,左下角由左上角直接加4(2k-1)得到。
       (3)右上角:将左下角直接复制到右上角得到另2k-1个选手在后半程的比赛日程。
       (4)右下角:将左上角直接复制到右下角得到2k-1个选手在后半程的比赛日程。
    */
    #include <stdio.h>
    #define MAX 101
    //问题表示
    int k;			//求解结果表示
    int a[MAX][MAX];	//存放比赛日程表(行列下标为0的元素不用)
    void Plan(int k)
    {   int i,j,n,t,temp;
        n=2;			//n从2^1=2开始
        a[1][1]=1; a[1][2]=2;   	//求解2个选手比赛日程,得到左上角元素
        a[2][1]=2; a[2][2]=1;
        for (t=1;t<k;t++)		//迭代处理2^2(t=1)…,2^k(t=k-1)个选手
        {	temp=n;					//temp=2^t
    	n=n*2; 					//n=2^(t+1)
    	for (i=temp+1;i<=n;i++ )		//填左下角元素
    	   for (j=1; j<=temp; j++)
    		a[i][j]=a[i-temp][j]+temp; 	//产生左下角元素
    	for (i=1; i<=temp; i++)		//填右上角元素
    	   for (j=temp+1; j<=n; j++)
    		a[i][j]=a[i+temp][(j+temp)% n];
    	for (i=temp+1; i<=n; i++)		//填右下角元素
    	   for (j=temp+1; j<=n; j++)
    		a[i][j]=a[i-temp][j-temp];
        }
    }
    
    
  6. 二分和归并排序,注意时间复杂度求解。

    §例2.1 设 是一个给定实数,计算 ,其中n为自然数。

    时间复杂度,暴力法o(n) 分治法,o(logn)image-20211215213410084

最长连续子序列

  1. 暴力枚举法

    1. 三维略

    2. 二维优化

      int maxSubSum2(int a[],int n)
      {  int i,j;
         int maxSum=a[0],thisSum;
         for (i=0;i<n;i++)  
         {  thisSum=0;
            for (j=i;j<n;j++)
            {  thisSum+=a[j];  //maxSum已经包含了a[i..j-1]的最大和
           if (thisSum>maxSum)
             maxSum=thisSum;
          }
         }
         return maxSum;
      }
      
      
    3. 一维优化

      优化思路:如果扫描中遇到负数,当前子序列和thisSum将会减小,若thisSum为负数,表明前面已经扫描的那个子序列可以抛弃了,则放弃这个子序列,重新开始下一个子序列的分析,并置thisSum为0。

      int maxSubSum3(int a[],int n)
      {  int i,maxSum=0,thisSum=0;
         for (i=0;i<n;i++)
         {  thisSum+=a[i];
            if (thisSum<0)        //若当前子序列和为负数,重新开始下一子序列
           thisSum=0;
            if (maxSum<thisSum)   //比较求最大连续子序列和
           maxSum=thisSum;
         }
         return maxSum;
      }
      
      

      若这个子序列和thisSum不断增加,那么最大子序列和maxSum也不断增加。

  2. 分治策略

    long maxSubSum(int a[],int left,int right)	
    //求a[left..high]序列中最大连续子序列和
    {  int i,j;
       long maxLeftSum,maxRightSum;
       long maxLeftBorderSum,leftBorderSum;
       long maxRightBorderSum,rightBorderSum;
       if (left==right)		//子序列只有一个元素时
       {  if (a[left]>0) 	//该元素大于0时返回它
          return a[left];
        else			//该元素小于或等于0时返回0
           return 0; 
       } 
       int mid=(left+right)/2;			//求中间位置
      maxLeftSum=maxSubSum(a,left,mid);	//递归求左边
      maxRightSum=maxSubSum(a,mid+1,right);	//递归求右边
      maxLeftBorderSum=0,leftBorderSum=0;// 初始化最大Left子序列和,Left子序列和
      for (i=mid;i>=left;i--) 			//求出以左边加上a[mid]元素
      {  leftBorderSum+=a[i]; 			//构成的序列的最大和
          if (leftBorderSum>maxLeftBorderSum) //更新Left最大值
          maxLeftBorderSum=leftBorderSum;
      }
      maxRightBorderSum=0,rightBorderSum=0; 
      for (j=mid+1;j<=right;j++)		//求出a[mid]右边元素
      {  rightBorderSum+=a[j];  		//构成的序列的最大和
         if (rightBorderSum>maxRightBorderSum)
          maxRightBorderSum=rightBorderSum;
      }
      return max3(maxLeftSum,maxRightSum,        //return三者最大值
    		   maxLeftBorderSum+maxRightBorderSum); 
    }
    
  3. 动态规划策略

    1. 表示方法:对于含有n个整数的序列a,设b,表示以a[i]结尾的最大子序列和,则b;表示以a[li-1]为结尾的最大连续子序列和。

    2. 核心转移方程

      image-20211215200425565

    3. 核心代码

      //问题表示,省略
      int n=6;
      int a[]={0,-2,11,-4,13,-5,-2};	//a数组不用下标为0的元素
      //求解结果表示
      int dp[MAXN];
      void maxSubSum()			//求dp数组
      {  dp[0]=0;
         for (int j=1;j<=n;j++)
      	dp[j]=max(dp[j-1]+a[j],a[j]);
      }
      
      
    4. 数组变化

      image-20211215200349179

  4. 时间复杂度比较

    image-20211215174316093

最长公共子序列

void LCSlength()			//求dp
{  int i,j;
   for (i=0;i<=m;i++)			//将dp[i][0]置为0,边界条件
      dp[i][0]=0;
   for (j=0;j<=n;j++)			//将dp[0][j]置为0,边界条件   
      dp[0][j]=0;
   for (i=1;i<=m;i++)
      for (j=1;j<=n;j++)		//两重for循环处理a、b的所有字符
      {  if (a[i]==b[j])		//情况(1)
            dp[i][j]=dp[i-1][j-1]+1;
         else				//情况(2)
            dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
      }
}
void Buildsubs()		    //由dp构造最长公共子序列
{  int k=dp[m][n];		    //k为a和b的最长公共子序列长度
   int i=m;
   int j=n;
   while (k>0)			    //在subs中放入最长公共子序列(反向)
     if (dp[i][j]==dp[i-1][j])
	i--;
     else if (dp[i][j]==dp[i][j-1])
	j--;
     else			    //与上方、左边元素值均不相等
     {	 printf(a[i]);  //最长公共子序列中有a[i]
	 i--; j--; k--;
     }
}

01背包问题

image-20211215203821679

//问题表示
int n=5,W=10;			//5种物品,限制重量不超过10
int w[MAXN]={0,2,2,6,5,4};	//下标0不用
int v[MAXN]={0,6,3,5,4,6};	//下标0不用
//求解结果表示
int dp[MAXN][MAXW];
int x[MAXN];
int maxv; 			//存放最优解的总价值
void Knap()			//动态规划法求0/1背包问题
{  int i,r;
   for (i=0;i<=n;i++)		//置边界条件dp[i][0]=0
      dp[i][0]=0;
   for (r=0;r<=W;r++)		//置边界条件dp[0][r]=0
      dp[0][r]=0;
   for (i=1;i<=n;i++)
   {  for (r=1;r<=W;r++)
        if (r<w[i]) //放不下该物品,则最大价值等于不放该物品的价值
           dp[i][r]=dp[i-1][r];
        else
           dp[i][r]=max(dp[i-1][r],dp[i-1][r-w[i]]+v[i]);
   }
}
void Buildx()				//回推求最优解
{  int i=n,r=W;
   maxv=0;
   while (i>=0)			//判断每个物品
   {
      if (dp[i][r]!=dp[i-1][r]) 
      {  x[i]=1;			//选取物品i
         r=r-w[i];
      }
      else
        x[i]=0;			//不选取物品i
      i--;
   }
}

资源分配问题

image-20211215203320731

//问题表示
int m=3,n=5;				//商店数为m,总人数为n
int v[MAXM][MAXN]={{0,0,0,0,0,0},{0,3,7,9,12,13},
	{0,5,10,11,11,11},{0,4,6,11,12,12}}; //不计v[0]行
//求解结果表示
int dp[MAXM][MAXN];
int pnum[MAXM][MAXN];

void Plan()			  //求最优方案dp
{  int maxf,maxj;
   for (int j=0;j<=n;j++)	  //置边界条件
     dp[m+1][j]=0;
   for (int i=m;i>=1;i--)	  //i从商店3到1进行处理
   {  for (int s=1;s<=n;s++)	  //分配的总人数为s
      {  maxf=0;
         maxj=0;
         for (j=0;j<=s;j++)	  //找该商店最优情况maxf和分配人数maxj
         {  if ((v[i][j]+dp[i+1][s-j])>=maxf)
            {  maxf=v[i][j]+dp[i+1][s-j];
               maxj=j;
            }
         }
         dp[i][s]=maxf;
         pnum[i][s]=maxj;
      }
   }
}
dp[m+1][j]=0			  	       //边界条件(类似终点的dp值为0)
dp[i][s]=max(v[i][j]+dp[i+1][s-j]) //pnum[i][s]=dp[i][s]取最大值的j(0≤j≤n)

多重背包问题

image-20211215204042375

/*
设置二维数组fk,其中fk[i][j]存放dp[i][j]得到最大值时物品i挑选的件数。
dp[i][j]=MAX{dp[i-1][j-k*w[i]]+k*v[i]}  当dp[i][j] < 
               dp[i-1][j-k*w[i]]+k*v[i](k*w[i]≤j)
fk[i][j]=k;			    	    物品i取k件
*/

//问题表示
int n,W;
int w[MAXN],v[MAXN];
//求解结果表示
int dp[MAXN+1][MAXW+1],fk[MAXN+1][MAXW+1];
int solve()	//求解多重背包问题
{  int i,j,k;
   for (i=0;i<=n;i++) 
    for (j=0;j<=W;j++) {dp[i][j]=0; fk[i][j]=0};
   for (i=1;i<=n;i++)
   {  for (j=0;j<=W;j++)
        for (k=0;k*w[i]<=j;k++)
        {  if (dp[i][j]<dp[i-1][j-k*w[i]]+k*v[i])
           {  dp[i][j]=dp[i-1][j-k*w[i]]+k*v[i];
              fk[i][j]=k;		//物品i取k件
           }
        }
   }
   return dp[n][W];
}



回溯法

  1. 解空间为子集树

    int x[n];			   //x存放解向量,全局变量
    void backtrack(int i)		   //求解子集树的递归框架
    {  if(i>n)			   //搜索到叶子结点,输出一个可行解
          输出结果;
       else                         //当前节点可以扩展,存在子节点
       {  for (j=下界;j<=上界;j++)   //用j枚举i所有可能的路径
          {  x[i]=j;		   //产生一个可能的解分量
             if (constraint(i) && bound(i))
                backtrack(i+1);	   //满足约束条件和限界函数,继续下一层
          }
       }
    }
    
    
  2. 解空间为排列树

    int x[n];			//x存放解向量,并初始化
    void backtrack(int i)		//求解排列树的递归框架
    {  if(i>n)			//搜索到叶子结点,输出一个可行解
    	输出结果;
       else
       {  for (j=i;j<=n;j++) //用j枚举i所有可能的路径,后面还没有排列的元素
          {  
            为第i层节点选择x[j],并保证排列中每个元素不同;	
             if (constraint(i) && bound(i))
    	     backtrack(i+1);	//满足约束条件和限界函数,进入下一层
          }
       }
    }
    
    
    7行:排列树和子集树算法框架最大的差别在于:j的取值意义和X[i]的取值意义不同!
  3. 求解货物装载问题

    【问题描述】有n个集装箱要装上一艘载重量为w的轮船,其中集装箱i(1≤i<n)的重量为wi。不考虑集装箱的体积限制,现要这些集装箱中选出若干装上轮船,使它们的重量之和小于等于W,当总重量相同时要求选取的集装箱个数尽可能少。
    如果换成让集装箱的个数尽可能多呢?如果再换成让集装箱的体积尽可能大呢?
    例如,n=5,W=10,wi={5,2,6,4,3}时,其最佳装载方案是(0,0,1,1,0),即装载第3、4个集装箱。

    //void dfs(int num,int tw,int rw,int op[],int i)
    //其中num表示选择的集装箱个数,tw表示选择的集装箱重量和,rw表示剩余集装箱的重量和,op表示一个
    //解,即一个选择方案,i表示考虑的集装箱i。
    void dfs(int num,int tw,int rw,int op[],int i) //考虑第i个集装箱
    {  if (i>n)			//找到一个叶子结点
       {  if (tw==W && num<minnum)
          {  maxw=tw;		//找到一个满足条件的更优解,保存它
             minnum=num;
             for (int j=1;j<=n;j++)
                x[j]=op[j];	 //复制最优解
          }
       }
       else				//尚未找完所有集装箱
       {  op[i]=1;			//选取第i个集装箱
          if (tw+w[i]<=W)		//左孩子结点剪枝:装载满足条件的集装箱
             dfs(num+1,tw+w[i],rw-w[i],op,i+1);
          op[i]=0;			//不选取第i个集装箱,回溯
          if (tw+rw>W)		//右孩子结点剪枝
            dfs(num,tw,rw-w[i],op,i+1);
       }
    }
    
    

    问题二:【子集和问题描述】给定n个不同的正整数集合w= (w,w,,)和一个正数w,要求找出w的子集s,使该子集中所有元素的和为
    w。
    例如,当n=4时,w= (11,13,24,7),W=31,则满足要求的子集为(11,13,7)和(24,7) 。

    解空间树

    image-20211215211838135

  4. 01背包问题

    (1)选择第i个物品放入背包:op[i]=1,tw=tw+w[i],tv=tv+v[i],转向下一个状态Knap_dfs(i+1,tw,tv,op)。该决策对应左分枝。
    (2)不选择第i个物品放入背包:op[i]=0,tw不变,tv不变,转向下一个状态Knap_dfs(i+1,tw,tv,op)。该决策对应右分枝。

    //问题表示
    int n=4;			//4种物品
    int W=6;			//限制重量为6
    int w[]={0,5,3,2,1};		//存放4个物品重量,不用下标0元素
    int v[]={0,4,4,3,1};		//存放4个物品价值,不用下标0元素
    //求解结果表示
    int x[MAXN];			//存放最终解
    int maxv; 			//存放最优解的总价值
    void Knap_dfs(int i,int tw,int tv,int op[]) //求解0/1背包问题
    {  if (i>n)				//找到一个叶子结点
       {  if (tw<=W && tv>maxv)		//找到一个满足条件的更优解,保存 
          {  maxv=tv;
             for (int j=1;j<=n;j++)
                x[j]=op[j];
          }
       }
       else					//尚未找完所有物品
       {  op[i]=1;				//选取第i个物品
          Knap_dfs(i+1,tw+w[i],tv+v[i],op);
          op[i]=0;				//不选取第i个物品,回溯
          Knap_dfs(i+1,tw,tv,op);
       }
    }
    
    

123image-20211215214439888

//左剪枝
else					//尚未找完所有物品
{  if (tw+w[i]<W)			//左孩子结点剪枝
   {  op[i]=1;			//选取第i个物品
      Knap_dfs (i+1,tw+w[i],tv+v[i],op);
   }
   op[i]=0;				//不选取第i个物品,回溯
   Knap_dfs (i+1,tw,tv,op);
}
//右剪枝
else					//尚未找完所有物品
{  if (tw+w[i]<=W)			//左孩子结点剪枝
   {  op[i]=1;			//选取第i个物品
      Knap_dfs (i+1,tw+w[i],tv+v[i],rw-w[i],op);
   }
   op[i]=0;		//不选取第i个物品,回溯
   if (tw+rw>W)	//右孩子结点剪枝,也就是总重量超过才继续递归扩展。
      Knap_dfs (i+1,tw,tv,rw-w[i],op);
}


image-20211215214525095

分枝限界法

分枝限界法的步骤:

  • 第一步:确定限界函数:
    限界函数把后面所有节点都扩展能够得到可能的最小成本。
    也就是说只有限界函数<当前最小解才值得扩展!

  • 第二步:通过分枝限界法画出解空间树:
    在约束条件不成立时减枝;
    在限界函数>当前最小解时剪枝。

  • 第三步:套用回溯法算法框架,并增加约束条件。

image-20211215214802867

image-20211215214911733

posted @ 2021-12-15 22:17  zzuli_DYS  阅读(212)  评论(0)    收藏  举报