【No.16】蓝桥杯动态规划下|线性DP装箱问题|计数DP0/1背包的方案数|过河卒|完全背包小明的背包2|最长公共子序列|蓝桥骑士|推荐练习题(C++)

线性DP,0/1背包简化版,装箱问题

【题目描述】有一个箱子容量为V(正整数, 0 ≤ V ≤ 20000 0 \le V \le 20000 0≤V≤20000),同时有n个物品( 0 < n ≤ 30 0 < n \le 30 0<n≤30),每个物品有一个体积(正整数)。要求 n 个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

【输入描述】输入第一行,一个整数,表示箱子容量。

第二行,一个整数n,表示有n个物品。

接下来n行,分别表示这n个物品的各自体积。

【输出描述】输出一行,表示箱子剩余空间。

0/1背包的简化版,不管物品的价格。把体积(不是价格)看成最优化目标:最大化体积。

#include<bits/stdc++.h>
using namespace std;
int dp[20010];
int w[40];
int main()
{
	int V,n;
	scanf("%d%d" ,&V,&n);
	for(int i = 1; i <= n; i ++)
		scanf("%d",&w[i]);
	for(int i = 1; i <= n; i ++)
		for(int j = V; j >= w[i]; j --)
			dp[j] = max(dp[j], dp[j-w[i]]+w[i])
	printf("%d\n", V-dp[V]);
	return 0;
}
计数DP,0/1背包的方案数

【题目描述】

将2022拆分成10个互不相同的正整数之和,总共有多少种拆分方法?

注意交换顺序视为同一种方法。

例如:

2022 = 1000+1022

2022 =1022+1000

视为同一种方法。

题目求10个数的组合情况,这十个数相加等于2022。因为是填空题可以不管运行时间,看起来可以用暴力for循环10次,加上剪枝。

然而暴力的时间极长,因为答案是:379187662194355221

这一题其实是0/1背包:

背包容量为2022,物品体积为1~2022,往背包中装10个物品,要求总体积为2022,问一共有多少种方案。

定义dp[][][]: dp[i][j][k]表示数字1~i取j个,和为k的方案数。下面的分析沿用标准0/1背包的分析方法,

从i-1扩展到i,分两种情况:

  1. k ≥ i k \ge i k≥i。数i可以要,也可以不要

    1. 要i。从1~i-1中取j-1个数,再取i,等价于dp[i-1][j-1][k-i]
    2. 不要i。从1~i-1中取j个数,等价于dp[i-1][j][k]
    3. 合起来:dp[i][j][k]= dp[i-1][j][k]+ dp[i-1][j-1][K-i]
  2. k < i.
    由于数i比总和k还术、显然i不能用。
    有:dp[i][j][k]= dp[i-1][j][k]

    for (int i = 0; i <= 2022; i ++)
    dp[i][0][0] = 1; //特别注意这个初始化

1~i个数,选0个和为0的情况只有一种,就是不选

(1)k≥i。dp[i][j][k]= dp[i-1][j][k]+ dp[i-1][j-1][k-i]

(2)k<i。dp[i][j][k]= dp[i-1][j][k]

不用滚动数组
#include <bits/stdc++.h>
using namespace std;
long long dp[2222][11][2222]={0};
int main()
{
	for(int i = 0; i <= 2022; i ++)
		dp[i][0][0] = 1;    //特别注意这个初始化
	for(int i = 1; i <= 2022; i ++)
		for(int j = 1; j <= 10; j ++)  //注意:j从小到大,或从大到小都行
			for(int k = 1; k <= 2022; k ++)
			{
				if(k<i)
					dp[i][j][k] = dp[i-1][j][k]; //无法装进背包
				else
					dp[i][j][k] = dp[i-1][j][k] + dp[i-1][j-1][k-i];
	cout << dp[2022][10][2022];
	return 0;
}
使用滚动数组
#include <bits/stdc++.h>
using namespace std;
long long dp[11][2222];
int main()
{
	dp[0][0]=1;
	for(int i = 1; i <= 2022; i ++)
		for(int j = 10; j >= 1; j --) //j一定要从大到小
			for(int k = i; k <= 2022; k ++)
				dp[j][k] += dp[j-1][k-i];
	cout << dp[10][2022];
	return 0;
}
网格上的DP,过河卒

【题目描述】

棋盘上A点有一个过河卒,需要走到目标B点。

卒行走的规则:可以向下、或者向右。同时在棋盘上C点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为"马拦过河卒"。

现在要求你计算出卒从 A点能够到达B点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。棋盘用坐标表示,A点(0,0)、B点(n,m),同样马的位置坐标是需要给出的。1≤n,m≤20,0≤马的坐标≤20。【输入格式】

一行四个正整数,表示B点坐标和马的坐标。

【输出格式】

一个整数,表示所有的路径条数。

统计路径条数,看起来是个搜索题,可以用DFS求解。把马的控制点标记为不能走,绕过它们。

不过,用DFS搜索的路径数量是天文数字,肯定超时

在小学上过奥数的都知道,这题应该用"标数法",就是在每个坐标点上记录能走的路径条数。

标数法实际上就是DP的递推。

定义状态dp[][]:
dp[i][j]表示卒走到坐标(i,j)时能走的路径条数。

如果不考虑马的控制点,有:dp[i][j]= dp[i-1][j]+ dp[i][j - 1]

也就是(i,j)点的路径条数等于它上面和左边的路径条数之和。这就是小学奥数的"标数法"的原理

本题的限制条件是马的控制点,只要令控制点的dp[i][j] = 0即可,即这个点上无路径

#include <bits/stdc++.h>
using namespace std;
long long dp[25][25];
bool s[25][25];  //标记马的9个控制点
int main()
{
	int bx,by,mx,my;
	scanf("%d%d%d%d",&bx,&by,&mx,&my);
	bx += 2;
	by += 2;
	mx += 2;
	my += 2;
	//技巧:坐标点加2,防止越界,因为马能向上向左跳2格
	dp[2][1] = 1;  	//初始化
	s[mx][my] = 1;	//标记马的控制点
	s[mx - 2][my + 1] = 1;
	s[mx - 2][my - 1] = 1;
	S[mx + 2][my - 1] = 1;
	s[mx + 2][my + 1] = 1;
	s[mx - 1][my - 2] = 1;
	s[mx - 1][my + 2] = 1;
	s[mx + 1][my + 2] = 1;
	s[mx + 1][my - 2] = 1;
	for(int i = 2; i <= bx; i ++)
		for(int j = 2; j <= by; j ++)
		{
			if(s[i][j])
				dp[i][j] = 0;  //这个点是控制点,不能走
			else
				dp[i][j] = dp[i-1][j] + dp[i][j - 1];
		}
	printf("%lld\n", dp[bx][by]);  //结果是个极大的数字
	return 0;
}
完全背包,小明的背包2

【题目描述】

小明有一个容量为C的背包。这天他去商场购物,商场一共有N 种物品,第i种物品的体积为 c i c_{i} ci,价值为 w i w_{i} wi,每种物品都有无限多个。小明想知道在购买的物品总体积不超过C的情况下所能获得的最大价值为多少,请你帮他算算。

【输入描述】

输入第1行包含两个正整数N,C,表示商场物品的数量和小明的背包容量。

第2~N+1行包含2个正整数c,w,表示物品的体积和价值。

1≤N≤10^3,1≤C≤10^3,1≤ci,wi≤10^3

【输出描述】

输出一行整数表示小明所能获得的最大价值

思路和0/1背包类似。0/1背包的每种物品只有1件,完全背包的每种物品有无穷多件,第i种可以装0件、1件、2件、C/ c i c_{i} ci件。

定义dp[i][j]:把前i种物品(从第1种到第i种)装入容量为j的背包中获得的最大价值。

把每个dp[i][j]都看成一个背包:背包容量为j,装1~i这些物品。最后得到的dp[N][C]就是问题的答案:把N种物品装进容量c的背包的最大价值。

在0/1背包问题中,每个物品只有拿与不拿两种;而完全背包问题,需要考虑拿几个。

1.定义变量并输入

参考 0-1 背包。

2.执行算法

我们先去找到状态转移方程

dp[i][j]=max(dp[i], dp[i][j-c[i]]+w[i])

选到第i件物品,且背包现在重是为j。

那么考虑这个状态会由什么状态转移而来,肯定是选到第 i-1 件的时候,或者选了若干次第i件

如果不选第i种,那么就是由dp[i-1][i]转移而来

如果选了第i件,那么就是由dp[i-1][j-c[i]]转移而来。

或者在某一刻,不在选第i件了,那么就是有dp[i][j]转移而来。

那么已知dp[i][j-c[i]]dp[i][j]都为各自最优的状态,那我们直接取最优状态即可

#include <iostream>
#include <cstring>

using namespace std;
#define Maxn 5000;

int c[Maxn], w[Maxn];
int dp[Maxn][Maxn];
int C;

int n;

int main()
{
	cin >> n;
	cin >> C;
	for (int i = 1; i <= n; i ++)
		cin >> c[i] >> w[i];
	memset(dp, 0, sizeof(dp));

	for (int i = 1; i <= n; i ++)
	{
		for (int j = 1; j <= C; j ++)
		{
			dp[i][j] = dp[i - 1][j];
			if (j >= c[i])
				dp[i][j] = max(dp[i][j], dp[i][j-c[i]] + w[i]);
		}
	}
	cout << dp[n][C] << endl;
}

因为状态转移每次只与上一层有关,所以用一个一维数组就可以。

为什么从小到大遍历,看

dp[j]=dp[j-c[i]]+w[i]

这一状态转移,是根据小的改大的,而此时的含义为选了x件后的容量与质量,跟01背包类似,但含义不同,处理方式上也有本质区别,处理完一件后在处理下件。

#include <iostream>
#include <cstring>

using namespace std;
#define Maxn 5000;

int c[Maxn], w[Maxn];
int dp[Maxn];
int C;

int n;

int main()
{
	cin >> n;
	cin >> C;
	for (int i = 1; i <= n; i ++)
		cin >> c[i] >> w[i];
		
	memset(dp, 0, sizeof(dp));

	for (int i = 0; i < n; i ++) //遍历每一件物品
	{
		//遍历背包容量,在上一层的基础上,容量为j时,第i件物品装或不装的最优解
		for (int j = c[i]; j <= C; j ++)
		{
			dp[j] = max(dp[j-c[i]] + w[i], dp[j]);
		}
	}
	cout << dp[C] << endl;
}
线性DP,最长公共子序列

【题目描述】

给定一个长度为n的数组A和一个长度为m的数组B。请你求出它们的最长公共子序列长度为多少

【输入描述】

输入第一行包含两个整数n、m。

第二行包含n个整数ai,

第三行包含m个整数bi,

1≤n,m≤10^3,1≤ai,bi≤10^9

【输出描述】

输出一行整数表示答案。

暴力法:先找出A的所有子序列,然后一一验证是否为Y的子序列。

如果A有m个元素,那么A有 2 m 2^{m} 2m个子序列;B有n个元素;总复杂度大于 O ( n ∗ 2 m ) O(n*2^m) O(n∗2m).

dp[i][j]:房列Ai(a1~ai)和Bj(b1~bj)的最长公共子序列的长度。

答案:dp[n][m]

分解为2种情况

  1. 当 a i = b j a_{i}=b_{j} ai=bj;时已求得 A i − 1 A_{i-1} Ai−1和 B j − 1 B_{j-1} Bj−1的最长公共子序列,在其尾部加上 a i a_{i} ai或 b j b_{j} bj即可得到Ai和Bj的最长公共子序列。 状态转移方程:
    dp[i][j]=dp[i-1][j-1]+ 1

  2. 当 a i ≠ b j a_{i}\ne b_{j} ai=bj时,求解两个子问题: A i − 1 A_{i-1} Ai−1和 B j B_{j} Bj的最长公共子序列; A i A_{i} Ai和 B j − 1 B_{j-1} Bj−1的最长公共子序列。取最大值,状态转移方程:
    dp[i][j]= max(dp[i][j-1], dp[i-1][j])

    #include <iostream>
    #include <cstring>
    using namespace std:
    #define Maxn 5000

    int dp[Maxnl[Maxn];
    //DP 辅助数组
    int a[Maxn], b[Maxn];

    int main()
    {
    int n, m;
    cin >> n >> m;
    for(int i = 0; i < n; i ++)
    cin >> a[i];
    for(int i = 0; i < m; i ++)
    cin >> b[i];

     dp[0][0] = 0;
     
     for (int i = 0; i < n; i ++)
     	for (int j = 0; j < m; j ++)
     		if (a[i] == b[j])
     			dp[i + 1][j + 1] = dp[i][j] + 1;
     		else
     			dp[i + 1][j + 1] = max(dp[i + 1][j], dp[i][j + 1]);
     cout << dp[n][m]<< endl;
    

    }

最长递增子序列,蓝桥骑士

【题目描述】

小明是蓝桥王国的骑士,他喜欢不断突破自我。这天蓝桥国王给他安排了N个对手,他们的战力值分别为 a 1 , a 2 , . ... ,, a n a_{1},a_{2},....,,a_{n} a1,a2,....,,an,且按顺序阻挡在小明的前方。对于这些对手小明可以选择挑战,也可以选择避战。身为高傲的骑士,小明从不走回头路,且只愿意挑战战力值越来越高的对手。

请你算算小明最多会挑战多少名对手。

【输入描述】

第一行是整数N,表示对手的个数,第二行是N个整数 a 1 , a 2 , . ... ,, a n a_{1},a_{2},....,,a_{n} a1,a2,....,,an,表示对手战力值。1≤N≤3x10^5

【输出描述】

输出一行整数表示答案。

求一个子序列,保证这个子序列时递增的,最长是多少

给定一个长度为n的数组,找出一个最长的单调递增子序列。

例:序列A={5,6,7,4,2,8,3},它最长的单调递增子序列为{5,6,7,8},长度为4。

定义状态dp[i]:表示以第i个数为结尾的最长递增子序列的长度。

状态转移方程:
dp[i]= max{dp[j]}+1, 0<j<i, A < Ai

答案:max{dp[i]}

复杂度:

j在0~i之间滑动,复杂度O(n);

i的变动范围也是O(n)的;

总复杂度O(n2)

我们首先定义状态:

我们定义dp[i]为以a[i]结尾的最长上升子序列长度。

设置j为小于i的某一点,那么当 a[j]<a[i] 时,必然有,以 a[j] 结尾的最长上升子序列,现在能以 a[i] 结尾,并且长度 +1。

因为 j<i且 a[j]< a[i],满足上升子序列的要求。

所以 dp[i] 的一条转移路径为 dp[i]=dp[j]+1

但是j是比i小的某一个值,所以转移到 dp[i] 这一状态的值很多,我们要选择最优状态,所以 dp[i]的状态转移:
dp[i]=max(dp[j]+1, dp[i]);

那么当 a[j]>a[i]

此时肯定不满足上升子序列,所以 dp[i]dp[j] 毫无关联。由此我们可以写出 LIS 的算法部分

#include <bits/stdc++.h>

using namespace std;
//求最长升序子序列的长度,并输出
#define Maxn 300006
int a[Maxn];
int dp[Maxn];
int ans=1:
int main()
{
	int m, k;
	int n;
	
	cin >> n;
	for(int i = 0; i < n; i ++)
	{
		cin >> a[i];
		dp[i] = 1;
	}
	for(int i = 1; i < n; i ++)
	{
		for(int j = 0; j < i; j ++)
		{
			if(a[j] < a[i])
			{
				dp[i] = max(dp[j] + 1, dp[i]);
			}
			ans=max(ans, dp[i]);
		}
	}
	cout << ans << endl;:
}
练习题
  1. 李白打酒加强版2114
  2. 分果果1459
  3. 开心的金明554
  4. 最优包含239
  5. 质数行者1027
  6. 货币系统331
  7. 合唱队形742
  8. 数组切分2148
  9. 括号序列1456
  10. 传球游戏525
  11. 画廊1032
  12. 游园安排1024
  13. 凑硬币1082
  14. 纪念品786。
  15. 积木画2110
  16. 采药563
  17. 摆花389;
  18. 蓝肽子序列1030
  19. 矩阵计数246
  20. 方格取数803
相关推荐
捕鲸叉5 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer5 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷7 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
幺零九零零8 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
捕鲸叉8 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
Dola_Pan9 小时前
C++算法和竞赛:哈希算法、动态规划DP算法、贪心算法、博弈算法
c++·算法·哈希算法
yanlou2339 小时前
KMP算法,next数组详解(c++)
开发语言·c++·kmp算法
小林熬夜学编程9 小时前
【Linux系统编程】第四十一弹---线程深度解析:从地址空间到多线程实践
linux·c语言·开发语言·c++·算法
阿洵Rain10 小时前
【C++】哈希
数据结构·c++·算法·list·哈希算法