算法分析与设计课程复习之动态规划

算法分析与设计课程复习之动态规划

一、基本思想

将待求解问题分解成若干个子问题。保存已解决的子问题的答案。

二、动态规划和分治法的区别

分治法是把大问题分解成一些相互独立的子问题,递归的求解这些子问题然后将他们合并来得到整个问题的解。分治法是自顶向下
动态规划是通过组合子问题的解来解决整个大问题。各个子问题不是独立的,也就是各个子问题包含公共子问题。它可以避免遇到的子问题的重复求解。动态规划法是自底向上

三、动态规划算法的基本要素

  • 最优子结构
  • 重叠子问题

四、动态规划解法的基本步骤

1. 找出最优解的性质,并刻划其结构特征。

2. 递归地定义最优值。

3. 以自底向上的方式计算出最优值。

4. 根据计算最优值时得到的信息,构造最优解

五、经典问题

1.背包问题(0-1背包)

设U = {u1,u2,. . . ,un}是一个准备放入容量为C的背包中的n项物品的集合。对于1 ≤ j ≤ n1。,令sj, 和vj分别为第j项物品的体积和价值,这里, C , sj,vj和j都是正整数
要解决的问题是用U中的一些物品来装满背包,这些物品的总体积不超过C,然而要使它们的总价值最大。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN];    // 体积
int w[MAXN];    // 价值 
int f[MAXN][MAXN];  // f[i][j], j体积下前i个物品的最大价值 

int main() 
{
    int n, m;   
    cin >> n >> m;
    for(int i = 1; i <= n; i++) 
        cin >> v[i] >> w[i];

    for(int i = 1; i <= n; i++) 
        for(int j = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           

    cout << f[n][m] << endl;

    return 0;
}

2.最长公共子序列(求出长度和值)

给定两个序列A = a1, a2, …,an and B = b1, b2, …, bm ,并希望找到A和B中的最长公共子序列以及序列长度。

设序列A={a1,a2,…,an}和B={b1,b2,…,bm}的最长公共子序列为C={c1,c2,…,ck} ,则
(1)若an=bm,则ck=an=bm,且Ck-1是An-1和Bm-1的最长公共子序列。
(2)若an≠bm且ck≠an, 则C是An-1和B的最长公共子序列。
(3)若an≠bm且ck≠bm,则C是A和Bn-1的最长公共子序列。

#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];//用来存最长公共子序列的长度
char temp[N];//用来存最长公共子序列的值
int k;//temp数组的下标
int main() {
  cin >> n >> m >> a + 1 >> b + 1;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i] == b[j]) {
        f[i][j] = f[i - 1][j - 1] + 1;//相等的话直接长度加1
      } else {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);//不相等的话是原来最大的长度
      }
    }
  }
   //恢复最长公共序列,这里从a串还原最长公共子序列
   int l1=n,l2=m;
   while(l1!=0&&l2!=0)
   {
     if(f[l1][l2]!=f[l1-1][l2])//此时a[i]一定包含在最长公共子序列中
     {
       temp[++k]=a[l1];
       l1--;//继续判断a串中的前一个字符
       l2--;//不管b[j]来自那个哪个表达式,b[j]都已经判断过了
     }
      else if(f[l1][l2]==f[l1][l2-1])l2--;//当f[i][j]来自f[i][j-1],说明b[j]不在最长公共子序列中
      else l1--;//说明a[i]不在最长公共子序列中
    }
  cout << f[n][m] <<endl;
  for(int i=f[n][m];i>=1;i--)
  {
    cout<<temp[i];
  }
  return 0;
}

3.Floyd算法(求最短路径)

初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

4.完全背包问题

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++)
    {
        cin>>v[i]>>w[i];
    }
    //i表示物品,j表示总体积,k表示个数
    for(int i = 1 ; i<=n ;i++)
    for(int j = 0 ; j<=m ;j++)
    {
        for(int k = 0 ; k*v[i]<=j ; k++)
            f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    }
    //f[i-1][j-k*v[i]]+k*w[i]表示选择第i个物品后的总价值
    cout<<f[n][m]<<endl;
}

5.硬币找零问题(完全背包问题)

给定 n 种不同面值的硬币,分别记为 c[0], c[1], c[2], … c[n],假设每种硬币的数量是无限的。同时还有一个总金额 k,编写一个动态规划计算出最少需要几枚硬币凑出这个金额 k?

#include <iostream>
#include <algorithm>
using namespace std;
const int N =1e2+10;
int sum;
int v[N];
int n;
int dp[N];
int main()
{
    cin>>sum;
    int x;
    while(cin>>x)
    {
        v[++n]=x;
        if(cin.get()=='\n')break;
    }
    for(int i=0;i<=sum;i++)dp[i]=N;//初始化
    dp[0]=0;//当金额为0时,需要的硬币数量为0
    for(int i=1;i<=sum;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(v[j]<=i)//硬币价值小于当前价值
                dp[i]=min(dp[i],dp[i-v[j]]+1);
        }
    }
    cout<<dp[sum]<<endl;
    return 0;
}

类比完全背包问题的解法:

#include <iostream>
#include <algorithm>
using namespace std;
const int N =1e2+10;
int sum;
int v[N];
int n;
int dp[N][N];
int main()
{
    cin>>sum;
    int x;
    while(cin>>x)
    {
        v[++n]=x;
        if(cin.get()=='\n')break;
    }
    //初始化操作,当金额为0时,dp赋值为0
    for(int i=0;i<=n;i++)
    {
        for(int j=1;j<=sum;j++)
        {
            dp[i][j]=N;
        }
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=sum;j++)
        {
            for(int k=0;k*v[i]<=j;k++)
            {
                dp[i][j]=min(dp[i][j],dp[i-1][j-k*v[i]]+k);
            }
        }
    }
    cout<<dp[n][sum]<<endl;
    return 0;
}

6.矩阵链相乘
详细可参考:矩阵链相乘
问题描述:给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2,…,n-1。要算出这n个矩阵的连乘积A1A2…An。

解法:将矩阵连乘积A(i)A(i+1)…A(j)简记为A[i:j]
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] + p(i-1)p(k)p(j)
这里A(i)的维数为p(i-1)*p (i)(注:p(i-1)为矩阵A(i)的行数,p(j)为矩阵A[j]的列数)

void MATRIX_CHAIN_ORDER(int p[],int Length,int m[][M],int s[][M])
{
	int q,n=Length-1;
	for(int i=1;i<=n;i++) m[i][i]=0;//初始化处理
	for(int l=2;l<=n;l++) 	// 矩阵链的长度 
	{
		for(int i=1;i<=n-l+1;i++) 
		{
			int j=i+l-1;         /* 等价于 l=j-i+1 */
			m[i][j]=INT_MAX;
			for(int k=i;k<=j-1;k++)
			{
				q=m[i][k]+m[k+1][j]+p[i-1]*p[k]*p[j];
				if(q<m[i][j])
				{
					m[i][j]=q;
					s[i][j]=k;
				}
			}
		}
	}
}

7.双机流水作业调度问题
设有n个作业的集合{0,1,…,n-1},每个作业都有两项任务要求在2台设备P1和P2组成的流水线上完成加工。每个作业加工的顺序总是先在P1上加工,然后在P2上加工。P1和P2加工作业i所需的时间分别为ai和bi。流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在设备P1上开始加工,到最后一个作业在设备P2上加工完成所需的时间最少。即求使F(S)有最小值的调度方案S。
流水作业调度问题的Johnson算法:
(1)令N1={i|ai<bi},N2={i|ai>=bi};
(2)将N1中作业按ai的 非减序排序;将N2中作业按bi的非增序排序
(3)N1中作业接N2中作业构成满足Johnson法则的最优调度。

/*
1)先执行t[i,1]<t[i,2],保证M2上没有等待。选择第一个作业时,让M2选择第一次等待时间最少的,
即t[i,1]越小,越靠前执行;
2)执行t[i,1]>=t[i,2]时,要保证最后一个作业在M2上执行时间最短,所以按照减序排列
*/
#include<algorithm>
#include <iostream>
using namespace std;
const int N = 100;
struct node
{
 int time;//执行时间
 int index;//作业序号
 bool group;//1代表第一个机器,0代表第二个机器
};
bool cmp(node a,node b)
{//升序排序
 return a.time<b.time;
}
int main()
{
 int n;
 int a[N]={0},b[N]={0};
 int best[N];//最优调度序列
 node c[N];
 cin>>n;
 for(int i=0;i<n;i++)
 {
     cin>>a[i]>>b[i];
 }
 for(int i=0;i<n;i++)
 {   //n个作业中,每个作业的最小加工时间
     c[i].time=a[i]>b[i]?b[i]:a[i];
     c[i].index=i;
     c[i].group=a[i]<b[i];//给符合条件a[i]<b[i]的放入到N1子集标记为true
     
 }
 sort(c,c+n,cmp);//按照c[]中作业时间增序排序
 int j=0,k=n-1;
 for(int i=0;i<n;i++)
 {
     if(c[i].group)
     {   //第一组,从i=0开始放入到best[]中
         best[j++]=c[i].index;//将排过序的数组c,取其中作业序号属于N1的从前面进入,实现N1的非减序排序
     }
     else
     {
         best[k--]=c[i].index;//将排过序的数组c,取其中作业序号属于N2的从后面进入,实现N2的非增序排序
     }
     
 }
 j=a[best[0]];//最优调度序列下的消耗总时间,第一个选取M1上的工作时间最少的
 k=j+b[best[0]];
 for(int i=1;i<n;i++)
 {
     j+=a[best[i]];
     k=j<k?(k+b[best[i]]):j+b[best[i]];//消耗总时间的最大值
 }
 cout<<k<<endl;
 for(int i=0;i<n;i++)
 {
     cout<<best[i]+1<<" ";
 }
 cout<<n<<endl;
 return 0;  
}   

8.最大子段和问题
求一个序列的最大子段和即最大连续子序列之和。例如序列[4, -3, 5, -2, -1, 2, 6, -2]的最大子段和为11=[4+(-3)+5+(-2)+(-1)+(2)+(6)]。
时间复杂度为:O(n)

#include<algorithm>
#include <iostream>
using namespace std;
const int N = 100;
int n;
int a[N];
int dp[N];
int main()
{
    cin>>n;
    int sum = 0;
    for(int i=1;i<=n;i++)
        cin>>a[i];
    dp[1]=max(dp[1],a[1]);//初始化dp[1]
    for(int i=2;i<=n;i++)
    {
        dp[i]=max(dp[i-1]+a[i],a[i]);
        sum=max(sum,dp[i]);
    }
    cout<<sum<<endl;
    return 0;
}

9.最长上升子序列
给定一个长度为N的数列(w[N]),求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数N。
第二行包含N个整数,表示完整序列。

#include <iostream>
using namespace std;
const int N = 1010;
int n;
int w[N], f[N];

int main()
 {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> w[i];

    int mx = 1;    // 找出所计算的f[i]之中的最大值,边算边找
    for (int i = 0; i < n; i++)
    {
        f[i] = 1;    // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
        for (int j = 0; j < i; j++)
        {
            if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1);    // 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
        }
        mx = max(mx, f[i]);
    }
    cout << mx << endl;
    return 0;
}

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值