动态规划算法-个人心得

类型一:背包问题

1. 0-1背包

问题描述:有一个容量为 V 的背包,和一些物品。这些物品分别有两个属性,体积 w 和价值 v,每种物品只有一个。要求用这个背包装下价值尽可能多的物品,求该最大价值,背包可以不被装满。

分析过程

         在最优解中,每个物品只有两种可能的情况,即在背包中或者不在背包中(背包中的该物品数为0或1),因此称为0-1背包问题。

步骤1-找子问题:子问题必然是和物品有关的,对于每一个物品,有两种结果:能装下或者不能装下。第一,包的容量比物品体积小,装不下,这时的最大价值和前i-1个物品的最大价值是一样的。第二,还有足够的容量装下该物品,但是装了不一定大于当前相同体积的最优价值,所以要进行比较。由上述分析,子问题中物品数和背包容量都应当作为变量。因此子问题确定为背包容量为j时,求前i个物品所能达到最大价值。

步骤2-确定状态:由上述分析,“状态”对应的“值”即为背包容量为j时,求前i个物品所能达到最大价值,设为dp[i][j]。初始时,dp[0][j](0<=j<=V)为0,没有物品也就没有价值。

步骤3-确定状态转移方程:由上述分析,第i个物品的体积为w,价值为v,则状态转移方程为

  • j //背包装不下该物品,最大价值不变
  • j>=w, dp[i][j] = max{ dp[i-1][j-list[i].w] + v, dp[i-1][j] } //和不放入该物品时同样达到该体积的最大价值比较

【正序代码示例】

#include

int max(int a, int b)//取最大值函数
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[101][1001];

int main()
{
    int s, n;//背包容量和物品总数
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//读入每个物品的体积和价值
        }
        for (int i = 0; i <= s; i++) dp[0][i] = 0;//初始化二维数组
        for (int i = 1; i <= n; i++)//循环每个物品,执行状态转移方程
        {
            for (int j = 0; j <= s; j++)
            {
                if (j >= list[i].w)dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);
                else dp[i][j] = dp[i - 1][j];
            }
        }
        printf("%d\n", dp[n][s]);
    }
    return 0;
}

 

        观察状态转移方程的特点,我们发现dp[i][j]的转移只与dp[i-1][j-list[i].w]和dp[i-1][j]有关,即仅与二维数组本行的上一行有关。因此,我们可以将二维数组优化为一维数组。不过这里要注意两点:1.j

优化后的状态转移方程:dp[j] = max{ dp[j-list[i].w] + v, dp[j] }

复杂度分析:其状态数量为n*s, n为物品数量,s为背包总体积,状态转移复杂度为O(1),所以综合时间复杂度为O(n*s),优化后的空间复杂度仅为O(s)。

【优化代码,倒序代码示例】

#include

const int INF = -999999;

int max(int a, int b)//取最大值函数
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[101][1001];

int main()
{
    int s, n;//背包容量和物品总数
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//读入每个物品的体积和价值
        }
        dp[0][0] = 0;
        for (int i = 1; i <= s; i++) dp[0][i] = INF;//初始化二维数组
        for (int i = 1; i <= n; i++)//循环每个物品,执行状态转移方程
        {
            for (int j = s; j >= list[i].w; j--)
            {
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);
            }
            for (int j = list[i].w - 1; j >= 0; j--)
            {
                dp[i][j] = dp[i - 1][j];
            }
        }
        printf("%d\n", dp[n][s]);
    }
    return 0;
}

2. 完全背包

问题描述: 有一个容积为 V 的背包,同时有 n 个物品,每个物品均有各自的体积 w 和价值 v,每个物品的数量均为无限个,求使用该背包最多能装的物品价值总和。

正好利用了上述0-1背包的优化算法,这时我们正序遍历j,正好可以实现每种物品的重复利用,即相当于每种物品有无限个。

#include

int max(int a, int b)//取最大值函数
{
    return a > b ? a : b;
}

struct Thing
{
    int w;
    int v;
}list[101];

int dp[1001];

int main()
{
    int s, n;//背包容量和物品总数
    while (scanf("%d%d", &s, &n) != EOF)
    {
        for (int i = 1; i <= n; i++)
        {
            scanf("%d%d", &list[i].w, &list[i].v);//读入每个物品的体积和价值
        }
        for (int i = 0; i <= s; i++) dp[i] = 0;//初始化二维数组
        for (int i = 1; i <= n; i++)//循环每个物品,正序遍历j执行状态转移方程
        {
            for (int j = list[i].w; j <= s; j++)
            {
                dp[j] = max(dp[j], dp[j - list[i].w] + list[i].v);
            }
        }
        printf("%d\n", dp[s]);
    }
    return 0;
}

类型二: 子序列问题

【说明】子串与子序列的区别

     (1)字符子串指的是字符串中连续的n个字符,如abcdefg中,ab,cde,fg等都属于它的字串。

     (2)字符子序列指的是字符串中不一定连续但先后顺序一致的n个字符,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。

1. 最长上升子序列

       最长上升子序列(Longest  Increasing Subsequence),简称LIS,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。假设我们有一个序列 b i,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们也可以从中得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N,但必须按照从前到后的顺序。比如,对于序列(1, 7, 3, 5, 9, 4, 8),我们就会得到一些上升的子序列,如(1, 7, 9), (3, 4, 8), (1, 3, 5, 8)等等,而这些子序列中最长的(如子序列(1, 3, 5, 8) ),它的长度为4,因此该序列的最长上升子序列长度为4。

问题描述: 求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。

分析过程:

前1个数 d(1)=1 子序列为2;

  前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7

  前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1

  前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5

  前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6

  前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4

  前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3

  前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8

  前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9

  d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5
总结,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1,当A[i]之前没有比A[i]更小的数时,d(i)=1。所有的d(i)里面最大的那个就是最长上升子序列。即每次都向前找比它小的数和比它大的数的位置,将第一个比它大的替换掉,虽然LIS序列的数字可能会变,但LIS长度还是不变的,因为只是把数换掉了,并没有改变增加或者减少长度。

状态设计:F [ i ] 代表以 A [ i ] 结尾的 LIS 的长度

状态转移:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j <  i,A[ j ] < A[ i ])

边界处理:F [ i ] = 1 (1 <= i <= n)

时间复杂度:O (n^2)

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
const int maxn = 103, INF = 0x7f7f7f7f;
int a[maxn], f[maxn];
int n,ans = -INF;
int main()
{
    scanf("%d", &n);
    for(int i=1; i<=n; i++) 
    {
        scanf("%d", &a[i]);
        f[i] = 1;
    }
    for(int i=1; i<=n; i++)
        for(int j=1; j

2. 最大连续子序列

给定一个数字序列A1,A2……,An,求i,j(1<=i<=j<=n),使得Ai + ……+ Aj最大,输出这个最大和。

如样例:-2 11 -4 13 -5 -2 显然 11+(-4)+13=20为最大的选取情况,因此最大和为20。

过程分析:

步骤1:令状态dp[i] 表示以A[i]作为末尾的连续序列的最大和(这里是说A[i]必须作为连续子序列的末尾)。

步骤2:dp[i]如果必须以A[i]结尾的连续序列,有两种情况(1).这个最大和的连续序列只有一个元素,就是以A[i]开始,以A[i]结尾。

(2).这个最大和的连续序列有多个元素,就是从前面某处A[p]开始(p

对于第一种情况,最大和就是A[i]本身。第二种情况,最大和是dp[i-1]+A[i]。

状态转移方程:

   dp[i] = max{A[i], dp[i-1]+A[i]}

  这个式子只和 i 与 i 之前的元素有关,且边界为 dp[0] = A[0],由此从小到大枚举 i,即可得到整个 dp 数组。接着输出 dp[0],dp[1],...,dp[n-1] 中的最大子即为最大连续子序列的和。

/*
    最大连续子序列和
*/

#include 
#include 
#include 
#include 
#include 
#include 

#define maxn 10010
int A[maxn], dp[maxn];    // A[i] 存放序列,dp[i] 存放以 A[i] 为结尾的连续序列的最大和

// 求较大值
int max(int a, int b) {
    return a>b ? a : b;
}

int main() {
    int n, i, k;
    scanf("%d", &n);
    for(i=0; i k) {
            k = dp[i];
        }
    }
    printf("%d\n", k);        // 输出

    return 0;
}

3. 最长公共子序列

     问题描述:一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X= { x1, x2,…, xm},则另一序列Z= {z1, z2,…, zk}是X的子序列是指存在一个严格递增的下标序列 {i1, i2,…, ik},使得对于所有j=1,2,…,k有 Xij=Zj。例如,序列Z={B,C,D,B}是序列X={A,B,C,B,D,A,B}的子序列,相应的递增下标序列为{2,3,5,7}。给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。例如,若X= { A, B, C, B, D, A, B}和Y= {B, D, C, A, B, A},则序列{B,C,A}是X和Y的一个公共子序列,序列{B,C,B,A}也是X和Y的一个公共子序列。而且,后者是X和Y的一个最长公共子序列,因为X和Y没有长度大于4的公共子序列。给定两个序列X= {x1, x2, …, xm}和Y= {y1, y2, … , yn},要求找出X和Y的一个最长公共子序列。

   过程分析:设X= { A, B, C, B, D, A, B},Y= {B, D, C, A, B, A}。求X,Y的最长公共子序列最容易想到的方法是穷举法。对X的多有子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列。由集合的性质知,元素为m的集合共有2^m个不同子序列,因此,穷举法需要指数级别的运算时间。进一步分解问题特性,最长公共子序列问题实际上具有最优子结构性质。

      设序列X={x1,x2,……xm}和Y={y1,y2,……yn}的最长公共子序列为Z={z1,z2,……zk}。则有:

      (1)若xm=yn,则zk=xm=yn,且zk-1是Xm-1和Yn-1的最长公共子序列。

      (2)若xm!=yn且zk!=xm,则Z是Xm-1和Y的最长公共子序列。

      (3)若xm!=yn且zk!=yn,则Z是X和Yn-1的最长公共子序列。

      其中,Xm-1={x1,x2……xm-1},Yn-1={y1,y2……yn-1},Zk-1={z1,z2……zk-1}。

     递推关系:用c[i][j]记录序列Xi和Yj的最长公共子序列的长度。其中,Xi={x1,x2……xi},Yj={y1,y2……yj}。当i=0或j=0时,空序列是xi和yj的最长公共子序列。此时,c[i][j]=0;当i,j>0,xi=yj时,c[i][j]=c[i-1][j-1]+1;当i,j>0,xi!=yj时,

c[i][j]=max{c[i][j-1],c[i-1][j]},由此建立递推动态规划算法-个人心得_第1张图片

代码示例

//3d3-2 最长公共子序列问题
#include "stdafx.h"
#include  
using namespace std; 
 
const int M = 7;
const int N = 6;
 
void output(char *s,int n);
void LCSLength(int m,int n,char *x,char *y,int **c);
void LCS(int i,int j,char *x,int **c);
 
int main()
{
	//X={A,B,C,B,D,A,B}
	//Y={B,D,C,A,B,A}
	char x[] = {' ','A','B','C','B','D','A','B'};
	char y[] = {' ','B','D','C','A','B','A'};
 
	int **c = new int *[M+1];
	for(int i=0;i<=M;i++)  
    {  
		c[i] = new int[N+1];
    } 
	
	cout<<"序列X:"<=c[i][j-1])
			{
				c[i][j]=c[i-1][j];
			}
			else
			{
				 c[i][j]=c[i][j-1];
			}
		}
	}
}
 
void LCS(int i,int j,char *x,int **c)
{
	if(i==0 || j==0)
	{
		return;
	}
	if(c[i][j]==c[i-1][j-1]+1)
	{
		LCS(i-1,j-1,x,c);
		cout<=c[i][j-1])
	{
		LCS(i-1,j,x,c);
	}
	else
	{
		LCS(i,j-1,x,c);
	}
}

 

你可能感兴趣的:(动态规划,算法)