背包问题
01背包问题
volume & weight 容量与权重
核心思想就是用二维数组f[i][j]
表示从前i个物品选择不超过重量j的最大重量方案
然后f[i][j]这个状态可以认为是,要么选择第i个物品,要么不选择第i个物品
如果不选择了第i个物品 那么 f[i][j] 由 f[i-1][j] 来决定
然后,只有当 v[i]<=背包最大重量, 才可以选择第i个物品放入背包
f[i][j] = max(f[i-1][j],f[i-1][j-v[i]]+w[i])
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n,m; //分别表示物品个数和背包容量
int v[N],w[N]; //分表表示价值和重量
int f[N][N]; // f[i][j]表示从前i个物品中选不超过重量j的最大方案值
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
//对于f[i][j]这样的,
//可以认为是两种情况,选了第i个和没选第i个物品
//也就是f[i-1][j]和f[i-1][j-v[i]]
//但是注意,当背包装不下v[i]的时候,他就不可能选第i个物品了,这时只能由f[i-1][j]来构成
for(int i=1;i<=n;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]);
}
cout << f[n][m];
return 0;
}
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 50;
int f[N][N];//f[i][j]表示总体积恰好为j的从前i个物品中选择的方案数
//那么f[i][j]这个集合可以分为选第i个物品和不选第i个物品
//f[i][j] = f[i-1][j] + f[i-1][j-v[i]]
int v[N];
int main(){
int n;
cin >> n;
f[0][0] = 1;
for(int i=1;i<=n;i++) cin >> v[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=40;j++){
f[i][j] = f[i-1][j];
if(j>=v[i]) f[i][j] += f[i-1][j-v[i]];
}
}
cout << f[n][40];
return 0;
}
完全背包问题
完全背包问题每个东西可以用无限次
01背包每个东西只能用一次
f(i,j) 从前i个物品中选总体积不超过j的最大价值方案
01背包问题是 把 f(i,j) 分为 选第 i 个物品 和 不选第 i 个物品
完全背包问题是分成 选 0 个第i 个物品,选1个第i个物品…一直到选k个第i个物品…
选0个第i个物品 f(i,j) = f(i-1,j)
选k个第i个物品的话 f(i,j) = max(f(i-1,j), f(i-1,j-weight[i]) + value[i], … … , f(i-1, j - kweight[i]) + kvalue[i] , … )
f(i,j) = max(f(i-1,j), f(i-1,j-weight[i]) + value[i], … … , f(i-1, j - kweight[i]) + kvalue[i] , … )
令 j = j - weight[i] , 带入那么有
f(i,j-weight[i]) = max(f(i-1,j-weight[i]) , f(i-1,j-2weight[i])+value[i] , … … , f(i-1,j-kweight[i])+(k-1)*value[i] , f(i-1,j-(k+1)weight[i])+kvalue[i], … … )
#include<algorithm>
#include <iostream>
using namespace std;
const int N = 10010;
int n,m;
int v[N],w[N];
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k*v[i]<=j;k++){//这里是k*v[i]<=j别写成m了
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout << f[n][m];
return 0;
}
然后可以进一步优化
可以看到f(i,j) = max(f(i-1,j), f(i-1,j-weight[i]) + value[i], … … , f(i-1, j - kweight[i]) + kvalue[i] , … )
从 f(i-1,j-weight[i]) + value[i] 开始 每一项 都比 f(i,j-weight[i]) 多了个 value[i]
那么 f(i,j) = max(f(i-1,j) , max(f(i,j-weight[i])+value[i])) = max(f(i-1,j),f(i,j-weight[i]+value[i
]))
选0个第i个物品 f(i,j) = f(i-1,j)
选k个第i个物品的话 f(i,j) = max(f(i-1,j), f(i-1,j-weight[i]) + value[i], ... .... , f(i-1, j - k*weight[i]) + k*value[i] , ....... )
f(i,j) = max(f(i-1,j), f(i-1,j-weight[i]) + value[i], ... .... , f(i-1, j - k*weight[i]) + k*value[i] , ....... )
令 j = j - weight[i] , 带入那么有
f(i,j-weight[i]) = max(f(i-1,j-weight[i]) , f(i-1,j-2*weight[i])+value[i] , ... ... , f(i-1,j-k*weight[i])+(k-1)*value[i] , f(i-1,j-(k+1)*weight[i])+k*value[i], .... ... )
可以看到f(i,j) = max(f(i-1,j), f(i-1,j-weight[i]) + value[i], ... .... , f(i-1, j - k*weight[i]) + k*value[i] , ....... )
从 f(i-1,j-weight[i]) + value[i] 开始 每一项 都比 f(i,j-weight[i]) 多了个 value[i]
那么 f(i,j) = max(f(i-1,j) , max(f(i,j-weight[i])+value[i])) = max(f(i-1,j),f(i,j-weight[i])+value[i]))
/*
01背包: f[i][j] = max(f[i-1][j],f[i-1][j-weight[i]]+value[i])
完全背包 f[i][j] = max(f[i-1][j],f[i][j-weight[i]]+value[i])
*/
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e3 + 10;
int w[N],v[N];
int n,m;
int f[N][N];
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >> w[i] >> v[i];
}
f[1][0] = 0;
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j] = f[i-1][j];
if(j>=w[i]) f[i][j] = max(f[i-1][j],f[i][j-w[i]]+v[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
多重背包问题
多重背包与完全背包的区别就是多重背包每个物品数量是固定有限的,完全背包是无限的
朴素版本
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010;
int n,m;
int f[N][N];
int v[N],w[N],s[N];
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i] >> s[i];
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
for(int k=0;k<=s[i];k++){
if(j - k*v[i]<0) break;
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout << f[n][m] << endl;
return 0;
}
优化版本
如果采用像完全背包问题那样的优化方式是不可以的
然后这里引入一种二进制优化的方法
任何一个数,随便举个例子比如说56,这个二进制为111000
111000 = 100000 + 010000 + 001000
即为 56 = 32 + 16 + 8
那对于第i个物品他的个数为s[i],我是不需要把它从0~s[i]枚举的,
举个例子,比如s[i] = 1023(二进制为111111111)
那我可以先分组分为1,2,4,8,…,256,512共计10组
意思是我们把原先含1023个物品的情况,打包成分别含1个,2个,4个,…,512个物品的新商品共计10组,把这每组看做是一个新的物品
然后这10组物品是否选择的排列组合,一定能表示选0-1023个物品的情况
举个例子,比如说实际上选281个物品的时候价值最大,我们就可以选择256个物品这一组,选16个物品这一组,选8个物品这一组,选一个物品这一组
然后选这10组中那几组组合起来价值最大,实际上就转换成了一个01背包问题
再举个更一般的例子
当s[i]为200的时候
我们需要分成1,2,4,8,16,32,64,73就可以表示0-200中的任何一个数了
(不可以是1,2,4,8,16,32,64,128因为这样表示的是0-255超过了我们最大只有200个物品)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N];
int f[N][M];
int main()
{
cin >> n >> m;
int cnt = 0;
for (int i = 1; i <= n; i ++ )
{
int a, b, s;
cin >> a >> b >> s;
int k = 1;
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;
}
}
n = cnt;
for (int i = 1; i <= n; 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-1][j],f[i-1][j-v[i]]+w[i]);
}
cout << f[n][m] << endl;
return 0;
}
分组背包问题
f[i][j] 从前i组中选,总体积不超过j的方案集合
f[i][j]可以分为这些情况:第i组中不选物品,选第i组中第一个物品,选第i组中第二个物品…
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int f[N][N];
int v[N][N],w[N][N],s[N];
int main()
{
int n,m;
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >> s[i];
for(int j=1;j<=s[i];j++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j] = f[i-1][j];
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] << endl;
return 0;
}
线性DP问题
数字三角形
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510,inf=1e9;
int n;
int a[N][N],f[N][N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin >> a[i][j];
}
}
for(int i=0;i<=n;i++){ //这里初始化的时候有坑
for(int j=0;j<=n;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 res = -inf;
for(int i=1;i<=n;i++) res = max(res,f[n][i]);
cout << res << endl;
return 0;
}
那个初始化f[i][j]的时候有坑,需要多初始化一部分为负无穷
最长上升子序列1
朴素版本
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N],f[N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++){
f[i] = 1; //f[i]至少这个序列会有第i号数这一个数
for(int j=1;j<i;j++){
if(a[j]<a[i]) f[i] = max(f[i],f[j]+1);
}
}
int res = -1;
for(int i=1;i<=n;i++) res = max(f[i],res);
cout << res;
return 0;
}
如果需要记录下最大长度的方案的话,就是把状态转移记录下来就好了
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N],f[N],g[N];//g[N]用来记录递推中的下标便于输出选择的方案
int main()
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
for(int i=1;i<=n;i++){
f[i] = 1; //f[i]至少这个序列会有第i号数这一个数
g[i] = -1; //-1表示没有下一个节点了,这个序列到头了
for(int j=1;j<i;j++){
if(a[j]<a[i]){
if(f[i]<(f[j]+1)){
f[i] = f[j]+1;
g[i] = j;
}
}
}
}
int res = -1;
for(int i=1;i<=n;i++) res = max(f[i],res);
cout <<"最大长度序列:" <<res << endl;
int num=-1;
for(int i=1;i<=n;i++) if(f[i]==res) num=i;
for(int i=num;i!=-1;i=g[i]){
cout<<a[i]<<" ";//这样输出是逆序的
}
return 0;
}
优化版本
未优化的最长上升子序列的f[i][j]集合是以序列倒数第二个数是什么来进行划分
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N];//a[N]存输入
int q[N];//q[i]存的是长度为i的上升子序列的最后一个数的值是多少
int main()
{
cin >> n;
for(int i=0;i<n;i++) cin >> a[i];
int len = 0; //len表示最长上升子序列的长度
for(int i=0;i<n;i++){
int l=0,r=len;
while(l<r){
int mid = (l+r+1)/2;
if(q[mid] < a[i]) l = mid;
else r = mid - 1;
}
len = max(len,r+1);
q[r+1] = a[i];
}
cout << len << endl;
return 0;
}
最长公共子序列
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 1010;
int n,m;
char a[N],b[N];
int f[N][N];
int main()
{
cin >> n >> m;
cin >> a+1 >> b+1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j] = max(f[i-1][j],f[i][j-1]);//那个f[i-1][j-1]的集合含在f[i-1][j]里面了不需要再写
if(a[i]==b[j]) f[i][j] = max(f[i][j],f[i-1][j-1]+1);//只有a[i]==b[j]才可能是选a[i]且选b[j]
}
}
cout << f[n][m] << endl;
return 0;
}
最短编辑距离
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N],b[N];
int f[N][N];
int main()
{
int n,m;
cin >> n >> (a+1) >> m >> (b+1);
//初始化
for(int i=0;i<=m;i++) f[0][i] = i;//a为空字时变成长度为i的字符串时只有添加操作需要添加i次
for(int i=0;i<=n;i++) f[i][0] = i;//b为空字时a要变成b的话只有删除操作长度为i需要删除i次
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);//min这个函数只能比较两个值别弄成三个了
if(a[i]==b[j]) f[i][j] = min(f[i][j],f[i-1][j-1]);
else f[i][j] = min(f[i][j],f[i-1][j-1]+1);
}
}
cout << f[n][m] << endl;
return 0;
}
接上题
#include <algorithm>
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n,m;
char str[N][N];
int dp(char a[],char b[])
{
int f[N][N];
int len1 = strlen(a+1);
int len2 = strlen(b+1);
for(int i=1;i<=len1;i++) f[i][0] = i;
for(int i=1;i<=len2;i++) f[0][i] = i;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
if(a[i]==b[j]) f[i][j] = min(f[i][j],f[i-1][j-1]);
else f[i][j] = min(f[i][j],f[i-1][j-1]+1);
}
}
return f[len1][len2];
}
int main()
{
cin >> n >> m;
for(int i=0;i<n;i++){
cin >> str[i] + 1;
}
while(m--){
char temp[N];
int max_num;
cin >> temp + 1 >> max_num;
int res = 0;
for(int i=0;i<n;i++){
if(dp(temp,str[i])<=max_num) res++;
}
cout << res << endl;
}
return 0;
}
区间DP
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310,inf=1e9;
int n;
int f[N][N];
int s[N]; //前缀和
int main()
{
cin >> n;
for(int i=1;i<=n;i++){
cin >> s[i];
s[i] += s[i-1];
}
//区间dp一般是先枚举长度
for(int len=2;len<=n;len++){//len表示[i,j]的长度,长度为1时不用合并所以从2开始
for(int i=1;i+len-1<=n;i++){//开始枚举左端点
int j = i + len -1;
f[i][j] = inf; //先赋值一个很大的数
for(int k=i;k<j;k++){//k取值从i到j-1
f[i][j] = min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
}
}
}
cout << f[1][n] << endl;
return 0;
}
计数类DP
背包做法
#include <iostream>
using namespace std;
const int N = 1010;
const int MOD = 1e9+7;
int f[N][N];
int n;
int main()
{
cin >> n;
for(int i=1;i<=n;i++) f[i][0] = 1;//从前i个物品中选总体积为0的方案个数只有一个就是不选
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j] = f[i-1][j]%MOD;
if(j>=i) f[i][j] = (f[i][j]+f[i][j-i])%MOD;
}
}
cout << f[n][n] << endl;
return 0;
}
另外一种考虑方式
#include <iostream>
using namespace std;
const int N = 1010;
const int MOD = 1e9+7;
int f[N][N];
int n;
int main()
{
cin >> n;
//f[i][j]表示j个数加起来总和为1的方案数,因为最小的数为1,那么就不可能会有j > i
f[0][0] = 1;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
f[i][j] = (f[i-1][j-1]+f[i-j][j])%MOD;
}
}
int res = 0;
for(int i=1;i<=n;i++) res = (res+f[n][i])%MOD;
cout << res << endl;
return 0;
}
状态压缩DP
蒙德里安的梦想
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N;//M的每一位二进制位储存一种状态
int n, m;
long long f[N][M];
bool st[M];//储存每一列上合法的摆放状态
//例如st[j]中,j如果存在连续奇数个零,那么st[j]=false,如果是只有连续偶数个零,那么st[j]=ture;
//f[i][j]表示摆放第i列,第i-1列向后伸出来横着的方格状态为j,的方案数,j为一个二进制数,用01表示是否戳出来
int main()
{
while (cin >> n >> m, n || m)
{
//枚举每一列的占位状态里哪些是合法的
for (int i = 0; i < 1 << n; i ++ )//一共n行,枚举n位不同的状态
{
int cnt = 0;//用来记录连续的0的个数
st[i] = true;//记录这个状态被枚举过且可行
for (int j = 0; j < n; j ++ )//从低位到高位枚举它的每一位
if (i >> j & 1)//如果为1
{
if (cnt & 1) st[i] = false;//如果之前连续0的个数是奇数,竖的方块插不进来,这种状态不行
cnt = 0;//清空计数器
}
else cnt ++ ;//如果不为1,计数器+1
if (cnt & 1) st[i] = false;//到末尾再判断一下前面0的个数是否为奇数,同前
}
memset(f, 0, sizeof f);//一定要记得初始化成0,对于每个新的输入要重新计算f[N][M]
f[0][0] = 1;
for (int i = 1; i <= m; i ++ )//对于每一列
for (int j = 0; j < 1 << n; j ++ )//枚举j的状态
for (int k = 0; k < 1 << n; k ++ )//再枚举前一行的伸出状态k
if ((j & k) == 0 && st[j | k])//如果它们没有冲突,i这一列被占位的情况也是合法的话
f[i][j] += f[i - 1][k];//那么这种状态下它的方案数等于之前每种k状态数目的和
cout << f[m][0] << endl;//求的是第m-1行排满,并且第m-1行不向外伸出块的情况
//0~m-1行是题目中可以摆方块的范围
}
return 0;
}