1.背包问题
(1)0/1背包问题
01背包问题即每个物品只能选1个
考虑第i件物品,当j<w[i]时,f[i][j]=f[i-1][j],当j>=w[i]时,此时有两种选择,选择第i件物品和不选第i件物品。此时f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i])
有 n件物品和一个容量是m的背包。每件物品只能使用一次,第i件物品的体积是vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大.输出最大价值。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int w[N],v[N];
int f[N][N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&v[i]);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j] = f[i-1][j];
if(j>=w[i]) f[i][j] = max(f[i][j],f[i-1][j-w[i]]+v[i]);
}
}
printf("%d",f[n][m]);
return 0;
}
(2)完全背包问题
完全背包问题即物品可以选多个
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int w[N],v[N];
int f[N][N];
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&v[i]);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
f[i][j] = f[i-1][j]; // 注意选择物品下标为i,区别于01背包
if(j>=w[i]) f[i][j] = max(f[i][j],f[i][j-w[i]]+v[i]);
}
}
printf("%d",f[n][m]);
return 0;
}
(3) 多重背包问题
多重背包问题可以转化为0-1背包问题进行优化。假设第i个物品可选c个,那么我们可以用二进制组合为0-c范围的数。此时,每一位二进制可视为一件物品。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e3+10;
const int V = 110;
int f[N][V];
int v[N],w[N];
int cnt = 0;
int main(){
int n,m;
scanf("%d%d",&n,&m);
int a,b,c,k;
while (n--)
{
scanf("%d%d%d",&a,&b,&c);
k = 1;
while (k<=c) // 将c用二进制进行拆分
{
cnt++;
w[cnt] = k*a;
v[cnt] = k*b;
c -= k;
k *=2;
}
if(c){ // 拆分为二进制后的剩余部分
cnt++;
w[cnt] = c * a;
v[cnt] = c*b;
}
}
for(int i=1;i<=cnt;i++) // 当成0-1背包问题求解
for(int j=1;j<=m;j++){
f[i][j]=f[i-1][j];
if(j>=w[i]) f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
}
printf("%d",f[cnt][m]);
return 0;
}
(3)分组背包问题
分组背包问题只需要考虑每一组选择某个物品的最大价值,只需要加一层循环。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
const int V = 110;
const int S = 110;
int f[N][V]; // DP表
int w[N][S]; // 重量
int v[N][S]; // 价值
int G[N]; // 每一组物品种类数
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&G[i]);
for(int j=1;j<=G[i];j++)
scanf("%d%d",&w[i][j],&v[i][j]);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
f[i][j] = f[i-1][j];
for(int k=1;k<=G[i];k++){ // 遍历每一组
if(j>=w[i][k]) f[i][j] = max(f[i][j],f[i-1][j-w[i][k]]+v[i][k]);
}
}
printf("%d",f[n][m]);
return 0;
}
2.线性DP
(1)数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
算法思想:对于每一个i,j,都有两条路径,即从左到右的一条与从左上角到右的一条。因此
f[i][j]=max(f[i-1][j]+a[i][j],f[i-1][j-1]+a[i][j])
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 510;
const int inf = 1e9;
int a[N][N];
int f[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=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(f[n][i],res);
printf("%d",res);
return 0;
}
(2) 最长上升子序列
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少
对于每一个f[i],如果前面有更小的数,f[i]=max(f[i],f[j]+1)。
#include <iostream>
using namespace std;
const int N = 1010;
int a[N],f[N];
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
f[i] = 1; // a[i]前面没有最小的数
for(int j=1;j<i;j++) // 如果a[i]前面有更小的数,则更新
if(a[j]<a[i]) f[i] = max(f[i],f[j]+1);
}
int res = 0;
for(int i=1;i<=n;i++) res = max(res,f[i]);
printf("%d",res);
return 0;
}
(3)最长公共子序列
当匹配到A[i]和B[j]时,有4种情况,如下图所示
其中,第一种情况包含在第2,3种情况中,第4种情况需要A[i]与B[j]匹配成功
给定两个长度分别为 N 和 M 的字符串 和 B,求既是 A的子序列又是 B 的子序列的字符长度最长是多少
输入格式
第一行包含两个整数 N和 M
第二行包含一个长度为 N的字符串,表示字符串 A
第三行包含一个长度为 M 的字符串,表示字符串 B
字符串均由小写字母构成。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
const int M = 1010;
char A[N],B[M];
int f[N][M];
int main(){
int n,m;
scanf("%d%d",&n,&m);
scanf("%s%s",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]); // 子串A[i-1]与B[j]匹配或者A[i]与B[j-1]匹配
if(A[i]==B[j]) f[i][j] = max(f[i][j],f[i-1][j-1]+1); // 子串A[i]与B[j]匹配且匹配成功
}
printf("%d",f[n][m]);
}
(4)最短编辑距离
增删改情况如下图
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
const int M = 1010;
char A[N],B[M];
int f[N][M];
int main(){
int n,m;
scanf("%d%s",&n,A+1);
scanf("%d%s",&m,B+1);
for(int i=0;i<=n;i++) f[i][0] = i; // 边界条件,初始化
for(int j=0;j<=m;j++) f[0][j] = j;
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); // 增与删
if(A[i]==B[j]) f[i][j] = min(f[i][j],f[i-1][j-1]); // A[i]与B[j]匹配成功
else f[i][j] = min(f[i][j],f[i-1][j-1]+1); // A[i]与B[j]匹配不成功
}
printf("%d",f[n][m]);
return 0;
}
应用:
给定 n 个长度不超过 10 的字符串以及 m 次询问,每次问给出一个字符串和一个操作次数上限.
对于每次询问,请你求出给定的n个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
算法思想:多次求编辑距离
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 1010;
int f[15][15];
char str[N][15];
int edit_dist(char a[],char b[]){
int la = strlen(a+1);
int lb = strlen(b+1);
for(int i=0;i<=la;i++) f[i][0] = i; // 边界情况
for(int i=0;i<=lb;i++) f[0][i] = i;
for(int i=1;i<=la;i++)
for(int j=1;j<=lb;j++){
f[i][j] = min(f[i][j-1]+1,f[i-1][j]+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[la][lb];
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++) scanf("%s",str[i]+1);
int limit,res;
while (m--)
{
char s[15];
scanf("%s%d",s+1,&limit);
res = 0;
for(int i=0;i<n;i++)
if(edit_dist(str[i],s)<=limit) res++;
printf("%d\n",res);
}
return 0;
}
3.区间类DP
石子合并:
算法思想:动态规划加分治法。对于区间[L,R],可以从第k个位置分开,合并的代价包含三部分:
(1)合并左侧;(2)合并右侧;(3) 将左右部分合并;k的位置从[l,r]。取代价最小值。
即f[l][r]=min(f[i][k]+f[k+1][r]+s[r]-s[l-1])。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 310;
int s[N]; // 前缀和数组
int f[N][N]; // 递推数组
int main(){
int n; // 石子堆数
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]+s[i-1];
int l,r;
for(int len=2;len<=n;len++)
for(int i=1;i+len-1<=n;i++){
l = i,r=i+len-1;
f[l][r] = 1e8;
for(int k=l;k<r;k++) // 最后一次合并从节点k将区间分为两部分
f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
printf("%d",f[1][n]);
return 0;
}
4.整数划分
算法思想:整数划分可以采用完全背包问题解决,当j<i时,不能选i,此时f[i][j]=f[i-1][j],当j>=i时,f[i][j] = f[i-1][j] + f[i][j-i](注意此时求的是方案数)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
const int M = 1e9+7;
int f[N][N];
int main(){
int n;
scanf("%d",&n);
for(int i=0;i<=n;i++)
f[i][0] = 1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
f[i][j] = f[i-1][j]; // j<i,不能选i
if(j>=i) // j>i,可以选i
f[i][j] = (f[i-1][j] + f[i][j-i]) % M;
}
printf("%d",f[n][n]);
return 0;
}
5.状态压缩DP
多米诺骨牌问题
首先,如果全部用横条填充完整个棋盘,剩下的用竖条填充,方案是确定的。
使用二进制表示每一列的填充情况。当第i-1与第i列两列没有被横向网格填充,且存在两个连续的网格接收横向网格,则填充。此时:f[i][j] += f[i-1][k]。即累加第i-1行的填充结果
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M];
bool st[M];
int main()
{
while (1)
{
scanf("%d%d",&n,&m);
if(n ==0 || m==0) break;
for(int i=0;i< 1<<n;i++){
int cnt = 0;
st[i] = 1;
for(int j=0;j<n;j++)
if(i >>j &1){ // 该位为1
if(cnt & 1) st[i] = 0; // 连续奇数个1
cnt = 0;
}
else cnt++; // 连续0的个数
if(cnt & 1) st[i] = 0; // 最后有奇数个1
}
memset(f, 0, sizeof f);
memset(f, 0, sizeof f);
f[0][0] = 1; // 一种方案,不填
for(int i=1;i<=m;i++)
for(int j=0;j< 1<<n; j++)
for(int k=0;k< 1<<n; k++)
if((j & k)==0 && st[j|k]) f[i][j] += f[i-1][k]; // 意味着没有一列是两行都被填充的且能够找到两列都没有被填充的
cout << f[m][0] << endl;
}
return 0;
}
哈密尔顿通路:
给定一张n 个点的带权无向图,点从0~ n -1标号,求起点0到终点n -1的最短 Hamilton 路径
Hamilton 路径的定义是从 0 到 n-1不重不漏地经过每个点恰好一次。
算法思想:基于动态规划的思想,对于每一次的f[i][j],表示j为当前最后一次经过的节点,首先,要走遍子集i中除去j以外的所有节点,到达节点k,然后再从第k个节点到达第j个节点,即f[i][j]=min(f[i-{j}][k]+w[k][j])
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 20;
const int M = 1 << N;
int G[N][N];
int f[M][N];
int main(){
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
scanf("%d",&G[i][j]);
memset(f,0x3f,sizeof f);
f[1][0] = 0; // 从0号点经过0号点到0
for(int i=0;i< 1<<n;i++) // 用二进制表示当前节点子集
for(int j=0;j<n;j++){
if(i>>j & 1) // 当前子集包含第j个节点
for(int k=0;k<n;k++)
if(i>>k &1) // 子集i中包含节点k
f[i][j] = min(f[i][j],f[i-(1<<j)][k]+G[k][j]);
}
printf("%d",f[(1<<n)-1][n-1]);
return 0;
}