【AcWing算法基础课】第五章 动态规划(未完待续)

前言

本专栏文章为本人AcWing算法基础课的学习笔记,课程地址在这。如有侵权,立即删除。

课前温习

image

初识DP

dp问题的优化:在基本形式dp上作等价变形。
dp问题的解题方法

1)状态表示

  • 集合
  • 属性:最大值/最小值/数量。

2)状态计算

  • 集合划分(不重不漏)

一、 背包问题

1、0-1背包问题

题目链接: 2. 01背包问题 - AcWing题库

1.1题目描述

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且 总价值最大

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000

0<vi,wi≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

8

1.2思路分析

1)状态表示

  • 集合:所有选法。而且选法需要满足①只从前i件物品中选;②满足总体积<=j
  • 属性:f[i][j]表示满足条件集合中的最大价值

2)状态计算

  • 集合划分:分成两类①满足集合条件而且不选i;②满足集合条件而且选i
  • 计算:①f[i-1][j];②先将所有集合中去掉第i件物品,然后求所有集合中的最大价值即f[i-1][j-v[i]],最后再把第i个物品加上,得到最大价值f[i-1][j-v[i]]+w[i]。两者取最大即可求得f[i][j]的最大值,即 f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])

1.3代码实现

#include <iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{   cin>>n>>m;
    for(int i=1;i<=n;i++){
    	cin>>v[i]>>w[i];
	} 
	//i=0,0=<j<=m时:放入0件物品时最大价值都是0,不需要初始化 
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
		//第一种情况一定存在 
			f[i][j]=f[i-1][j];
		//第二种情况当j>=v[i],即背包可以放下第i件物品时情况才存在 
		if(j>=v[i])
			f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
		}
	}
	cout<<f[n][m];
	return 0;
}

代码优化 :计算数组f[i]这一层时,只用到了f[i-1]这一层,所以可以利用滚动数组(原理:两行的二维数组,交替使用来计算下面层的元素值)来实现。所以,f[j]=max(f[j],f[j-v[i]]+w[i])

注意 :背包容量需要从大到小遍历,即背包容量需逆序遍历,因为计算下一层时,如果从小到大遍历,则排在前面的就会先被更新成下一层的值,然后就把当前值给覆盖了,影响到了排在它后面的元素的更新。

优化代码

#include <iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N];
int main()
{   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=m;j>=v[i];j--){
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
}

2、完全背包问题

题目链接: 3. 完全背包问题 - AcWing题库

2.1题目描述

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

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且 总价值最大

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000

0<vi,wi≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例

10

2.2思路分析

1)状态表示

  • 集合:所有选法。而且选法需要满足①只从前i件物品中选;②满足总体积<=j
  • 属性:f[i][j]表示满足条件集合中的最大价值

2)状态计算

  • 集合划分:按照第i个物品选几个来划分。
  • 计算:①i个物品选0个:f[i-1][j];②i个物品选1~k个:先将所有集合中去掉k个第i件物品,然后求所有集合中的最大价值即f[i-1][j-k*v[i]],最后再把k个第i件物品加上,得到最大价值f[i-1][j-k*v[i]]+k*w[i]。将两种条件综合可得,f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])

2.3代码实现

#include <iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{   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=0;j<=m;j++){
			//物品i的个数不能超过背包只放物品i最多可以存放的个数 
			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]);
			}
		}
	}
	cout<<f[n][m];
	return 0;
}

代码优化

将原有递推式展开,f[i][j]f[i-1][j]这一项,其余每一项都比f[i][j-v[i]]w[i],所以,f[i][j]f[i-1][j]之外的最大值也比f[i][j-v[i]]的最大值多w[i]。所以 f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])

优化代码

#include <iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{   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=0;j<=m;j++){
			//第一种情况一定存在 
		    f[i][j]=f[i-1][j];
		    //第二种情况当j>=v[i],即背包可以放下第i件物品时情况才存在 
		    if(j>=v[i])
				f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i]);
		}
	}
	cout<<f[n][m];
	return 0;
}

再次优化 :与0-1背包问题转一维类似,但是背包容量要正序遍历,原因:要求f[i][j]需知道该层的f[i][j- v[i]],因为f[i][j-v[i]]f[i][j]之前,所以需要先更新f[i][j-v[i]],然后再更新f[i][j],也就是说需要从前往后遍历背包容量。

#include <iostream>
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N];
int main()
{   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=v[i];j<=m;j++){
				f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
}

3、多重背包问题 1

题目链接: 4. 多重背包问题 I - AcWing题库

3.1题目描述

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且 价值总和最大

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100

0<vi,wi,si≤100

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例

10

3.2思路分析

:本题数据数据范围较小,可以完全利用完全背包的朴素思路来过掉。

1)状态表示

  • 集合:所有选法。而且选法需要满足①只从前i件物品中选;②满足总体积<=j
  • 属性:f[i][j]表示满足条件集合中的最大价值

2)状态计算

  • 集合划分:按照第i个物品选几个来划分。
  • 计算:①i个物品选0个:f[i-1][j];②i个物品选1~k(k最大为s[i])个:先将所有集合中去掉k个第i件物品,然后求所有集合中的最大价值即f[i-1][j-k*v[i]],最后再把k个第i件物品加上,得到最大价值f[i-1][j-k*v[i]]+k*w[i]。将两种条件综合可得,f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i])

3.3代码实现

朴素二维

#include <iostream>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int f[N][N];
int main()
{   cin>>n>>m;
    for(int i=1;i<=n;i++){
    	cin>>v[i]>>w[i]>>s[i];
	} 
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			//和完全背包的条件类似:物品i的个数不能超过背包只放物品i最多可以存放的个数而且不能超过物品i的最大个数 
			for(int k=0;k<=s[i]&&k*v[i]<=j;k++){
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
			}
				
		}
	}
	cout<<f[n][m];
	return 0;
}

优化一维

#include <iostream>
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int f[N];
int main()
{   cin>>n>>m;
    for(int i=1;i<=n;i++){
    	cin>>v[i]>>w[i]>>s[i];
	} 
	for(int i=1;i<=n;i++){
		for(int j=m;j>=v[i];j--){
			//和完全背包的条件类似:物品i的个数不能超过背包只放物品i最多可以存放的个数而且不能超过物品i的最大个数 
			for(int k=0;k<=s[i]&&k*v[i]<=j;k++){
				f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
			}
				
		}
	}
	cout<<f[m];
	return 0;
}

4、多重背包问题 2

题目链接: 5. 多重背包问题 II - AcWing题库

4.1题目描述

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且 价值总和最大

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤1000

0<V≤2000

0<vi,wi,si≤2000

提示:

本题考查多重背包的二进制优化方法。

输入样例

4 5
1 2 3
2 4 1
3 4 3
4 5 2

输出样例

10

4.2思路分析

无法依据完全背包优化方式进行优化 ,原因:因为完全背包问题中,每个物品的数量是无限的,即

f[i][j] = max(f[i-1][j],f[i-1][j-v[i]]+w[i],f[i-1][j-2v[i]]+2w[i],…,f[i - 1][j - k * v[i]] + k * w[i],...);

f[i][j - v[i]] = max( f[i-1][j-v[i]] ,f[i-1][j-2v[i]]+w[i],…,f[i - 1][j - k * v[i]] + (k - 1) * w[i],...);

因为是无限项,所以第一个式子f[i][j]除去第一项f[i-1][j]后和第二个式子项数是完全相等的,而每一项仅仅相差一个w[i],所以f[i][j-v[i]]+w[i]的最大值和f[i][j]去掉第一项后之后所有项的最大值相等,也就是说 f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])

然而多重背包所放的每件物品都是有最大数量限制的,所以如上图,f[i][j]除去第一项后剩下的项数与f[i][j-v[i]]的项数不同,多了一个f[i][j-v[i]]的最后一项,所以不能像完全背包一样,像上面那样的优化。

优化方式(二进制优化):将第i个物品分组打包,针对 每组可以选也可以不选 ,即 0-1背包问题 ,针对某一个s[i]

每次打包个数分别为:20,21,22,…,2k,而最后的 C<2(k+1)

从1到2k可以凑出0 ~ 2~~(k+1)~~-1个第i个物品,在此基础上加上C之后可以拼出C ~ 2(k+1)-1+C个第i个物品,即[c~s]

由于C<2(k+1),所以两个区间的并集就是 [0,S]

所以可以总结出,我们可以将s[i]分组为 logS[i]个,可以将时间复杂度从O(NVS)降到O(NVlogS[i])

4.3代码实现

#include <iostream>
using namespace std;
const int N=25000,M=2000;
int n,m;
int v[N],w[N];
int f[N];
int main()
{   cin>>n>>m;
    int cnt=0;   //打包的组号 
    for(int i=1;i<=n;i++){
    	int a,b,s;//当前物品的体积、价值、个数 
    	cin>>a>>b>>s; 
    	int k=1;
    	//打包过程 
    	//k<=s时可以分组打包 
    	while(k<=s){
    		cnt++;
    		v[cnt]=a*k;
    		w[cnt]=b*k;
    		s-=k;  //每次分完一次后,物品总数减去k 
    		k*=2;  //每次分组,物品个数为2的幂
		}
		//如果没有正好分完,将剩余物品打包成一组。 
		if(s>0){
			cnt++;
			v[cnt]=a*s;
			w[cnt]=b*s;
		} 
	} 
	n=cnt; //分组数即为物品数,每组有不同个物品,可选可不选,转化成0-1背包 
	//0-1背包 
	for(int i=1;i<=n;i++){
		for(int j=m;j>=v[i];j--){
		    f[j]=max(f[j],f[j-v[i]]+w[i]);	
		}
	}
	cout<<f[m];
	return 0;
}

5、分组背包问题

题目链接9. 分组背包问题 - AcWing题库

5.1题目描述

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个

每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且 总价值最大

输出最大价值。

输入格式

第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N 组数据:

  • 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
  • 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100

0<Si≤100

0<vij,wij≤100

输入样例

3 5
2
1 2
2 4
1
3 4
1
4 5

输出样例

8

5.2思路分析

1)状态表示

  • 集合:所有选法。而且选法需要满足①只从前i件物品中选;②满足总体积<=j
  • 属性:f[i][j]表示满足条件集合中的最大价值

2)状态计算

  • 集合划分:按照第i组物品选几个来划分。
  • 计算:①第i组物品选0个:f[i-1][j];②第i组物品选第k个:f[i-1][j-v[i][k]]+w[i][k]。将两种条件综合可得,f[i][j]=max(f[i-1][j],f[i-1][j-v[i][k]]+w[i][k])
  • 遍历顺序:如果递推需要由上一层推得,则需要逆序遍历背包容量;如果递推需要由本层推得,则需要正序遍历背包容量
  • 可以类似优化为一维:f[j]=max(f[j],f[j-v[i][k]]+w[i][k])

5.3代码实现

#include <iostream>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int f[N];
int main()
{   cin>>n>>m;   
    for(int i=1;i<=n;i++){
    	cin>>s[i];
		for(int j=1;j<=s[i];j++){
			cin>>v[i][j]>>w[i][j];
		}	
	} 
	for(int i=1;i<=n;i++){
		//背包容量逆序遍历 
		for(int j=m;j>=0;j--){
		    for(int k=1;k<=s[i];k++){
		    	// f[j-v[i][k]]存在才判断是否更新最大,若不存在,f[j]=f[j] 
		    	if(v[i][k]<=j){
		    		f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
				}
			}	
		}
	}
	cout<<f[m];
	return 0;
}

二、线性DP

线性DP:递推方式为线性的递推式。

1、数字三角形

题目链接898. 数字三角形

1.1题目描述

给定一个如下图所示的数字三角形,从 顶部 出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点, 一直走到底层 ,要求找出一条路径,使 路径上的数字的和最大

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

输入格式

第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式
输出一个整数,表示最大的路径数字和。

数据范围

1≤n≤500
−10000≤三角形中的整数≤10000

输入样例

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

输出样例

30

1.2思路分析

在这里插入图片描述

  1. DP问题下标从0开始还是从1开始
  • 如果递推式中存在f[i-1],即下标存在i-1,下标 从1开始,将f[0]设为了边界值。
  • 否则,下标 从0开始,防止数组下标越界。
  1. 动态规划问题时间复杂度的计算
  • 时间复杂度 = 状态数 * 转移的计算量

1)状态表示

:如图,从 上到下 定义每 ,从 左上到右下 定义每

  • 集合:所有从起点走到(i,j)的路径。
  • 属性:f[i][j]表示满足条件集合中的最大路径的数字和

2)状态计算

  • 集合划分:从左上走到(i,j)和从右上走到(i,j)
  • 计算:①左上:先走到左上(i-1,j-1),最大路径的数字和为f[i-1][j-1],然后再加上(i,j)点的数字,即f[i-1][j-1]+a[i][j];②右上:先走到右上(i-1,j),最大路径的数字和为f[i-1][j],然后再加上(i,j)点的数字,即f[i-1][j]+a[i][j]。将两种条件综合可得,f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j])

1.3代码实现

#include <iostream>
using namespace std;
const int N=510,INF=1e9;
int n;
int a[N][N];
int f[N][N];
int main()
{   cin>>n;
    for(int i=1;i<=n;i++){
    	for(int j=1;j<=i;j++){
    		cin>>a[i][j];
		}
	}
	/*注意:不能初始化f数组全为0,因为a数组中存在负数,所以f数组也有可能是负数
	若初始化全为0,就会将f数组中的负数给忽略掉,换句话说,若都初始化为0,说明
	所有点一开始都存在最大值,我们需要使所有点一开始都不存在最大值,即初始化
	全为负无穷,而对于下面边界处理时,如果该点的左上或者右上不存在时,仍然按
	递推式计算后的值一定是负的,一定小于从存在的上方的点转移过来的最大值,所以
	从不存在的那个点转移过来的就直接被max函数所比下去了,后续处理也比较方便。
	*/
	for(int i=1;i<=n;i++){
		/*对于每行的边界,需要用到的左上和右上的数,
		 个别数超出了a数组初始化的范围,所以要在f数组中每
		行多初始化两个数,最前和最末两个数*/ 
    	for(int j=0;j<=i+1;j++){
    		f[i][j]=-INF;
		}
	}
	f[1][1]=a[1][1];
	int res=-INF;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=i;j++){
			f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
		}
	}
	for(int i=1;i<=n;i++){
		res=max(f[n][i],res);
	}
	cout<<res;
	return 0;
}

2、最长上升子序列

题目链接895. 最长上升子序列

2.1题目描述

给定一个 长度为 N 的数列,求 数值严格 单调递增的子序列长度最长 是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000
−109≤数列中的数≤109

输入样例

7
3 1 2 1 8 5 6

输出样例

4

2.2思路分析

状态转移,从 一维 开始来判断能否表示,依次上升维度。
在这里插入图片描述

1)状态表示

  • 集合:所有以第i个数结尾的上升子序列。
  • 属性:f[i]表示满足条件集合中的最长序列长度

2)状态计算

  • 集合划分:以第i个数前一位数是原序列的第几个元素划分集合。
  • 计算:针对每一种状态,f[j]表示除第i个数后上升子序列的长度,即以第j个数结尾的上升子序列的最大长度。所以再加上第i个数的长度,即加上1,代表以第i个数结尾的最长上升子序列的长度,即f[j]+1。所以可得 f[i]=max(f[j]+1)(j为不超过i-1的数)。

2.3代码实现

#include <iostream>
using namespace std;
const int N=1010;
int n;
int a[N],f[N];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		//初始化:只有a[i]一个数,子序列长度为1 
		f[i]=1;
		//子序列中的数大于1,依次枚举第i个数前面的数 
		for(int j=1;j<i;j++){
			//如果第j个数可以作为i的前一个数,才满足条件,进行递推 
			if(a[j]<a[i]){
				f[i]=max(f[i],f[j]+1);
			} 
		}
	}
	int ans=0;
	    //找出最长上升子序列的长度 
	for(int i=1;i<=n;i++){
			ans=max(f[i],ans);
		} 
	cout<<ans;
return 0;
}

如何记录最长上升序列各元素为多少,并输出?

//如何记录最长上升子序列并且输出
//方法如下: 
#include <iostream>
using namespace std;
const int N=1010;
int n;
int a[N],f[N],g[N];//利用g数组来记录序列中最后一位的前一位数字 
//利用g数组从而可以来记录出最长序列的序列具体为哪几个数 
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		f[i]=1;
		g[i]=0;
		for(int j=1;j<i;j++){
			if(a[j]<a[i]){
				if(f[i]<f[j]+1){
				  	f[i]=f[j]+1;
				    g[i]=j;   //用g数组记录i的前一位数字为第几个元素 	
				}
			} 
		}
	}
	int k=1;//用k记录以第几个数字结尾 
	for(int i=1;i<=n;i++){
		//找出最长序列是以第几个数字结尾 
		if(f[k]<f[i])
		   k=i;
	}
	cout<<f[k]<<endl;
	//从大到小来输出最长上升子序列具体为多少 
	for(int i=0,len=f[k];i<len;i++){
		cout<<a[k]<<" ";
		k=g[k];	
	}
	return 0;
}

3、最长公共子序列

题目链接897. 最长公共子序列

3.1题目描述

给定两个长度分别为 N 和 M 的字符串 A 和 B ,求 既是 A 的子序列又是 B 的子序列字符串长度最长 是多少。

输入格式

第一行包含两个整数 N 和 M 。
第二行包含一个长度为 N 的字符串,表示字符串 A 。
第三行包含一个长度为 M 的字符串,表示字符串 B 。
字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N,M≤1000

输入样例

4 5
acbd
abedc

输出样例

3

3.2思路分析

在这里插入图片描述

1)状态表示

  • 集合:所有在第一个序列的前i个字母中出现,而且在第二个序列的前j个字母中出现的子序列。
  • 属性:f[i][j]表示满足条件集合中的最长子序列(字符串)长度

2)状态计算

  • 集合划分:以满足集合条件的集合中是否包含第一个序列的第i个字 a[i] 和第二个序列的第j个字母 b[j] 来划分集合,总共分为四类:不包含a[i]且不包含b[j]不包含a[i]且包含b[j]包含a[i]且不包含b[j]包含a[i]且包含b[j]
  • 计算:针对每一种状态
  •    1.f[i-1][j-1]。(该状态可省,因为后一种情况涵盖了它)根据集合定义可得。
       2.f[i-1][j]。因为f[i-1][j]包含了第二个序列中第j个字母存在和不存在两种情况,所以范围比较大,与其他情况有重叠,但是对于求f[i][j]的最大值无影响。
       3.f[i][j-1]。因为f[i][j-1]包含了第一个序列中第i个字母存在和不存在两种情况,所以范围比较大,与其他情况有重叠,但是对于求f[i][j]的最大值无影响。
       4.f[i-1][j-1]+1。因为该情况中每一项都包含第一个序列的第i个字母(或者说是第二个序列的第j个字母),所以长度先都减去1,就是转化为第一种情况,最后再加上去掉字母的长度1,即可得到最大长度。
    
    综上,f[i][j]=max(f[i-1][j],f[i][j-1],f[i-1][j-1]+1)

3.3代码实现

string实现

#include <iostream>
#include <string>
using namespace std;
const int N=1010;
int n,m;
string a="1",b="1";
int f[N][N]; 
int main(){
	cin>>n>>m;
	string temp;
	cin>>temp;
	a+=temp;
	cin>>temp;
	b+=temp;
    for(int i=1;i<=n;i++){
    	for(int j=1;j<=m;j++){
    		f[i][j]=max(f[i-1][j],f[i][j-1]);
    		if(a[i]==b[j]){
    			f[i][j]=max(f[i][j],f[i-1][j-1]+1);
			}
		}
	}
	cout<<f[n][m];
	return 0;
}

char实现

#include <iostream>
using namespace std;
const int N=1010;
int n,m;
char a[N],b[N];
int f[N][N]; 
int main(){
	cin>>n>>m;
	scanf("%s%s",a+1,b+1);
    for(int i=1;i<=n;i++){
    	for(int j=1;j<=m;j++){
    		//前两种情况一定存在 
    		f[i][j]=max(f[i-1][j],f[i][j-1]);
    		//第三种情况只有第一个序列第i个字母和第二个序列第j个字母相同,才存在 
    		if(a[i]==b[j]){
    			f[i][j]=max(f[i][j],f[i-1][j-1]+1);
			}
		}
	}
	cout<<f[n][m];
	return 0;
}

三、区间DP

石子合并

题目链接282. 石子合并

1.1题目描述

设有 N 堆石子排成一排,其编号为1,2,3,…,N。每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2,我们可以先合并 1、2 堆,代价为 4,得到 4 5 2,又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出 最小代价

输入格式

第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

输入样例

4
1 3 5 2

输出样例

22

1.2思路分析

在这里插入图片描述

1)状态表示

  • 集合:所有将第i堆石子第j堆石子合并成一堆的合并方式
  • 属性:f[i][j]表示满足条件集合中的合并的最小代价

2)状态计算

  • 集合划分:以最后一次合并时的区间位置来划分,如图。
    在这里插入图片描述

  • 计算:针对每一种状态。合并的最小代价为,合并区间左边的最小代价+合并区间右边的最小代价+最后一次合并(合并整个区间的代价),即 f[i][j]=min(f[i][k]+f[k+1][j]+s[j]-s[i-1])。(s[j]-s[i-1]利用前缀和来计算合并整个区间的代价)

1.3代码实现

#include <iostream>
using namespace std;
const int N=310;
int n;
int s[N];
int f[N][N]; 
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>s[i];
	}
	//计算前缀和 
	for(int i=1;i<=n;i++){
		s[i]+=s[i-1];
	}
	//从大到小枚举所有状态
	//枚举长度	 
	for(int len=2;len<=n;len++){
		//枚举起点
		for(int i=1;i+len-1<=n;i++){
			int l=i,r=i+len-1;
			f[l][r]=1e8;//求最小值应初始化成较大的数,否则,可能不经过递推式就输出 
			//枚举分界点 
	        for(int k=l;k<r;k++){
	        	f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);	
			}	
		}	
	}
	cout<<f[1][n];
	return 0;
}

四、记忆化搜索

题目链接901. 滑雪

4.1题目描述

给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

下面给出一个矩阵作为例子:

 1  2  3  4 5

16 17 18 19 6

15 24 25 20 7

14 23 22 21 8

13 12 11 10 9

在给定矩阵中,一条可行的滑行轨迹为24−17−2−1。

在给定矩阵中,最长的滑行轨迹为25−24−23−…−3−2−1,沿途共经过 25 个区域。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)

输入格式

第一行包含两个整数 R 和 C。

接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。

输出格式

输出一个整数,表示可完成的最长滑雪长度。

数据范围

1≤R,C≤300,0≤矩阵中整数≤10000

输入样例

5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

输出样例

25

4.2思路分析

image
1)状态表示

  • 集合:所有从 (i,j) 开始滑的路径。
  • 属性:f[i][j]表示满足条件集合中的滑行距离的最大值

2)状态计算

  • 集合划分:按照从当前位置开始第一步是从上、下、左、右哪个方向开始滑行的进行分类。
  • 计算:针对每一种状态,假设第一步走到的点是(x,y),则我们在计算f[i][j]时可以先将从(i,j)到(x,y)的距离去掉,则就变成了求从(x,y)开始滑行,可以滑行的最大距离。按照集合定义我们可知即为f[x][y]。所以,可得转移方程为 f[i][j]=max(f[i][j],f[x][y]+1)。而每一次的f[][]我们需要用dfs来求,所以我们用dfs+记忆化搜索,也相当于是dp来进行求解了。

4.3代码实现

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=310;
int dx[]={1,-1,0,0},dy[]={0,0,1,-1};   //方向数组,存储四个方向的偏移量
int f[N][N];
int h[N][N];
int r,c;
//dp(x,y)返回从(x,y)开始滑行,可以滑行的最大距离
int dp(int x,int y){
    if(f[x][y]!=-1) return f[x][y];    //如果当前状态已经被计算过,直接返回即可
    f[x][y]=1;      //否则,当前的状态的滑行距离至少为1(当前位置)
    //枚举上、下、左、右四个方向
    for(int i=0;i<4;i++){
        int a=x+dx[i],b=y+dy[i];
        //如果该方向的点在范围内,而且可以滑过去
        if(a>=1&&a<=r&&b>=1&&b<=c&&h[a][b]<h[x][y])
          f[x][y]=max(f[x][y],dp(a,b)+1);    //转移方程
    }
    return f[x][y];
}
int main(){
    cin>>r>>c;
    for(int i=1;i<=r;i++){
        for(int j=1;j<=c;j++){
            cin>>h[i][j];
        }
    }
    memset(f,-1,sizeof f);    //将每种状态初始化为-1
    int res=1;
    //枚举每个点,求出从每个点开始滑行的最大距离,然后再在其中取一个最大值即为答案
    for(int i=1;i<=r;i++){
        for(int j=1;j<=c;j++){
            res=max(res,dp(i,j));
        }
    }
    cout<<res;
    return 0;
}

五、树形DP

题目链接285. 没有上司的舞会

5.1题目描述

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值

输入格式

第一行一个整数 N。

接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。

输出格式

输出最大的快乐指数。

数据范围

1≤N≤6000,−128≤Hi≤127

输入样例

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

输出样例

5

5.2思路分析

image
image
1)状态表示

  • 集合:f[u][0]表示所有从以u为根的子树中选择,并且不选择u这个点的方案。f[u][1]表示所有从以u为根节点的子树中选,并且选择u这个点的方案。
  • 属性:所有人快乐指数总和的最大值。

2)状态计算

  • 集合划分:分为f[u][0]f[u][1]
  • 计算:设u的每个子树的根节点为si。每次递归地对子树进行下述处理。针对f[u][0]由于不选择u这个根结点,所以每次累加的时候可以依据选上子树根节点后值的变化来选,如果选上子树根节点总值大,则就加上该条路径,否则就加上不选子树根节点的这条路径即f[u][0]+=max(f[si][0],f[si][1]);针对f[u][1]由于选了u这个根节点,所以每次不能选包含子树根结点的路径,所以f[u][1]+=f[si][0]

5.3代码实现

#include <iostream>
#include <cstring>
using namespace std;
const int N=6010;
int happy[N];
int h[N],e[N],ne[N],idx;   //邻接表存储树
int f[N][2];
bool has_father[N];    //记录每个点是否有父结点,便于后续找到根节点
int n;
//邻接表加边
void add(int a,int b){
    e[idx]=b;
    ne[idx]=h[a];
    h[a]=idx++;
}
void dfs(int u){
    f[u][1]+=happy[u];     //f[u][1]因为一定选u这个点,所以其初始值为结点u的快乐值
    //枚举u的每条边
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        dfs(j);     //递归求解      
        f[u][0]+=max(f[j][0],f[j][1]);   //转移方程
        f[u][1]+=f[j][0];                //转移方程
    }
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>happy[i];
    memset(h,-1,sizeof h);
    for(int i=0;i<n-1;i++){
        int l,k;
        cin>>l>>k;
        has_father[l]=k;
        add(k,l);
    }
    int root=1;
    while(has_father[root]) root++;   //寻找根节点
    dfs(root);
    cout<<max(f[root][0],f[root][1]);
    return 0;
}
  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 26
    评论
评论 26
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马看到什么是人决定的

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值