那些正常的动态规划

news2025/3/28 23:25:44

文章目录

  • 前言
    • 动态规划到底是啥?
  • 线性dp
    • 最长上升子序列
      • 子集和子序列和子串的区别
      • 内容分析
    • 最大上升子序列
    • 例题1——[NOIP2004 提高组] 合唱队形
      • 分析
    • 最长公共子序列
    • 最长公共子串
  • 平面dp
    • 例题2——[NOIP2000 提高组] 方格取数
      • 分析
    • 例题3——[NOIP2008 提高组] 传纸条
      • 分析
    • 例题4——最大加权矩形
      • 分析
        • 补充:最大字段和
        • 最大子矩阵和和最大字段和的关联
    • 例题5——最大全1子矩阵
      • 分析
    • 例题6——穿衣服
      • 分析
  • 背包问题
    • 什么是背包问题
    • 背包的分类
    • 01背包
      • 思路一
      • 思路二
      • 思路三(重头戏)
        • 常规代码
        • 空间优化
    • 例题7——[NOIP2001 普及组] 装箱问题
      • 分析
    • 例题8——最大约数和
      • 分析
    • 多重背包
        • 位置1
        • 位置2
      • 二进制优化
      • 完整代码
    • 例题9——[NOIP1996 提高组] 砝码称重
      • 分析
    • 完全背包
      • 优化
        • 优化办法一
        • 优化办法二(重点)(敲黑板)
    • 例题10——疯狂的采药
      • 分析
    • 混合背包
    • 多维背包
    • 例题11——「一本通入门 2.9.2」 潜水员
      • 分析
    • 分组背包
    • 例题12——旅行者
      • 分析
    • 泛化背包
    • 例题13——精力的分配
      • 分析
    • 有依赖的背包
    • 例题14——[NOIP2006 提高组] 金明的预算方案
      • 分析
    • 背包方案数
    • 例题15——方案数
      • 分析
    • 01背包问题输出方案
    • 背包问题总结
  • 区间dp
    • 区间型动态规划的特点:
    • 例题16——一本通改编——衣服合并
      • 分析
    • 小结1
    • 例题17——1275:【例9.19】乘积最大
      • 分析
    • 小结2
    • 例题18——[IOI2000] 回文字串
      • 分析
    • 环形区间dp
    • 例题19——[NOI1995] 石子合并
      • 分析
    • 例题20——[NOIP2006 提高组] 能量项链
      • 分析
    • 例题21——[NOIP2001 提高组] 数的划分
      • 分析
    • 例题22——放苹果
      • 分析
  • 总结

前言

在说线性dp之前,我们先来聊一聊动态规划是啥?

动态规划到底是啥?

动态规划是普及组内容中最难的一个部分,也是每年几乎必考的内容。它对思维的要求极高,它和图论、数据结构不同的地方在于它没有一个标准的数学表达式和明确清晰的解题方法。

动态规划是对求解最优解的一种途径,而不是一种特殊的算法。由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的阶梯方法,而不存在一种万能的动态规划算法。为了方便学习,我们把若干具有代表性的动态规划问题归纳总结成不同的几类,并建立对应的数学模型。

动态规划一般用来求解多阶段决策问题的最优解,可以将过程分成若干个互相联系的阶段,在它的每一阶段都需要做决策,从而使整个过程达到最好的活动效果。各个阶段决策的选取不是任意确定的,它依赖于当前面临的状态,又影响以后的发展,当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线。

image

对于动态规划问题,最重要的是分析出:
一、决策对象:我们需要对题目中的哪个变量进行决策。
二、阶段:需要将问题的全过程恰当的分成若干个相互联系的阶段,以便按一定的次序去求解。阶段的划分一般是根据时间(先后顺序)空间(大小关系) 的自然特征来划分。
三、状态:某一阶段的出发位置称为状态,一个阶段可能包含多个状态,状态大多都是用 数组 来表示,即表示我们需要的答案。
四、决策:分析完问题后我们应该怎么解决问题。
五、状态转移方程:描述前一阶段到后一阶段的状态演变规律

线性dp

线性动态规划是动态规划中比较简单的一类问题,他的状态转移是线性的,即状态的转移是固定的,常见的如从前到后,或者从后到前。线性动态规划和递推比较类似,在很多情况下,这两种做法大致是相同的。一般的,递推是当前要求解的答案和前面某个固定答案有关,而线性动态规划是当前答案和前面的某个答案存在关系,这个位置在不同的情况下是不相同的。

最长上升子序列

言归正传,我们继续聊线性dp。

子集和子序列和子串的区别

a b f s g e g s a s abfsgegsas abfsgegsas为例, f a g g a s faggas faggas是它的子集,因为子集是不计顺序的, a b g g s s abggss abggss 则是他的子序列,因为子序列是要求有顺序的,而 a b f abf abf则是他的字串

内容分析

决策对象
每个位置上的数。

阶段
共有 n n n 个数,因此有 n n n 个阶段。

状态
因为每个数都有可能是子序列的结尾,所以使用 dp[i] 表示以第 i i i 个数作为结尾的最长上升子序列的长度。

决策
如果以第 i i i 个数作为结尾,那么在这个序列中,上一个数一定要小于 a [ i ] a[i] a[i]。因此,我们需要在前面的数中找到比 a [ i ] a[i] a[i] 小的数,而且我们应该选择以这个数结尾的最长上升子序列中最长的那个,这样接在这个数后面得到的序列也是最长的。

状态转移方程
如果 h [ k ] < h [ i ] h[k]<h[i] h[k]<h[i],则 d p [ i ] = m a x ( d p [ i ] , d p [ k ] + 1 ) dp[i]=max(dp[i],dp[k]+1) dp[i]=max(dp[i],dp[k]+1),其中 k ∈ [ 1 , i − 1 ] k\in[1,i-1] k[1,i1]

最后,我们找到最大的 d p [ i ] dp[i] dp[i] 就是答案。

#include<bits/stdc++.h>
using namespace std;

int a[10010],dp[10010],ans=INT_MIN;
int main(){
	int n;
	cin>>n;
	for (int i=1;i<=n;i++){
		cin>>a[i];
		dp[i]=1;
	}	
	for (int i=1;i<=n;i++){
		for (int j=1;j<i;j++){
			if (a[j]<a[i]){
				dp[i]=max(dp[i],dp[j]+1);
			}
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}

最大上升子序列

【题目描述】
一个数的序列
你的任务,就是对于给定的序列,求出最大上升子序列和。注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

【输入】
输入的第一行是序列的长度N(1≤N≤1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

【输出】
最大上升子序列和。

如果前面的最长上升子序列那道题理解清楚了,那么该题做起来就会发现本质上和前一道题是一样的。该题的决策对象和阶段都和前一道题一样,只是状态变为dp[i]表示以第i个数作为结尾的最大上升子序列的长度。
决策:这里要求子序列之和最大,对于每一个位置的数,只能从前面的比当前值小的数转移过来,要求序列之和最大,那么这个序列前面选择的数之和要最大,那么我们需要从前面可以选择的点中序列和最大的点转移过来。

我们设dp[i]表示以i结尾的最大子序列之和,那么我们就可以很轻松的得到一下内容
d p [ i ] = m a x ( d p [ i ] , d p [ j ] + a [ i ] ) j ∈ [ 1 , i − 1 ] dp[i]=max(dp[i],dp[j]+a[i]) j\in[1,i-1] dp[i]=max(dp[i],dp[j]+a[i])j[1,i1]
所以就有以下ACcode

#include<bits/stdc++.h>
using namespace std;

int a[10010],dp[10010],ans=INT_MIN;
int main(){
	int n;
	cin>>n;
	for (int i=1;i<=n;i++){
		cin>>a[i];
		dp[i]=a[i];
	}	
	for (int i=1;i<=n;i++){
		for (int j=1;j<i;j++){
			if (a[j]<a[i]){
				dp[i]=max(dp[i],dp[j]+a[i]);
			}
		}
		ans=max(ans,dp[i]);
	}
	cout<<ans;
	return 0;
}

例题1——[NOIP2004 提高组] 合唱队形

题目描述

n n n 位同学站成一排,音乐老师要请其中的 n − k n-k nk 位同学出列,使得剩下的 k k k 位同学排成合唱队形。

合唱队形是指这样的一种队形:设 k k k 位同学从左到右依次编号为 1 , 2 , 1,2, 1,2, , k ,k ,k,他们的身高分别为 t 1 , t 2 , t_1,t_2, t1,t2, , t k ,t_k ,tk,则他们的身高满足 t 1 < ⋯ < t i > t i + 1 > t_1< \cdots <t_i>t_{i+1}> t1<<ti>ti+1> > t k ( 1 ≤ i ≤ k ) >t_k(1\le i\le k) >tk(1ik)

你的任务是,已知所有 n n n 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

输入格式

共二行。

第一行是一个整数 n n n 2 ≤ n ≤ 100 2\le n\le100 2n100),表示同学的总数。

第二行有 n n n 个整数,用空格分隔,第 i i i 个整数 t i t_i ti 130 ≤ t i ≤ 230 130\le t_i\le230 130ti230)是第 i i i 位同学的身高(厘米)。

输出格式

一个整数,最少需要几位同学出列。

样例 #1

样例输入 #1

8
186 186 150 200 160 130 197 220

样例输出 #1

4

提示

对于 50 % 50\% 50% 的数据,保证有 n ≤ 20 n \le 20 n20

对于全部的数据,保证有 n ≤ 100 n \le 100 n100

分析

该题实际上就是前面求一个最长上升子序列,对后面求一个最长下降子序列。但是问题在于这两个序列的连接点我们不知道,没有办法直接求解出来,也就是说任意一个点都有可能作为这个连接点,所以我们需要先枚举这个连接点x,再分别对区间[1,x]求最长上升子序列,对区间[x+1,n]求最长下降子序列。这种做法的时间复杂度为O(n3),但是该题的数据范围改为n<=2000,所以还需要优化。

我们先看前面的最长上升子序列,在枚举的连接点i逐渐增大的时候,如果我们已经存储了前i-1个数的最长上升子序列的答案,那么前i个数的最长上升子序列的答案就只有两种情况,第一种就是前面i-1的答案,第二种就是以第i个点作为结尾的答案,没有必要再去前面重新计算一次,每一次的时间复杂度为O(n)。对于后面的下降序列,在i增大时,范围在逐渐缩小,我们只需要把它看做是增大就可以,这种做法在之前做过的一些题中使用过,我们只需要倒着枚举连接点i即可,这样,i在逐渐变小,后面的区间在增大,做法和前面一样。

#include<bits/stdc++.h>
using namespace std;

int n,dp1[5000],dp2[5000];//dp1为从前往后的最长上升子序列,dp2为从后往前 
int a[5000],minn=INT_MAX;
int main(){
	cin>>n;
	for (int i=1;i<=n;i++){
		cin>>a[i];
		dp1[i]=1;
		dp2[i]=1;
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<i;j++){
			if (a[j]<a[i]){
				dp1[i]=max(dp1[i],dp1[j]+1);
			}
		}
	}
	for (int i=n;i>=1;i--){
		for (int j=n;j>i;j--){
			if (a[i]>a[j]){
				dp2[i]=max(dp2[i],dp2[j]+1);
			}
		}
	}
	for (int i=n;i>=1;i--){
		minn=min(n-dp1[i]-dp2[i],minn);
	}
	
	cout<<minn+1;
	return 0;
}

最长公共子序列

我们要得到两个字符串的最长公共子序列,暴力做法是先枚举一个字符串的开头,再去枚举另一个字符串的开头,然后找出最大公共子列,时间复杂度为O(n^3)。我们考虑如何优化,前面依然是枚举两个字符串的开头,主要考虑求最大公共子序列时是否可以通过前面算出的答案直接得到正确答案。考虑开头无法知道匹配的最长公共子序列到底匹配到了哪一个位置,所以我们记录下以当前位置结尾的最长公共序列的长度。因为有两个字符串,所以dp[i]无法分别记录两个字符串的结尾位置,所以需要用dp[i][j]。

状态:我们设dp[i][j]表示前缀长度为i的a子串和一个长度为j的b序列的最长公共子序列的长度。决策对象:每个位置的字符。阶段:a串的每个位置和b串的每个位置都可以组合,一共有n*m个阶段。决策:如果a[i]==b[j],那么(i,j)相对于(i1,j-1)会多匹配一个,即dp[i][j]=dp[i-1][j-1]。如果a[i]!=b[j],答案就是dp[i-1][j-1]吗?其实并不是,因为a[i]可以和b[j-1]匹配,或者b[j]可以和a[i-1]匹配,这种情况下,dp[i-1][j-1]的答案是错误的。

当a[i]!=b[j]时,正确答案是dp[i][j]=max (dp[i-1][j],dp[i][j-1]),那么万一是a[i]和b[j1]、a[i-1]和b[j]都无法匹配的情况,是否还需要和dp[i-1][j-1]比较呢?不需要,因为在这种情况下,计算dp[i][j-1]时,a[i]!=b[j-1],那么dp[i][j-1]=max(dp[i-1][j-1], dp[i][j-2]),这个答案中已经报了dp[i-1][j-1]的情况,所以不需要再和dp[i-1][j-1]比较。还需要设置初始值,当a串和b串都没有字符时,最长公共串长度为1,即dp[0][0]=0。

#include<bits/stdc++.h>
using namespace std;

int dp[1010][1010];
int main(){
    string a,b;
    cin>>a>>b;
    dp[0][0]=0;
    for (int i=1;i<=a.length();i++){
        for (int j=1;j<=b.length();j++){
            if (a[i-1]==b[j-1]) dp[i][j]=dp[i-1][j-1]+1;
            else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
        }
    }
    cout<<dp[a.length()][b.length()];
    return 0;
}

最长公共子串

该题和上一题的状态、对象、阶段都是相同的,不同的是状态的转移。dp[i][j]表示以位置i结尾的a串和以位置j结尾的b串的最长公共子串. 如果a[i]!=b[j],那么这个子串一定是不可以匹配的,因为这里说了分别以i,j结尾,所以dp[i][j]=0。如果a[i]=b[j],长度就在之前的dp[i-1][j-1]的基础上增加1,即dp[i][j]=dp[i-1][j-1]+1。

#include<bits/stdc++.h>
using namespace std;

int dp[1500][1510];
int maxn=INT_MIN;
int main(){
	string a,b;
	cin>>a>>b;
	dp[0][0]=0;
	for (int i=1;i<=a.length();i++){
		for (int j=1;j<=b.length();j++){
			if (a[i-1]==b[j-1]){
				dp[i][j]=dp[i-1][j-1]+1;
			}
			maxn=max(dp[i][j],maxn);
		}
	}
	cout<<maxn;
	return 0;
}

文章目录

  • 前言
    • 动态规划到底是啥?
  • 线性dp
    • 最长上升子序列
      • 子集和子序列和子串的区别
      • 内容分析
    • 最大上升子序列
    • 例题1——[NOIP2004 提高组] 合唱队形
      • 分析
    • 最长公共子序列
    • 最长公共子串
  • 平面dp
    • 例题2——[NOIP2000 提高组] 方格取数
      • 分析
    • 例题3——[NOIP2008 提高组] 传纸条
      • 分析
    • 例题4——最大加权矩形
      • 分析
        • 补充:最大字段和
        • 最大子矩阵和和最大字段和的关联
    • 例题5——最大全1子矩阵
      • 分析
    • 例题6——穿衣服
      • 分析
  • 背包问题
    • 什么是背包问题
    • 背包的分类
    • 01背包
      • 思路一
      • 思路二
      • 思路三(重头戏)
        • 常规代码
        • 空间优化
    • 例题7——[NOIP2001 普及组] 装箱问题
      • 分析
    • 例题8——最大约数和
      • 分析
    • 多重背包
        • 位置1
        • 位置2
      • 二进制优化
      • 完整代码
    • 例题9——[NOIP1996 提高组] 砝码称重
      • 分析
    • 完全背包
      • 优化
        • 优化办法一
        • 优化办法二(重点)(敲黑板)
    • 例题10——疯狂的采药
      • 分析
    • 混合背包
    • 多维背包
    • 例题11——「一本通入门 2.9.2」 潜水员
      • 分析
    • 分组背包
    • 例题12——旅行者
      • 分析
    • 泛化背包
    • 例题13——精力的分配
      • 分析
    • 有依赖的背包
    • 例题14——[NOIP2006 提高组] 金明的预算方案
      • 分析
    • 背包方案数
    • 例题15——方案数
      • 分析
    • 01背包问题输出方案
    • 背包问题总结
  • 区间dp
    • 区间型动态规划的特点:
    • 例题16——一本通改编——衣服合并
      • 分析
    • 小结1
    • 例题17——1275:【例9.19】乘积最大
      • 分析
    • 小结2
    • 例题18——[IOI2000] 回文字串
      • 分析
    • 环形区间dp
    • 例题19——[NOI1995] 石子合并
      • 分析
    • 例题20——[NOIP2006 提高组] 能量项链
      • 分析
    • 例题21——[NOIP2001 提高组] 数的划分
      • 分析
    • 例题22——放苹果
      • 分析
  • 总结

平面dp

其实平面DP和正常的dp没有什么本质上的区别,只不过是在二维的面上进行DP,而且,客观的说,其实和递推没有什么区别,不要把他想的太难了

本蒟蒻思前想后,好像关于平面DP的理论知识好像没有什么,所以我们直接上题,从题来入手

例题2——[NOIP2000 提高组] 方格取数

题目传送门

题目背景

NOIP 2000 提高组 T4

题目描述

设有 N × N N \times N N×N 的方格图 ( N ≤ 9 ) (N \le 9) (N9),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字 0 0 0。如下图所示(见样例):

某人从图的左上角的 A A A 点出发,可以向下行走,也可以向右走,直到到达右下角的 B B B 点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字 0 0 0)。
此人从 A A A 点到 B B B 点共走两次,试找出 2 2 2 条这样的路径,使得取得的数之和为最大。

输入格式

输入的第一行为一个整数 N N N(表示 N × N N \times N N×N 的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的 0 0 0 表示输入结束。

输出格式

只需输出一个整数,表示 2 2 2 条路径上取得的最大的和。

样例 #1

样例输入 #1

8
2 3 13
2 6  6
3 5  7
4 4 14
5 2 21
5 6  4
6 3 15
7 2 14
0 0  0

样例输出 #1

67

提示

数据范围: 1 ≤ N ≤ 9 1\le N\le 9 1N9

分析

如果该题只取一次数或者取走一次之后原来的数还在,就是一道简单的递推的题,但是该题需要来回取两次,如果我们按照贪心+递推的思想,取完一次之后修改方格中的数,然后再取一次,那么很容易举出反例,所以我们要思考其他办法。
我们要知道,无论是从A–>B还是从B–>A,对于取数的答案是不会有影响的,我们不妨看做从A–>B取数取两次,我们让这两次取数同时进行。
如果要同时表示表示这种状态就需要开四维数组dp[i][j][k][l],表示分别走到点(i,j)和(k,l)的最优解。
这种的思路还是比较好想的,代码如下

#include<bits/stdc++.h>
using namespace std;

int mp[10][10],dp[10][10][10][10];
int main(){
	int n;
	cin>>n;
	while (1){
		int a,b,c;
		cin>>a>>b>>c;
		if (a==0&&b==0&&c==0)break;
		mp[a][b]=c;
	}
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			for (int l=1;l<=n;l++){
				for (int k=1;k<=n;k++){
					dp[i][j][l][k]=max(max(dp[i-1][j][l-1][k],dp[i-1][j][l][k-1]),max(dp[i][j-1][l-1][k],dp[i][j-1][l][k-1]))+mp[i][j]+mp[l][k];
					if (i==l&&j==k)dp[i][j][l][k]-=mp[i][j];//如果相同则要减去一个
				}
			}
		}
	}
	cout<<dp[n][n][n][n];
	return 0;
}

因为这是2000年的题,那是的信息竞赛的水平不高,所以范围只有9,但是我们要有科学家钻研的品格(我们老师的梗),所以我们来探寻一下O(n3)的方法
不难想到我们可以发现,每当我们走一步,那么x坐标和y坐标之间总会有一个数加1所以,我们可以用k来表示x坐标和y坐标的和,从而通过y坐标来计算出x坐标。由于k对于两条同时处理的路径可以是公共的,所以我们就可以用 f [ k ] [ y 1 ] [ y 2 ] f[k][y1][y2] f[k][y1][y2] 来表示当前状态。

#include<bits/stdc++.h>
using namespace std;

int mp[700][700],dp[700][700][700];
int main(){
	int n;
	cin>>n;
	while (1){
		int a,b,c;
		cin>>a>>b>>c;
		if (a==0&&b==0&&c==0)break;
		mp[a][b]=c;
	}
	for (int i=1;i<=n+n;i++){
		for (int l=1;l<=min(i,n);l++){
			for (int k=1;k<=min(i,n);k++){
				dp[i][l][k]=max(max(dp[i-1][l-1][k],dp[i-1][l][k]),max(dp[i-1][l][k-1],dp[i-1][l-1][k-1]))+mp[i-l][l]+mp[i-k][k];
				if (l==k)dp[i][l][k]-=mp[i-l][l];
			}
		}
	}
	cout<<dp[n+n][n][n];
	return 0;
}

例题3——[NOIP2008 提高组] 传纸条

题目传送门

题目描述

小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排坐成一个 m m m n n n 列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标 ( 1 , 1 ) (1,1) (1,1),小轩坐在矩阵的右下角,坐标 ( m , n ) (m,n) (m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。

在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙。反之亦然。

还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用 0 0 0 表示),可以用一个 [ 0 , 100 ] [0,100] [0,100] 内的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度之和最大。现在,请你帮助小渊和小轩找到这样的两条路径。

输入格式

第一行有两个用空格隔开的整数 m m m n n n,表示班里有 m m m n n n 列。

接下来的 m m m 行是一个 m × n m \times n m×n 的矩阵,矩阵中第 i i i j j j 列的整数表示坐在第 i i i j j j 列的学生的好心程度。每行的 n n n 个整数之间用空格隔开。

输出格式

输出文件共一行一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值。

样例 #1

样例输入 #1

3 3
0 3 9
2 8 5
5 7 0

样例输出 #1

34

提示

【数据范围】

对于 30 % 30\% 30% 的数据,满足 1 ≤ m , n ≤ 10 1 \le m,n \le 10 1m,n10
对于 100 % 100\% 100% 的数据,满足 1 ≤ m , n ≤ 50 1 \le m,n \le 50 1m,n50

【题目来源】

NOIP 2008 提高组第三题。

分析

这道题其实和上一道题差不多,所以我们直接写代码,值得注意的是,这里的每一个人都是正数,所以不用考虑走了一次后就不能走了的情况,基本上可以完全参照上一道题的写法

#include<bits/stdc++.h>
using namespace std;

long long mp[120][120],dp[120][120][120];
int main(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			cin>>mp[i][j];
		}
	}
	for (int i=2;i<=n+m-1;i++){
		for (int l=1;l<=min(i,n);l++){
			for (int k=1;k<=min(i,n);k++){
				dp[i][l][k]=max(max(dp[i-1][l-1][k],dp[i-1][l][k]),max(dp[i-1][l][k-1],dp[i-1][l-1][k-1]))+mp[l][i-l+1]+mp[k][i-k+1];
				if (l==k)dp[i][l][k]-=mp[l][i-l+1];
			}
		}
	}
	cout<<dp[n+m-1][n][n];
	return 0;
}

但是话又说回来,如果这道题是有负数的呢?那么我们就要考虑如何排除又重复的情况了。其实解题思路是一样的,只是状态转移方程不同,既然两条路径不能重叠,那么一定有一条路径在上上,一条路径在下方,这里我们始终让i<j就行了,但是注意特判起点和终点。

#include<bits/stdc++.h>
using namespace std;

long long mp[220][220],dp[420][220][220];
int main(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			cin>>mp[i][j];
		}
	}
	dp[2][1][1]=mp[1][1];
	for (int i=2;i<=n+m;i++){
		for (int j=0;j<=n;j++){
			for (int k=0;k<=n;k++){
				dp[i][j][k]=INT_MIN;
			}
		}
	}
	dp[2][1][1]=mp[1][1];
	for (int i=2;i<=n+m;i++){
		for (int l=1;l<=n&&l<i;l++){
			for (int k=1;k<min(l,i);k++){
				dp[i][l][k]=max(max(dp[i-1][l-1][k],dp[i-1][l][k]),max(dp[i-1][l][k-1],dp[i-1][l-1][k-1]))+mp[l][i-l]+mp[k][i-k];
			}
		}
	}
	dp[n+m][n][n]=dp[n+m-1][n][n-1]+mp[n][m];
	cout<<dp[n+m][n][n];
	return 0;
}

例题4——最大加权矩形

题目传送门

题目描述

为了更好的备战 NOIP2013,电脑组的几个穿黑丝的女孩子 LYQ,ZSC,ZHQ 认为,我们不光需要机房,我们还需要运动,于是就决定找校长申请一块电脑组的课余运动场地,听说她们都是电脑组的高手,校长没有马上答应他们,而是先给她们出了一道数学题,并且告诉她们:你们能获得的运动场地的面积就是你们能找到的这个最大的数字。

校长先给他们一个 n × n n\times n n×n 矩阵。要求矩阵中最大加权矩形,即矩阵的每一个元素都有一权值,权值定义在整数集上。从中找一矩形,矩形大小无限制,是其中包含的所有元素的和最大 。矩阵的每个元素属于 [ − 127 , 127 ] [-127,127] [127,127] ,例如

 0 –2 –7  0 
 9  2 –6  2
-4  1 –4  1 
-1  8  0 –2

在左下角:

9  2
-4  1
-1  8

和为 15 15 15

几个女孩子有点犯难了,于是就找到了电脑组精打细算的 HZH,TZY 小朋友帮忙计算,但是遗憾的是他们的答案都不一样,涉及土地的事情我们可不能含糊,你能帮忙计算出校长所给的矩形中加权和最大的矩形吗?

输入格式

第一行: n n n,接下来是 n n n n n n 列的矩阵。

输出格式

最大矩形(子矩阵)的和。

样例 #1

样例输入 #1

4
0 -2 -7 0
 9 2 -6 2
-4 1 -4  1 
-1 8  0 -2

样例输出 #1

15

提示

1 ≤ n ≤ 120 1 \leq n\le 120 1n120

分析

首先可以看到这道题的数据范围似乎不是很大,好像O(n4)就可以过,那么我们就先这样去想想,是不是可以利用前缀和呢?答案是可以的,代码如下

#include<iostream>
using namespace std;
int n,mx=INT_MIN;
int a[130][130],sum[130][130],qz[130][130];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>a[i][j];
            qz[i][j]=qz[i][j-1]+a[i][j];
            sum[i][j]=qz[i][j]+sum[i-1][j];
        }
    }
    for(int x1=1;x1<=n;x1++){
    	for(int y1=1;y1<=n;y1++){
    		for(int x2=1;x2<=n;x2++){
    			for(int y2=1;y2<=n;y2++){
    				if (x2<x1||y2<y1)continue;
    				mx=max(mx,sum[x2][y2]+sum[x1-1][y1-1]-sum[x2][y1-1]-sum[x1-1][y2]);
    			}
    		}
    	}
    }
    cout<<mx;
	return 0;
}

但是我们再想想,如果数据再大一点怎么办呢?

首先我们可以想到,这道题其实是要求一个和,那么我们不难想到可以用前缀和来解决,但是这样的时间复杂度过于高了,那么我们怎么办呢?其实我们这里可以用一点最大字段和的知识。

补充:最大字段和

给定一个长为 n 的序列,任意选择其中连续的 x(0≤𝑥≤n)项所确定的一段更短的连续序列叫作一个子段。一个子段的得分为其每个元素之和,请求出原序列的最大子段和。
我们正常来说应该是用枚举的思想,但是这样的时间复杂度为O(n2),如果n稍微大一点就过不了了,那么我们还是考虑动态规划。

首先我们可以想到 d p [ i ] dp[i] dp[i]可能与 d p [ i − 1 ] dp[i-1] dp[i1] a [ i ] a[i] a[i]有关,那么我们就可以想到这个关系式 d p [ i ] = m a x ( d p [ i − 1 ] + a [ i ] , a [ i ] ) dp[i]=max(dp[i-1]+a[i],a[i]) dp[i]=max(dp[i1]+a[i],a[i])
那么我们就可以写出以下代码

#include<bits/stdc++.h>
using namespace std;
long long a[1000000],dp[1000000],n,maxn=0;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		dp[i]=0;
	}
	for(int i=1;i<=n;i++){
		dp[i]=max(dp[i-1]+a[i],a[i]);
		if(dp[i]>maxn){
			maxn=dp[i];
		}
	}
	cout<<maxn;
	return 0;
}

这样我们就可以在O(n)的时间复杂度里求完答案

最大子矩阵和和最大字段和的关联

讲了这么多,我们该怎么用呢?
其实我们可以枚举一个上下边界,然后中间就先用前缀和解出中间的值再用最大字段和的方法来解决,这样的时间复杂度就为O(n3),显然可以通过
那么我们就可以得到以下转换代码
d p [ k ] = m a x ( d p [ k − 1 ] + ( t o t [ j ] [ k ] − t o t [ i − 1 ] [ k ] ) , ( t o t [ j ] [ k ] − t o t [ i − 1 ] [ k ] ) ) ; dp[k]=max(dp[k-1]+(tot[j][k]-tot[i-1][k]),(tot[j][k]-tot[i-1][k])); dp[k]=max(dp[k1]+(tot[j][k]tot[i1][k]),(tot[j][k]tot[i1][k]));

那么我们现在就好处理了
代码如下

#include<bits/stdc++.h>
using namespace std;

int mp[310][310],tot[310][310];
int dp[310],maxn=INT_MIN;
int main(){
	int n;
	cin>>n;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=n;j++){
			cin>>mp[i][j];
			tot[i][j]=tot[i-1][j]+mp[i][j];
		}
	}

	for (int i=1;i<=n;i++){
		for (int j=i;j<=n;j++){
			for (int k=1;k<=n;k++){
				dp[k]=tot[j][k]-tot[i-1][k];
			}
			for (int k=1;k<=n;k++){
				dp[k]=max(dp[k-1]+(tot[j][k]-tot[i-1][k]),(tot[j][k]-tot[i-1][k]));
				maxn=max(dp[k],maxn);
			}
		}
	}
	cout<<maxn;
	return 0;
}

例题5——最大全1子矩阵

给定一个01矩阵,求其最大的全1子矩阵。

第一行n,m。 后面n行每行m个数描述这个矩阵

最大子矩阵的面积

输入数据 1

3 4
1 1 1 1
1 1 1 1 
1 1 1 0

输出数据 1

9

Limitation
对于100%的数据,1<=n,m<=300

分析

这道题其实和上一道题其实差不多还是正常的进行判断,然后在进行dp的时候只需要进行判断,看看是不是都是1就可以了
代码如下

#include<bits/stdc++.h>
using namespace std;

int mp[310][310],tot[310][310];
int dp[310],maxn=0;
int main(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			cin>>mp[i][j];
			tot[i][j]=tot[i-1][j]+mp[i][j];
		}
	}
	for (int i=0;i<n;i++){//上边界 
		for (int j=i+1;j<=n;j++){//下边界 
			memset(dp,0,sizeof(dp));
			for (int k=1;k<=m;k++){
				if (tot[j][k]-tot[i][k]==j-i){
					dp[k]=dp[k-1]+(tot[j][k]-tot[i][k]);
					maxn=max(maxn,dp[k]);
				}
			} 
		}
	}
	cout<<maxn;
	return 0;
}

例题6——穿衣服

Description
wjq是一名来自520宿舍的漂亮妹子,她喜欢穿的很性感。 wjq刚刚从床上起来,喜欢性感的她要去她寝室的不同位置去拿衣服,可是她的舍友把她的衣服丢的到处都是,黑丝,白丝,制服等衣服散在不同的位置上,wjq只好挨个去拿。 520宿舍可以看作是一个n行m列的矩形,wjq在(0,0)这个格子(位于宿舍的左上角),寝室的门在(n-1,m-1)这个格子。每次wkc可以向相邻的格子走一步,走到某个格子时,她会穿起这个格子的衣服。wjc由于没有穿鞋,而且用她的粉粉嫩嫩的脚走起来非常的不舒服,然而鞋又在寝室门口,所以她想走一条最短路到教室的门口,但是性感度太少了她自己又不舒服,所以她决定走一条性感程度最高的最短路线(即保证路径最短的前提下性感程度最高),你能帮帮她吗?

Input
第一行三个整数n,m,k。 接下来k行,每行三个整数a,b,c,表示在(a,b)这个格子的衣服的性感程度为c。

Output
输出1个整数,表示wjq的最大性感程度。

Samples
输入数据 1
2 2 2
0 0 1
1 1 1
输出数据 1
2

n,m>=10*9,k>=8000。

分析

如果正常来说,我们肯定使用二维dp来做他,但是可以看到n和m的值非常的大,空间肯定要爆,同时这道题我们又不能用滚动数组来压缩,所以我们只能采取其他的思路

首先我们来想想最短路的问题,不难想到,只要wjq不往左或上走,就一定是最短路。
然后我们再来想想dp的问题,首先我们知道,当前的这一个点只能从这个点的左上方的区域转移而来,那么我们只需要对每一个点进行遍历。然后去找在它左上方的点就可以了。
但是我们又会想到,这样写出来的dp可能有一些值没有被算到,所以我们就需要先对每一个点进行排序,按照从左上方到右下方的顺序来排序。

#include<bits/stdc++.h>
using namespace std;

struct f{
	int x,y;
	int num;
}a[8000];

bool cmp(f a1,f a2){
	if (a1.y!=a2.y)return a1.y<a2.y;
	else return a1.x<a2.x;
}
int dp[8000],maxn=INT_MIN;
int main(){
	int n,m,k;
	cin>>n>>m>>k;
	for (int i=1;i<=k;i++){
		cin>>a[i].x>>a[i].y>>a[i].num;
	}
	sort(a+1,a+1+k,cmp);
	for (int i=1;i<=k;i++){//当前转移的值 
		dp[i]=a[i].num;
		for (int j=1;j<=k;j++){
			if (a[j].x<=a[i].x&&j!=i)dp[i]=max(dp[i],dp[j]+a[i].num);
		}
		maxn=max(maxn,dp[i]);
	}
	cout<<maxn; 
	return 0;
}

文章目录

  • 前言
    • 动态规划到底是啥?
  • 线性dp
    • 最长上升子序列
      • 子集和子序列和子串的区别
      • 内容分析
    • 最大上升子序列
    • 例题1——[NOIP2004 提高组] 合唱队形
      • 分析
    • 最长公共子序列
    • 最长公共子串
  • 平面dp
    • 例题2——[NOIP2000 提高组] 方格取数
      • 分析
    • 例题3——[NOIP2008 提高组] 传纸条
      • 分析
    • 例题4——最大加权矩形
      • 分析
        • 补充:最大字段和
        • 最大子矩阵和和最大字段和的关联
    • 例题5——最大全1子矩阵
      • 分析
    • 例题6——穿衣服
      • 分析
  • 背包问题
    • 什么是背包问题
    • 背包的分类
    • 01背包
      • 思路一
      • 思路二
      • 思路三(重头戏)
        • 常规代码
        • 空间优化
    • 例题7——[NOIP2001 普及组] 装箱问题
      • 分析
    • 例题8——最大约数和
      • 分析
    • 多重背包
        • 位置1
        • 位置2
      • 二进制优化
      • 完整代码
    • 例题9——[NOIP1996 提高组] 砝码称重
      • 分析
    • 完全背包
      • 优化
        • 优化办法一
        • 优化办法二(重点)(敲黑板)
    • 例题10——疯狂的采药
      • 分析
    • 混合背包
    • 多维背包
    • 例题11——「一本通入门 2.9.2」 潜水员
      • 分析
    • 分组背包
    • 例题12——旅行者
      • 分析
    • 泛化背包
    • 例题13——精力的分配
      • 分析
    • 有依赖的背包
    • 例题14——[NOIP2006 提高组] 金明的预算方案
      • 分析
    • 背包方案数
    • 例题15——方案数
      • 分析
    • 01背包问题输出方案
    • 背包问题总结
  • 区间dp
    • 区间型动态规划的特点:
    • 例题16——一本通改编——衣服合并
      • 分析
    • 小结1
    • 例题17——1275:【例9.19】乘积最大
      • 分析
    • 小结2
    • 例题18——[IOI2000] 回文字串
      • 分析
    • 环形区间dp
    • 例题19——[NOI1995] 石子合并
      • 分析
    • 例题20——[NOIP2006 提高组] 能量项链
      • 分析
    • 例题21——[NOIP2001 提高组] 数的划分
      • 分析
    • 例题22——放苹果
      • 分析
  • 总结

背包问题

什么是背包问题

背包问题是线性DP的一个拓展,它的模型一般为:
有一个体积为V的背包,有n种物品,每种物品的数量有限或者无限,每个物体有它的属性(体积、质量等),问在不超过背包体积的情况下如何选择物品才能让物品的属性之和最大。

首先,很容易想到贪心是错误的,无论是从大到小贪,还是从小到大贪心的往背包里放物品,都可以找到反例。那么我们在这个地方就要考虑动态规划了

背包的分类

背包的类型有很多,比如最简单的01背包,还有稍微复杂一点的多重背包和完全背包以及混合背包,除此之外,还有更难的泛化背包,多维背包,组合背包等,本文先讲解最简单的01背包,至于其余的背包,其实都是从01背包去进行更改的

01背包

有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

01背包特点:每种物品有且仅有一件,并且不可以再分。该物品要么取走,要么不取,只有两种状态,所以叫01背包。同时,01背包也是所有背包模型的基础。

思路一

每一种物品有两种状态(0和1),判断所有组成的方案,然后找出其中价值最大的。时间复杂度O(2^n).理论上是不可能实现的。

思路二

回溯法。时间复杂度和上面差不多。

思路三(重头戏)

动态规划
每种物品只有两种状态(0和1)。要求背包的最大价值。设前i件物品(部分/全部)放入体积为v的背包中的最大价值是 f [ i ] [ v ] f[i][v] f[i][v]

取走: f [ i ] [ v ] = f [ i − 1 ] [ v − c [ i ] ] + w [ i ] ; f[i][v]=f[i-1][v-c[i]]+w[i]; f[i][v]=f[i1][vc[i]]+w[i];
如果要把第i件物品放入背包中,说明背包中肯定空余第i件物品体积的位置。说明前(i-1)件物品占的体积为v-c[i],那么最大价值就是f[i-1][v-c[i]].既然把第i件物品放进去了,最大价值就+w[i].
不取: f [ i ] [ v ] = f [ i − 1 ] [ v ] ; f[i][v]=f[i-1][v]; f[i][v]=f[i1][v];
如果第i件物品不放入背包中,说明第i件物品是否存在对结果没有影响。前i-1件物品占用的最大体积就是v。最大价值就是f[i-1][v].
所以我们就可以得出这个状态转移方程: f [ i ] [ v ] = m a x ( f [ i − 1 ] [ v − c [ i ] ] + w [ i ] , f [ i − 1 ] [ v ] ) ; f[i][v]=max(f[i-1][v-c[i]]+w[i],f[i-1][v]); f[i][v]=max(f[i1][vc[i]]+w[i],f[i1][v]);
有了状态转移方程,那么我们就可以得出以下代码

常规代码
for(int i=1;i<=n;i++){//枚举所有物品
	for(int v=V,v>0,v--){//枚举范围内所有体积
		if(c[i]<=v){//如果该物品可以放进去
			f[i][v]=max(f[i-1][v-c[i]]+w[i],f[i-1][v]);
		}else f[i][v]=f[i-1][v];//如果该物品肯定放不进去
 	}
}

对于前i件物品,他的状态都是通过前i-1件物品转移过来的,所以v的范围无论是[0,V]还是[V,0]都没有任何关系。初始状态i=0的时候f[0][v]的值就是0,同样的,当v=0的时候,f[i][0]的值也仍然为0.所以不需要额外的初始化。

我们要求的n件物品放入V的背包中的最大价值就是f[N][V]的值

空间优化

不难想到,如果N的值比较大的话,很容易爆空间,那么我们就要想办法进行优化,可以发现,我们的这个状态转移方程只和 v − c [ i ] v-c[i] vc[i]有关,那么我们就可以使用滚动数组进行优化
但是这里要注意,如果我们采用自滚的话,在枚举v的时候就必须从大往小去枚举,因为我们可以发现在计算dp[j]的答案时,会用到dp[j-v[i]]的答案,如果j是正着枚举,dp[j-v[i]]的答案就是已经更新过的,答案是错误的;但是如果我们把j倒着枚举,dp[j]会比dp[j-v[i]]先更新,那么用到的dp[j-v[i]]的值就是上一层的,也就是我们需要的答案。所以01背包如果用一维数组表示,背包体积j的枚举只能倒着枚举,否则答案就是错误的。

for(int i=1;i<=n;i++){
	for(int j=m;j>=v[i];j--){
		dp[j]=max(dp[j-v[i]]+w[i],dp[j]);
	}
}

例题7——[NOIP2001 普及组] 装箱问题

题目描述

有一个箱子容量为 V V V,同时有 n n n 个物品,每个物品有一个体积。

现在从 n n n 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。

输入格式

第一行共一个整数 V V V,表示箱子容量。

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

接下来 n n n 行,每行有一个正整数,表示第 i i i 个物品的体积。

输出格式

  • 共一行一个整数,表示箱子最小剩余空间。

样例 #1

样例输入 #1

24
6
8
3
12
7
9
7

样例输出 #1

0

提示

对于 100 % 100\% 100% 数据,满足 0 < n ≤ 30 0<n \le 30 0<n30 1 ≤ V ≤ 20000 1 \le V \le 20000 1V20000

【题目来源】

NOIP 2001 普及组第四题

分析

这道题其实就是一道裸的01背包问题,可以直接解决

#include<bits/stdc++.h>
using namespace std;

int v[20100],dp[20100];
int main(){
	int V,n;
	cin>>V>>n;
	for (int i=1;i<=n;i++){
		cin>>v[i];
	}
	for (int i=1;i<=n;i++){
		for (int j=V;j>=v[i];j--){
			dp[j]=max(dp[j],dp[j-v[i]]+v[i]);
		}
	}
	cout<<V-dp[V];
	return 0;
}

例题8——最大约数和

题目描述

选取和不超过 S S S 的若干个不同的正整数,使得所有数的约数(不含它本身)之和最大。

输入格式

输入一个正整数 S S S

输出格式

输出最大的约数之和。

样例 #1

样例输入 #1

11

样例输出 #1

9

提示

【样例说明】

取数字 4 4 4 6 6 6,可以得到最大值 ( 1 + 2 ) + ( 1 + 2 + 3 ) = 9 (1+2)+(1+2+3)=9 (1+2)+(1+2+3)=9

【数据规模】

对于 100 % 100 \% 100% 的数据, 1 ≤ S ≤ 1000 1 \le S \le 1000 1S1000

分析

这题很容易想到搜索,预处理出来每一个数的约数的数量,然后爆搜,但是数据范围很大。既然每一个数字都有两种状态,选或者不选,而且任意两个数字之间也没联系,所以可以直接考虑背包,题目中说了不同的正整数,所以就是一个裸的01背包。先预处理出来[1,n]中每个数的约数的数量,再以n作为背包体积,数字1—n作为体积,约数之和作为价值跑一遍01背包即可。

#include<bits/stdc++.h>
using namespace std;

int a[5100],dp[5100];
void yueshu(int k){
	for (int i=1;i<k;i++){
		if (k%i==0)a[k]+=i;
	}
	return;
}
int main(){
	int s;
	cin>>s;
	for (int i=1;i<=s;i++){
		yueshu(i);
	}
	for (int i=1;i<=s;i++){
		for (int j=s;j>=i;j--){
			dp[j]=max(dp[j],dp[j-i]+a[i]);
		}
	}
	cout<<dp[s];
	return 0;
}

多重背包

有N种物品和一个容量为V的背包。每种物品都有一个体积和价值,以及该物品有多少件。求解怎么装可使这些物品的体积总和不超过背包容量,且价值总和最大。问最大价值是多少
输入:

10 4
3 4 3
4 5 3
4 3 1
5 4 4

输出:

13

N<=2000,V<=20000.
这种背包问题和前面的背包问题不同的地方在于,一种物品可以有很多件,并不是只可以取一件。我们可以把它转换成01背包来求解。每种物品有m[i]件,可以理解为有m[i]种该物品,每种物品只有一件。这就转换成了01背包。
但是我们可以注意到这里会去枚举一个数量k,那么这个地方就有问题了,不难发现,k有两个位置可以放如图所示

for (int i=1;i<=n;i++){
	for (int k1;;){//位置1
		for (int j=1;j<=m;j++){
			for (int k2;;){//位置2
			
			}
		}
	}
}

那么我们就要仔细想想这里的问题了

位置1

如果放在位置1,那么我们就会得到以下代码

	for (int i=1;i<=n;i++){
		for (int k=0;k<=c[i];k++){
			for (int j=m;j>=k*v[i];j--){
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*m[i]);
			}
		}
	}

来分析以下这段代码,不难发现dp[i][j]似乎确凿被更新了许多次,但是都没有被保存下来,只有在k=c[i]的时候才有答案被保存了下来,所以这样是不行的
那么这时候就有人说了,那我们把他开成一维的滚动数组不久完了?好,我们来试试

	for (int i=1;i<=n;i++){
		for (int k=0;k<=c[i];k++){
			for (int j=m;j>=k*v[i];j--){
				dp[j]=max(dp[j],dp[j-k*v[i]]+k*m[i]);
			}
		}
	}

不难发现,dp[i]确实被持续更新了,但是,我们要注意一下,我们是按照01背包的方法来进行的,01背包的物品有一个特点,就是每个物品是独立的,是不会相互影响的,那么我们这里这样写了之后,就会发现我们其实是把选零个物品,选一个物品,选两个物品等看成了一个个单一的物品,那么这么说,我们在极端情况下是可以都选的,也就是说会选 c [ i ] × ( c [ i ] − 1 ) 2 \frac{c[i]\times(c[i]-1)}{2} 2c[i]×(c[i]1)个,那么这样算出来的答案就一定是错的了,而且会大上不少。

位置2

如果我们放在位置二,就一定是对了吗?我们先写一个代码来看看

	for (int i=1;i<=n;i++){
		for (int j=m;j>=0;j--){
			for (int k=0;k<=c[i];k++){
				if (j<k*v[i])break;
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*m[i]);
			}
		}
	}

虽然我们将顺序挑换了,但是可以发现 d p [ i ] [ j ] dp[i][j] dp[i][j]还是没有保存数据,因为它是按照 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] d p [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ m [ i ] dp[i-1][j-k*v[i]]+k*m[i] dp[i1][jkv[i]]+km[i]来进行动态规划的,和 d p [ i ] [ j ] dp[i][j] dp[i][j]无关,所以我们这里要稍微改一改代码,把 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1]改成 d p [ i ] [ j ] dp[i][j] dp[i][j],这样我们就可以持续更新了,只不过以防万一,我们还是先对 d p [ i ] [ j ] dp[i][j] dp[i][j]附上值,如下

	for (int i=1;i<=n;i++){
		for (int j=m;j>=0;j--){
			dp[i][j]=dp[i-1][j];
			for (int k=0;k<=c[i];k++){
				if (j<k*v[i])break;
				dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*m[i]);
			}
		}
	}

那么我们似乎可以把它改成一维的滚动数组诶

	for (int i=1;i<=n;i++){
		for (int j=m;j>=0;j--){
			for (int k=0;k<=c[i];k++){
				if (j<k*v[i])break;
				dp[j]=max(dp[j],dp[j-k*v[i]]+k*m[i]);
			}
		}
	}

二进制优化

根据代码可知,该题的时间复杂度为 O ( V × N ) O(V\times N) O(V×N)(N 表示所有物品的总数量)),如果每一种物品的数 量比较多,该题就容易超时,所以我们需要想办法优化一下。
对于每一种物品,可以选择的范围是[1,k], 暴力的做法是依次枚举物品选择0,1…k的情况, 但是这样枚举很冗余,存在重复枚举的情况。
比如枚举了1,2,表示有两种该物品,当把这两种物品都选择时,相当于就是选择该物品3个时 的情况,后面枚举的3就重复了,同样,后面的很多数字都可以通过前面枚举过的数字组合而成。

那么对于[1,k],怎么用最少的数表示出来这个区间内的所有数呢?
首先,1肯定是要有的。
其次,只有一个1,最大表示的数是1,没有办法表示出来2,所以2也需要。
3:可以通过1+2组合得到,没有要枚举。
4:1和2最多可以表示3,4是需要的。
5:1+4=5
6:2+4=6
7:1+2+4=7
8:(1,2,4)最多表示7, 8是需要的。 以此类推……
我们可以从中找到规律,只需要1,2,4,8…等2次方数就可以表示出来区间[1,k]之间的数。那么最后一个二进制数是多少呢?比如k=12如果最后一个二进制数是16,那么久表示这个物品我最多可以选16个,和题意不符。如果最后一个二进制数是8,[1,2,4,8],最大可以表示的数是15,但是实际最多可以选12个,也和题意不符。但是只有[1,2,4]没办法表示出来[8,12]。所以数应该是[1,2,4,5],虽然有的数可能会重复表示,但是这样表示的数的范围不会扩大,也不会遗漏。
代码如下:

	int t=1;
	while (c>=t){
		tot++;
		v[tot]=b*t;
		w[tot]=a*t;
		c-=t;
		t*=2;
	}
	if (c>0){
		tot++;
		v[tot]=b*c;
		w[tot]=a*c;
	}

那么我们这样优化完了之后,就可以直接用01背包的方法求解啦

完整代码

#include<bits/stdc++.h>
using namespace std;

int tot=0,v[100000],w[100000],dp[20010];
int main(){
	int m,n;
	cin>>m>>n;
	for (int i=1;i<=n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		int t=1;
		while (c>=t){
			tot++;
			v[tot]=b*t;
			w[tot]=a*t;
			c-=t;
			t*=2;
		}
		if (c>0){
			tot++;
			v[tot]=b*c;
			w[tot]=a*c;
		}
	}
	for (int i=1;i<=tot;i++){
		for (int j=m;j>=w[i];j--){//标准的01背包
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	cout<<dp[m];
	return 0;
}

例题9——[NOIP1996 提高组] 砝码称重

题目描述

设有 1 g 1\mathrm{g} 1g 2 g 2\mathrm{g} 2g 3 g 3\mathrm{g} 3g 5 g 5\mathrm{g} 5g 10 g 10\mathrm{g} 10g 20 g 20\mathrm{g} 20g 的砝码各若干枚(其总重 ≤ 1000 \le 1000 1000),可以表示成多少种重量?

输入格式

输入方式: a 1 , a 2 , a 3 , a 4 , a 5 , a 6 a_1 , a_2 ,a_3 , a_4 , a_5 ,a_6 a1,a2,a3,a4,a5,a6

(表示 1 g 1\mathrm{g} 1g 砝码有 a 1 a_1 a1 个, 2 g 2\mathrm{g} 2g 砝码有 a 2 a_2 a2 个, … \dots 20 g 20\mathrm{g} 20g 砝码有 a 6 a_6 a6 个)

输出格式

输出方式:Total=N

N N N 表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)

样例 #1

样例输入 #1

1 1 0 0 0 0

样例输出 #1

Total=3

提示

【题目来源】

NOIP 1996 提高组第四题

分析

该题很容易想到爆搜,但是数据范围比较大,很容易超时,我们可以考虑其他做法。所有的砝码可以表示的最大数是砝码总重量之和,可以表示的最小的数是1。但是这里面哪些数是可以表示出来的呢?我们可以把题意转换成:有一个体积为V的背包,有n种不同的物品,是否刚好可以填满。即可以选择的物品的最大体积刚好等于V,那么就算一种方法,否则就不算一种方法,最后统计有多少种符合条件的情况。


完全背包

有N种物品和一个容量为V的背包。每种物品可以取无限次,每种物品有一个体积和价值。求解怎么装可使这些物品的体积总和不超过背包容量,且价值总和最大。 完全背包特点:每一种物品都可以取无限次且不可分割。
输入:

10 4
2 1
3 3
4 5
7 9

输出:

12

N ≤ 2000 , V ≤ 2000 N\le2000,V\le2000 N2000,V2000

虽然物品有无限件,但是由于背包的体积有限,每种物品最多可以放的数量是有限的,即最多放V/c[i]件,这就转换成了多重背包,然后再用二进制优化转换成01背包即可。

但是这样写,时间复杂度也要超,所以我们再来谈谈优化的方法

优化

优化办法一

因为物品有无限件,所以对于任意的两种物品,如果w[i]<w[j]并且c[i]>c[j],那么就说明i这种物品是肯定不可以取的。这样就可以筛选出很多不合理的物品。只不过这种的优化方法所优化的幅度不大,所以我们只做了解

优化办法二(重点)(敲黑板)

完全背包的写法和01背包的写法有点类似,只是内层循环的时候,01背包是[V,0],而完全背包是[0,V].前面我们已经探讨过了。如果用一维数组来表示01背包,内循环只能是[V,0],因为对于当物品种类为i的时候v的值,所需要的f[v-c[i]]的值实际上是当物品种类为i-1的时候的未更新的值,因为v>v-c[i]。如果循环写出[0,V],那么第一次更新了f[v1]的值,第二次求f[v2]的值的时候,假设v2-c[i]=v1.实际上是已经取过一次第i件物品的时候的f[v1]的值了,这和01背包不相符合。但是这刚好符合完全背包的要求。没一件物品可以取多次,直到背包放不下为止。
内循环的范围是[0,V],所以我们求值的顺序是 v 1 → v 2 → v 3 → v 4 v1 \rightarrow v2 \rightarrow v3\rightarrow v4 v1v2v3v4
f[v1]:取第i件物品一次时的值.(f[v1]值已经更新)
v2=v1+c[i];
f[v2]=max(f[v2c[i]]+w[i],f[v2])=max(f[v1]+w[i],f[v2]),因为在计算f[v2]之前就已经计算过f[v1]了,所以该表达式表示的是取第i件物品两次时的价值
v3=v2+c[i];
f[v3]=max(f[v2-c[i]]+w[i],f[v2])=max(f[v2]+w[i],f[v3]);在计算f[v3]之前就已经计算过f[v2]了,所以f[v2]表示取两次第i件物品的时候的最大价值,那么该表达时就是表示取取第i件物品三次时的价值
v4=v3+c[i];
也是也前面相同。所以,对于每一个i,随着v值的不断增加,实际上里面就包含了取第i件物品的时候的所有情况了。所以直接O(V*N)就搞定了。
代码如下

for(int i=1;i<=n;i++){
	for(int v=c[i];v<=V;v++){
		f[v]=max(f[v-c[i]]+w[i],f[v]);
	}
}

例题10——疯狂的采药

题目背景

此题为纪念 LiYuxiang 而生。

题目描述

LiYuxiang 是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同种类的草药,采每一种都需要一些时间,每一种也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是 LiYuxiang,你能完成这个任务吗?

此题和原题的不同点:

1 1 1. 每种草药可以无限制地疯狂采摘。

2 2 2. 药的种类眼花缭乱,采药时间好长好长啊!师傅等得菊花都谢了!

输入格式

输入第一行有两个整数,分别代表总共能够用来采药的时间 t t t 和代表山洞里的草药的数目 m m m

2 2 2 到第 ( m + 1 ) (m + 1) (m+1) 行,每行两个整数,第 ( i + 1 ) (i + 1) (i+1) 行的整数 a i , b i a_i, b_i ai,bi 分别表示采摘第 i i i 种草药的时间和该草药的价值。

输出格式

输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

样例 #1

样例输入 #1

70 3
71 100
69 1
1 2

样例输出 #1

140

提示

数据规模与约定

  • 对于 30 % 30\% 30% 的数据,保证 m ≤ 1 0 3 m \le 10^3 m103
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ m ≤ 1 0 4 1 \leq m \le 10^4 1m104 1 ≤ t ≤ 1 0 7 1 \leq t \leq 10^7 1t107,且 1 ≤ m × t ≤ 1 0 7 1 \leq m \times t \leq 10^7 1m×t107 1 ≤ a i , b i ≤ 1 0 4 1 \leq a_i, b_i \leq 10^4 1ai,bi104

分析

不难发现,这就是一道最最最简单的完全背包问题,只不过请注意,需要开 l o n g l o n g long long longlong否则见祖宗
代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5,M=1e7+5;
int n,m,w[N],v[N],f[M];
int main(){
	scanf("%lld%lld",&m,&n);
	for(int i=1;i<=n;i++)
		scanf("%lld%lld",&w[i],&v[i]);
	for(int i=1;i<=n;i++)
		for(int j=w[i];j<=m;j++)
			f[j]=max(f[j],f[j-w[i]]+v[i]);
	printf("%lld",f[m]);
	return 0;
}

注:此处借鉴了一下⚡小林子⚡的代码


混合背包

有n种物品,里面有的物品只可以取一次,有的物品可以取m次,有的物品可以取无限次。求最大价值是多少经过前面的学习,我们会发现,多重背包可以用二进制优化直接转换成01背包。写法完全一样。所以当只含有多重背包和01背包时直接转换成01背包求解。但是完全背包写法和01背包是相反的。所以就分情况讨论。

#include <bits/stdc++.h>
using namespace std;
int m, n;
int w[31], c[31], p[31];
int f[201];
int main(){
	cin>>m>>n;
    for (int i = 1; i <= n; i++){
    	cin>>w[i]>>c[i]>>p[i];
    }
    for (int i = 1; i <= n; i++){
        if (p[i] == 0){
       	   for(int j = w[i]; j <= m; j++){//完全背包
        	  f[j] = max(f[j], f[j-w[i]]+c[i]);
           }
        }else{
	       for(int j = 1; j <= p[i]; j++){          //01背包和多重背包
              for (int k = m; k >= w[i]; k--){
                  f[k] = max(f[k],f[k-w[i]]+c[i]);  
              }
           }
        }   
    }
    cout<<f[m];
    return 0;
}

但是这样写出来的代码的时间复杂度可能会比较高,所以我们可以在多重背包问题的时候,用二进制优化一下,这样写出来的代码就不会超时了

#include<bits/stdc++.h>
using namespace std;

int m,n;
int dp[205];
int v[135];
int w[135];
int p[135];
int cnt=0;

int main(){
	scanf("%d%d",&m,&n);
	for(int i=1;i<=n;i++){
		int t1,t2,t3;
		scanf("%d%d%d",&t1,&t2,&t3);
		if(t3!=0){
			int t=1,sum=0;
			while(sum+t<=t3){
				cnt++;
				v[cnt]=t1*t;
				w[cnt]=t2*t;
				p[cnt]=1;
				sum+=t;
				t*=2;
			}
			if(sum<t3){
				cnt++;
				v[cnt]=t1*(t3-sum);
				w[cnt]=t2*(t3-sum);
				p[cnt]=1;
			}
		}else{
			cnt++;
			v[cnt]=t1;
			w[cnt]=t2;
			p[cnt]=0;
		}
	}
	for(int i=1;i<=cnt;i++){
		if(p[i]==0){
			for(int j=v[i];j<=m;j++){
				dp[j]=max(dp[j-v[i]]+w[i],dp[j]);
			}
		}else{
			for(int j=m;j>=v[i];j--){
				dp[j]=max(dp[j-v[i]]+w[i],dp[j]);
			}
		}
	}
	cout<<dp[m]
	return 0;
}

多维背包

有N件物品和一个容量为V,承重量为M的背包。第i件物品的体积是c[i],重量是z[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,并且重量在背包的承重范围之内,且价值总和最大。

多维背包:前面的背包问题一般只有一个限制(背包体积),而多维背包问题的限制条件不止一个,可能两个,甚至多个,要求必须要同时满足多个条件。

多维背包问题的分析方法和普通背包相同,还是以物品为阶段,考虑第i种物品选还是不选,状态在原有基础上多一维。
f[i][v][m]表示当背包体积为v,承重量为m,选择前i件物品放入时的最大价值。
按照普通01背包的推导方法。第i件物品可以取可以不取。
如果取第i件物品
f [ i ] [ v ] [ m ] = f [ i − 1 ] [ v − c [ i ] ] [ m − z [ i ] ] ; f[i][v][m]=f[i-1][v-c[i]][m-z[i]]; f[i][v][m]=f[i1][vc[i]][mz[i]]
如果不取第i件物品
f [ i ] [ v ] [ m ] = f [ i − 1 ] [ v ] [ m ] ; f[i][v][m]=f[i-1][v][m]; f[i][v][m]=f[i1][v][m];
然后取最大值即可。

例题11——「一本通入门 2.9.2」 潜水员

【题目描述】
潜水员为了潜水要使用特殊的装备。他有一个带2种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种的数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?

例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:

3 36 120

10 25 129

5 50 250

1 45 130

4 20 119

如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。

你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

【输入】
第一行有2整数m,n(1≤m≤21,1≤n≤79)。它们表示氧,氮各自需要的量。

第二行为整数k(1≤n≤1000)表示气缸的个数。

此后的k行,每行包括
a i a_i ai b i b_i bi c i c_i ci 1 ≤ a i ≤ 21 1\le a_i \le 21 1ai21 1 ≤ b i ≤ 79 1≤b_i≤79 1bi79 1 ≤ c i ≤ 800 1≤c_i≤800 1ci800)3个整数。这些各自是:第i个气缸里的氧和氮的容量及汽缸重量。

【输出】
仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。

【输入样例】

5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119

【输出样例】

249

【来源】
一本通在线评测

分析

这道题就是典型的多维背包问题,不难发现,我们只需要在普通的情况下增加一维即可,只不过我们需要注意一下,如果氧气和氮气超过需求时,直接等于需求

#include<bits/stdc++.h>
using namespace std;

int w[1010],v[1010],M[1010];
int dp[25][100];
int main(){
	int m,n,k;//m为氧气,n为氮气 
	cin>>m>>n>>k;
	for (int i=1;i<=k;i++){
		cin>>w[i]>>v[i]>>M[i];//w为氧气,v为氮气,m为称重 
	}
	memset(dp,0x7f7f7f,sizeof(dp));
	dp[0][0]=0;
	for (int i=1;i<=k;i++){
		for (int j=m;j>=0;j--){
			for (int l=n;l>=0;l--){
				dp[j][l]=min(dp[j][l],dp[max(0,j-w[i])][max(0,l-v[i])]+M[i]);
			}
		}
	}
	cout<<dp[m][n];
	return 0;
}

分组背包

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

输入:

10 6 3
2 1 1 
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3

输出:

20

第一行输入依次是背包容量,物品种类以及物品组数。接下来每行依次是体积,价值和所属组数。

对于任何一种背包问题,分析过程都是按照01背包的方式来分解,所以分组背包还是可以看做01背包来求解,01背包是对于每种物品,有取和不取两种状态,而对于分组背包而言,是对于每一组物品,也只有两种状态,取走该组内其中一个和一个都不取(每组物品相互冲突)。所以分组背包的每一组仍然可以看做01背包求解。接着再考虑每一组,这一组内取哪一个数是最优解是不确定的,所以我们还需要枚举组内的每一个数。

分组背包有三层循环,一层是枚举组数,一层是枚举组内选择哪一个物品,一层是背包的体积。具体的枚举顺序是怎么样的?
首先,阶段为第几组物品,所以最外层循环肯定是组数
如果第二层循环是组内第几个物品,最后枚举背包体积,那么就类似于多重背包的错误写法了,(详情请参考浅说背包问题(中))
最外层为第几种物品,第二层循环为每种物品有几个,表示的含义就不再是每组里面只能选择一个,而是这一组内的物品都可以选择
所以第二层循环枚举背包体积,第三层循环枚举组内第几个物品。

例题12——旅行者

【题目描述】
一个旅行者有一个最多能装V公斤的背包,现在有n件物品,它们的重量分别是 W 1 W_1 W1 W 2 W_2 W2,…, W n W_n Wn,它们的价值分别为 C 1 C_1 C1, C 2 C_2 C2,…, C n C_n Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

【输入】
第一行:三个整数,V(背包容量,V≤200),N(物品数量,N≤30)和T(最大组号,T≤10);

第2…N+1行:每行三个整数 W i W_i Wi, C i C_i Ci, P P P,表示每个物品的重量,价值,所属组号。

【输出】
仅一行,一个数,表示最大总价值。

【输入样例】

10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3

【输出样例】

20

【来源】
一本通在线评测

分析

这道题就是典型的模板题

#include<bits/stdc++.h>
using namespace std;

int W[30][30],C[30][30],tot[30],dp[300];
int main(){
	int V,n,T;
	cin>>V>>n>>T;
	for (int i=1;i<=n;i++){
		int a,b,c;
		cin>>a>>b>>c;
		tot[c]++;
		W[c][tot[c]]=a;
		C[c][tot[c]]=b;
	}
	for (int i=1;i<=T;i++){
		for (int j=V;j>=0;j--){
			for (int k=1;k<=tot[i];k++){
				if (j>=W[i][k]){
					dp[j]=max(dp[j],dp[j-W[i][k]]+C[i][k]);
				}
			}
		}
	}
	cout<<dp[V];
	return 0;
}

泛化背包

对于某些物品而言,他没有固定的费用和价值,他的价值是随着你分配的费用而发生着变化。比如你分配的费用较多,他的单个价值就变大。你分配的费用较少,他的单个价值就变小。他的价值不是固定的。这类背包问题也可以看做完全背包来求解。即该类物品理论上有无限种,但是考虑到背包的体积他最多只可以放V/c[i]个。解题办法和不优化的完全背包类似。尝试该物品所有可以放的可能性然后从中得到最优的解决办法。

物品的价值随着分配的空间变化而变化,分配任意空间都有可能是最优解,所以每一种情况都要枚举到,类似于完全背包的暴力做法,第三层循环枚举给物品i分配的空间大小。

例题13——精力的分配

Description
作为桐皇学院的校草,有非常多的女生喜欢你,但是一个人的精力是有限的,而另外一个人对你的爱也是根据你的所给的精力的多少变化而变化的,所以说如果你想要得到更多的爱,你就需要分配好你的精力,这样才能给天下少女一个家。

现在你所拥有的精力为m(m<=1000)的背包,现在有n(n<=1000)位少女,每位少女都是独一无二的,但是少女对你的爱w会随着给该少女所分配到的精力v的变化而变化,变化规律为w= a i 2 × v + b i × v + c i ai^2×v+bi×v+ci ai2×v+bi×v+ci(变化规律仅在分配精力为1时生效),现要你要和一些少女开房,请你在所使用的精力值不大于m的情况下,求得所可以获得的最多的爱是多少?

Format
Input
第一行输入两个正整数n,m分别表示你的精力值和喜欢你的少女的个数。

后面n行每行三个整数a[i],b[i],c[i]分别表示变化系数。

Output
一个正整数,最大能收获的爱。

Samples
输入数据 1

6 100
1 5 5
-1 1 -5
-8 9 5
2 -1 4
7 7 5
2 0 -10

输出数据 1

7305

Limitation
对于100%的数据, 1 < = m , n < = 1000 , − 100 < = a i , b i , c i < = 100 1<=m,n<=1000,-100<=ai,bi,ci<=100 1<=m,n<=1000100<=ai,bi,ci<=100

分析

这道题就是一道最为典型的泛化背包问题

#include<bits/stdc++.h>
using namespace std;

int a[1010],b[1010],c[1010],dp[1010];
int main(){
	int n,m;
	cin>>n>>m;
	for (int i=1;i<=n;i++){
		cin>>a[i]>>b[i]>>c[i];
	}
	for (int i=1;i<=n;i++){
		for (int j=m;j>=0;j--){//上限空间 
			for (int k=1;k<=j;k++){//分配空间 
				dp[j]=max(dp[j],dp[j-k]+a[i]*a[i]*k+b[i]*k+c[i]);
			}
		}
	}
	cout<<dp[m];
	return 0;
}

有依赖的背包

普通背包问题各个物品之间没有联系,而有依赖的背包是指一些物品必须依赖于另一些物品而存在,我们把他们称为主件和附件。如果要买附件,就一定要买对应的主件,反之,如果要买主件,不一定必须要买其附件。比如买音响之前需要先买电脑,不能单独买音响,但是买了电脑后不一定要买音响。有依赖的背包问题是背包问题中最难的一个部分,一个主件有多个附件,一个附件下面可能还有多个附件,这种情况下一般涉及到树形结构,目前我们能解决的一般都是一个附件只从属于一个主件,附件之下没有附件的情况。

例题14——[NOIP2006 提高组] 金明的预算方案

题目描述

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 n n n 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件附件
电脑打印机,扫描仪
书柜图书
书桌台灯,文具
工作椅

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 0 0 个、 1 1 1 个或 2 2 2 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 n n n 元。于是,他把每件物品规定了一个重要度,分为 5 5 5 等:用整数 1 ∼ 5 1 \sim 5 15 表示,第 5 5 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 10 10 元的整数倍)。他希望在不超过 n n n 元的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第 j j j 件物品的价格为 v j v_j vj,重要度为 w j w_j wj,共选中了 k k k 件物品,编号依次为 j 1 , j 2 , … , j k j_1,j_2,\dots,j_k j1,j2,,jk,则所求的总和为:

v j 1 × w j 1 + v j 2 × w j 2 + ⋯ + v j k × w j k v_{j_1} \times w_{j_1}+v_{j_2} \times w_{j_2}+ \dots +v_{j_k} \times w_{j_k} vj1×wj1+vj2×wj2++vjk×wjk

请你帮助金明设计一个满足要求的购物单。

输入格式

第一行有两个整数,分别表示总钱数 n n n 和希望购买的物品个数 m m m

2 2 2 到第 ( m + 1 ) (m + 1) (m+1) 行,每行三个整数,第 ( i + 1 ) (i + 1) (i+1) 行的整数 v i v_i vi p i p_i pi q i q_i qi 分别表示第 i i i 件物品的价格、重要度以及它对应的的主件。如果 q i = 0 q_i=0 qi=0,表示该物品本身是主件。

输出格式

输出一行一个整数表示答案。

样例 #1

样例输入 #1

1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0

样例输出 #1

2200

提示

数据规模与约定

对于全部的测试点,保证 1 ≤ n ≤ 3.2 × 1 0 4 1 \leq n \leq 3.2 \times 10^4 1n3.2×104 1 ≤ m ≤ 60 1 \leq m \leq 60 1m60 0 ≤ v i ≤ 1 0 4 0 \leq v_i \leq 10^4 0vi104 1 ≤ p i ≤ 5 1 \leq p_i \leq 5 1pi5 0 ≤ q i ≤ m 0 \leq q_i \leq m 0qim,答案不超过 2 × 1 0 5 2 \times 10^5 2×105

NOIP 2006 提高组 第二题

分析

该题情况比较简单,一个附件只从属于一个主件,一个主件最多只有两个附件。我们还是可以用01背包来考虑。先根据主件做01背包。然后只有五种情况(一个都不选择,只选择主件,选择主件和附件1,选择主件和附件2,选择主件和附件1,2),然后选择五种情况中的最大值。但是一定要注意必须先满足在背包的容量范围之内。对于存储的问题,我们可以用c[i][0]表示主件,c[i][1]表示第一个附件,c[i][2]表示第二个附件。考虑的时候先考虑主件,在考虑附件,不单独考虑附件。

#include <bits/stdc++.h>
using namespace std;

int n,m;
int v[32100],w[32100],fjw[32100][3],fjv[32100][3];//v为主件的价值,w为主件的重量,fjw为附件的重量,fjv为附件的价值
int dp[33300];
int main() {
	cin>>n>>m;
	for (int i=1;i<=m;i++){
		int a,b,c;
		cin>>a>>b>>c;
		if (c==0){
			v[i]=a*b;
			w[i]=a;
		}else {
			fjw[c][0]++;
			fjw[c][fjw[c][0]]=a;
			fjv[c][fjw[c][0]]=a*b;
		}
	}
	
	for (int i=1;i<=m;i++){
		for (int j=n;j>=w[i];j--){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
			//情况1只要主件
			if (j>=w[i]+fjw[i][1])dp[j]=max(dp[j],dp[j-w[i]-fjw[i][1]]+v[i]+fjv[i][1]);
			//情况2只要主件和附件1
			if (j>=w[i]+fjw[i][2])dp[j]=max(dp[j],dp[j-w[i]-fjw[i][2]]+v[i]+fjv[i][2]);
			//情况2只要主件和附件2
			if (j>=w[i]+fjw[i][1]+fjw[i][2])dp[j]=max(dp[j],dp[j-w[i]-fjw[i][1]-fjw[i][2]]+v[i]+fjv[i][1]+fjv[i][2]);
			//情况3都要
		}
	}
	cout<<dp[n];
	return 0;
}

背包方案数

在01背包的前提下,求解得到最大数的方案有多少种
这个类似于一个递推,其实可以直接列出以下状态转移方程
f [ j ] + = f [ j − v ] f[j] += f[j - v] f[j]+=f[jv]
只不过这里要注意一下,需要对发f[0]附上初值

例题15——方案数

Description
有一个容量为m(m<=20000)的背包,现在有n(n<=2000)个物品,体积为v[i],现要你装入一些物品,使得在体积剩余体积最小,最小体积是多少,以及在最小体积的情况下,有多少种方案。

Format
Input
第一行输入两个正整数m,n分别表示背包体积和物品数量。

第二行n个正整数v[i]分别表示物品体积。

Output
第一行一个整数表示最小剩余体积。

第二行一个正整数表示方案数。

Samples
输入数据 1

99 8
3 5 8 13 21 34 55 89

输出数据 1

2
6

Limitation
对于100%的数据,1<=m<=20000,1<=n<=2000。

分析

这道题其实就是正常的01背包加上一个求方案数,代码如下

#include<bits/stdc++.h>
using namespace std;

int n,m;
long long f[21000],dp[21000],v[2100];
int main() {
    cin>>m>>n;
    for (int i=1;i<=n;i++){
    	cin>>v[i];
	}
	for (int i=1;i<=n;i++){
		for (int j=m;j>=v[i];j--){
			dp[j]=max(dp[j],dp[j-v[i]]+v[i]);
		}
	}
	long long ans=dp[m];
	cout<<m-ans<<endl;
    f[0]=1;
    for (int i=1;i<=n;i++) {
        for (int j=ans;j>=v[i];j--) 
            f[j]+=f[j-v[i]];
    }
    cout<<f[ans];
    return 0;
}

01背包问题输出方案

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

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

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

输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。

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

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

输出格式
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1…N。

数据范围
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<vi,wi≤1000 0<vi,wi1000
输入样例

4 5
1 2
2 4
3 4
4 6

输出样例

1 4

这道题的思路其实不难,其实就是如果我们找到了正确答案,那么在从结尾往前推的的时候,那个dp转移方程一定是成立的,而且我们想要字典序最小,也应该从后往前进行枚举,所以我们很容易得到以下代码

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int v[N],w[N],f[N][N]; 

int main() {
    cin>>n>>m;
    for (int i=1;i<=n;i++){
    	cin>>v[i]>>w[i];
	}
    for (int i=n;i>=1;i--){
    	for (int j=0;j<=m;j++) {
            f[i][j]=f[i+1][j];
            if (j>=v[i])f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
        }
	}
	//注:此时的最大值为f[1][m] 
    cout<<f[1][m]<<endl;
    int j = m;
    for (int i=1;i<=n;i++){
    	if (j>=v[i]&&f[i][j]==f[i+1][j-v[i]]+w[i]) {
            cout<<i<<' ';
            j-=v[i];
        }
	}
    return 0;
}

背包问题总结

背包问题是线性DP的一个衍生,同时也是比较复杂的动态规划问题的一个简单雏形。动态规划是信息学竞赛的一个重难点,要想学好动态规划,先打好基础是非常有必要的。同样的,背包问题远不止这九类,还有很多类型的比较复杂的背包问题,这里先不做累述了。如果遇到比较复杂的背包问题,或者没有讲解过的背包问题,先看看能否转换前面我们学习过的简单背包问题来处理。一定要仔细分析问题,想出解决该类问题的状态状态转移方程(当前状态怎么由前一状态到达)

文章目录

  • 前言
    • 动态规划到底是啥?
  • 线性dp
    • 最长上升子序列
      • 子集和子序列和子串的区别
      • 内容分析
    • 最大上升子序列
    • 例题1——[NOIP2004 提高组] 合唱队形
      • 分析
    • 最长公共子序列
    • 最长公共子串
  • 平面dp
    • 例题2——[NOIP2000 提高组] 方格取数
      • 分析
    • 例题3——[NOIP2008 提高组] 传纸条
      • 分析
    • 例题4——最大加权矩形
      • 分析
        • 补充:最大字段和
        • 最大子矩阵和和最大字段和的关联
    • 例题5——最大全1子矩阵
      • 分析
    • 例题6——穿衣服
      • 分析
  • 背包问题
    • 什么是背包问题
    • 背包的分类
    • 01背包
      • 思路一
      • 思路二
      • 思路三(重头戏)
        • 常规代码
        • 空间优化
    • 例题7——[NOIP2001 普及组] 装箱问题
      • 分析
    • 例题8——最大约数和
      • 分析
    • 多重背包
        • 位置1
        • 位置2
      • 二进制优化
      • 完整代码
    • 例题9——[NOIP1996 提高组] 砝码称重
      • 分析
    • 完全背包
      • 优化
        • 优化办法一
        • 优化办法二(重点)(敲黑板)
    • 例题10——疯狂的采药
      • 分析
    • 混合背包
    • 多维背包
    • 例题11——「一本通入门 2.9.2」 潜水员
      • 分析
    • 分组背包
    • 例题12——旅行者
      • 分析
    • 泛化背包
    • 例题13——精力的分配
      • 分析
    • 有依赖的背包
    • 例题14——[NOIP2006 提高组] 金明的预算方案
      • 分析
    • 背包方案数
    • 例题15——方案数
      • 分析
    • 01背包问题输出方案
    • 背包问题总结
  • 区间dp
    • 区间型动态规划的特点:
    • 例题16——一本通改编——衣服合并
      • 分析
    • 小结1
    • 例题17——1275:【例9.19】乘积最大
      • 分析
    • 小结2
    • 例题18——[IOI2000] 回文字串
      • 分析
    • 环形区间dp
    • 例题19——[NOI1995] 石子合并
      • 分析
    • 例题20——[NOIP2006 提高组] 能量项链
      • 分析
    • 例题21——[NOIP2001 提高组] 数的划分
      • 分析
    • 例题22——放苹果
      • 分析
  • 总结

区间dp

前面我们学习的线性dp以及常见的背包问题,都和递推有很大关系,主要考虑当前状态可以由前面某种状态转移过来,前面的一定会比后面的先选择。一般状态转移都是从前往后或者从后往前。而很多时候选择并没有先后顺序,选择后面的时候并不一定要选前面的,比如区间型动态规划。区间型动态规划套路也比较固定,一般是从小到大,从小区间到大区间,他一般以区间长度为阶段。解决办法也比较简单,一般都是先枚举区间长度,再枚举左端点,确定区间右端点后进行状态转移。

区间型动态规划是线性动态规划的拓展,他在分阶段划分问题时,与阶段中元素出现的顺序和由前一阶段的那些元素合并而来有很大关系。

区间型动态规划的特点:

合并/拆分:如果是合并,那么就是将两个或多个部分进行整合,如果为拆分,就是把一个问题分解成两个或者多个部分。
特征:能将问题分解成为两两合并的形式
求解:对整个问题设最优值,枚举合并点,将问题分解成左右两个部分,最后合并左右两个部分的最优值得到原问题的最优值,有点类似于分治算法的解题思想。

这种类型没有什么固定的套路,或者代码之类的,所以我们只能从例题从去感受

例题16——一本通改编——衣服合并

题目描述
又是wjq~~
wjq是520女寝的一位少女,在她的床上摆了一排衣服,例如黑丝,jk,短裙等,总计有 N N N 件衣服,每件衣服有一个遮挡程度,现要将衣服有次序地合并成一堆,规定每次只能选相邻的 2 2 2 件合并成新的一堆,并将新的一堆的遮挡程度,记为该次合并的得分。

熟悉她的人知道,她喜欢性感,所以她跑过来问你,请你设计一个算法,使 N N N 件衣服合并成 1 1 1 堆的最小遮挡程度,如果你能完成,她会给你一个惊喜~~

输入格式

数据的第 1 1 1 行是正整数 N N N,表示有 N N N 件衣服。

2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i件衣服的遮挡程度。

输出格式

输出共 2 2 2 行,第 1 1 1 行为最小遮挡程度。

样例 #1

样例输入 #1

4
4 5 9 4

样例输出 #1

43

提示

1 ≤ N ≤ 100 1\leq N\leq 100 1N100 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0ai20

分析

该题和前面做过的一道合并果子的题类似,但是不一样的地方在于每次只能合并相邻两堆,如果我们贪心的每次选择相邻的最小的两堆合并,那么答案明显是错误的,因为前面合并顺序的不同会影响后面每堆的数量,所以我们要考虑动态规划。
我们思考问题的时候不能笼统,需要一步一步来,这和动态规划中的分阶段决策的思想是一致的。这里的每一阶段就是每一次合并, n n n件衣服需要合并 n − 1 n-1 n1次,那么一共就有 n − 1 n-1 n1个阶段。
对于合并之后的区间 [ l , r ] [l,r] [l,r],我们先思考最后一次合并,肯定是合并区间 [ l , k ] [l,k] [l,k] [ k + 1 , r ] [k+1,r] [k+1,r],而最优解中的点 k k k可能是 [ l , r ] [l,r] [l,r]中的任意一个位置,所以我们需要枚举 k k k,这样一个完整的区间就被我们划分成了两个小区间,那么对于划分成的两个小区间 [ l , k ] [l,k] [l,k] [ k + 1 , r ] [k+1,r] [k+1,r],我们还需要求出合并的最小代价,怎么求呢?还是一样,把这个区间划分成 [ l , k ] [l,k] [l,k] [ k + 1 , r ] [k+1,r] [k+1,r]两个区间,只要这两个小区间的答案我们知道了,那么 [ l , r ] [l,r] [l,r]的最小代价我们也就知道了。
先从小到大枚举阶段,也就是区间长度l,当区间长度为1的时候,区间内只有一个点,答案就是该点的值。当确定区间长度后,再枚举区间左端点i,这样就能算出右端点j=i+l-1,表示要求出合并区间[i,i+l-1]的最优解。最后枚举区间断点k(左区间的右端点),从而得到状态转移方程为:
d p [ i ] [ j ] = m i n ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + x ) dp[i][j]=min(dp[i][k]+dp[k+1][j]+x) dp[i][j]=min(dp[i][k]+dp[k+1][j]+x)
其中x为最后一次合并的代价,对于区间[l,r],无论怎么合并,最后合并的代价都是区间[l,r]之和,为了方便,我们维护一个前缀和。

#include<bits/stdc++.h>
using namespace std;

int a[110],sum[110];
int dp[110][110];
int main(){
	int n;
	cin>>n;
	for (int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];//维护前缀和
	} 
	//dp[i][j]表示区间i,j所合并的最小值 
	memset(dp,0x7f7f7f,sizeof(dp));//求最小值,所以附成极大值 
	for (int i=1;i<=n;i++){
		dp[i][i]=0; 
	}
	for (int k=1;k<=n;k++){//枚举长度 
		for (int l=1;l+k-1<=n;l++){//枚举左端点 
			int r=l+k-1;//右端点
			for (int i=l;i<r;i++){//断点,即左区间的右端点 
				dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1]);
			} 
		}
	}
	cout<<dp[1][n]; 
	return 0;
}

小结1

在做区间型动态规划的时候,一定要注意状态转移的过程,一旦转移方向错了,那么写出来的状态转移方程虽然是对的,但是结果肯定是错误的,转移过程中用到的数组值都必须是已知的。在思考问题的时候着重思考每一阶段,即每一步,每一次操作,有的时候也可以先用记忆化搜索的方式思考,然后用动态规划的写法来实现。但是写的时候还是建议用循环实现,因为有的时候一些比较难的动态规划用记忆化搜索的方式不是很容易实现,或者冗余相对比较大。 实际写代码过程中一定要注意初始值以及边界问题,以免造成不必要的过失

例题17——1275:【例9.19】乘积最大

题目描述

今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰 90 周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友 XZ 也有幸得以参加。活动中,主持人给所有参加活动的选手出了这样一道题目:

设有一个长度为 N N N 的数字串,要求选手使用 K K K 个乘号将它分成 K + 1 K+1 K+1 个部分,找出一种分法,使得这 K + 1 K+1 K+1 个部分的乘积能够为最大。

同时,为了帮助选手能够正确理解题意,主持人还举了如下的一个例子:

有一个数字串: 312 312 312,当 N = 3 , K = 1 N=3,K=1 N=3,K=1 时会有以下两种分法:

  1. 3 × 12 = 36 3 \times 12=36 3×12=36
  2. 31 × 2 = 62 31 \times 2=62 31×2=62

这时,符合题目要求的结果是: 31 × 2 = 62 31 \times 2 = 62 31×2=62

现在,请你帮助你的好朋友 XZ 设计一个程序,求得正确的答案。

输入格式

程序的输入共有两行:

第一行共有 2 2 2 个自然数 N , K N,K N,K

第二行是一个长度为 N N N 的数字串。

输出格式

结果显示在屏幕上,相对于输入,应输出所求得的最大乘积(一个自然数)。

样例 #1

样例输入 #1

4 2
1231

样例输出 #1

62

提示

数据范围与约定
对于所有测试数据, 6 ≤ N ≤ 10 , 1 ≤ K ≤ 6 6≤N≤10,1≤K≤6 6N10,1K6

分析

该题和前一道题最大的区别在于前面的题是区间合并,而该题相当于是区间拆分,用乘号把一个完整的数分割成多个独立的数。区间拆分型动态规划和区间合并型动态规划的做法有相似的地方,但是也有不一样的地方。我们以放置一个乘号作为阶段,最开始是一个完整的区间,放置一个乘号后的最大值我们可以求出来,只需要枚举乘号的位置即可。接下来我们分别再考虑两个区间,就会发现有点问题,两个区间的乘号之和应该等于总的乘号,但是左右两边各有多少个乘号是未知的,又要枚举,就很麻烦,需要改变一下思路。

对于一个数,当他把所有的乘号都添加好之后,这些乘号之间是有先后顺序的,我们依次把乘号放进去,即每次放进去的都是最后一个乘号。那么,当我们枚举最后一个乘号的位置k时,说明前面区间[1,k-1]一共放了j-1个乘号,后面是一个整体。
我们设dp[i][j]表示前i个数中放置j个乘号的最大值,接下来我们枚举最后一个乘号的位置。那么就有
d p [ i ] [ j ] = m a x ( d p [ k ] [ j − 1 ] ∗ s u m [ k + 1 ] [ n ] , d p [ i ] [ j ] ) dp[i][j]=max(dp[k][j-1]*sum[k+1][n],dp[i][j]) dp[i][j]=max(dp[k][j1]sum[k+1][n],dp[i][j])
要保证我们的状态转移方程中的dp的结果都是已知的,我们应该先枚举前i个数,再枚举放置j个乘号,这样就没有问题。
只不过这里要注意,初始值为前 i i i个数中放0个乘号的最大值,即 d p [ i ] [ 0 ] = s u m [ i ] dp[i][0]=sum[i] dp[i][0]=sum[i]

#include<bits/stdc++.h>
using namespace std;

int a[50],num[50][50];//num[i][j]表示区间i,j的数字大小 
int dp[50][50];
int main(){
	long long n,k,m;
	cin>>n>>k>>m;
	//分段 
	int tmp=n;
	while (m){
		a[tmp]=m%10;
		m/=10;
		tmp--;	
	} 
	for (int i=1;i<=n;i++){
		for (int j=i;j<=n;j++){
			num[i][j]=num[i][j-1]*10+a[j];
		}
	}
	for (int i=1;i<=n;i++){
		dp[i][0]=num[1][i];
	}
	for (int i=1;i<=n;i++){//数字的长度,即前i个数 
		for (int j=1;j<=k;j++){//乘号的个数 
			for (int m=j-1;m<i;m++){//乘号的位置
				dp[i][j]=max(dp[i][j],dp[m][j-1]*num[m+1][i]);
			}
		}
	}
	cout<<dp[n][k];
	return 0;
}

小结2

对于拆分型区间dp的做法大部分都和乘积最大的做法差不多,考虑每一次拆分。但是和前面合并型区间dp的思考方式有一点区别,我们并不是考虑最后一次拆分,然后前面有k个乘号,那么后面区间就有j-k-1个乘号,然后两这相乘的最大值,如果是这样,那么数组就需要dp[i][j][k]来表示,虽然这样也可以做,但是效率上会低一些,而且消耗的空间也比较大。我们同样是考虑最后一次拆分,但是我们这里让乘号是有序的,因为乘号放置的位置一样,顺序不同不影响答案,我们的最后一次拆分就是最后一个乘号的位置,这样我们就是一个前缀型dp数组,就不需要区间型的数组来记录。

例题18——[IOI2000] 回文字串

题目背景

IOI2000 第一题

题目描述

回文词是一种对称的字符串。任意给定一个字符串,通过插入若干字符,都可以变成回文词。此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数。

比如 Ab3bd \verb!Ab3bd! Ab3bd 插入 2 2 2 个字符后可以变成回文词 dAb3bAd \verb!dAb3bAd! dAb3bAd Adb3bdA \verb!Adb3bdA! Adb3bdA,但是插入少于 2 2 2 个的字符无法变成回文词。

注意:此问题区分大小写。

输入格式

输入共一行,一个字符串。

输出格式

有且只有一个整数,即最少插入字符数。

样例 #1

样例输入 #1

Ab3bd

样例输出 #1

2

提示

数据范围及约定

记字符串长度为 l l l

对于全部数据, 0 < l ≤ 1000 0<l\le 1000 0<l1000

分析

该题看上去和DP毫无关系,但是仔细分析后发现就是一个简单的区间DP的题。
对于一个字符串,如果他是回文字符串,那么首尾是相等的。如果某个字符串s[1]==s[n],那么求这个字符串需要经过多少次转换,相当于就是求字符串s[2…n-2]的转换次数,因为首尾已经相等了,不需要再去转换。
但是如果s[1]!=s[n],那么我们可以在最前面添加一个s[n]或者最后添加一个s[1]右边长首尾相等,如果在开头添加,那么接下来就是求s[1,n-1]的转换次数,如果在末尾添加,那么就下来求[2,n]的转换次数,同时,当前增加一次转换次数。
状态:dp[i][j]表示区间[i,j]变为回文串的最少操作数。
状态转移:如果s[i]==s[j],那么首尾两个字符已经回文了,我们只需要考虑[i+1,j-1],并且操作次数不变;如果s[i]!=s[j],那么要么在前面添加,要么在末尾添加尾或者首字符,又构成首尾相等的字符串,操作次数+1,即
d p [ i ] [ j ] = m i n ( d p [ i + 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + 1 dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1 dp[i][j]=min(dp[i+1][j],dp[i][j1])+1
要求长度为l的字符串操作次数,就需要知道长度为l-1的字符串的操作次数,所以该题仍然是一道区间dp的题,以字符串长度作为阶段进行转移。初始时字符串长度为1,自身就是回文串。

#include<bits/stdc++.h>
using namespace std;

char a[3000];
int dp[2100][2100];
int main(){
	cin>>a+1;
	int len=strlen(a+1);//这样处理可以使字符串从1开始存储
	
	for (int i=1;i<=len;i++){//长度 
		for (int j=1;j+i-1<=len;j++){//起点
			int r=j+i-1; 
			if (a[j]==a[r])dp[j][r]=dp[j+1][r-1];
			else dp[j][r]=min(dp[j+1][r],dp[j][r-1])+1;
		}
	}
	cout<<dp[1][len]; 
	return 0;
}

环形区间dp

前面我们主要是讲解的链式的区间dp,但是我们经常会遇见一个环的dp问题,那么我们这时候应该怎么办呢,我们还是以一个生活例子来引入
又是wjq~~
wjq又要开始合并衣服了,最近的wjq中了邪,喜欢圆圆的东西,所以这一次她把衣服放成了一个圆圈,总计有 N 件衣服,每件衣服有一个遮挡程度,现要将衣服有次序地合并成一堆,规定每次只能选相邻的 2 件合并成新的一堆,并将新的一堆的遮挡程度,记为该次合并的得分。
首先我们不难想到,每一件衣服都有可能是起点,所以我们现在就有以下几种情况:

假设现在圆圈里有“黑丝”,“白丝”,“泳装”,“jk”
那么我们就有:
“黑丝”,“白丝”,“泳装”,“jk”
“白丝”,“泳装”,“jk”,“黑丝”
“泳装”,“jk”,“黑丝”,“白丝”
“泳装”,“jk”,“黑丝”,“白丝”
总计四种情况

如果我们去找起点的话,太过繁琐了,那么我们来想想怎么优化。不难发现,当我们求解“黑丝”,“白丝”,“泳装”,“jk”时,“白丝”,“泳装”,“jk”,已经有了答案,那么我们在计算“白丝”,“泳装”,“jk”,“黑丝”的时候,就不用再次计算“白丝”,“泳装”,“jk”了,所以我们这里可以将整个数组copy一遍,放到后面,这样我们就可以避免重复计算了
这样无论哪个点为起点,再向后面枚举3个点都是一个完整的区间,即区间[i,i+n-1]为一个完整的区间。环变链后再做一次和前面一样的dp,注意下范围和边界,最后因为所有的长度为n的区间都有可能是答案,所以答案在min(dp[i,i+n-1])中。
要注意i要枚举到n的后面,因为后面的dp会用到这些值
这一招叫做化环为链,长度翻倍,是区间dp问题中常见的处理手段


例题19——[NOI1995] 石子合并

题目描述

在一个圆形操场的四周摆放 N N N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 2 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 N N N 堆石子合并成 1 1 1 堆的最小得分和最大得分。

输入格式

数据的第 1 1 1 行是正整数 N N N,表示有 N N N 堆石子。

2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i 堆石子的个数。

输出格式

输出共 2 2 2 行,第 1 1 1 行为最小得分,第 2 2 2 行为最大得分。

样例 #1

样例输入 #1

4
4 5 9 4

样例输出 #1

43
54

提示

1 ≤ N ≤ 100 1\leq N\leq 100 1N100 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0ai20

分析

这道题其实和上面的引入大同小异,可以直接套用

#include <iostream>
using namespace std;
const int N = 300;
const int INF = 10e9;
int n, dp[N][N], dp2[N][N];
int sum[N], s[N];

int main(){
	cin >> n;
	for (int i = 1; i <= n; i++) {
 	   cin >> s[i];
 	   s[i + n] = s[i];
	}
	for(int i = 1; i <= n * 2; i++){
		sum[i] = s[i] + sum[i - 1];
  	}
    for(int len = 1; len < n; len++){
   		for (int i = 1; i <= (n * 2 - len); i++){
      		int j = i + len;
        	dp[i][j] = INF;
        	for(int k = i; k < j; k++){
        		dp[i][j] = min (dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
       			dp2[i][j] = max (dp2[i][j], dp2[i][k] + dp2[k + 1][j] + sum[j] - sum[i - 1]);
       		}
        }
    }
    int ans = INF, ans2 = 0;
    for(int i = 1; i <= n; i++){
    	ans = min(ans, dp[i][i + n - 1]);
    	ans2 = max(ans2,dp2[i][i + n - 1]);
	}
    cout << ans << endl << ans2;
}

例题20——[NOIP2006 提高组] 能量项链

题目描述

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 N N N 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 m m m,尾标记为 r r r,后一颗能量珠的头标记为 r r r,尾标记为 n n n,则聚合后释放的能量为 m × r × n m \times r \times n m×r×n(Mars 单位),新产生的珠子的头标记为 m m m,尾标记为 n n n

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N = 4 N=4 N=4 4 4 4 颗珠子的头标记与尾标记依次为 ( 2 , 3 ) ( 3 , 5 ) ( 5 , 10 ) ( 10 , 2 ) (2,3)(3,5)(5,10)(10,2) (2,3)(3,5)(5,10)(10,2)。我们用记号 ⊕ \oplus 表示两颗珠子的聚合操作, ( j ⊕ k ) (j \oplus k) (jk) 表示第 j , k j,k j,k 两颗珠子聚合后所释放的能量。则第 4 4 4 1 1 1 两颗珠子聚合后释放的能量为:

( 4 ⊕ 1 ) = 10 × 2 × 3 = 60 (4 \oplus 1)=10 \times 2 \times 3=60 (41)=10×2×3=60

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:

( ( ( 4 ⊕ 1 ) ⊕ 2 ) ⊕ 3 ) = 10 × 2 × 3 + 10 × 3 × 5 + 10 × 5 × 10 = 710 (((4 \oplus 1) \oplus 2) \oplus 3)=10 \times 2 \times 3+10 \times 3 \times 5+10 \times 5 \times 10=710 (((41)2)3)=10×2×3+10×3×5+10×5×10=710

输入格式

第一行是一个正整数 N N N 4 ≤ N ≤ 100 4 \le N \le 100 4N100),表示项链上珠子的个数。第二行是 N N N 个用空格隔开的正整数,所有的数均不超过 1000 1000 1000。第 i i i 个数为第 i i i 颗珠子的头标记( 1 ≤ i ≤ N 1 \le i \le N 1iN),当 i < N i<N i<N 时,第 i i i 颗珠子的尾标记应该等于第 i + 1 i+1 i+1 颗珠子的头标记。第 N N N 颗珠子的尾标记应该等于第 1 1 1 颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式

一个正整数 E E E E ≤ 2.1 × 1 0 9 E\le 2.1 \times 10^9 E2.1×109),为一个最优聚合顺序所释放的总能量。

样例 #1

样例输入 #1

4
2 3 5 10

样例输出 #1

710

提示

NOIP 2006 提高组 第一题

分析

该题和合并石子(环)在处理方式上一样,都是通过把小区间合并成大区间,枚举合并两个区间的中间节点,计算出整个区间合并的最大能量因为也是一个环,所以我们还是先把环拆链,长度加倍,注意枚举顺序,因为是把小区间合并成大区间,所以先枚举区间长度,再枚举区间左端点,计算出区间右端点,最后再枚举最后一次合并的点,也就是断点,再计算答案。还是和前面一样,注意边界问题,区间长度不能大于n,也不能小于1.左右端点的范围不能小于或者大于真实情况。

#include <bits/stdc++.h>
using namespace std;

const int MAX_N = 301;

int dp[MAX_N][MAX_N];
int a[MAX_N];
int maxn=INT_MIN;

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        a[n+i]=a[i];
    }
    for (int len = 1; len <= 2*n; ++len) {
        for (int i = 1; i + len - 1 <= 2*n; ++i) {
            int j = i + len - 1;
            for (int k = i+1; k < j; ++k) {
                dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + a[i]*a[k]*a[j]);
            }
        }
    }
	for (int i=1;i<=n;i++){
		maxn=max(dp[i][i+n],maxn);
	}
    cout << maxn ;

    return 0;
}

例题21——[NOIP2001 提高组] 数的划分

题目描述

将整数 n n n 分成 k k k 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。

例如: n = 7 n=7 n=7 k = 3 k=3 k=3,下面三种分法被认为是相同的。

1 , 1 , 5 1,1,5 1,1,5;
1 , 5 , 1 1,5,1 1,5,1;
5 , 1 , 1 5,1,1 5,1,1.

问有多少种不同的分法。

输入格式

n , k n,k n,k 6 < n ≤ 200 6<n \le 200 6<n200 2 ≤ k ≤ 6 2 \le k \le 6 2k6

输出格式

1 1 1 个整数,即不同的分法。

样例 #1

样例输入 #1

7 3

样例输出 #1

4

提示

四种分法为:
1 , 1 , 5 1,1,5 1,1,5;
1 , 2 , 4 1,2,4 1,2,4;
1 , 3 , 3 1,3,3 1,3,3;
2 , 2 , 3 2,2,3 2,2,3.

【题目来源】

NOIP 2001 提高组第二题

分析

首先因为这道题的数据范围比较小,所以我们可以直接打dfs,但是我们想象,如果N的范围比较大呢,自然,我们还是用动态规划
该题是一道相对比较复杂的区间DP,看上去和前面的区间DP类似,以每次划分作为阶段,枚举最后一次划分的位置k,那么求出所有dp[k][j-1]之和即为答案。这种解决办法是错误的,因为存在重复方案,为了避免重复,最后一次分的数量不能比前面少,但是这种做法没有考虑这个问题。
该题正确的做法是考虑分解方案中是否有一份中只有1个方案和没有一个的方案。如果有一个,那么答案相当于是dp[i-1][j-1],如果没有,那么每一份都减少一个1的方案也是合理的,即 d p [ i − j ] [ j ] dp[i-j][j] dp[ij][j]

#include<bits/stdc++.h>
using namespace std;

int dp[2100][2100];
int main(){
	int n,k;
	cin>>n>>k;
	for (int i=1;i<=n;i++){
		dp[i][0]=1;
		dp[i][1]=1;
	}
	for (int x=2;x<=k;x++) {
	    dp[1][x]=0;
	    dp[0][x]=0;
	}
	for (int i=2;i<=n;i++){//i个数 
		for (int x=2;x<=k;x++){//每一个位置 
			if (i<=x)dp[i][x]=dp[i-1][x-1];
			else dp[i][x]=(dp[i-1][x-1]+dp[i-x][x]);
		}
	}
	cout<<dp[n][k];
	return 0;
}

例题22——放苹果

题目描述

m m m 个同样的苹果放在 n n n 个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法。( 5 , 1 , 1 5,1,1 5,1,1 1 , 1 , 5 1,1,5 1,1,5 是同一种方法)

输入格式

第一行是测试数据的数目 t t t,以下每行均包括二个整数 m m m n n n,以空格分开。

输出格式

对输入的每组数据 m m m n n n,用一行输出相应的结果。

样例 #1

样例输入 #1

1
7 3

样例输出 #1

8

样例 #2

样例输入 #2

3
3 2
4 3
2 7

样例输出 #2

2
4
2

提示

对于所有数据,保证: 1 ≤ m , n ≤ 10 1\leq m,n\leq 10 1m,n10 0 ≤ t ≤ 20 0 \leq t \leq 20 0t20

分析

同样的,这道题我们还是考虑动态规划。
该题和上一道题的解题思路一样,如果上一道题理解了,那么该题很容易想出状态转移方程。对于每一个盘子,最少放0个,如果存在放0个的盘子,答案为dp[i][j-1],如果不存在放0个的盘子,从每个盘子里面都拿掉一个,答案为 d p [ i − j ] [ j ] dp[i-j][j] dp[ij][j]
该题的难点不仅仅在于状态转移方程,还有初始状态。首先,i<j,上一道题为0,所以不用处理,但是该题当i<j时, d p [ i ] [ j ] = d p [ i ] [ i ] dp[i][j]=dp[i][i] dp[i][j]=dp[i][i],如果不单独处理那么状态转移中的 d p [ i − j ] [ j ] dp[i-j][j] dp[ij][j]就不存在,因为i-j为负数。
状态转移方程:
d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + d p [ i − j ] [ j ] ; dp[i][j]=dp[i][j-1]+dp[i-j][j]; dp[i][j]=dp[i][j1]+dp[ij][j];当i,j都从1开始枚举时,需要用到 d p [ i ] [ 0 ] dp[i][0] dp[i][0],这到底算一算一种方案,我们也不太好确定,所以j最好从2开始枚举,这样我们需要初始化 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的值,很明显 d p [ i ] [ 1 ] = 1 dp[i][1]=1 dp[i][1]=1
还需要用到 d p [ i − j ] [ j ] dp[i-j][j] dp[ij][j]的值,我们先判断i,j的大小,此时i>=j,我们会用到 d p [ 0 ] [ j ] dp[0][j] dp[0][j]的值,我们还需要初始化 d p [ 0 ] [ j ] dp[0][j] dp[0][j]的值,很明显也为1。

#include<bits/stdc++.h>
using namespace std;

int dp[2100][2100];
int main(){
	int n,k,t;
	cin>>t;
	while (t--){
		memset(dp,0,sizeof(dp));
		cin>>n>>k;
		for (int i=1;i<=n;i++){
			dp[i][0]=1;
			dp[i][1]=1;
		}
		for (int i=1;i<=k;i++){
			dp[0][i]=1;
			dp[1][i]=1;
		}
		for (int i=2;i<=n;i++){//i个数 
			for (int x=2;x<=k;x++){//每一个位置 
				if (i<x)dp[i][x]=dp[i][i];
				else dp[i][x]=(dp[i][x-1]+dp[i-x][x])%1000007;
			}
		}
		cout<<dp[n][k]<<endl;;
	}

	return 0;
}

总结

好了,到目前为止,普及组所要用到的动态规划问题我们基本上是讲完了,不知道大家有没有收获呢?如果有问题,欢迎到评论区留言,或者私信博主,如果喜欢博主的博客的话,请点一个赞,蟹蟹

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2321204.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为交换相关

端口模式 &#xff08;1&#xff09;access&#xff1a;只能属于单个VLAN&#xff0c;一般用于连接计算机端口 &#xff08;2&#xff09;trunk&#xff1a;端口允许多个VLAN通过&#xff0c;可以接收和发送多个VLAN报文&#xff0c;默认情况下只有管理VLAN不携带标签信息 &…

Chrome Performance 面板完全指南:从卡顿到丝滑的终极调试术

1.写在前面 前端性能调试是优化网页加载速度和运行效率的关键步骤&#xff0c;Chrome DevTools 的 Performance 面板 是核心工具; 2.Performance 面板使用步骤 ★ 基础 打开面板 在 Chrome 中按 F12 → 切换到 Performance 标签页。 开始录制 方式一&#xff1a;点击 ⚫️ 圆…

JDK 24:Java 24 中的新功能

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;历代文学&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编程&#xff0c;高并发设计&#xf…

ubuntu服务器server版安装,ssh远程连接xmanager管理,改ip网络连接。图文教程

ventoy启动服务器版iso镜像&#xff0c;注意看server名称&#xff0c;跟之前desktop版ubuntu不一样。没有gui界面。好&#xff0c;进入命令行界面。语言彻底没汉化了&#xff0c;选英文吧&#xff0c;别的更看不懂。 跟桌面版ubuntu类似&#xff0c;选择是否精简系统&#xff0…

python机器学习——新手入门学习笔记

一&#xff0c;概论 1.什么是机器学习 定义&#xff1a; 机器学习是从数据中自动分析获得模型&#xff0c;并利用模型对未知数据进行预测。 其实就是通过问题和数据&#xff0c;发现规律&#xff0c;并进行预测&#xff0c;与人脑相似。目的就是从历史数据当中获得规律&#x…

LabVIEW 与 PLC 通讯的常见方式

在工业自动化和数据采集系统中&#xff0c;PLC&#xff08;可编程逻辑控制器&#xff09; 广泛用于控制和监测各种设备&#xff0c;而 LabVIEW 作为强大的图形化编程工具&#xff0c;常用于上位机数据处理和可视化。为了实现 LabVIEW 与 PLC 的高效通讯&#xff0c;常见的方法包…

深度学习 Deep Learning 第9章 卷积网络 CNN

深度学习 Deep Learning 第9章 卷积网络 章节概述 本章深入探讨了卷积网络的原理、变体及其在深度学习中的应用。卷积网络通过卷积操作实现了参数共享和稀疏连接&#xff0c;显著提高了模型的效率和性能。本章首先介绍了卷积操作的基本形式及其在不同数据维度上的应用&#x…

Tekton系列之实践篇-从触发到完成的完整执行过程

以下介绍的是基于 Gitee 仓库 的 Tekton 工作流程 操作流程 定义task 克隆代码的task # task-clone.yaml apiVersion: tekton.dev/v1beta1 kind: Task metadata:name: git-clone spec:workspaces:- name: source # 工作目录params:- name: repo-url # 你的 Gitee 仓库地址…

【简单学习】Prompt Engineering 提示词工程

一、Prompt 1、Prompt 是什么&#xff1f; Prompt 是一种人为构造的输入序列&#xff0c;用于引导 GPT 模型根据先前输入的内容生成相关的输出。简单来说&#xff0c;就是你向模型提供的 “提示词”。 在 ChatGpt 中&#xff0c;我们可以通过设计不同的 prompt&#xff0c;让…

零基础入门网络爬虫第5天:Scrapy框架

4周 Srapy爬虫框架 不是一个简单的函数功能库&#xff0c;而是一个爬虫框架 安装&#xff1a;pip install scrapy 检测&#xff1a;scrapy -h Scrapy爬虫框架结构 爬虫框架 爬虫框架是实现爬虫功能的一个软件结构和功能组件集合爬虫框架是一个半成品&#xff0c;能够帮助…

C#设计模式快速回顾

知识点来源&#xff1a;人间自有韬哥在&#xff0c;豆包 目录 一、七大原则1. 单一职责原则 (Single Responsibility Principle)2. 开放封闭原则 (Open-Closed Principle)3. 里氏替换原则 (Liskov Substitution Principle)4. 接口隔离原则 (Interface Segregation Principle)5…

分页查询互动问题(用户端)

文章目录 概要整体架构流程技术细节小结 概要 需求分析以及接口设计 技术细节 1.Controller层 GetMapping("/page")ApiOperation("分页查询问题")public PageDTO<QuestionVO> queryQuestionPage(QuestionPageQuery query){return questionService…

【全队项目】智能学术海报生成系统PosterGenius(项目介绍)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f3c0;大模型实战训练营_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前…

【线程安全问题的原因和方法】【java形式】【图片详解】

在本章节中采用实例图片的方式&#xff0c;以一个学习者的姿态进行描述问题解决问题&#xff0c;更加清晰明了&#xff0c;以及过程中会发问的问题都会一一进行呈现 目录 线程安全演示线程不安全情况图片解释&#xff1a; 将上述代码进行修改【从并行转化成穿行的方式】不会出…

解决IDEA中maven找不到依赖项的问题

直接去官网找到对应的依赖项jar包&#xff0c;并且下载到本地&#xff0c;然后安装到本地厂库中。 Maven官网&#xff1a;https://mvnrepository.com/ 一、使用mvn install:install-file命令 Maven提供了install:install-file插件&#xff0c;用于手动将jar包安装到本地仓库…

pyside6的QGraphicsView体系,当鼠标位于不同的物体,显示不同的右键菜单

代码&#xff1a; # 设置样本图片的QGraphicsView模型 from PySide6.QtCore import Qt, QRectF, QObject from PySide6.QtGui import QPainter, QPen, QColor, QAction, QMouseEvent from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGra…

Python自动化测试 之 DrissionPage 的下载、安装、基本使用详解

Python自动化测试 之 DrissionPage 使用详解 &#x1f3e1;前言&#xff1a;一、☀️DrissionPage的基本概述二、 &#x1f5fa;️环境安装2.1 ✅️️运行环境2.2 ✅️️一键安装 三、&#x1f5fa;️快速入门3.1 页面类&#x1f6f0;️ChromiumPage&#x1f6eb; SessionPage&…

Java替换jar包中class文件

在更新java应用版本的运维工作中&#xff0c;由于一些原因&#xff0c;开发没办法给到完整的jar包&#xff0c;这个时候&#xff0c;就可以只将修改后的某个Java类的class文件替换掉原来iar包中的class文件&#xff0c;重新启动服务即可&#xff1a; 1、将jar包和将要替换的cl…

AI Tokenization

AI Tokenization 人工智能分词初步了解 类似现在这个&#xff0c;一格子 一格子&#xff0c;拼接出来的&#xff0c;一行或者一句&#xff0c;像不像&#xff0c;我们人类思考的时候组装出来的话&#xff0c;并用嘴说出来了呢。

关于大数据的基础知识(四)——大数据的意义与趋势

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于大数据的基础知识&#xff08;四&a…