动态规划(Dynamic Programming,DP)题是算法竞赛中的必出题型。DP算法的效率高、代码少,竞赛队员不仅需要掌握很多编程技术,而且需要根据题目灵活设计具体的解题方案,能考察其思维力、建模抽象能力、灵活性等。。
和贪心法、分治法一样,DP并不是一个特定的算法,而是一种算法思想。
DP算法思想可以简单解释如下:DP问题一般是多阶段决策问题,它把一个复杂问题分解为相对简单的子问题,再一个个解决,最后得到原复杂问题的最优解;这些子问题是前后相关的,并且非常相似,处理方法几乎一样。把前面子问题的计算结果记录为“状态”,并存储在“状态表”中,后面子问题可以直接查找前面得到的状态表,避免了重复计算,极大地减少了计算复杂度。
DP适用于有重叠子问题和最优子结构性质的问题,具体的解释请参考算法类相关教材。
求解DP问题有3步:
- 定义状态
- 状态转移
- 算法实现
DP的核心是状态转移方程。用状态转移方程求解状态,状态往往就是问题的解。在DP问题中,只要分析出状态以及状态转移方程,差不多就完成了90%的工作量。
引入
观察下面的数字金字塔。
写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。
在上面的样例中,从
7
→
3
→
8
→
7
→
5
7 \to 3 \to 8 \to 7 \to 5
7→3→8→7→5 的路径产生了最大权值。
输入格式
第一个行一个正整数
r
r
r ,表示行的数目。
后面每行为这个数字金字塔特定行包含的整数。
对于
100
%
100\%
100% 的数据,
1
≤
r
≤
1000
1\le r \le 1000
1≤r≤1000,所有输入在
[
0
,
100
]
[0,100]
[0,100] 范围内。
输出格式
单独的一行,包含那个可能得到的最大的和。
样例输入 #1
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出 #1
30
根据题面意思,是要我们找出一条路,既然是找路,那就用搜索呗,搜索深度控制在 r 处结束,每个节点 a [ i ] [ j ] a[i][j] a[i][j] 都有两个岔路: a [ i + 1 ] [ j ] 和 a [ i + 1 ] [ j + 1 ] a[i + 1][j]和a[i + 1][j + 1] a[i+1][j]和a[i+1][j+1],代码如下:
#include<bits/stdc++.h>
using namespace std;
int room[1005][1005];
int ans = 0, r;
void dfs(int now_i, int now_j, int sum){
if(now_i == r + 1){
ans = max(ans, sum);
return;
}
dfs(now_i + 1, now_j, sum + room[now_i][now_j + 1]);
dfs(now_i + 1, now_j + 1, sum + room[now_i + 1][now_j + 1]);
}
void input(int n){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
cin >> room[i][j];
}
}
}
int main(){
cin >> r;
input(r);
dfs(1, 1, room[1][1]);
cout << ans;
return 0;
}
代码看起来很合理,但是在 r 超过20时,需要的运行时间太长了,所以尝试用记忆化搜索来优化,通过设置dp数组: d p [ i ] [ j ] 表示从 r o o m [ i ] [ j ] 到底端的最大权值 dp[i][j]表示从room[i][j]到底端的最大权值 dp[i][j]表示从room[i][j]到底端的最大权值 :
#include<bits/stdc++.h>
using namespace std;
int room[1005][1005];
int dp[1005][1005];
int r;
int dfs(int now_i, int now_j, int sum){
if(dp[now_i][now_j] != -1)return dp[now_i][now_j];
if(now_i == r + 1)return 0;
dp[now_i][now_j] = room[now_i][now_j] +
max(dfs(now_i + 1,now_j,sum + room[now_i + 1][now_j]),
dfs(now_i + 1,now_j+1,sum + room[now_i + 1][now_j+1]));
return dp[now_i][now_j];
}
void input(int n){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
cin >> room[i][j];
}
}
}
int main(){
cin >> r;
memset(dp, -1, sizeof dp);
input(r);
cout << dfs(1, 1, room[1][1]);
return 0;
}
我们发现:
d
p
[
i
]
[
j
]
=
r
o
o
m
[
i
]
[
j
]
+
m
a
x
(
左到底,右到底
)
;
dp[i][j] = room[i][j] + max(左到底,右到底);
dp[i][j]=room[i][j]+max(左到底,右到底);
这个步骤在倒数第二行的时候会变成:
d
p
[
i
]
[
j
]
=
r
o
o
m
[
i
]
[
j
]
+
m
a
x
(
r
o
o
m
[
i
+
1
]
[
j
]
,
r
o
o
m
[
i
+
1
]
[
j
+
1
]
)
;
dp[i][j] = room[i][j] + max(room[i + 1][j],\ room[i + 1][j + 1]);
dp[i][j]=room[i][j]+max(room[i+1][j], room[i+1][j+1]);
这时候我们会发现我们只要逆推,就可以实现整个程序了:
#include<bits/stdc++.h>
using namespace std;
int room[1005][1005];
void dp(int n){
for(int i = n - 1; i >= 1; i--){
for(int j = 1; j <= i; j++){
room[i][j] += max(room[i + 1][j], room[i + 1][j + 1]);
}
}
}
void input(int n){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
cin >> room[i][j];
}
}
}
int main(){
int r; cin >> r;
input(r);
dp(r);
cout << room[1][1];
return 0;
}
01背包问题
还记得上本书的坑吗?背包问题在之前我们用的是贪心法解决,并且我们已经举出了反例,那我们今天就来解决这个问题。
现在你有一个空间为 m 的背包,有 t 件物品,每个物品的体积为
c
o
s
t
[
i
]
cost[i]
cost[i],价值为
v
a
l
u
e
[
i
]
value[i]
value[i]。
现在我们定义一个dp数组
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示在只用了 j 空间,选择第 i 件物品时最多能获得多少钱。
此时会有两个情况:物品 i 放得下 & 物品 i 放不下。如果空间不够放下 i ,那就不选,反之又有两种情况:腾出空间放 i ,还是保持原样不放 i 。即:
$$
\begin{cases}
if\ j < cost[i]\ \ \ dp[\ i\ ][\ j\ ] = dp[\ i-1\ ][\ j\ ]\
\
else\ \ \ dp[\ i\ ][\ j\ ] = max (\ dp[\ i - 1\ ][\ j - cost[\ i\ ]\ ] + value[\ i\ ],dp[\ i-1\ ][\ j\ ]\ )
\end{cases}
$$
此时
d
p
[
t
]
[
m
]
dp[t][m]
dp[t][m] 就是最大价值。
假如现在有 10 的空间和 5 个物品,物品信息如下:
nums | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
cost | 2 | 3 | 4 | 4 | 5 |
value | 3 | 4 | 4 | 5 | 1 |
此时的dp表格图下: |
nums | j:1 | j:2 | j:3 | j:4 | j:5 | j:6 | j:7 | j:8 | j:9 | j:10 |
---|---|---|---|---|---|---|---|---|---|---|
i : 1 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
i : 2 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 | 7 | 7 |
i : 3 | 0 | 3 | 4 | 4 | 7 | 7 | 8 | 8 | 11 | 11 |
i : 4 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 | 9 | 12 |
i : 5 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 | 9 | 12 |
结合表格解释一下上面的公式:
- 如果此时空间连一个 i 物品都放不下的话,就延续”在这个空间下,没有放这个物品的最佳情况“
- 如果这个空间可以放下第 i 个物品,那就有两种选择:在”没有放这个物品的最佳情况中,腾出这个物品的空间,放入这个物品“ 和 ”在这个空间下,不放入这个物品的最佳情况“。
- 因为每一行都是最优的情况,所以第 i 行只需要从 i - 1 行考虑就行了。
代码实现如下:
#include<bits/stdc++.h>
using namespace std;
int cost[6] = {0, 2, 3, 4, 4, 5};
int value[6] = {0,3, 4, 4, 5, 1};
int dp[20][20];
int m = 10, t = 5;
int main(){
for(int i = 1; i <= t; i++){
for(int j = 1; j <= m; j++){
if(j < cost[i])dp[i][j] = dp[i - 1][j];
else{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cost[i]] + value[i]);
}
}
}
cout << dp[t][m];
return 0;
}
此时的dp数组如下:
0 3 3 3 3 3 3 3 3 3
0 3 4 4 7 7 7 7 7 7
0 3 4 4 7 7 8 8 11 11
0 3 4 5 7 8 9 9 12 12
0 3 4 5 7 8 9 9 12 12
那我们怎么样才能知道背包里装了什么呢?由图表来看:
nums | j:1 | j:2 | j:3 | j:4 | j:5 | j:6 | j:7 | j:8 | j:9 | j:10 |
---|---|---|---|---|---|---|---|---|---|---|
i : 1 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
i : 2 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 | 7 | 7 |
i : 3 | 0 | 3 | 4 | 4 | 7 | 7 | 8 | 8 | 11 | 11 |
i : 4 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 | 9 | 12 |
i : 5 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 | 9 | 12 |
- 因为
d
p
[
5
]
[
10
]
=
m
a
x
{
d
p
[
4
]
[
10
]
,
d
p
[
4
]
[
10
−
c
o
s
t
[
5
]
]
+
v
a
l
u
e
[
5
]
}
=
d
p
[
4
]
[
10
]
dp[\ 5\ ][\ 10\ ] = max\{\ dp[\ 4\ ][\ 10\ ]\ ,\ dp[\ 4\ ][\ 10 - cost[\ 5 \ ]\ ] + value[\ 5\ ]\ \} = dp[\ 4\ ][\ 10\ ]
dp[ 5 ][ 10 ]=max{ dp[ 4 ][ 10 ] , dp[ 4 ][ 10−cost[ 5 ] ]+value[ 5 ] }=dp[ 4 ][ 10 ],
所以没有选物品5,之后来到 d p [ 4 ] [ 10 ] dp[\ 4\ ][\ 10\ ] dp[ 4 ][ 10 ]。 - 因为 d p [ 4 ] [ 10 ] = m a x { d p [ 3 ] [ 10 ] , d p [ 3 ] [ 10 − c o s t [ 4 ] ] + v a l u e [ 4 ] } = d p [ 3 ] [ 10 − c o s t [ 4 ] ] + v a l u e [ 4 ] dp[\ 4\ ][\ 10\ ] = max\{\ dp[3][10]\ ,\ dp[3][\ 10 - cost[\ 4\ ]\ ] + value[4]\ \} = dp[3][10-cost[4] \ ]+value[4] dp[ 4 ][ 10 ]=max{ dp[3][10] , dp[3][ 10−cost[ 4 ] ]+value[4] }=dp[3][10−cost[4] ]+value[4],说明选了4,之后来到 d p [ 3 ] [ 6 ] dp[3][6] dp[3][6]。
- 因为 d p [ 3 ] [ 6 ] = d p [ 2 ] [ 6 ] dp[3][6] = dp[2][6] dp[3][6]=dp[2][6],没选3 ,来到 d p [ 2 ] [ 6 ] dp[2][6] dp[2][6]。
- 因为 d p [ 2 ] [ 6 ] = d p [ 1 ] [ 3 ] + c o s t [ 2 ] dp[2][6] = dp[1][3] + cost[2] dp[2][6]=dp[1][3]+cost[2],选了 2,来到 d p [ 1 ] [ 3 ] dp[1][3] dp[1][3]。
- 因为 d p [ 1 ] [ 3 ] = d p [ 0 ] [ 1 ] + c o s t [ 1 ] dp[1][3] = dp[0][1] + cost[1] dp[1][3]=dp[0][1]+cost[1];选了1,来到 d p [ 0 ] [ 1 ] dp[0][1] dp[0][1]。
- 此时 i = 0,结束。
代码实现:
int flag[N]; //flag[i] == true 表示选了 i
void Print(int t, int m){
memset(flag, false, sizeof flag);
int i = t, j = m;
while(i != 0){
if(dp[i][j] != dp[i -1][j]){
i = i - 1;
}
else{
flag[i] = true;
j = j - cost[i];
i = i - 1;
}
}
for(int i = 0; i < t; i++){
if(flag[i])cout << i <<"\t";
}
}
找零问题
如果我们有五种面值的纸币:1、5、10、20、50,凑出m元至少需要几张纸币?
假设我们只有前三张纸币,要凑出11元,
- 我们先只用 1 元的纸币,则 i 元需要 i 张纸币:
金额 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
张数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
- 在加上 5 元纸币后:
金额 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
张数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
新张 | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 2 | 3 |
此时我们发现对于最小张数N,有: N = m i n ( N [ i ] , N [ i − 5 ] + 1 ) N \ =\ min(N[i],N[i - 5] + 1) N = min(N[i],N[i−5]+1)。继续加入 10 元纸币: |
金额 i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
张数 | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 2 | 3 |
新张 | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 1 | 2 |
此时我们发现对于最小张数N,有: N = m i n ( N [ i ] , N [ i − 10 ] + 1 ) N \ =\ min(N[i],N[i - 10] + 1) N = min(N[i],N[i−10]+1)。即: | ||||||||||||
$$ | ||||||||||||
N[i]\ =\ min(\ N[\ i\ ],N[\ i\ -\ money[\ j\ ]\ ]\ + \ 1) | ||||||||||||
$$ | ||||||||||||
代码实现: |
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
int money[5] = {1,5,10,20,50};
int dp[N]; //记录最少的张数
int main(){
int n; cin >> n;
for(int i = 0; i <= n; i++)dp[i] = i;
//初始化,只用 1 元时
for(int i = 1; i < 5; i++){//从第二种面值开始凑钱
for(int j = money[i]; j <= n; j++){
dp[j] = min(dp[j], dp[j - money[i]] + 1);
}
}
cout << dp[n];
return 0;
}
那么新问题又来了,如何输出最少张数的组合呢?
我们可以引入一个 path 数组,path[ i ]记录凑齐 i 元需要的最后一张纸币,再利用 path 倒推即可,例:
cost | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
dp[i] | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 1 | 2 |
path[i] | 0 | 1 | 1 | 1 | 1 | 5 | 5 | 5 | 5 | 5 | 10 | 10 |
如果此时金额为 7,此时path[ 7 ] = 5,然后查path[7-5] = 1(path[ 2 ] = 1),然后再查path[2 - 1 ],直到查到path[ 0 ],经过的 path 顺序:5 1 1 便是最小组合。 | ||||||||||||
代码实现查询 x 元的最少纸币组合: |
for(int i = 0; i <= n; i++){
dp[i] = i;path[i] = 1;
}
for(int i = 1; i < 5; i++){
for(int j = money[i]; j <= n; j++){
dp[j] = min(dp[j], dp[j - money[i]] + 1);
path[j] = money[i];
}
}
int x; cin >> x;
while(x > 0){
cout << path[ x ] <<" ";
x = x - path[x];
}
最长公共子序列(LCS问题)
一个从序列中删去一些元素后得到的序列就是原序列的子序列。
子序列和子串的概念是不一样的,子序列可以不是连续的,但是子串必须是连续的。
现在有两个序列:
-
X = (a,b,c,f,b,c) 子序列为: X i X_i Xi
-
Y = (a,b,f,c,a,b) 子序列为: Y i Y_i Yi
我们用 L [ i ] [ j ] L[i][j] L[i][j] 表示 X i 或 Y i X_i\ 或\ Y_i Xi 或 Yi ,即最长公共子序列。
现在我们有以下策略: -
如果 X i = Y j X_i=Y_j Xi=Yj ,那就找 X i − 1 和 Y j − 1 X_{i - 1} \ 和 \ Y_{j - 1} Xi−1 和 Yj−1 的最长公共子序列,然后在其结尾加上 X [ i ] X[i] X[i]。
-
如果 X i ≠ Y j X_i \ne Y_j Xi=Yj ,那就找 X i − 1 和 Y j X_{i - 1} \ 和 \ Y_{j} Xi−1 和 Yj 与 X i 和 Y j − 1 X_{i} \ 和 \ Y_{j - 1} Xi 和 Yj−1 的最长公共子序列中较大的一个。
即:
L [ i ] [ j ] = { L [ i − 1 ] [ j − 1 ] + 1 X i = Y j , i > 0 , j > 0 m a x ( L [ i − 1 ] [ j ] , L [ i ] [ j − 1 ] ) X i ≠ Y i , i > o , j > 0 L[\ i\ ][\ j \ ]\ =\ \begin{cases} L[\ i - 1\ ][\ j -1\ ] +1\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ X_i = Y_j\ \ ,\ i > 0\ \ ,\ j>0 \\ \\ max(\ L[\ i-1\ ][\ j\ ]\ ,\ L[\ i\ ][\ j - 1\ ]\ )\ \ \ \ \ \ \ \ \ X_i \ne Y_i\ \ ,\ i>o\ \ ,\ j>0 \end{cases} L[ i ][ j ] = ⎩ ⎨ ⎧L[ i−1 ][ j−1 ]+1 Xi=Yj , i>0 , j>0max( L[ i−1 ][ j ] , L[ i ][ j−1 ] ) Xi=Yi , i>o , j>0
下面列表说明: -
求 L [ 1 ] [ 1 ] L[1][1] L[1][1] ,有 X 1 = Y 1 X_1 = Y_1 X1=Y1 ;此时有: L [ 1 ] [ 1 ] = L [ 0 ] [ 0 ] + 1 L[1][1] = L[0][0] + 1 L[1][1]=L[0][0]+1 :
L [ i ] [ j ] L[i][j] L[i][j] | Y= | a | b | f | c | a | b |
---|---|---|---|---|---|---|---|
X= | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | 1 | 1 | |||||
b | 2 |
- 求 L [ 1 ] [ 2 ] L[1][2] L[1][2] ,有 X 1 ≠ Y 2 X_1 \ne Y_2 X1=Y2 ,此时: L [ 1 ] [ 2 ] = m a x { L [ i ] [ i ] , L [ 0 ] [ 2 ] } = 1 L[1][2] = max\ \{L[i][i]\ ,\ L[0][2]\}\ =\ 1 L[1][2]=max {L[i][i] , L[0][2]} = 1 :
L [ i ] [ j ] L[i][j] L[i][j] | Y= | a | b | f | c | a | b |
---|---|---|---|---|---|---|---|
X= | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | 1 | 1 | 1 | ||||
b | 2 |
- 由此继续:补充完表格:
L [ i ] [ j ] L[i][j] L[i][j] | Y= | a | b | f | c | a | b |
---|---|---|---|---|---|---|---|
X= | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
b | 2 | 1 | 2 | 2 | 2 | 2 | 2 |
c | 3 | 1 | 2 | 2 | 3 | 3 | 3 |
f | 4 | 1 | 2 | 3 | 3 | 3 | 3 |
b | 5 | 1 | 2 | 3 | 3 | 3 | 4 |
c | 6 | 1 | 2 | 3 | 4 | 4 | 4 |
此时 L [ 6 ] [ 6 ] L[6][6] L[6][6] 就是答案。 | |||||||
代码实现: |
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
string str1, str2;
int dp[N][N], len1, len2;
int LCS(){
memset(dp, 0, sizeof dp);
for(int i = 1; i <= len1; i++){
for(int j = 1; j <= len2; j++){
if(str1[i - 1] == str2[j - 1]){
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else{
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[len1][len2];
}
int main(){
while(cin >> str1 >> str2){
len1 = str1.size();
len2 = str2.size();
cout << LCS() << endl;
}
return 0;
}
打印最长公共子序列的逻辑和01背包输出类似,可以自行尝试。
最长递增子序列(LIS问题)
怎样找出一个序列最长的递增子序列呢?
有两个办法:
- 对序列排序,用新序列与原序列求LCS。
- 动态规划,用dp{ i },表示以第 i 个数字结尾的递增子序列长度,然后max{ d p [ i ] dp[i] dp[i] }就是。
- 利用递增子序列的性质
1 不做解释,2 代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int arr[N], dp[N];
int n;
int LIS(int len){
for(int i = 1; i <= len; i++){
cin >> arr[i];
}
for(int i = 1; i <= n; i++){
int cnt = 0, maxx = arr[i];
for(int j = 1; j <= i; j++){
if(arr[j] < maxx)cnt++;
}
dp[i] = cnt;
}
int maxx = 0;
for(int i = 0; i <= len; i++){
maxx = max(maxx, dp[i]);
}
return maxx;
}
int main(){
while(cin >> n){
cout << LIS(n) << endl;
}
return 0;
}
2 方法的复杂度是 O ( N 2 ) O(N^2) O(N2),效率还是不够高,我们考虑用 3 降低复杂度至 O ( N l o g 2 N ) O(Nlog_{2}N) O(Nlog2N)。方法如下:
- 定义 d [ ] d[\ ] d[ ] , l e n len len , A [ ] A[\ ] A[ ]; d [ ] d[\ ] d[ ]记录最长子序列的内容, l e n len len 记录 d [ ] d[\ ] d[ ] 的长度, A [ ] A[\ ] A[ ]表示原序列
- d [ 1 ] = A [ 1 ] ; l e n = 1 d[1] = A[1];len = 1 d[1]=A[1];len=1;
- 对
A
[
]
A[\ ]
A[ ]里的数字逐个处理,如果大于
d
[
]
d[\ ]
d[ ]的最后一个,那就把这个数字放在
d
[
]
d[\ ]
d[ ]的最后面,反之,则把用这个数字替换掉序列中第一个大于它的数字。
为什么 d [ ] d[\ ] d[ ]的长度等于 h i g h [ ] high[\ ] high[ ]的LIS的长度?
分析算法对 h i g h [ ] high[\ ] high[ ]的两个关键操作: - 如果 h i g h [ k ] high[k] high[k]比 d [ ] d[\ ] d[ ]末尾的数字更大,就加到 d [ ] d[\ ] d[ ]后面, h i g h [ ] high[\ ] high[ ]的LIS加1, d [ ] d[\ ] d[ ]的长度加1。
- 如果
h
i
g
h
[
k
]
high[k]
high[k]比
d
[
]
d[\ ]
d[ ]末尾的数字小,就替换
d
[
]
d[\ ]
d[ ]中第1个大于它的数字”,有两个作用:首先,这个操作不影响LIS的长度,也不影响
d
[
]
d[\ ]
d[ ]的长度;其次,
h
i
g
h
[
]
high[\ ]
high[ ]后面还没有处理的数很多都比已经处理过的数小,但是有可能序列更长,这里的替换给后面更小的数字留下了机会。为什么用
h
i
g
h
[
k
]
high[k]
high[k]替换
d
[
]
d[\ ]
d[ ]中第1个比它大的数字?因为数字
h
i
g
h
[
k
]
high[k]
high[k]可能在LIS中,而被它替换的数字由于更大,不在LIS中的可能性更大。
假设 h i g h [ ] = { 4 , 8 , 9 , 5 , 6 , 7 } high[\ ]\ =\ \{\ 4,8,9,5,6,7\ \} high[ ] = { 4,8,9,5,6,7 }
如下表:
i i i | h i g h [ ] high[\ ] high[ ] | d [ ] d[\ ] d[ ] | l e n len len | 说明 说明 说明 |
---|---|---|---|---|
1 1 1 | 4,8,9,5,6,7 | 4 | 1 | 初始 d [ 1 ] = h i g h [ 1 ] d[1]=high[1] d[1]=high[1] |
2 2 2 | 4,8,9,5,6,7 | 4,8 | 2 | h i g h [ 2 ] > d [ 1 ] ,加到 d [ ] 后面 high[2]>d[1],加到d[\ ]后面 high[2]>d[1],加到d[ ]后面 |
3 3 3 | 4,8,9,5,6,7 | 4,8,9 | 3 | d [ ] 后面加上 9 d[\ ]后面加上9 d[ ]后面加上9 |
4 4 4 | 4,8,9,5,6,7 | 4,5,9 | 3 | 5比d末尾的9小,用5替换d中第1个比5大的数:8 |
5 5 5 | 4,8,9,5,6,7 | 4,5,6 | 3 | 用 6 替换 9 用6替换9 用6替换9 |
6 6 6 | 4,8,9,5,6,7 | 4,5,6,7 | 4 | d [ ] 后面加上 7 d[\ ]后面加上7 d[ ]后面加上7 |
现在怎么找到第一个比 h i g h [ k ] high[k] high[k]大的数?还记得二分法吗? | ||||
代码如下: |
#include<bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int high[N], d[N];
int len = 1;
void input(int n){
for(int i = 1; i <= n; i++){
cin >> high[i];
}
d[1] = high[1];
}
int b_search(int*arr, int left, int right, int aim) {
while (left < right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= aim)right = mid;
else left = mid + 1;
}
return right;
}
void LIS(int n){
for(int i = 2; i <= n; i++){
if(high[i] > d[len]){
len++;
d[len] = high[i];
}
else{
int loc = b_search(d, 0, len + 1, high[i]);
d[loc] = high[i];
}
}
for(int i = 1; i <= len; i++){
cout << d[i] << " ";
}
}
int main(){
int n;
while(cin >> n){
input(n);
LIS(n);
}
return 0;
}
标签:10,now,room,int,high,引入,动态,规划,dp
From: https://blog.csdn.net/n666fgh/article/details/139552771