题意:有三个人去写编号从$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$个人,所以状态转移方程如下:
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$循环感觉更简洁,但上面的状态转移方程更容易理解。
#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 ", min(dp[n][1], min(dp[n][2], dp[n][3]))); return 0; }