动态规划问题2
最长升子序问题
问题:有一个长度为n的数列 a 0 , a 1 , ⋯ , a n − 1 a_0,a_1,\cdots,a_{n-1} a0,a1,⋯,an−1.请求出这个序列中最长的上升子序列的长度. 上升子序列是指对于任意的 i < j i<j i<j都满足 a i < a j a_i < a_j ai<aj的子序列.
限制条件
1
⩽
n
⩽
1000
0
⩽
a
1
⩽
1000000
1 \leqslant n \leqslant 1000 \\ 0 \leqslant a_1 \leqslant 1000000
1⩽n⩽10000⩽a1⩽1000000
输入:
n = 5
a = {4,2,3,1,5}
输出:
3(a1,a2,a4构成的子序列2,3,5)
问题分析:
方法一
假设使用 dp[i] := 长度为i的序列的最长子序列长度.
该递推关系可以表示为;
- 不包括 a i a_i ai时, 长度为 d p [ i − 1 ] dp[i-1] dp[i−1]
- 包括 a i a_i ai 时, 长度为 d p [ j ] + 1 , j < i , a j < a i dp[j] +1, j<i, a_j < a_i dp[j]+1,j<i,aj<ai,
d p [ 0 ] = 0 d p [ i + 1 ] = max { d p [ i ] , d p [ j ] + 1 } , j < i + 1 , a j < a i + 1 \begin{aligned} &dp[0] = 0 \\ &dp[i+1] = \max \{ dp[i], dp[j]+1 \}, \qquad j<i+1, a_j <a_i+1 \end{aligned} dp[0]=0dp[i+1]=max{dp[i],dp[j]+1},j<i+1,aj<ai+1
代码如下:
int dp[MAX_N+1];
int n,a[MAX_N];
int maxList(){
for(int i =0; i< n ; i++){
dp[i+1] = dp[i];
for(int j = 0;j<=i;j++){
if(a[j]<a[i]){
dp[i+1] = max(dp[i+1],dp[j]+1);
}else{
dp[i+1] = max(dp[i+1],1); // 如果a_j 不必 a_i 大,那么,包含a_i的长度只能是1.
}
}
}
return dp[n];
}
方法二
假设:
d
p
[
i
]
:
=
以
a
i
为
末
尾
的
最
长
上
升
子
序
列
的
长
度
dp[i] := 以a_i 为末尾的最长上升子序列的长度
dp[i]:=以ai为末尾的最长上升子序列的长度
递推关系为:
- 只包含a_i 的子序列
- 在满足 j < i , a j < a i j<i, a_j<a_i j<i,aj<ai的前提下, 以 a j a_j aj 为结尾的上升子序列末尾加上 a i a_i ai 得到的子序列.
满足关系式为:
d p [ i ] = max { 1 , d p [ j ] + 1 ∣ j < i 且 a j < a i } dp[i] =\max \{ 1, dp[j]+1 | j<i且 a_j < a_i \} dp[i]=max{1,dp[j]+1∣j<i且aj<ai}
在此关系式中与以往的dp不同,这里的dp[0] 表示以 a 0 a_0 a0 结尾的数列,即长度为1,而不是0.
最终的结果也不是dp[n-1], 而是整个dp序列中的最大值.
代码为;
int maxList2(){
int res = 0;
for(int i =0; i< n; i++){
dp[i] = 1;
for(int j = 0; j<i;j++){
if(a[j]<a[i]){
dp[i] = max(dp[i],dp[j]+1);
}
}
res = max(res,dp[i]);
}
return res;
}
方法三
定义:
d p [ i ] : = 长 度 为 i + 1 的 上 升 子 序 列 中 末 尾 元 素 的 最 小 值 ( 默 认 为 I N F ) dp[i] := 长度为i+1的上升子序列中末尾元素的最小值(默认为INF) dp[i]:=长度为i+1的上升子序列中末尾元素的最小值(默认为INF)
分析:
- 最开始全部的dp[i] 初始化为INF
- 对于每个 a j a_j aj , 如果 i = 0 i=0 i=0 或者 d p [ i − 1 ] < a j dp[i-1] < a_j dp[i−1]<aj 的话, 就用 d p [ i ] = min ( d p [ i ] , a j ) dp[i] = \min(dp[i] , a_j ) dp[i]=min(dp[i],aj) 进行更新
- 最终找出使得dp[i] < INF 的最大的i+1就是结果
代码:
int maxList3(){
fill(dp,dp+n, INF);
for(int i =0 ;i<n; i++){
*lower_bound(dp,dp+n, a[i]) = a[i];
}
return int(lower_bound(dp,dp+n,INF)-dp);
}
lower_bound()函数是一个STL函数, 利用二分查找,在有序序列a中找出指向 a i ⩾ k a_i \geqslant k ai⩾k 的 a i a_i ai 的最小指针.
upper_bound()函数利用二分查找,在有序序列a中找出指向 a i > k a_i > k ai>k 的 a i a_i ai 的最小指针.
计数问题的dp
划分数
题目:
有n个无区别的物品,将他们划分成不超过m组,求出划分方法数模M的余数.
限制条件:
1
⩽
m
⩽
n
⩽
1000
,
1 \leqslant m \leqslant n \leqslant 1000,
1⩽m⩽n⩽1000,
2
⩽
M
⩽
10000
2 \leqslant M \leqslant 10000
2⩽M⩽10000
输入:
n =4
m = 3
M = 10000
输出:
4 (1+1+2=1+3=2+2=4)
这种问题被成为n的m划分
定义:
dp[i][j] := j的i划分总数
思考:
将j划分成i个,那么可以先取出k个,然后将剩下的 j-k个划分成[i-1]份,那么公式为:
d
p
[
i
]
[
j
]
=
∑
k
=
0
j
d
p
[
i
−
1
]
[
j
−
k
]
dp[i][j] = \sum_{k=0}^j dp[i-1][j-k]
dp[i][j]=k=0∑jdp[i−1][j−k]
然而这样划分是不正确的,其中包含了重复的组合,例如1+2+1 和1+1+2.
- 考虑n的m划分 a i ( ∑ i = 1 m a i = n } a_i(\sum_{i=1}^m a_i = n\} ai(∑i=1mai=n}
- 如果对于每个i 都有 a i > 0 a_i >0 ai>0, 那么 { a i − 1 } \{ a_i-1 \} {ai−1} 就对应了n-m的m划分.
- 如果存在 a i = 0 a_i = 0 ai=0,那么就对应了n的m-1划分.
上述解释比较抽象,那么我们可以如下考虑:
上述问题可以类比为:将相同的小球装到m个相同的盘子中,可以有空盘子.
dp[m][n] : =将n个小球放入m个盘中.
- 如果至少有空盘子, d p [ m ] [ n ] = d p [ m − 1 ] [ n ] dp[m][n] = dp[m-1][n] dp[m][n]=dp[m−1][n]
- 没有空盘子,就相当于预先在每个盘子中放入一个小球, 所以 d p [ m ] [ n ] = d p [ n − m ] dp[m][n] = dp[n-m] dp[m][n]=dp[n−m]
所以我们得到递推公式为:
d
p
[
0
]
[
j
]
=
0
dp[0][j] = 0
dp[0][j]=0
d
p
[
m
]
[
n
]
=
{
d
p
[
m
−
1
]
[
n
]
,
n
<
m
d
p
[
m
]
[
n
−
m
]
+
d
p
[
m
−
1
]
[
n
]
,
n
>
=
m
\begin{aligned} dp[m][n] = \left \{ \begin{aligned} & dp[m-1][n] ,\qquad n<m \\ & dp[m][n-m] + dp[m-1][n],\qquad n>=m \end{aligned} \right. \end{aligned}
dp[m][n]={dp[m−1][n],n<mdp[m][n−m]+dp[m−1][n],n>=m
代码:
int parted(){
fill(dp[0],dp[0]+n+1,0);
dp[0][0] = 1;
for (int i = 0; i < m; i++){
for(int j =0; j<= n;j++){
if(i>j){
dp[i+1][j] = dp[i][j];
}else{
dp[i+1][j] = dp[i][j]+dp[i+1][j-i-1] % M;
}
}
}
return dp[m][n];
}
多重集组合数
题目:
有n种物品,第i 种物品有 a i a_i ai个. 不同种类的物品可以相互区分但是同种类的无法区分. 从这些物品
限制条件:
1 ⩽ n ⩽ 1000 1 \leqslant n \leqslant 1000 1⩽n⩽1000
1 ⩽ m ⩽ 1000 1 \leqslant m \leqslant 1000 1⩽m⩽1000
1 ⩽ a i ⩽ 1000 1 \leqslant a_i \leqslant 1000 1⩽ai⩽1000
2 ⩽ M ⩽ 10000 2 \leqslant M \leqslant 10000 2⩽M⩽10000
输入:
n = 3
m = 3
a = {1,2,3}
输出:
6 (0+0+3, 0+1+2, 0+2+1, 1+0+2, 1+1+1, 1+2+0)
定义:
d
p
[
i
+
1
]
[
j
]
:
=
从
前
i
种
物
品
中
取
出
j
个
的
组
合
数
dp[i+1][j] := 从前i种物品中取出j个的组合数
dp[i+1][j]:=从前i种物品中取出j个的组合数
- 从前i-1种物品中取出j-k个
- 从第i个物品中取出k个
可以得到递推关系式为:
d
p
[
i
+
1
]
[
j
]
=
∑
k
=
0
min
(
j
,
a
[
i
]
)
d
p
[
i
]
[
j
−
k
]
dp[i+1][j] = \sum_{k=0}^{\min(j,a[i])}dp[i][j-k]
dp[i+1][j]=k=0∑min(j,a[i])dp[i][j−k]
可以进行化简操作:
∑ k = 0 min ( j , a [ i ] ) d p [ i ] [ j − k ] ) = ∑ k = − 1 min ( j − 1 , a [ i ] − 1 ) d p [ i ] [ j − 1 − k ] = ∑ k = 0 min ( j − 1 , a [ i ] ) d p [ i ] [ j − 1 − k ] + d p [ i ] [ j ] − d p [ i ] [ j − 1 − a i ] = d p [ i + 1 ] [ j ] = d p [ i + 1 ] [ j − 1 ] + d p [ i ] [ j ] − d p [ i ] [ j − 1 − a 1 ] \begin{aligned} \sum_{k=0}^{\min(j,a[i])}dp[i][j-k]) &= \sum_{k=-1}^{\min(j-1,a[i]-1)}dp[i][j-1-k] \\ & = \sum_{k=0}^{\min(j-1,a[i])}dp[i][j-1-k]+dp[i][j] - dp[i][j-1-a_i] \\ & = dp[i+1][j] = dp[i+1][j-1] +dp[i][j] -dp[i][j-1-a_1] \end{aligned} k=0∑min(j,a[i])dp[i][j−k])=k=−1∑min(j−1,a[i]−1)dp[i][j−1−k]=k=0∑min(j−1,a[i])dp[i][j−1−k]+dp[i][j]−dp[i][j−1−ai]=dp[i+1][j]=dp[i+1][j−1]+dp[i][j]−dp[i][j−1−a1]
#include<iostream>
using namespace std;
#define MAX_N 1000
#define MAX_M 1000
int dp[MAX_N+1][MAX_M+1];
int n,m,M;
int a[MAX_N];
void init(){
cin>>n>>m;
for(int i =0;i<n;i++){
cin>>a[i];
}
cin>>M;
}
void solve(){
//dp[i][0] =1
for(int i =0;i<=n;i++){
dp[i][0]=1;
}
for(int i =0;i<n;i++){
for(int j = 1;j<=m;j++){
if(j-1 >= a[i]){
dp[i+1][j] = (dp[i+1][j-1]+dp[i][j]-dp[i][j-1-a[i]]+M)%M;
}else{
dp[i+1][j] = (dp[i+1][j-1]+dp[i][j])%M;
}
}
}
cout<<dp[n][m]<<endl;
}
int main(int argc, char const *argv[])
{
init();
solve();
return 0;
}