目录
一、概况
二、背包
2.0闫式dp分析法
2.1 0-1背包
朴素解法
滚动数组
2.2 完全背包
朴素解法
优化降维
滚动数组
2.3完全背包和0-1背包的区别与联系
2.4多重背包问题
朴素解法
二进制枚举优化
贪心算法
单调队列优化
2.5分组背包问题
朴素算法
优化降维
二进制枚举优化
三、线性DP
3.1概述
3.2数字三角形
正序解法
倒序解法
二维优化为一维
记忆化搜索
3.3最长上升子序列
朴素解法
二分查找优化·
回溯路径
记忆化搜索
3.4 最长公共子序列
实验书模板
朴素解法
记忆化搜索递归解法
四、区间DP
4.1概述
4.2石子合并
朴素解法
记忆化搜索
4.3最大子段和
动态规划是一种算法设计技术,它可以有效地解决一些复杂的优化问题。动态规划的主要思想是将原问题分解成若干个子问题进行求解,并将子问题的解缓存起来,避免重复计算,从而达到减少计算量、提高计算效率的目的。
动态规划通常适用于具有以下两个特点的问题:
最优子结构性质:问题的最优解可以由其子问题的最优解推导出来。
重叠子问题性质:问题可以被分解为多个子问题,这些子问题的求解都会重复使用相同的计算过程。
动态规划算法的基本思路可以概括为以下三个步骤:
定义状态:将原问题分解成若干个子问题,定义子问题的解。
状态转移方程:定义子问题之间的递推关系。
边界条件:确定初始状态或边界状态的值。
闫式动态规划分析法(Yan's DP Analysis Method)是一种系统化的动态规划算法设计方法,由计算机科学家闫学灿于1993年提出。该方法将动态规划问题分解为子问题,并通过归纳法推导出状态转移方程,从而构造出整个问题的解。
具体来说,闫式dp分析法包含以下步骤:
定义状态:根据问题的特点,定义出状态变量,通常是一个或多个变量,代表问题中的某些量,如长度、位置、状态等。
定义状态转移方程:根据问题的特点,通过归纳法推导出状态转移方程,即如何从当前状态转移到下一个状态。在推导状态转移方程时,需要考虑状态的边界和状态转移的约束条件。
确定初始状态:根据问题的特点,确定初始状态,通常是问题的边界条件。
确定最终状态:根据问题的特点,确定最终状态,即问题的解。
计算解:根据状态转移方程,通过动态规划算法计算问题的解。
闫式dp分析法的优点是系统化、规范化,能够使动态规划算法的设计更加清晰、简单、易于实现。该方法常用于算法竞赛中,特别是动态规划类问题的解决。
有 N件物品和一个容量是 V的背包。每件物品只能使用一次。
第 i件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi用空格隔开,分别表示第 i件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 输入样例
4 5 1 2 2 4 3 4 4 5
输出样例:
8
#include
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
#include
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN]; // f[i][j]体积下前i个物品的最大价值
int main()
{
int n, m;
cin >> n >> m; // 输入物品个数和背包容量
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i]; // 输入第i个物品的体积和价值
// 进行01背包动态规划
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]); // 取当前状态和加上第i个物品的最大价值中的最大值
}
cout << f[m] << endl; // 输出背包容量为m时的最大价值
return 0;
}
完全背包问题是指有一个容量为V的背包,和N个体积为v[i],价值为w[i]的物品,每个物品可以选取无限次,问如何选择物品放入背包,使得背包中的物品总价值最大。
对于完全背包问题,与01背包问题的状态转移方程不同,其状态转移方程为:
f[i][j] = max(f[i-1][j], f[i][j-v[i]]+w[i])
有 N 种物品和一个容量是 V的背包,每种物品都有无限件可用。
第 i种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 输入样例
4 5 1 2 2 4 3 4 4 5
输出样例:
10
#include
using namespace std;
const int N = 1010;
int f[N][N]; // f[i][j]表示前i个物品,背包容量为j时的最大价值
int v[N], w[N]; // v[i]表示第i个物品的体积,w[i]表示第i个物品的价值
int main()
{
int n, m; // n为物品个数,m为背包容量
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> v[i] >> w[i];
}
// 动态规划过程
for(int i = 1; i <= n; i++) // 遍历每个物品
{
for(int j = 0; j <= m; j++) // 枚举背包容量
{
for(int k = 0; k * v[i] <= j; k++) // 枚举第i个物品的数量
{
f[i][j] = max(f[i][j], f[i-1][j-k*v[i]] + k*w[i]);
// f[i][j]表示前i个物品,背包容量为j时的最大价值
// f[i-1][j-k*v[i]]表示不选第i个物品,背包容量为j-k*v[i]时的最大价值
// k*w[i]表示选k个第i个物品的价值
// 取max就是考虑选或不选第i个物品,取最大值
}
}
}
cout << f[n][m] << endl; // 输出前n个物品,背包容量为m时的最大价值
return 0;
}
#include
using namespace std;
const int N = 1010;
int f[N][N]; // f[i][j]表示前i个物品,背包容量为j时的最大价值
int v[N], w[N]; // v[i]表示第i个物品的体积,w[i]表示第i个物品的价值
int main()
{
int n, m; // n为物品个数,m为背包容量
cin >> n >> m;
// 输入每个物品的体积和价值
for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
// 动态规划过程
for (int i = 1; i <= n; i++) { // 遍历每个物品
for (int j = 0; j <= m; j++) { // 枚举背包容量
f[i][j] = f[i - 1][j]; // 不选择第i个物品
if (j >= v[i]) { // 背包容量足够装下第i个物品
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); // 考虑选择第i个物品
}
}
}
// 输出前n个物品,背包容量为m时的最大价值
cout << f[n][m] << endl;
return 0;
}
#include
using namespace std;
const int N = 1010;
int f[N]; // f[i]表示背包容量为i时的最大价值
int v[N], w[N]; // v[i]表示第i个物品的体积,w[i]表示第i个物品的价值
int main()
{
int n, m; // n为物品个数,m为背包容量
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> v[i] >> w[i];
}
// 动态规划过程
for(int i = 1; i <= n; i++) // 遍历每个物品
{
for(int j = m; j >= v[i]; j--) // 枚举背包容量
{
f[j] = max(f[j], f[j - v[i]] + w[i]); // 转移方程
}
}
cout << f[m] << endl; // 输出背包容量为m时的最大价值
return 0;
}
完全背包和0-1背包是背包问题中的两种经典问题,它们的区别和联系如下:
区别:
物品数量限制不同:0-1背包中每个物品只能选择一次,而完全背包中每个物品可以选择无限次。
决策状态转移方程不同:在0-1背包中,对于每个物品,决策只能是放或不放,因此需要分别考虑物品放入和不放入背包两种情况,而在完全背包中,每个物品可以选择多次,因此只需要考虑每个物品放入背包的情况。
联系:
状态转移方程的形式相同:0-1背包和完全背包都可以使用动态规划来求解,它们的状态转移方程都可以写成f(i,j)=max(f(i-1,j),f(i-1,j-w[i])+v[i])的形式。
求解的思路相同:都是通过逐步遍历物品和背包容量,求解每个状态的最优解,最终得到整个问题的最优解。
综上所述,0-1背包和完全背包问题虽然存在一些区别,但它们的解决思路和算法思想都是相似的。
多重背包问题是背包问题的一种变形,与0-1背包问题和完全背包问题不同的是,每种物品有一个数量限制,不再是仅有一个或无限个可用。
具体来说,给定一个背包容量为C,n种不同的物品,其中第i种物品的体积为v[i],价值为w[i],每种物品的数量有一个上限c[i],现在从这些物品中选择一些装入背包,使得背包的容量不超过C,且所装物品的总价值最大。
多重背包问题可以通过类似于0-1背包问题和完全背包问题的动态规划算法来求解,但需要对状态转移方程进行修改。具体来说,可以将多重背包问题转化为若干个0-1背包问题,每种物品拆分为多个体积相等但价值不同的物品,数量上限即为该物品的可选数量。这样问题就可以被转化为0-1背包问题,采用类似的动态规划算法求解。
多重背包问题也可以使用优化的算法来解决,例如使用单调队列优化的动态规划算法或者基于贪心算法的解法。这些算法能够在保证正确性的同时,减少时间和空间复杂度。
有 N种物品和一个容量是 VV 的背包。
第 i种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入格式
第一行两个整数,N,V用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 输入样例
4 5 1 2 3 2 4 1 3 4 3 4 5 2
输出样例:
10
#include
using namespace std;
const int N = 1010;
int f[N][N]; // f[i][j]表示前i个物品,背包容量为j时的最大价值
int v[N], w[N], s[N]; // v[i]表示第i个物品的体积,w[i]表示第i个物品的价值
int main()
{
int n, m; // n为物品个数,m为背包容量
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++) // 枚举背包容量
{
for(int k = 0; k <= s[i] && k * v[i] <= j; k++) // 枚举第i个物品的数量
{
f[i][j] = max(f[i][j], f[i-1][j-k*v[i]] + k*w[i]);
// f[i][j]表示前i个物品,背包容量为j时的最大价值
// f[i-1][j-k*v[i]]表示不选第i个物品,背包容量为j-k*v[i]时的最大价值
// k*w[i]表示选k个第i个物品的价值
// 取max就是考虑选或不选第i个物品,取最大值
}
}
}
cout << f[n][m] << endl; // 输出前n个物品,背包容量为m时的最大价值
return 0;
}
#include
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; // 体积> n >> m;
int cnt = 0; //分组的组别
for(int i = 1;i <= n;i ++)
{
int a,b,s;
cin >> a >> b >> s;
int k = 1; // 组别里面的个数
while(k<=s)
{
cnt ++ ; //组别先增加
v[cnt] = a * k ; //整体体积
w[cnt] = b * k; // 整体价值
s -= k; // s要减小
k *= 2; // 组别里的个数增加
}
//剩余的一组
if(s>0)
{
cnt ++ ;
v[cnt] = a*s;
w[cnt] = b*s;
}
}
n = cnt ; //枚举次数正式由个数变成组别数
//01背包一维优化
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] << endl;
return 0;
}
可以通过将多个同种物品拆分成2的幂次个,将其转化为01背包问题。具体来说,将每个物品的数量拆分成若干个2的幂次方(如1,2,4,8),每个幂次对应一种物品,将其看作一个物品组,其中物品组的体积和价值分别为组内所有物品的体积和和价值和,然后使用01背包问题的一维优化来求解最大价值。
具体实现时,首先读入多重背包问题中每个物品的体积、价值和数量,然后将每个物品拆分成2的幂次个,将每个幂次看作一种物品组,将组内所有物品的体积和价值和分别作为该组的体积和价值,最后使用一维的01背包问题求解。
#include
#include
using namespace std;
const int N = 1010;
int f[N]; // f[i]表示背包容量为i时的最大价值
int v[N], w[N], s[N]; // v[i]表示第i个物品的体积,w[i]表示第i个物品的价值,s[i]表示第i个物品的数量
int main()
{
int n, m; // n为物品个数,m为背包容量
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> v[i] >> w[i] >> s[i];
}
// 贪心策略:将多重背包转化为01背包,将每个物品拆分成多个物品
// 将第i个物品拆分成s[i]个物品,每个物品的体积和价值分别为v[i]和w[i]
// 这样就将多重背包问题转化为了01背包问题
for(int i = 1; i <= n; i++) // 遍历每个物品
{
for(int k = 1; k <= s[i]; k *= 2) // 将第i个物品拆分成多个物品,每次将数量翻倍
{
for(int j = m; j >= k * v[i]; j--) // 枚举背包容量,倒序枚举,防止重复选取
{
f[j] = max(f[j], f[j-k*v[i]] + k*w[i]); // 01背包的动态转移方程
}
s[i] -= k; // 更新第i个物品的数量,减去已经处理的数量
}
if(s[i] > 0) // 处理剩余的物品
{
for(int j = m; j >= s[i] * v[i]; j--)
{
f[j] = max(f[j], f[j-s[i]*v[i]] + s[i]*w[i]);
}
}
}
cout << f[m] << endl; // 输出背包容量为m时的最大价值
return 0;
}
#include
using namespace std;
const int N = 1005;
const int M = 20005;
int v[N],w[N],s[N]; //体积,价值,数量
int dp[M],g[M], q[M],n,m;
int main(){
int i,j,k;
cin>>n>>m;
for(i = 1;i<=n;i++){
cin>>v[i]>>w[i]>>s[i];
}
for(i = 1;i<=n;i++){
//枚举v[i]的每个余数
memcpy(g,dp,sizeof g); //滚动数组优化
//g表示dp[i-1][],dp表示dp[i][];
for(j = 0;j s[i]) l++;
//除去比当前小的元素
while(l= g[q[l+1]] + (k-q[l+1])/v[i]*w[i]) r--;
q[++r]=k;
dp[k] = g[q[l+1]]+(k-q[l+1])/v[i]*w[i];
}
}
}
cout<
单调队列优化的多重背包问题是在多重背包问题的基础上,使用单调队列优化动态规划过程中的状态转移。多重背包问题是一个经典的动态规划问题,其中一个难点在于体积和价值的数量是有限的,需要考虑如何进行状态转移。
传统的多重背包问题使用的是类似01背包问题的动态规划思路,即对于每个物品,将其分解成若干个体积为1的物品,然后使用01背包的方式进行动态规划。但是,这种方法的时间复杂度较高,达到了O(NVM),其中N、V、M分别为物品数、背包容量和最大数量。
单调队列优化的多重背包问题是在此基础上使用单调队列对状态转移过程进行优化,从而将时间复杂度优化至O(NVlogM)。其主要思路是,将同一件物品拆分成若干个体积为v[i]的物品,然后根据v[i]的余数分成v[i]个子问题。对于每个子问题,使用单调队列维护状态转移过程中的最优解,从而优化状态转移的效率。
具体来说,对于每个物品i,先将dp数组中的值拷贝到g数组中,然后枚举v[i]的每个余数j,并对每个余数j使用单调队列维护状态转移过程中的最优解。由于同一个物品i会分解成v[i]个子问题,因此需要对每个子问题分别进行单调队列优化。
单调队列中存储的是dp[j]加上一定数量的物品i后的最大价值,队列头的元素始终是最优解。在进行状态转移时,将当前状态的价值更新为单调队列中的最大价值即可。
总之,单调队列优化的多重背包问题通过拆分同一件物品并使用单调队列维护状态转移过程中的最优解,将多重背包问题的时间复杂度优化至O(NVlogM),是一种较为高效的解法。
分组背包问题是背包问题的一种变种,它的物品被分成了若干组,每组中的物品最多只能选一个,且每组物品只能选一个或不选。求在总体积不超过背包容量的前提下,能获得的最大价值是多少。
具体来说,假设有n个物品,它们被分成了m组,第i组有si个物品,第j个物品的体积为vi,j,价值为wi,j。设f[i][v]表示前i组物品,背包容量为v时能获得的最大价值,则有状态转移方程:
f[i][v] = max(f[i-1][v-kvi,j]+kwi,j) (0<=k<=si,j)
其中vi,j和wi,j分别表示第i组第j个物品的体积和价值。
这个状态转移方程的意义是:在前i-1组物品中,背包容量为v-k*vi,j时的最大价值加上第i组中选取第j个物品k次的价值,可以得到在前i组物品中,背包容量为v时的最大价值
。
实际实现时,可以将每组物品当成一个单独的0-1背包问题,对每个物品都进行一次背包计算,最后累加每组物品所能贡献的最大价值即可
有 N组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V用空格隔开,分别表示物品组数和背包容量。
接下来有 NN 组数据:
- 每组数据第一行有一个整数 Si,表示第 i个物品组的物品数量;
- 每组数据接下来有 Si 行,每行有两个整数 vij,wij用空格隔开,分别表示第 i 个物品组的第 jj 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 0 输入样例
3 5 2 1 2 2 4 1 3 4 1 4 5
输出样例:
8
#include
using namespace std;
const int N=110;
int f[N][N]; //只从前i组物品中选,当前体积小于等于j的最大值
int v[N][N],w[N][N],s[N]; //v为体积,w为价值,s代表第i组物品的个数
int n,m,k;
int main(){
cin>>n>>m; //读入物品组数和背包容量
for(int i=1;i<=n;i++){ //循环读入每组物品信息
cin>>s[i]; //第i组物品的数量
for(int j=0;j>v[i][j]>>w[i][j];
}
}
for(int i=1;i<=n;i++){ //循环计算最大价值
for(int j=0;j<=m;j++){ //循环当前背包容量
f[i][j]=f[i-1][j]; //先不选第i组物品
for(int k=0;k=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]); //如果能放进背包中,则选择放或不放
}
}
}
cout<
#include
using namespace std;
const int N=110;
int f[N]; //f[i]表示当前背包容量为i时的最大价值
int v[N][N],w[N][N],s[N]; //v[i][j]表示第i组物品中第j个物品的体积,w[i][j]表示第i组物品中第j个物品的价值,s[i]表示第i组物品的数量
int n,m,k;
int main(){
cin>>n>>m; //输入物品组数和背包容量
for(int i=0;i>s[i]; //输入当前物品组数的物品数量
for(int j=0;j>v[i][j]>>w[i][j]; //输入当前物品的体积和价值
}
}
for(int i=0;i=0;j--){ //枚举背包容量
for(int k=0;k=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]); //进行状态转移
}
}
}
cout<
#include
#include
using namespace std;
const int MAXN = 105; // 最大物品种类数
const int MAXM = 100005; // 最大背包容量
int n, m;
int w[MAXN][MAXN], v[MAXN][MAXN]; // w[i][j] 表示第 i 种物品的第 j 个重量,v[i][j] 表示第 i 种物品的第 j 个价值
int f[MAXM]; // f[j] 表示背包容量为 j 时的最大价值
int main() {
cin >> n >> m; // 输入物品种类数和背包容量
for(int i = 1; i <= n; i++) {
int cnt;
cin >> cnt; // 输入第 i 种物品的数量
w[i][0] = cnt; // 记录第 i 种物品的数量
for(int j = 1; j <= cnt; j++) {
cin >> w[i][j] >> v[i][j]; // 输入第 i 种物品的第 j 个重量和价值
}
}
for(int i = 1; i <= n; i++) { // 遍历每种物品
for(int j = m; j >= 0; j--) { // 从大到小遍历每种背包容量
for(int k = 0; k < (1 << w[i][0]); k++) { // 枚举每种可能的选择情况,这里要用 w[i][0] 表示第 i 种物品的数量
int ww = 0, vv = 0; // 记录选中的物品的总重量和总价值
for(int l = 1; l <= w[i][0]; l++) { // 遍历第 i 种物品的所有物品
if((1 << (l - 1)) & k) { // 如果第 l 个物品被选中
ww += w[i][l]; // 累加重量
vv += v[i][l]; // 累加价值
}
}
if(ww <= j) { // 如果选中的物品总重量不超过背包容量 j
f[j] = max(f[j], f[j - ww] + vv); // 更新背包容量为 j 时的最大价值
}
}
}
}
cout << f[m] << endl; // 输出背包容量为 m 时的最大价值,即为答案
return 0;
}
线性DP(Dynamic Programming)是指一种使用动态规划的方法解决问题的过程。通常来说,线性DP可以分为两种,即基于序列的线性DP和基于区间的线性DP。
基于序列的线性DP通常解决的是序列上的问题,如最长上升子序列(LIS)、最长公共子序列(LCS)等。这类问题的主要思路是定义状态,设计状态转移方程,以及求解最终结果。在设计状态转移方程时,需要考虑当前状态和之前状态之间的关系,以及如何根据之前的状态计算当前状态。在实现上,通常需要使用一维或二维数组来保存状态值。
基于区间的线性DP通常解决的是区间上的问题,如区间最大子段和、区间最小割等。这类问题的主要思路是定义状态,设计状态转移方程,以及求解最终结果。在设计状态转移方程时,需要考虑区间内的状态和区间之间的关系,以及如何根据区间内的状态计算区间之间的状态。在实现上,通常需要使用二维数组来保存状态值。
无论是基于序列的线性DP还是基于区间的线性DP,其思路都是相似的。需要注意的是,在实现时需要注意空间的使用,以及避免重复计算。此外,对于一些特殊的问题,还可以采用一些优化策略,如状态压缩、滚动数组、斜率优化等,从而提高算法的效率
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输入格式
第一行包含整数 nn,表示数字三角形的层数。
接下来 nn 行,每行包含若干整数,其中第 ii 行表示数字三角形第 ii 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤5001≤n≤500,
−10000≤三角形中的整数≤10000−10000≤三角形中的整数≤10000输入样例:
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输出样例:
30
#include
using namespace std;
const int N = 510, INF = 1e9;
int f[N][N]; // f[i][j]表示到第i行第j列的最大路径和
int a[N][N]; // 存放输入的矩阵
int main()
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
scanf("%d", &a[i][j]);
// 初始化
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
f[i][j] = -INF;
f[1][1] = a[1][1];
// 动态转移
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]);
// 求解答案
int max0 = -INF;
for (int j = 1; j <= n; j++)
max0 = max(max0, f[n][j]);
cout << max0 << endl;
return 0;
}
f(i,j)含义: 从最底层出发到第 i 行第 j 个数的最大路径和
每个点有两种选择: 向左上方走 和 向右上方走
对应的子问题为: f(i+1,j) 和 f(i+1,j+1)
倒序情况下不需要考虑边界条件
结果: f(1,1)
#include
using namespace std;
const int N = 510;
int f[N][N];
int main()
{
int n;
cin >> n;
// 读入三角形中的数值
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
cin >> f[i][j];
// 倒序 DP,从最后一行开始向上推
for(int i = n; i >= 1; i--)
for(int j = 1; j <= i; j++)
f[i][j] += max(f[i+1][j], f[i+1][j+1]);
cout << f[1][1] << endl; // 输出最终的结果
return 0;
}
#include
using namespace std;
const int N = 510, INF = 1e9;
int f[N]; // f[j]表示到达当前行第j列的最大路径和
int a[N][N]; // 存放输入的矩阵
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
scanf("%d", &a[i][j]);
// 初始化
for (int j = 1; j <= n; j++)
f[j] = -INF;
f[1] = a[1][1];
// 动态转移
for (int i = 2; i <= n; i++)
for (int j = i; j >= 1; j--)
f[j] = max(f[j], f[j - 1]) + a[i][j];
// 求解答案
int max0 = -INF;
for (int j = 1; j <= n; j++)
max0 = max(max0, f[j]);
cout << max0 << endl;
return 0;
}
#include
using namespace std;
const int N = 510;
int n;
int d[N][N];
int f[N][N];
int dfs(int i, int j) {
if (i == n) return d[i][j]; // 最底层
if (f[i][j] != -1) return f[i][j]; // 记忆化
int l = dfs(i + 1, j);
int r = dfs(i + 1, j + 1);
return f[i][j] = max(l, r) + d[i][j];
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
cin >> d[i][j];
f[i][j] = -1; // 初始化
}
}
cout << dfs(1, 1) << endl;
return 0;
}
给定一个长度为 NN 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 NN。
第二行包含 NN 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤10001≤N≤1000,
−109≤数列中的数≤109−109≤数列中的数≤109输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
#include
using namespace std;
const int N = 1010;
int n;
int w[N], f[N];
int main() {
scanf("%d",&n);
for (int i = 1; i <= n; i++) cin >> w[i];
// 动态规划求解最长上升子序列
for (int i = 1; i <= n; i++) {
f[i] = 1; // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
for (int j = 1; j < i; j++) {
if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1); // 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
}
}
// 找出最大的 f[i] 值,即最长上升子序列的长度
int mx = 0;
for (int i = 1; i <= n; i++)
mx = max(mx,f[i]);
cout << mx << endl;
return 0;
}
#include
using namespace std;
const int N = 1010;
int n;
int w[N], f[N];
// 在已经找到的上升子序列中二分查找第一个大于等于v的数的位置
int binary_search(int l, int r, int v) {
while (l < r) {
int mid = l + r >> 1;
if (f[mid] < v) l = mid + 1;
else r = mid;
}
return l;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
int len = 1; // LIS的长度,初始化为1,即只有第一个数自己本身是一个上升子序列
f[1] = w[1]; // 第一个上升子序列就是第一个数自己本身
for (int i = 2; i <= n; i++) { // 从第二个数开始计算
if (w[i] > f[len]) { // 如果当前的数比已经找到的上升子序列中最后一个数还大,那么直接加入上升子序列中
f[++len] = w[i];
} else { // 否则,在已经找到的上升子序列中找到第一个大于等于当前数的位置,将其加入上升子序列中
int pos = binary_search(1, len, w[i]);
f[pos] = w[i];
}
}
printf("%d\n", len); // 输出最长上升子序列的长度
return 0;
}
#include
using namespace std;
const int N = 1010;
int n;
int w[N], f[N], g[N];
int main() {
// 输入数据
scanf("%d", &n);
for (int i = 1; i <= n; i++)
cin >> w[i];
// 动态规划求解最长上升子序列
for (int i = 1; i <= n; i++) {
f[i] = 1; // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
g[i] = 0; // 记录转移的来源,初始化为0
for (int j = 1; j < i; j++) {
if (w[i] > w[j]) // 如果i位置的数字比j位置的大
if (f[i] < f[j] + 1) { // 如果j位置到i位置的数字可以构成更长的子序列
f[i] = f[j] + 1; // 更新f[i]的值
g[i] = j; // 记录i位置的最优来源
}
}
}
// 找出最大的 f[i] 值,即最长上升子序列的长度
int k = 1;
for (int i = 1; i <= n; i++)
if (f[k] < f[i])
k = i;
printf("%d\n", f[k]);
// 输出最长上升子序列
int res[N], len = 0;
while (k) {
res[++len] = w[k];
k = g[k];
}
for (int i = len; i >= 1; i--)
printf("%d ", res[i]);
return 0;
}
#include
#include
using namespace std;
const int MAXN = 1005;
int seq[MAXN]; // 存储输入的数字序列
int memo[MAXN]; // memo数组用于记忆化搜索,存储已经计算过的子问题的答案
int n; // 数字序列的长度
// dfs函数表示以第i个数字为开头的最长上升子序列的长度
int dfs(int i) {
if (memo[i] != -1) return memo[i]; // 如果已经计算过这个子问题的答案,则直接返回答案
int res = 1; // 初始化答案为1,表示最长上升子序列的长度至少为1
for (int j = i+1; j < n; j++) { // 枚举所有可能的下一个数字
if (seq[j] > seq[i]) { // 如果这个数字比当前数字大,则可以将它添加到当前上升子序列中
res = max(res, dfs(j) + 1); // 递归求解以它为开头的最长上升子序列的长度,再加上当前数字,得到当前上升子序列的长度
}
}
memo[i] = res; // 将当前子问题的答案保存到memo数组中,以便下次计算时直接返回答案
return memo[i];
}
int main() {
memset(memo, -1, sizeof(memo)); // 初始化memo数组为-1,表示所有子问题的答案都还没有计算过
cin >> n;
for (int i = 0; i < n; i++) {
cin >> seq[i]; // 输入数字序列
}
int ans = 0;
for (int i = 0; i < n; i++) {
ans = max(ans, dfs(i)); // 对于每一个数字,求解以它为开头的最长上升子序列的长度,并更新答案
}
cout << ans << endl; // 输出最长上升子序列的长度
return 0;
}
最长公共子序列(Longest Common Subsequence,LCS)是指在给定两个序列中,找到一个最长的子序列,使得该子序列在两个序列中均出现。
例如,对于序列 "ABCD" 和 "BDCA",它们的最长公共子序列是 "BD",长度为 2
最长公共子序列可以使用多种算法来解决,其中比较常见的算法包括:
动态规划:上面已经给出了动态规划的解法,时间复杂度为 O(n^2),空间复杂度也为 O(n^2)。
递归:可以将问题递归地划分为子问题,递归求解。时间复杂度为指数级别,不太实用,但可以作为理解动态规划的一种方法。
滑动窗口:可以将最长公共子序列转化为最长连续子序列,使用滑动窗口来求解。时间复杂度为 O(n),空间复杂度为 O(1)。
给定两个长度分别为 N和 M 的字符串 A 和 B,求既是 A 的子序列又是B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5 acbd abedc
输出样例:
3
#include
using namespace std;
const int N = 105;
char A[N], B[N];
int from[N][N]; //记录LCS构造路径
int dp[N][N]; //记录LCS长度
int n, m;
void LCSLength(int n, int m) {
//初始化
for (int i = 1; i <= n; i++) {
dp[i][0] = 0; //第一列初始化为0
}
for (int i = 1; i <= m; i++) {
dp[0][i] = 0; //第一行初始化为0
}
//递推求解
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (A[i] == B[j]) { //字符匹配,LCS长度+1
dp[i][j] = dp[i - 1][j - 1] + 1;
from[i][j] = 1; //记录路径为“左上角”
} else if (dp[i - 1][j] >= dp[i][j - 1]) { //选择左边的值
dp[i][j] = dp[i - 1][j];
from[i][j] = 2; //记录路径为“上边”
} else { //选择上面的值
dp[i][j] = dp[i][j - 1];
from[i][j] = 3; //记录路径为“左边”
}
}
}
}
//LCS构造
void LCS(int i, int j) {
if (i == 0 || j == 0) return; //递归终止条件
if (from[i][j] == 1) { //当前字符匹配,输出后递归“左上角”
LCS(i - 1, j - 1);
cout << A[i]; //输出当前字符
} else if (from[i][j] == 2) { //选择上面的值,递归“上边”
LCS(i - 1, j);
} else { //选择左边的值,递归“左边”
LCS(i, j - 1);
}
}
int main() {
scanf("%d %d", &n, &m);
scanf("%s %s", A + 1, B + 1);
LCSLength(n, m);
cout << dp[n][m] << endl; //输出LCS长度
LCS(n, m);
return 0;
}
#include
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N]; // f[i][j] 表示 a 的前 i 个字符和 b 的前 j 个字符的最长公共子序列长度
int main() {
cin >> n >> m >> a + 1 >> b + 1; // 输入 n、m 和字符串 a、b,注意要从下标 1 开始读入
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i] == b[j]) { // 如果当前字符相同,则最长公共子序列长度加一
f[i][j] = f[i - 1][j - 1] + 1;
} else { // 如果当前字符不同,则最长公共子序列长度为 a 的前 i-1 个字符和 b 的前 j 个字符的最长公共子序列长度,或者是 a 的前 i 个字符和 b 的前 j-1 个字符的最长公共子序列长度中的较大值
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
}
cout << f[n][m] << '\n'; // 输出最长公共子序列长度
return 0;
}
具体步骤如下:
定义一个记忆数组 memo,其中 memo[i][j] 表示字符串 a 的前 i 个字符和字符串 b 的前 j 个字符的最长公共子序列长度。
对于每个位置 (i, j),如果 a[i] == b[j],则最长公共子序列长度加一,否则最长公共子序列长度为 a 的前 i-1 个字符和 b 的前 j 个字符的最长公共子序列长度,或者是 a 的前 i 个字符和 b 的前 j-1 个字符的最长公共子序列长度中的较大值。
递归计算 memo[i][j],如果 memo[i][j] 已经计算过,则直接返回 memo[i][j]。
计算 memo[n][m],其中 n 和 m 分别表示字符串 a 和 b 的长度。
返回 memo[n][m]。
#include
#include
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int memo[N][N]; // 定义记忆数组
int dfs(int i, int j) {
if (i == 0 || j == 0) return 0; // 边界情况
if (memo[i][j] != -1) return memo[i][j]; // 如果 memo[i][j] 已经计算过,则直接返回 memo[i][j]
if (a[i] == b[j]) memo[i][j] = dfs(i - 1, j - 1) + 1; // 如果 a[i] == b[j],则最长公共子序列长度加一
else memo[i][j] = max(dfs(i - 1, j), dfs(i, j - 1)); // 否则最长公共子序列长度为 a 的前 i-1 个字符和 b 的前 j 个字符的最长公共子序列长度,或者是 a 的前 i 个字符和 b 的前 j-1 个字符的最长公共子序列长度中的较大值
return memo[i][j];
}
int main() {
cin >> n >> m >> a + 1 >> b + 1;
memset(memo, -1, sizeof memo); // 初始化记忆数组
int ans = dfs(n, m);
cout << ans << '\n'; // 输出最长公共子序列长度
return 0;
}
区间DP是动态规划算法中的一种常见形式,它通常用于解决区间问题,例如最长回文子序列、石子合并,区间最大子段和等问题。
区间DP的基本思想是将原问题划分成若干子问题,然后通过计算子问题的最优解来推导出原问题的最优解。通常情况下,一个区间问题的最优解可以通过其子区间的最优解来求解。
区间DP的状态转移方程通常具有以下形式:
其中 $dp_{i,j}$ 表示原问题中区间 $[i,j]$ 的最优解,$k$ 表示将区间 $[i,j]$ 划分成两个子区间 $[i,k]$ 和 $[k+1,j]$,$cost(i,j,k)$ 表示将区间 $[i,j]$ 划分成子区间 $[i,k]$ 和 $[k+1,j]$ 的代价。
在实际应用中,通常需要根据具体问题来设计状态转移方程
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 44 堆石子分别为
1 3 5 2
, 我们可以先合并 1、2堆,代价为 4,得到4 5 2
, 又合并 1、2堆,代价为 9,得到9 2
,再合并得到 11,总代价为 4+9+11=244+9+11=24;如果第二步是先合并 2、3 堆,则代价为 7,得到
4 7
,最后一次合并代价为 11,总代价为 4+7+11=224+7+11=22。问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4 1 3 5 2
输出样例:
22
#include
#include
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];
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] = 1e9; // 初始化为极大值
for (int k = l;k <= r - 1;k++) { // 枚举区间分割点
f[l][r] = min (f[l][r],f[l][k] + f[k + 1][r] + s[r] - s[l - 1]); // 状态转移方程
}
}
}
cout << f[1][n] << endl; // 输出最终结果
return 0;
}
#include
#include
using namespace std;
const int N = 310;
int n;
int s[N]; // 前缀和数组
int f[N][N]; // 动态规划状态数组
// 记忆化搜索函数
int dfs (int l,int r) {
if (f[l][r] != -1) return f[l][r]; // 已经计算过,直接返回
if (l == r) return 0; // 只有一个石子,不需要合并
int res = 1e9;
for (int k = l;k < r;k++) { // 枚举分割点
res = min (res,dfs (l,k) + dfs (k + 1,r) + s[r] - s[l - 1]); // 更新最小代价
}
return f[l][r] = res; // 记录并返回结果
}
int main () {
cin >> n;
memset (f,-1,sizeof (f)); // 初始化为 -1
for (int i = 1;i <= n;i++) {
cin >> s[i];
s[i] += s[i - 1]; // 计算前缀和
}
cout << dfs (1,n) << endl; // 输出最终结果
return 0;
}
最大子段和问题是一个经典的算法问题,指的是在一个数列中,找到一个子段(可以为空),使得该子段内所有数的和最大。例如( -2,11,-4,13,-5,-2 )最大子段是{ 11,-4,13 }其和为20。
#include
#include
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
int f[N]; // f[i]表示以i结尾的最大子段和
int main () {
cin >> n;
for (int i = 1;i <= n;i++) {
cin >> a[i];
}
f[1] = a[1];
int ans = f[1];
for (int i = 2;i <= n;i++) {
f[i] = max (f[i-1]+a[i],a[i]); // 状态转移方程
ans = max (ans,f[i]); // 更新最大子段和
}
cout << ans << endl;
return 0;
}
#include
#include
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
int memo[N];
// 递归计算以位置i结尾的最大子段和
int dp(int i) {
// 如果i=1,则以位置1结尾的最大子段和就是a[1]
if (i == 1) return memo[1] = a[1];
// 如果memo[i]已经被计算过,则直接返回memo[i]
if (memo[i]) return memo[i];
// 如果dp(i-1)>0,则以位置i结尾的最大子段和就是dp(i-1)+a[i]
if (dp(i-1) > 0) return memo[i] = dp(i-1) + a[i];
// 否则以位置i结尾的最大子段和就是a[i]
return memo[i] = a[i];
}
int main () {
cin >> n;
for (int i = 1;i <= n;i++) cin >> a[i];
int res = a[1];
// 枚举所有的i,取它们的最大值作为最终答案
for (int i = 1;i <= n;i++) res = max(res, dp(i));
cout << res << endl;
return 0;
}
#include
#include
#include
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
// 求跨越中点的最大子段和
int cross(int l, int r, int mid) {
int sum = 0, lmax = a[mid], rmax = a[mid+1];
for (int i = mid;i >= l;i--) {
sum += a[i];
lmax = max(lmax, sum);
}
sum = 0;
for (int i = mid+1;i <= r;i++) {
sum += a[i];
rmax = max(rmax, sum);
}
return lmax + rmax;
}
// 递归求解最大子段和
int maxSub(int l, int r) {
if (l == r) return a[l];
int mid = l + r >> 1;
int lmax = maxSub(l, mid);
int rmax = maxSub(mid+1, r);
int cmax = cross(l, r, mid);
return max({lmax, rmax, cmax});
}
int main () {
cin >> n;
for (int i = 1;i <= n;i++) cin >> a[i];
cout << maxSub(1, n) << endl;
return 0;
}
#include
using namespace std;
const int N = 1e5+10;
int n;
int a[N];
int main () {
// 输入数据
cin >> n;
for (int i = 1;i <= n;i++) cin >> a[i];
// 贪心求解最大子段和
int sum = 0, res = a[1];
for (int i = 1;i <= n;i++) {
if (sum < 0) sum = a[i];
else sum += a[i];
res = max(res, sum);
}
// 输出结果
cout << res << endl;
return 0;
}
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n 的一种划分。
现在给定一个正整数 n,请你求出n 共有多少种不同的划分方法。
输入格式
共一行,包含一个整数 n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 109+7取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7
#include
#include
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];
int main()
{
cin >> n;
for(int i = 0;i <= n;i++) f[i][0] = 1;
for (int i = 1; i <= n; i ++ )
for(int j = 0; j <= n; j ++ ){
f[i][j] = f[i-1][j]; // j < i ,一个i都放不下的情况
// j >= i的情况
if(j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
}
cout << f[n][n] << endl;
return 0;
}
#include
#include
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];
int main()
{
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i ++ )
for(int j = i; j <= n; j ++ ){
f[j] = (f[j] + f[j - i]) % mod;
}
cout << f[n] << endl;
return 0;
}