目录
内容总览
-
背包问题
-
线性DP
-
区间DP
-
计数类DP
-
数位统计DP
-
状态压缩DP
-
树形DP
-
记忆化搜索
背包问题
假设你在小岛上,上面有很多物品,没件物品的价值都不一样,你有一个背包,体积有限,你如何装才能使背包里的物品价值最大呢?(背包不一定能被装满)
0 1 背包
特点:每件物品最多只能用一次
完全背包
题目链接https://www.acwing.com/problem/content/3/
特点:每件物品有无限个,只要体积够用就可以装
-
三重循环无优化版
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int v[N],w[N];
int f1[N];
int main()
{
int 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=1;j<=m;j++)
for(int k=0;v[i]*k<=j;k++)
{
//注意此时与f[i][j]比较
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+w[i]*k);
}
cout<<f[n][m];
return 0;
}
-
优化版本1 -- 二重循环二维数组
与01背包的不同点:
f[i][j] = max( f[i][j] , f[i-1][j-v[i]] + w[i] ); //01背包
f[i][j] = max( f[i][j] , f[i][j-v[i]] + w[i] ); //完全背包问题
因为完全背包的物品可以选无限次
代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int v[N],w[N];
int f1[N];
int main()
{
int n,m;
cin>>n>>m; //输入物品的个数和背包的容量
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i]; //输入每个物品的体积和价值
//优化版本1--二重循环二维数组
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
//不选第i个物品
f[i][j]=f[i-1][j];
//选第i个物品
//与01背包唯一的不同点就是f[i][j-v[i]]+w[i]
//因为完全背包第i个物品可以放无限次
if(j>=v[i])
{
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
}
cout<<f[n][m];
return 0;
}
-
优化版本2 -- 二重循环一维数组
与01背包的不同点是逆序遍历体积改为顺序遍历体积;
因为因为每个物品可以取任意多次,所以不再强求用上一轮的状态,即本轮放过的物品,在后面还可以再放;
而01背包每次更新需要用到上一轮的状态,为了防止数据被污染,逆序遍历
代码:
#include<iostream>
using namespace std;
const int N=1010;
int f[N][N];
int v[N],w[N];
int f1[N];
int main()
{
int 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=v[i];j<=m;j++)
{
f1[j]=max(f1[j],f1[j-v[i]]+w[i]);
}
cout<<f[m];
return 0;
}
思路来源连接https://www.acwing.com/solution/content/3986/
多重背包
特点:每个物品的个数不一样,第i个物品的个数为s[i]
多重背包普通版
题目链接https://www.acwing.com/problem/content/4/
题目:
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。输入格式
第一行两个整数,N,V 用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 ii 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤1000输入样例
4 5 1 2 3 2 4 1 3 4 3 4 5 2
输出样例:
10
分析:
与完全背包三重循环二维数组差不多,但是多了一个数量限制
代码:
#include<iostream>
using namespace std;
const int N=110;
int n,m; //物品的个数和背包的容量
int v[N],w[N],s[N]; //物品的体积、价值、数量
int f[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i]>>s[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
{
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
cout<<f[n][m];
return 0;
}
多重背包之二进制优化
题目链接https://www.acwing.com/problem/content/5/
分析:
N=15000的原因:题目中N最大为1000,每个物品数量最多为2000,则每个物品最多拆分为1000*log2000个新物品
代码
#include<iostream>
using namespace std;
const int N=15000;
int n,m; //物品的个数和背包的容量
int v[N],w[N]; //物品的体积、价值
int f[N];
int main()
{
cin>>n>>m;
int cnt=0;
for(int i=1;i<=n;i++)
{
int a,b,s; //分别第i个物品的体积,价值和数量
cin>>a>>b>>s;
int k=1; //从1开始分
while(k <= s) //凑数
{
cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)
{
cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
}
//来一遍01背包
n=cnt; //cnt为我们新组成的物品个数
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[m];
return 0;
}
分组背包
特点:物品被分为N组,每一组里只能选择一个物品
题目链接https://www.acwing.com/problem/content/9/
题目:
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
- 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
- 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100输入样例
3 5 2 1 2 2 4 1 3 4 1 4 5
输出样例:
8
分析:
一定要搞清楚每层循环要干什么!!不要瞎想乱写
二维数组代码:
//二维版本
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int f[N][N];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=1;j<=s[i];j++)
{
cin>>v[i][j]>>w[i][j];
}
}
//正戏
for(int i=1;i<=n;i++) //第i组
for(int j=1;j<=m;j++) //容量大小
{
//不选
f[i][j]=f[i-1][j];
//选 第i组第k个,逐个遍历
for(int k=1;k<=s[i];k++)
{
if(j>=v[i][k])
f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
}
cout<<f[n][m];
return 0;
}
一维数组代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=110;
int n,m;
int v[N][N],w[N][N],s[N];
int f[N];
//直接写成一维版的
//我们只要记住如果用到了上一轮的状态,则逆序遍历体积
//如果只用到了本轮的状态,则顺序遍历体积
int main()
{
cin>>n>>m;
//i为第几组,j为组内第几个物品
for(int i=1;i<=n;i++)
{
cin>>s[i];
for(int j=1;j<=s[i];j++)
{
cin>>v[i][j]>>w[i][j];
}
}
for(int i=1;i<=n;i++) //枚举每组
for(int j=m;j>=0;j++) //枚举容量大小
for(int k=1;k<=s[i];k++) //枚举选择每组的第几个数
{
if(v[i][k]<=j)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
cout<<f[m];
return 0;
}
线性DP
数字三角形
题目链接https://www.acwing.com/problem/content/900/
题目:
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500
−10000≤三角形中的整数≤10000输入样例:
5 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5
输出样例:
30
分析:
有两种思路:
- 正序dp:
- 此时需要开两个二维数组f[i,j]和a[i,j],一个用来记录路径的最大值,一个用来记录输入的每点的权值
- 需要处理边界问题,因为题目中有负数,所以需要把边界都置为负无穷,再将dp数组第一个值赋初值,即f[1,1]=a[1,1]
- 写时一定要注意数组赋值时边界为多少!!
- 倒叙dp:不需要处理边界问题,一个f数组就可以搞定,但比较难想到
代码:
- 正序:
#include<iostream> #include<algorithm> using namespace std; const int N=510,INF=1e9; int f[N][N]; //表示从起点走到(i,j)的最大加权值 int a[N][N]; //每个点上的值 int n; int main() { cin>>n; for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) { cin>>a[i][j]; } //为了避免边界问题,即防止某点的值为负值与边界进行比较时 for(int i=0;i<=n;i++) for(int j=0;j<=i+1;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],f[i-1][j])+a[i][j]; } int res=-INF; //用来保存答案 for(int i=1;i<=n;i++) res=max(res,f[n][i]); cout<<res; return 0; }
- 倒序:
#include<iostream> #include<algorithm> using namespace std; const int N=510; int f[N][N]; int n; int main() { cin>>n; for(int i=1;i<=n;i++) for(int j=1;j<=i;j++) cin>>f[i][j]; 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])+f[i][j]; } cout<<f[1][1]; return 0; }
最长上升子序列
acw-895.最长上升子序列https://www.acwing.com/problem/content/897/
题目详情:
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
−1e9≤数列中的数≤1e9输入样例:
7 3 1 2 1 8 5 6
输出样例:
4 (即1 2 5 6)
分析
f[i]:为当前状态即第i个数为结尾倒数第二个数为f[j]的最长上升子序列的长度,初始值f[i]=1,i∈[0,n-1],表示自己就是最长上升子序列,长度为1
状态转移:把前 i-1 个数字中所有满足条件(w[j]<w[i],因为是上升序列)的 j 找出来,那么f[i]就可以试着更新为以 w[j] 结尾的最长上升子序列的长度 再加上 自己的长度 1,但可能更新完的结果没有之前更新过的 f[i] 大,最后两者取一个 max,所以状态转移方程就是 f[i]=max(f[i],f[j]+1)。
eg 5 9 12 4 6 17这个序列,求f[5],f[3]=3,f[4]=1,w[4]<w[5],但f[4]+1<之前已经更新的状态,所以f[5]=3
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n;
int w[N],f[N];
int main()
{
cin>>n;
for(int i=0;i<n;i++)
cin>>w[i];
for(int i=0;i<n;i++) //n个状态
{
f[i]=1; //假如整个序列只有a[i]一个数
for(int j=0;j<i;j++) //每个状态需要计算的次数,即倒数第二个数的取法
{
if(w[j]<w[i]) //如果比较的数比当前状态i小,则可以进行转移
f[i]=max(f[i],f[j]+1);
}
}
int res=0;
for(int i=0;i<n;i++) res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
扩 展: 假如我们要把最长上升子序列保存下来,如何保存呢?
解决办法: 加一个g[N]数组,将每次的状态转移记录下来
代码:
//扩展:将最长子序列保存下来
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int n;
int w[N],f[N];
int g[N]; //保存最长子序列
//先求f[i]的最长上升子序列,如果a[j]>a[i],则
int main()
{
cin>>n;
for(int i=0;i<n;i++)
cin>>w[i];
for(int i=0;i<n;i++) //n个状态
{
f[i]=1; //假如整个序列只有a[i]一个数
for(int j=0;j<i;j++) //每个状态需要计算的次数
{
if(w[j]<w[i]) //如果比较的数比当前状态i小,则可以进行转移
if(f[i]<f[j]+1)
{
f[i]=f[j]+1;
g[i]=j; //在每次转移的地方记下该数的下标
}
}
}
//求该序列在下标为k时取得最长子序列
int k=1;
for(int i=1;i<=n;i++)
if(f[k]<f[i]) k=i;
//最长子序列的长度
cout<<f[k]<<endl;
//倒序输出最长子序列
for(int i=0,len=f[k];i<len;i++)
{
cout<<w[k]<<" ";
k=g[k];
}
return 0;
}
最长公共子序列
给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。(不需要在序列中连续但顺序相同)
示例数据(来源于acwing):
4 5
acbd
abedf
输出:
3
分析:
后面三种情况往往包含第一种情况,所以在代码中不体现
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10010;
int n,m;
char a[N],b[N];
int f[N][N];
int main()
{
cin>>n>>m;
scanf("%s%s",a+1,b+1); //相当于a[0],b[0]没存字符
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
//第二种和第三种情况 01 10
f[i][j]=max(f[i-1][j],f[i][j-1]);
//第四种情况 11
if(a[i]==b[j])
f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
cout<<f[n][m];
return 0;
}
区间DP
石子合并
题目详情:
分析:
数据演示
以数据:4 1 3 5 2 举例 (共四堆石子,每堆石子重量依次排列)
s[1]=1,s[2]=4,s[3]=9,s[4]=11
- 当len=2时
第一层循环
i=1,l=1,r=2,f[1][2]=10^8
第二层循环
k=l=1,f[1][2]=min(f[1][2],f[1][1]+f[2][2]+s[2]-s[0])=4 (合并第一堆和第二堆石子的代价)
第一层循环
i=2,l=2,r=3,f[2][3]=10^8
第二层循环
k=l=2,f[2][3]=min(f[2][3],f[2][2]+f[3][3]+s[3]-s[1])=8 (合并第二堆和第三堆石子的代价)
第一层循环
i=3,l=3,r=4,f[3][4]=10^8
第二层循环
k=l=3,f[3][4]=min(f[3][4],f[3][3]+f[4][4]+s[4]-s[2])=7 (合并第三堆和第四堆石子的代价)
- 当len=3时
第一层循环
i=1,l=1,r=3,f[1][3]=10^8
第二层循环
合并第一堆到第三堆有两种不同的方式,所以此层循环了两次
k=1,f[1][3]=min(f[1][3],f[1][1]+f[2][3]+s[3]-s[0])=17
k=2,f[1][3]=min(f[1][3],f[1][2]+f[3][3]+s[3]-s[0])=13
对应图解:
![]()
第一层循环
i=2,l=2,r=4,f[2][4]=10^8
第二层循环
合并第二堆到第四堆有两种不同的方式,所以此层循环了两次
k=2,f[2][4]=min(f[2][4],f[2][2]+f[3][4]+s[4]-s[1])=7+10=17
k=3,f[2][4]=min(f[2][4],f[2][3]+f[4][4]+s[4]-s[1])=min(17,18)=17
- 当len=4时
第一层循环
i=1,l=1,r=4,f[1][4]=10^8
第二层循环
合并第一堆到第四堆有三种不同的方式,所以此层循环了三次
k=1,f[1][4]=min(f[1][4],f[1][1]+f[2][4]+s[4]-s[0])=17+11=28
k=2,f[1][4]=min(f[1][4],f[1][2]+f[3][4]+s[4]-s[0])=min(28,4+7+11)=22
k=3,f[1][4]=min(f[1][4],f[1][3]+f[4][4]+s[4]-s[0])=min(22,13+7+11)=22
可见最终结果是f[1][4]=22
代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N=310;
int n;
int s[N]; //前缀和
int f[N][N];
int main()
{
scanf("%d",&n);
//输入
for(int i=1;i<=n;i++)
scanf("%d",&s[i]);
//求前缀和
for(int i=1;i<=n;i++)
s[i] += s[i-1];
//len为区间中的点数,即石子数量
//当区间石子数量大于1时才有合并的意义
for(int len=2;len<=n;len++) //先枚举区间长度,再枚举区间左端点
{
//i-len+1 :表示当前区间右端点的下标
for(int i=1;i+len-1<=n;i++)
{
//区间的左端点,右端点
int l = i, r = i + len - 1;
f[l][r] = 1e8;
//这里k枚举的是左半边最后一个石子的位置,所以可以取左端点,但不能取右端点
//因为如果取右端点,那么右半边就不存在了。
for (int k = l; k < r; k ++ )
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
cout<<f[1][n];
return 0;
}
此题思路来源链接https://www.acwing.com/activity/content/code/content/58545/