介绍
动态规划是算法中的重点内容。本博客从动态规划常见的模板题出发,对多类型问题进行剖析,同时给出相应的代码,希望能对您有所帮助!求个三连呀
常见的分析思路
对于动态规划类问题,一般从两个方面进行考虑。一、状态表示,【集合:所有**的集合】、【属性:一般包括最大值、最小值、方案数】二、状态计算【常见考虑方法:①最后一步怎么走,例如:摘花生、数字三角形②最后一件物品选多少件③公共子序列④多注重积累!!】。
经典问题
一、背包问题
1.1 问题一:背包问题(每件物品只能用一次)
- 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])//只有能装得下第i件物品时,才进行求max
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]。
1.2 问题二:完全背包问题(每种物品可以选无限个)
- 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++)//这里的j是从小到大枚举,和01背包不一样
{
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];
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=v[i];j<=m;j++)
{
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
cout << f[m] <<endl;
}
1.3 问题三:多重背包问题Ⅰ(每件物品最多选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--)//直接拆,把多重背包拆解为01背包
{
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];
}
1.4 问题四:多重背包问题Ⅱ
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++代码:
//设有s个商品,也就是将s划分
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;
}
1.5 问题五:分组背包问题
- 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
2.1 问题一:数字三角形
- 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;
}
2.2 问题二:摘花生
- 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;
}
}
2.3 问题三:最长上升子序列
- 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;
}
2.4 问题四:最长上升子序列Ⅱ
算法思想:找到一个最大的小于等于当前数的数, 我们可以用 二分 来优化。
二分思路
- 先定义边界,
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++)// 从短到长解决各个子串的 LIS
{
int l=0,r=len;//二分搜索得到能接在a[i]前面的上升子序列的最大长度l
while(l<r)//二分找到最大的小于当前数x的数c
{
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];//将本次发现的 LIS 的末尾元素赋值给q[l+1]
}
cout << len<<endl;
}
2.5 问题五:最长公共子序列
- 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;
}
2.6 问题六:最短编辑距离
- 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;
}
2.7 问题七:编辑距离
- 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
3.1 问题一:石子合并
题意: 合并 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−1minf[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++)//len表示i和j堆下标的差值
{
for(int i=1;i+len-1<=n;i++)//左端点从1开始,右端点小于等于n
{
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
4.1 问题一:整数划分
思路:
把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;// 容量为0时,前 i 个物品全不选也是一种方案
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
5.1 问题一:计数问题
n
=
a
b
c
d
e
f
g
n=abcdefg
n=abcdefg
1 < = x x x 1 y y y < = a b c d e f g 1<=xxx1yyy<=abcdefg 1<=xxx1yyy<=abcdefg
- C a s e 1 : x x x = 000 ∼ a b c − 1 , y y y = 000 ∼ 999 此 时 方 案 数 : a b c ∗ 1000 Case 1:xxx=000 \sim abc-1,yyy=000 \sim 999 此时方案数:abc*1000 Case1:xxx=000∼abc−1,yyy=000∼999此时方案数:abc∗1000
- C a s e 2 : x x x = a b c Case 2:xxx=abc Case2:xxx=abc
- C a s e 2.1 : d < 1 , a b c 1 y y y > a b c 0 e f g 此 时 方 案 数 : 0 Case2.1:d<1,abc1yyy>abc0efg此时方案数:0 Case2.1:d<1,abc1yyy>abc0efg此时方案数:0
- C a s e 2.2 : d = 1 , y y y = 000 ∼ e f g 此 时 的 方 案 数 : e f g + 1 Case2.2:d=1,yyy=000 \sim efg 此时的方案数:efg+1 Case2.2:d=1,yyy=000∼efg此时的方案数:efg+1
- C a s e 2.3 : d > 1 , y y y = 000 ∼ 999 此 时 的 方 案 数 : 10000 Case2.3:d>1,yyy=000 \sim 999此时的方案数:10000 Case2.3:d>1,yyy=000∼999此时的方案数:10000
- f [ i ] [ j ] f[i][j] f[i][j]:
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int get(vector<int> num,int l,int r)
{
int res=0;
for(int i=l;i>=r;i--)
{
res=res*10+num[i];
}
return res;
}
int power10(int x)
{
int res=1;
while(x--) res*=10;
return res;
}
int count(int n,int x)
{
if(!n) return 0;
vector<int> num;
while(n)
{
num.push_back(n%10);
n/=10;
}
n=num.size();
int res=0;
for(int i=n-1-!x;i>=0;i--)//-!x 最高位不能取0,从下一位开始取
{
if(i<n-1)//Case 1
{
res+=get(num,n-1,i+1)*power10(i);
if(!x) res-=power10(i);//当x=0时,从001~abc-1
}
if(num[i]==x)//Case 2.2
{
res+=get(num,i-1,0)+1;
}
else if(num[i]>x) res+=power10(i);//Case 2.3
}
return res;
}
int main()
{
int a,b;
while(cin >> a >> b,a||b)
{
if(a>b) swap(a,b);
for(int i=0;i<10;i++)
cout << count(b,i)-count(a-1,i) <<' ';
cout << endl;
}
}
六、状态压缩DP
6.1 问题一:蒙德里安的梦想
状态<=>二进制数
摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的合法方案数。
如何判断,当前方案数是否合法? 所有剩余位置能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块需要是偶数个。
- 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];//存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。
int n,m;
int main()
{
while(cin >> n >> m,n||m)
{
memset(f,0,sizeof f);//全部初始化为0,因为是连续读入,这里是一个清空操作。类似上面的state[j].clear()
//第一部分:预处理1
//对于每种状态,先预处理每列不能有奇数个连续的0
for(int i=0;i<1<<n;i++)
{
st[i]=true;
int cnt=0;//记录连续的0的个数
for(int j=0;j<n;j++)//遍历这一列,从上到下
{
if(i>>j&1)//i>>j位运算,表示i(i在此处是一种状态)的二进制数的第j位; &1为判断该位是否为1,如果为1进入if
{
if(cnt&1)//这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
{
st[i]=false;
}
else cnt=0;// 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。//其实清不清零没有影响
}
else cnt++;//否则的话该位还是0,则统计连续0的计数器++。
}
if(cnt&1) st[i]=false;// 扫完后要判断一下最后一段有多少个连续的0
}
f[0][0]=1;// 这里需要回忆状态表示的定义,按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
//首先,这里没有-1列,最少也是0列。其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。
for(int i=1;i<=m;i++)//遍历每一列:第i列合法范围是(0~m-1列)
{
for(int j=0;j<1<<n;j++)//遍历当前列(第i列)所有状态j
{
for(int k=0;k<1<<n;k++)// 遍历第i-1列的状态k,如果“真正”可行,就转移
{
if((j&k)==0 && st[j|k])//第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
f[i][j]+=f[i-1][k];// 当前列的方案数就等于之前的第i-1列所有状态k的累加。
//解释一下st[j | k]
//已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
//我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,还要考虑自己这一列(i-1列)横插到第i列的
//比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
//那么合在第i-1列,到底有多少个1呢?自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
//这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
}
}
}
cout << f[m][0] << endl;
//f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
//即整个棋盘处理完的方案数
}
}
6.2 问题二:最短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;//零是起点,所以f[1][0]=0;
for(int i=0;i<1<<n;i++)//i表示所有的情况,i代表着是一个方案集合,其中每一个位置1和0,代表着这个点经过还是没有经过
for(int j=0;j<n;j++)//j表示当前走到哪一个点
if(i>>j&1)//如果i集合中第j位是1,也就是到达过这个点
for(int k=0;k<n;k++)//枚举到达j的点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;//表示所有点都走过了,且终点是n-1的最短距离
//位运算的优先级低于'+'-'所以有必要的情况下要打括号
}
七、树形DP
7.1 问题一:没有上司的舞会
状态机模型
- 状态表示:
f
(
u
,
0
)
f(u,0)
f(u,0):所有以
u
u
u为根的子树中,并且不选
u
u
u这个点的方案
f ( u , 1 ) f(u,1) f(u,1):所有以 u u u为根的子树中,选 u u u这个点的方案 - 状态计算:
f ( u , 0 ) = Σ i = s o n s max ( f ( s i , 0 ) , f ( s i , 1 ) ) f\left( u,0 \right) =\underset{i=sons}{\varSigma}\max \left( f\left( s_i,0 \right) ,f\left( s_i,1 \right) \right) f(u,0)=i=sonsΣmax(f(si,0),f(si,1))当前结点不选,子结点任意 f ( u , 1 ) = Σ i = s o n s f ( s i , 0 ) f\left( u,1 \right) =\underset{i=sons}{\varSigma}f\left( s_i,0 \right) f(u,1)=i=sonsΣf(si,0) 当前节点选,子节点不能选
求解代码:
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 6010;
int n;
int h[N], e[N], ne[N], idx;
int happy[N];
int f[N][2];
bool has_fa[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
f[u][1] = happy[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][1] += f[j][0];
f[u][0] += max(f[j][0], f[j][1]);
}
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &happy[i]);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
add(b, a);
has_fa[a] = true;
}
int root = 1;
while (has_fa[root]) root ++ ;
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));
return 0;
}
八、记忆化搜索
8.1 问题一:滑雪
是一个拓扑图、不能存在环
- f [ i ] [ j ] f[i][j] f[i][j]:所有从 ( i , j ) (i,j) (i,j)开始滑的路径
- 属性:Max
- 状态计算:第一步从哪个方向开始滑分为四类:👈、👉、👇、👆
例如向右滑: ( i , j ) (i,j) (i,j)-> ( i , j + 1 ) (i,j+1) (i,j+1)->… 即: f [ i ] [ j + 1 ] + 1 f[i][j+1]+1 f[i][j+1]+1
求解代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 310;
int h[N][N];
int f[N][N];
int n,m;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int dp(int x,int y)
{
int &v=f[x][y];
if(v!=-1) return v;
v=1;//v的最小值为1,走当前格子
for(int i=0;i<4;i++)
{
int a=x+dx[i],b=y+dy[i];
if(a>=1 && a<=n && b>=1 && b<=m && h[a][b]<h[x][y])
v=max(v,dp(a,b)+1);
}
return v;
}
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin >> h[i][j];
memset(f,-1,sizeof f);
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
res=max(res,dp(i,j));
cout << res << endl;
}