题意:有三个人去写编号从$1$到$n$的$n$个题目,现在这三个人分别有$k_1$,$k_2$,$k_3$个题目($k_1+k_2+k_3=n$),每次操作你可以将一个人的某一个题目给另一个人,问你最少经过多少次操作使得第一个人写这$n$个题目的前缀,第三个人写这$n$个题目的后缀,第二个人写其他部分,可以有人不写题目。

思路:设$dp[i][j]$表示把第$i$个题目给第$j$个人最少的次数

当第$i$个题目本来就属于第一个人时,分析如下:

  • 当把第$i$个题目给第一个人时,显然$dp[i][1]=dp[i-1][1]$,不用进行操作
  • 当把第$i$个题目给第二个人时,因为每个人最后的序列都是连续单调递增的,所以$dp[i][2]$只能从$dp[i-1][1]$和$dp[i-1][2]$转移过来,这两种情况都需要进行一次操作,即$dp[i][2] = min(dp[i - 1][1] + 1, dp[i - 1][2] + 1)$
  • 当把第$i$个题目给第三个人时,同理得$dp[i][3] = min(dp[i - 1][1] + 1, min(dp[i - 1][2] + 1, dp[i - 1][3] + 1))$

当第$i$个题目本来就属于第二个人或者第三个人时,分析是一样的。

设$a[x]=i$表示第$x$个题目本来就属于第$i$个人,所以状态转移方程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (int i = 1; i <= n; i++) {
if (1 == a[i]) {
dp[i][1] = dp[i - 1][1];
dp[i][2] = min(dp[i - 1][1] + 1, dp[i - 1][2] + 1);
dp[i][3] = min(dp[i - 1][1] + 1, min(dp[i - 1][2] + 1, dp[i - 1][3] + 1));
}
else if (2 == a[i]) {
dp[i][1] = dp[i - 1][1] + 1;
dp[i][2] = min(dp[i - 1][1], dp[i - 1][2]);
dp[i][3] = min(dp[i - 1][1] + 1, min(dp[i - 1][2] + 1, dp[i - 1][3] + 1));
}
else {
dp[i][1] = dp[i - 1][1] + 1;
dp[i][2] = min(dp[i - 1][1] + 1, dp[i - 1][2] + 1);
dp[i][3] = min(dp[i - 1][1], min(dp[i - 1][2], dp[i - 1][3]));
}
}

写成两个$for$循环感觉更简洁,但上面的状态转移方程更容易理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 200010;
const int M = 5;
const int INF = 0x3f3f3f3f;

int k[M], a[N], dp[N][M];

int main()
{
scanf("%d%d%d", &k[1], &k[2], &k[3]);
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= k[i]; j++) {
int x;
scanf("%d", &x);
a[x] = i;
}
}
int n = k[1] + k[2] + k[3];
memset(dp, INF, sizeof(dp));
dp[0][1] = dp[0][2] = dp[0][3] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 3; j++) {
for (int k = 1; k <= j; k++) {
dp[i][j] = min(dp[i][j], dp[i - 1][k] + (a[i] != j));
}
}
}
printf("%d\n", min(dp[n][1], min(dp[n][2], dp[n][3])));
return 0;
}