介绍
动态规划是算法中的重点内容。
常见的分析思路
对于动态规划类问题,一般从两个方面进行考虑。一、状态表示,【集合:所有**的集合】、【属性:一般包括最大值、最小值、方案数】二、状态计算【常见考虑方法:①最后一步怎么走,例如:摘花生、数字三角形②最后一件物品选多少件③公共子序列④多注重积累!!】。
经典问题
背包问题
问题一:背包问题(每件物品只能用一次)
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示从前
i
i
i个物品中选总体积不超过
j
j
j的所有选法,属性:Max
- 状态计算:最后一个物品
i
i
i选还是不选
①选第
i
i
i个物品,即
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
?
v
[
i
]
]
+
w
[
i
]
f[i][j]=f[i-1][j-v[i]]+w[i]
f[i][j]=f[i?1][j?v[i]]+w[i](考虑能装下该物品
j
>
=
v
[
i
]
j>=v[i]
j>=v[i]) ②不选第
i
i
i个物品,即
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
]
f[i][j]=f[i-1][j]
f[i][j]=f[i?1][j] 由此得出,状态转移方程为:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
?
1
]
[
j
?
v
[
i
]
]
+
w
[
i
]
,
f
[
i
?
1
]
[
j
]
)
f[i][j]=max(f[i-1][j-v[i]]+w[i],f[i-1][j])
f[i][j]=max(f[i?1][j?v[i]]+w[i],f[i?1][j])
求解代码:二维做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int w[N],v[N],f[N][N];
int n,m;
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++)
{
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] << endl;
}
- 状态
f
[
j
]
f[j]
f[j]定义:
N
N
N 件物品,背包容量
j
j
j下的最优解。
- 注意枚举背包容量
j
j
j必须从
m
m
m开始。
- 为什么一维情况下枚举背包容量需要逆序? 在二维情况下,状态
f
[
i
]
[
j
]
f[i][j]
f[i][j]是由上一轮
i
?
1
i - 1
i?1的状态得来的,
f
[
i
]
[
j
]
f[i][j]
f[i][j]与
f
[
i
?
1
]
[
j
]
f[i - 1][j]
f[i?1][j]是独立的。而优化到一维后,如果我们还是正序,则有
f
[
较
小
体
积
]
f[较小体积]
f[较小体积]更新到
f
[
较
大
体
积
]
f[较大体积]
f[较大体积],则有可能本应该用第
i
?
1
i-1
i?1轮的状态却用的是第
i
i
i轮的状态。
例如,一维状态第
i
i
i轮对体积为 3 的物品进行决策,则
f
[
7
]
f[7]
f[7]由
f
[
4
]
f[4]
f[4]更新而来,这里的
f
[
4
]
f[4]
f[4]正确应该是
f
[
i
?
1
]
[
4
]
f[i - 1][4]
f[i?1][4],但从小到大枚举j这里的
f
[
4
]
f[4]
f[4]在第i轮计算却变成了
f
[
i
]
[
4
]
f[i][4]
f[i][4]。当逆序枚举背包容量
j
j
j时,我们求
f
[
7
]
f[7]
f[7]同样由
f
[
4
]
f[4]
f[4]更新,但由于是逆序,这里的
f
[
4
]
f[4]
f[4]还没有在第
i
i
i轮计算,所以此时实际计算的
f
[
4
]
f[4]
f[4]仍然是
f
[
i
?
1
]
[
4
]
f[i - 1][4]
f[i?1][4]。 - 简单来说,一维情况正序更新状态
f
[
j
]
f[j]
f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。
- 状态转移方程为:
f
[
j
]
=
m
a
x
(
f
[
j
]
,
f
[
j
?
v
[
i
]
]
+
w
[
i
]
)
f[j] = max(f[j], f[j - v[i]] + w[i])
f[j]=max(f[j],f[j?v[i]]+w[i]) 求解代码:一维做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int w[N],v[N],f[N];
int n,m;
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=m;j>=v[i];j--)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
cout << f[m] << endl;
}
关于状态f[j] 的补充说明 二维下的状态定义
f
[
i
]
[
j
]
f[i][j]
f[i][j]是前
i
i
i 件物品,背包容量
j
j
j 下的最大价值。一维下,少了前
i
i
i 件物品这个维度,我们的代码中决策到第
i
i
i 件物品(循环到第i轮),
f
[
j
]
f[j]
f[j]就是前i轮已经决策的物品且背包容量
j
j
j 下的最大价值。 因此当执行完循环结构后,由于已经决策了所有物品,
f
[
j
]
f[j]
f[j]就是所有物品背包容量
j
j
j 下的最大价值。即一维
f
[
j
]
f[j]
f[j]等价于二维
f
[
n
]
[
j
]
f[n][j]
f[n][j]。
问题二:完全背包问题(每种物品可以选无限个)
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示从前
i
i
i个物品中选择,且选出物品的总体积不超过
j
j
j的所有选法
- 属性:Max
- 状态计算:考虑第
i
i
i件物品选多少件
①第
i
i
i件物品选0件,即不选择该物品,则有:
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
]
f[i][j]=f[i-1][j]
f[i][j]=f[i?1][j] ②第
i
i
i件物品选择
k
k
k件,则有:
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
?
k
?
v
[
i
]
]
+
k
?
w
[
i
]
f[i][j]=f[i-1][j-k*v[i]]+k*w[i]
f[i][j]=f[i?1][j?k?v[i]]+k?w[i] 由此得出,状态转移方程为:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
?
1
]
[
j
]
,
f
[
i
?
1
]
[
j
?
k
?
v
[
i
]
]
+
k
?
w
[
i
]
)
f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i])
f[i][j]=max(f[i?1][j],f[i?1][j?k?v[i]]+k?w[i])
求解代码一:朴素做法(TLE)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n,m;
int f[N][N],v[N],w[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++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
cout << f[n][m]<<endl;
}
优化思路:
f
[
i
,
j
]
=
m
a
x
(
f
[
i
?
1
,
j
]
,
f
[
i
?
1
,
j
?
v
]
+
w
,
f
[
i
?
1
,
j
?
2
?
v
]
+
2
?
w
,
f
[
i
?
1
,
j
?
3
?
v
]
+
3
?
w
,
.
.
.
.
.
)
f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i,j]=max(f[i?1,j],f[i?1,j?v]+w,f[i?1,j?2?v]+2?w,f[i?1,j?3?v]+3?w,.....)
f
[
i
,
j
?
v
]
=
m
a
x
(
f
[
i
?
1
,
j
?
v
]
,
f
[
i
?
1
,
j
?
2
?
v
]
+
w
,
f
[
i
?
1
,
j
?
3
?
v
]
+
2
?
w
,
.
.
.
.
.
)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
f[i,j?v]=max( f[i?1,j?v] ,f[i?1,j?2?v]+w, f[i?1,j?3?v]+2?w,.....) 由上两式,可得出如下递推关系:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
?
1
]
[
j
]
,
f
[
i
,
j
?
v
]
+
w
)
f[i][j]=max(f[i-1][j],f[i,j-v]+w)
f[i][j]=max(f[i?1][j],f[i,j?v]+w)
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][j-v[i]]+w[i]);
}
}
对比01背包问题的核心代码:
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]>=0) f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
两者的差异 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>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N],f[N][N];
int n,m;
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++)
{
f[i][j]=f[i-1][j];
if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
}
cout << f[n][m] <<endl;
}
对二维进行优化:
for(int i = 1 ; i<=n ;i++)
{
for(int j = v[i] ; j<=m ;j++)
{
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
}
求解代码三:优化代码(一维)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int v[N],w[N],f[N][N];
int n,m;
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++)
{
f[i][j]=f[i-1][j];
if(j>=v[i])
f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
}
}
cout << f[n][m] << endl;
}
问题三:多重背包问题Ⅰ(每件物品最多选s[i]件)
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示从前
i
i
i个物品中选择,且选出物品的总体积不超过
j
j
j的所有选法
- 属性:Max
- 状态计算:考虑第
i
i
i个物品有多少个来划分.含
0
0
0个、含
1
1
1个···含
k
k
k个.
①第
i
i
i件物品选0件,即不选择该物品,则有:
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
]
f[i][j]=f[i-1][j]
f[i][j]=f[i?1][j] ②第
i
i
i件物品选择
k
k
k件,则有:
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
?
k
?
v
[
i
]
]
+
k
?
w
[
i
]
f[i][j]=f[i-1][j-k*v[i]]+k*w[i]
f[i][j]=f[i?1][j?k?v[i]]+k?w[i] 由此得出,状态转移方程为: 状态表示与完全背包朴素代码一样
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
?
1
]
[
j
]
,
f
[
i
?
1
]
[
j
?
k
?
v
[
i
]
]
+
k
?
w
[
i
]
)
f[i][j]=max(f[i-1][j],f[i-1][j-k*v[i]]+k*w[i])
f[i][j]=max(f[i?1][j],f[i?1][j?k?v[i]]+k?w[i])
求解代码一:朴素解法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N],w[N],s[N],f[N][N];
int n,m;
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*v[i]<=j && k<=s[i];k++)
f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
cout << f[n][m];
}
思路 将多重背包问题进行拆解,拆解为每件物品的体积为
a
[
i
]
a[i]
a[i],价值为
b
[
i
]
b[i]
b[i]的01背包
求解代码二:多重背包拆解为01背包
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int a[N],b[N];
int f[N];
int n,m,v,w,s;
int main()
{
cin >> n >> m;
int t=0;
while (n -- )
{
cin >> v >> w >> s;
while(s--)
{
a[++t]=v;
b[t]=w;
}
}
for(int i=1;i<=t;i++)
for(int j=m;j>=a[i];j--)
f[j]=max(f[j],f[j-a[i]]+b[i]);
cout << f[m];
}
求解代码三:上一种解法的优化版本
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int f[N];
int n,m,v,w,s;
int main()
{
cin >> n >> m;
while (n -- )
{
cin >> v >> w >> s;
for(int i=1;i<=s;i++)
for(int j=m;j>=v;j--)
f[j]=max(f[j],f[j-v]+w);
}
cout << f[m];
}
问题四:多重背包问题Ⅱ
f
[
i
,
j
]
=
m
a
x
(
f
[
i
?
1
,
j
]
,
f
[
i
?
1
,
j
?
v
]
+
w
,
f
[
i
?
1
,
j
?
2
v
]
+
2
?
w
,
.
.
.
.
.
f
[
i
?
1
,
j
?
S
?
v
]
+
S
?
w
,
)
f[i , j ] = max( f[i-1,j] ,f[i-1,j-v]+w ,f[i-1,j-2v]+2*w ,..... f[i-1,j-S*v]+S*w, )
f[i,j]=max(f[i?1,j],f[i?1,j?v]+w,f[i?1,j?2v]+2?w,.....f[i?1,j?S?v]+S?w,)
f
[
i
,
j
?
v
]
=
m
a
x
(
f
[
i
?
1
,
j
?
v
]
,
f
[
i
?
1
,
j
?
2
v
]
+
w
,
.
.
.
.
.
f
[
i
?
1
,
j
?
S
?
v
]
+
(
S
?
1
)
?
w
,
f
[
i
?
1
,
j
?
(
S
+
1
)
?
v
]
+
S
?
w
)
f[i , j-v]= max( f[i-1,j-v] , f[i-1,j-2v]+w, ..... f[i-1,j-S*v]+(S-1)*w, f[i-1,j-(S+1)*v]+S*w )
f[i,j?v]=max( f[i?1,j?v], f[i?1,j?2v]+w, .....f[i?1,j?S?v]+(S?1)?w,f[i?1,j?(S+1)?v]+S?w) 可以发现,比完全背包方程比较就多出了一项 为什么会出现上述情况呢?这是因为完全背包由于对每种物品没有选择个数的限制,所以只要体积够用就可以一直选,没有最后一项。
介绍二进制优化
关键:用二进制取出来的结果和一次一次问是完全一样的
二进制优化:假设有一组商品,一共有11个。我们知道,十进制数字 11 可以这样表示:
11
=
1011
(
B
)
=
0111
(
B
)
+
(
11
?
0111
(
B
)
)
=
0111
(
B
)
+
0100
(
B
)
11=1011(B)=0111(B)+(11?0111(B))=0111(B)+0100(B)
11=1011(B)=0111(B)+(11?0111(B))=0111(B)+0100(B)正常背包的思路下,我们要求出含这组商品的最优解,我们要枚举12次(枚举装0,1,2…12个)。
现在,如果我们把这11个商品分别打包成含商品个数为1个,2个,4个,4个(分别对应0001,0010,0100,0100)的四个”新的商品 “, 将问题转化为01背包问题,对于每个商品,我们都只枚举一次,那么我们只需要枚举四次 ,就可以找出这含组商品的最优解。 这样就大大减少了枚举次数。
这种优化对于大数尤其明显,例如有1024个商品,在正常情况下要枚举1025次 , 二进制思想下转化成01背包只需要枚举10次。
二进制合理性的证明
其中0111通过枚举这三个1的取或不取(也就是对0001(B),0010(B),0100(B)的组合),可以表示十进制数0~7( 刚好对应了 1,2,4 可以组合出0~7 ) , 0~7的枚举再组合上0100(B)( 即 十进制的 4 ) ,可以表示十进制数 0~11。其它情况也可以这样证明。
怎么合理划分一个十进制数
是因为 0111(B)刚好是小于11的最大的尾部全为1的二进制 ( 按照上面的证明,这样的划分没毛病 ) , 然后那个尾部全为1的数又可以 分解为 0000…1 , 0000…10 , 0000…100 等等。
对应的C++代码:
for(int k = 1 ; k <= s ;k*=2)
{
s-=k;
goods.push_back({v*k,w*k});
}
if(s>0)
goods.push_back({v*s,w*s});
问题求解
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示从前
i
i
i个物品中选择,且选出物品的总体积不超过
j
j
j的所有选法
- 属性:Max
- 状态计算:01背包的思想
f
[
j
]
=
m
a
x
(
f
[
j
]
,
f
[
j
?
v
[
i
]
]
+
w
[
i
]
)
f[j]=max(f[j],f[j-v[i]]+w[i])
f[j]=max(f[j],f[j?v[i]]+w[i])
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int v[N],w[N],f[N];
int 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;
for(int k=1;k<=s;k*=2)
{
v[++cnt]=a*k;
w[cnt]=b*k;
s-=k;
}
if(s>0)
{
v[++cnt]=s*a;
w[cnt]=s*b;
}
}
n=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] << endl;
}
问题五:分组背包问题
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示只从前
i
i
i组物品中选,每组从前
k
k
k个物品中选,且总体积不超过
j
j
j的所有选法的集合
- 属性:Max
- 状态计算:选不选第
i
i
i组物品中第
k
k
k件物品
①不选,表示只从前
i
?
1
i-1
i?1组物品中选,且总体积不超过
j
j
j,则有:
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
]
f[i][j]=f[i-1][j]
f[i][j]=f[i?1][j] ②选,则有:
f
[
i
]
[
j
]
=
f
[
i
?
v
[
i
]
[
k
]
]
+
w
[
i
]
[
k
]
f[i][j]=f[i-v[i][k]]+w[i][k]
f[i][j]=f[i?v[i][k]]+w[i][k] 由此得出,状态转移方程为 :
f
[
j
]
=
m
a
x
(
f
[
j
]
,
f
[
j
?
v
[
i
]
[
k
]
]
+
w
[
i
]
[
k
]
)
f[j]=max(f[j],f[j-v[i][k]]+w[i][k])
f[j]=max(f[j],f[j?v[i][k]]+w[i][k])
求解代码:一维做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N][N],w[N][N],s[N],f[N];
int n,m;
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)
{
cin >> s[i];
for(int k=0;k<s[i];k++)
cin >> v[i][k] >> w[i][k];
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
for(int k=0;k<s[i];k++)
if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
}
}
cout << f[m] << endl;
}
求解代码:二维做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int v[N][N],w[N][N],s[N],f[N][N];
int n,m;
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)
{
cin >> s[i];
for(int k=0;k<s[i];k++)
cin >> v[i][k] >> w[i][k];
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)
{
f[i][j]=f[i-1][j];
for(int k=0;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;
}
线性DP
问题一:数字三角形
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示从起点走到
(
i
,
j
)
(i,j)
(i,j)这一点所有走法的最大值
- 状态计算:考虑最后一步怎么走。例如,从下到上,可以是从
f
[
i
+
1
]
[
j
]
f[i+1][j]
f[i+1][j],也可以是从
f
[
i
+
1
]
[
j
+
1
]
f[i+1][j+1]
f[i+1][j+1]过渡到下一个状态。因此有如下的代码实现:
求解代码一:从下到上
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n;
int f[N][N],w[N][N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin >> w[i][j];
for(int i=1;i<=n;i++) f[n][i]=w[n][i];
for(int i=n-1;i>=1;i--)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i+1][j],f[i+1][j+1])+w[i][j];
cout << f[1][1] <<endl;
}
求解代码二:从上到下
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010;
int n;
int f[N][N],w[N][N];
int main()
{
cin >> n;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
cin >> w[i][j];
for(int i=0;i<=n;i++)
{
f[i][0]=-1e9;
f[i][i+1]=-1e9;
}
f[1][1]=w[1][1];
for(int i=1;i<=n;i++)
{
f[i][0]=-2e9;
f[i][i+1]=-2e9;
}
for(int i=2;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=max(f[i-1][j],f[i-1][j-1])+w[i][j];
int res=-1e9;
for(int i=1;i<=n;i++) res=max(res,f[n][i]);
cout << res << endl;
}
问题二:摘花生
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:从起点走到该点
(
i
,
j
)
(i,j)
(i,j)经过的所有格子的价值总和的最大值
- 状态计算:考虑最后一步怎么走。对于在当前位置如何选择,只能向下走,即
f
[
i
?
1
]
[
j
]
f[i-1][j]
f[i?1][j],只能向右走,即
f
[
i
]
[
j
?
1
]
f[i][j-1]
f[i][j?1],进而过渡到下一个状态。因此有如下的代码实现:
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int f[N][N],w[N][N];
int T;
int main()
{
cin >> T;
while(T--)
{
int m,n;
cin >> n >> m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin >> w[i][j];
for(int i=0;i<=n;i++) f[i][0]=0,f[0][i]=0;
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])+w[i][j];
cout << f[n][m] <<endl;
}
}
问题三:最长上升子序列
-
f
[
i
]
f[i]
f[i]:表示从第一个数字开始算,以
a
[
i
]
a[i]
a[i]结尾的最大的上升序列。(以
a
[
i
]
a[i]
a[i]结尾的所有上升序列中属性为最大值的那一个)
- 状态计算:
①只有该数自己,
f
[
i
]
=
1
f[i]=1
f[i]=1 ②考虑倒数第二个数
a
[
j
]
a[j]
a[j],若
a
[
j
]
<
a
[
i
]
a[j]<a[i]
a[j]<a[i]且满足
j
<
i
j<i
j<i的前提下,
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
j
]
+
1
)
f[i]=max(f[i],f[j]+1)
f[i]=max(f[i],f[j]+1)
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int a[N],f[N];
int 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;
for(int j=1;j<i;j++)
{
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);
}
}
int res=-1e9;
for(int i=1;i<=n;i++) res=max(res,f[i]);
cout << res << endl;
}
问题四:最长上升子序列Ⅱ
算法思想:找到一个最大的小于等于当前数的数, 我们可以用 二分 来优化。 二分思路
- 先定义边界,
l=0, r=len , 其中len 是q 数组的长度 - 然后确定
check 函数, 可以先找到不等式中c<x≤a≤b 的c - 通过
q[r+1]=a[i] 来将x 覆盖a 的值 - 同时也要考虑算法1的情况1, 需要扩大
q 数组的长度 - 即
r+1>len 时, 表示超出了二分边界,这时就要len++ 更新q 的长度
解题思想:
要求串a[0...i] 的 LIS ,就要知道:串a[0...i-1] 中各长度的上升子序列末尾元素的最小值。
可以用一个q 数组来存储,q[i] 是所有长度为i的上升子序列末尾元素的最小值。这个数组是严格单调递增的(原因不赘述),所以每次只要用二分查找,在
O
(
l
o
g
n
)
O(logn)
O(logn)的时间内就能从串a[0...i-1] 对应的p数组求得串a[0...i] 的 LIS ,完成状态转移。
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N],q[N];
int n;
int main()
{
cin >> n;
for(int i=0;i<n;i++) cin>>a[i];
int len=0;
q[0]=-2e9;
for(int i=0;i<n;i++)
{
int l=0,r=len;
while(l<r)
{
int mid=l+r+1>>1;
if(q[mid]<a[i]) l=mid;
else r=mid-1;
}
len=max(len,r+1);
q[r+1]=a[i];
}
cout << len<<endl;
}
问题五:最长公共子序列
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:字符串A的前
i
i
i个字母与字符串B的前
j
j
j个字母相同的字母的最长公共子序列的长度。
- 状态计算:
①
a
[
i
]
=
=
b
[
j
]
a[i]==b[j]
a[i]==b[j],
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
?
1
]
+
1
f[i][j]=f[i-1][j-1]+1
f[i][j]=f[i?1][j?1]+1 ②
a
[
i
]
!
=
b
[
j
]
a[i]!=b[j]
a[i]!=b[j],
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
?
1
]
[
j
]
,
f
[
i
]
[
j
?
1
]
)
+
1
f[i][j]=max(f[i-1][j],f[i][j-1])+1
f[i][j]=max(f[i?1][j],f[i][j?1])+1 解释:不相等的话,两个字符一定有一个可以抛弃,可以对
f
[
i
?
1
]
[
j
]
f[i-1][j]
f[i?1][j],
f
[
i
]
[
j
?
1
]
f[i][j-1]
f[i][j?1]两种状态取max来转移。 求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1100;
char a[N],b[N];
int n,m;
int f[N][N];
int main()
{
cin >> n >> m >> 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]);
if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
}
}
cout << f[n][m] << endl;
}
问题六:最短编辑距离
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示
A
A
A字符串中的前
i
i
i个字符变成
B
B
B字符串中的前
j
j
j个字符至少经过操作数的集合,即将
a
[
1
?
i
]
a[1~i]
a[1?i]变成
b
[
1
?
j
]
b[1~j]
b[1?j]的所有操作方式的最小值。
- 状态计算:
①删除操作:把
a
[
i
]
a[i]
a[i]删掉之后
a
[
1
?
i
]
a[1~i]
a[1?i]和
b
[
1
?
j
]
b[1~j]
b[1?j]匹配 所以之前要先做到
a
[
1
?
(
i
?
1
)
]
a[1~(i-1)]
a[1?(i?1)]和
b
[
1
?
j
]
b[1~j]
b[1?j]匹配
f
[
i
?
1
]
[
j
]
+
1
f[i-1][j] + 1
f[i?1][j]+1 ②插入操作:插入之后
a
[
i
]
a[i]
a[i]与
b
[
j
]
b[j]
b[j]完全匹配,所以插入的就是b[j] 那填之前
a
[
1
?
i
]
a[1~i]
a[1?i]和
b
[
1
?
(
j
?
1
)
]
b[1~(j-1)]
b[1?(j?1)]匹配
f
[
i
]
[
j
?
1
]
+
1
f[i][j-1] + 1
f[i][j?1]+1 ③替换操作:把
a
[
i
]
a[i]
a[i]改成
b
[
j
]
b[j]
b[j]之后想要
a
[
1
?
i
]
a[1~i]
a[1?i]与
b
[
1
?
j
]
b[1~j]
b[1?j]匹配 那么修改这一位之前,
a
[
1
?
(
i
?
1
)
]
a[1~(i-1)]
a[1?(i?1)]应该与
b
[
1
?
(
j
?
1
)
]
b[1~(j-1)]
b[1?(j?1)]匹配
f
[
i
?
1
]
[
j
?
1
]
+
1
f[i-1][j-1] + 1
f[i?1][j?1]+1 但是如果本来
a
[
i
]
a[i]
a[i]与
b
[
j
]
b[j]
b[j]这一位上就相等,那么不用改,即
f
[
i
?
1
]
[
j
?
1
]
+
0
f[i-1][j-1] + 0
f[i?1][j?1]+0 求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
char a[N],b[N];
int n,m;
int f[N][N];
int main()
{
cin >> n >> a+1 >> m >> b+1;
for(int i=1;i<=n;i++) f[i][0]=i;
for(int i=1;i<=m;i++) f[0][i]=i;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
f[i][j]=min(f[i-1][j],f[i][j-1])+1;
f[i][j]=min(f[i][j],f[i-1][j-1]+(a[i]!=b[j]));
}
}
cout << f[n][m] << endl;
}
问题七:编辑距离
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示
A
A
A字符串中的前
i
i
i个字符变成
B
B
B字符串中的前
j
j
j个字符至少经过操作数的集合,即将
a
[
1
?
i
]
a[1~i]
a[1?i]变成
b
[
1
?
j
]
b[1~j]
b[1?j]的所有操作方式的最小值。
- 状态计算:
①删除操作:把
a
[
i
]
a[i]
a[i]删掉之后
a
[
1
?
i
]
a[1~i]
a[1?i]和
b
[
1
?
j
]
b[1~j]
b[1?j]匹配 所以之前要先做到
a
[
1
?
(
i
?
1
)
]
a[1~(i-1)]
a[1?(i?1)]和
b
[
1
?
j
]
b[1~j]
b[1?j]匹配
f
[
i
?
1
]
[
j
]
+
1
f[i-1][j] + 1
f[i?1][j]+1 ②插入操作:插入之后
a
[
i
]
a[i]
a[i]与
b
[
j
]
b[j]
b[j]完全匹配,所以插入的就是b[j] 那填之前
a
[
1
?
i
]
a[1~i]
a[1?i]和
b
[
1
?
(
j
?
1
)
]
b[1~(j-1)]
b[1?(j?1)]匹配
f
[
i
]
[
j
?
1
]
+
1
f[i][j-1] + 1
f[i][j?1]+1 ③替换操作:把
a
[
i
]
a[i]
a[i]改成
b
[
j
]
b[j]
b[j]之后想要
a
[
1
?
i
]
a[1~i]
a[1?i]与
b
[
1
?
j
]
b[1~j]
b[1?j]匹配 那么修改这一位之前,
a
[
1
?
(
i
?
1
)
]
a[1~(i-1)]
a[1?(i?1)]应该与
b
[
1
?
(
j
?
1
)
]
b[1~(j-1)]
b[1?(j?1)]匹配
f
[
i
?
1
]
[
j
?
1
]
+
1
f[i-1][j-1] + 1
f[i?1][j?1]+1 但是如果本来
a
[
i
]
a[i]
a[i]与
b
[
j
]
b[j]
b[j]这一位上就相等,那么不用改,即
f
[
i
?
1
]
[
j
?
1
]
+
0
f[i-1][j-1] + 0
f[i?1][j?1]+0 求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
char g[N][N];
int f[N][N];
int n,m;
int getdistance(char a[],char b[])
{
int lena=strlen(a+1);
int lenb=strlen(b+1);
for(int i=1;i<=lena;i++) f[i][0]=i;
for(int i=1;i<=lenb;i++) f[0][i]=i;
for(int i=1;i<=lena;i++)
for(int j=1;j<=lenb;j++)
{
f[i][j]=min(f[i-1][j],f[i][j-1])+1;
f[i][j]=min(f[i][j],f[i-1][j-1]+(a[i]!=b[j]));
}
return f[lena][lenb];
}
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> g[i]+1;
while (m -- )
{
char a[N];
int limit;
cin >> a+1 >> limit;
int res=0;
for(int i=1;i<=n;i++)
{
if(getdistance(g[i],a)<=limit)
res++;
}
cout << res << endl;
}
}
区间DP
问题一:石子合并
题意: 合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价 问题的核心:最后一次合并一定是左边连续的一部分和右边连续的一部分进行合并
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示将
i
i
i 到
j
j
j 合并成一堆的方案的集合,属性 Min
- 状态计算:
(1)
i
<
j
i<j
i<j 时,
f
[
i
]
[
j
]
f[i][j]
f[i][j]=
min
?
i
≤
k
≤
j
?
1
f
[
i
]
[
k
]
+
f
[
k
+
1
]
[
j
]
+
s
[
j
]
?
s
[
i
?
1
]
\underset{i≤k≤j?1}{\min}f[i][k]+f[k+1][j]+s[j]?s[i?1]
i≤k≤j?1min?f[i][k]+f[k+1][j]+s[j]?s[i?1] (2)
i
=
j
i=j
i=j 时,
f
[
i
]
[
i
]
=
0
f[i][i]=0
f[i][i]=0 (合并一堆石子代价为 0) 问题的答案:
f
[
1
]
[
n
]
f[1][n]
f[1][n]
区间DP问题常见的分析套路: 所有的区间dp问题,第一维都是枚举区间长度,一般
l
e
n
=
1
len = 1
len=1 用来初始化,枚举从
l
e
n
=
2
len = 2
len=2 开始,第二维枚举起点
i
i
i (右端点
j
j
j 自动获得,
j
=
i
+
l
e
n
?
1
j = i + len - 1
j=i+len?1)
for (int i = 1; i <= n; i++) {
dp[i][i] = 初始值
}
for (int len = 2; len <= n; len++)
for (int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) {
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
}
}
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
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]=2e9;
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] << endl;
}
计数类DP
问题一:整数划分
思路:
把1,2,3, … n分别看做n个物体的体积,这n个物体均无使用次数限制,问恰好能装满总体积为n的背包的总方案数(完全背包问题变形)
初值:
- 求最大值时,当都不选时,价值显然是0
- 求方案数时,当都不选时,方案数是1(即前 i 个物品都不选的情况也是一种方案),所以需要初始化为1
即:for (int i = 0; i <= n; i ++) f[i][0] = 1; 等价变形后: f[0] = 1
状态计算:
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示前
i
i
i个整数
(
1
,
2
…
,
i
)
(1,2…,i)
(1,2…,i)恰好拼成
j
j
j的方案数
- 求方案数:把集合选0个i,1个i,2个i,…全部加起来
①选
0
0
0个,
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
]
f[i][j]=f[i-1][j]
f[i][j]=f[i?1][j] ②选
k
k
k个,
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
?
s
?
i
]
f[i][j]=f[i-1][j-s*i]
f[i][j]=f[i?1][j?s?i] f[i][j] = f[i - 1][j] + f[i - 1][j - i] + f[i - 1][j - 2 * i] + ...; f[i][j - i] = f[i - 1][j - i] + f[i - 1][j - 2 * i] + ...; - 因此,有
f
[
i
]
[
j
]
=
f
[
i
?
1
]
[
j
]
+
f
[
i
]
[
j
?
i
]
f[i][j]=f[i?1][j]+f[i][j?i]
f[i][j]=f[i?1][j]+f[i][j?i]
求解代码:二维做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010,MOD=1e9+7;
int f[N][N];
int 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]%MOD;
if(j>=i) f[i][j]=(f[i-1][j]+f[i][j-i])%MOD;
}
}
cout << f[n][n];
}
求解代码:一维做法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010,MOD=1e9+7;
int f[N];
int 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];
}
数位统计DP
问题一:计数问题
状态压缩DP
问题一:蒙德里安的梦想
状态<=>二进制数
摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的合法方案数。
如何判断,当前方案数是否合法? 所有剩余位置能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块需要是偶数个。
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示已经将前
i
?
1
i -1
i?1 列摆好,且从第
i
?
1
i?1
i?1列,伸出到第
i
i
i 列的状态是
j
j
j 的所有方案
- 状态计算:枚举第
i
?
1
i-1
i?1的状态,只考虑横向摆放的格子
①伸出的行不能冲突,即 (
j
j
j &
k
k
k)==0,表示 k和j没有1位相同, 即没有1行有冲突。 ②第
i
?
1
i-1
i?1列剩余的连续空白格子的个数为偶数,保证能放下纵向摆放的格子,
j
j
j |
k
k
k不存在连续奇数个0。 状态转移方程为:
f
[
i
]
[
j
]
+
=
f
[
i
?
1
]
[
k
]
f[i][j]+=f[i-1][k]
f[i][j]+=f[i?1][k] 答案是:f[m][0] ,即前m-1列全部摆好,且从第m-1列到m列状态是0(意即从第m-1列到第m列没有伸出来的)的所有方案,即整个棋盘全部摆好的方案。
求解代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 12,M=1<<N;
long long f[N][M];
bool st[M];
int n,m;
int main()
{
while(cin >> n >> m,n||m)
{
memset(f,0,sizeof f);
for(int i=0;i<1<<n;i++)
{
st[i]=true;
int cnt=0;
for(int j=0;j<n;j++)
{
if(i>>j&1)
{
if(cnt&1)
{
st[i]=false;
}
else cnt=0;
}
else cnt++;
}
if(cnt&1) st[i]=false;
}
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;
}
}
问题二:最短Hamilton路径
-
f
[
i
]
[
j
]
f[i][j]
f[i][j]:表示所有从0走到
j
j
j,走过的所有点的情况是
i
i
i的所有路径.例如,走0,1,2,4四个点,
i
i
i表示为10111。
- 属性:Min
- 状态计算:根据倒数第二个点经过分别是0、1、2、…、n-1
即路径为:0--> k --> j 状态转移方程为:
f
[
i
]
[
j
]
=
m
i
n
(
f
[
i
]
[
j
]
,
f
[
i
?
j
]
[
k
]
+
w
[
k
]
[
j
]
)
f[i][j]=min(f[i][j],f[i-{j}][k]+w[k][j])
f[i][j]=min(f[i][j],f[i?j][k]+w[k][j])
求解代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 21,M=1<<N;
int f[M][N];
int w[N][N];
int n;
int main()
{
cin >> n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin >> w[i][j];
memset(f,0x3f,sizeof f);
f[1][0]=0;
for(int i=0;i<1<<n;i++)
for(int j=0;j<n;j++)
if(i>>j&1)
for(int k=0;k<n;k++)
if((i-(1<<j))>>k&1)
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
cout << f[(1<<n)-1][n-1] <<endl;
}
树形DP
问题一:没有上司的舞会
求解代码:
记忆化搜索
滑雪
|