题目理解
我们有两条车道,每条车道上的车辆颜色用字符串表示。我们需要将这两条车道的车辆合并成一条车道,合并时需要保持各自车道内原有顺序,但可以任意交叉(类似归并排序的合并过程)。
对于合并后的序列,每种颜色 ccc 的 color length\texttt{color length}color length 定义为:
L(c)=max{位置索引}−min{位置索引}
L(c) = \max\{\text{位置索引}\} - \min\{\text{位置索引}\}
L(c)=max{位置索引}−min{位置索引}
其中位置索引从 111 开始编号。
目标:找到一种合并方式,使得所有颜色的 L(c)L(c)L(c) 之和最小。
关键思路
1. 颜色跨度与合并过程
对于某种颜色 ccc,在合并后的序列中,它的第一个出现位置和最后一个出现位置决定了 L(c)L(c)L(c)。我们希望同一种颜色的车辆在合并后尽量紧凑。
2. 动态规划状态设计
设第一个字符串为 A[0...n−1]A[0...n-1]A[0...n−1],第二个字符串为 B[0...m−1]B[0...m-1]B[0...m−1]。
定义 dp[i][j]dp[i][j]dp[i][j] 表示已经取了 iii 个字符来自 AAA,jjj 个字符来自 BBB 时的最小总 color length\texttt{color length}color length 和。
3. 代价计算的关键观察
在状态 (i,j)(i, j)(i,j) 时,有些颜色已经开始但未结束,这些颜色在后续的每一步都会贡献 1 的长度(因为它们的起点已定,终点未定,所以中间每步都会拉长它们的跨度)。
因此,我们可以在 DP\texttt{DP}DP 转移时,累加 当前已经开始但未结束的颜色数量 作为这一步的代价。
4. 开始与结束的判断
对于颜色 ccc:
- 开始条件:该颜色在 AAA 的前 iii 个字符中出现,或在 BBB 的前 jjj 个字符中出现
- 结束条件:该颜色在 AAA 的 i...n−1i...n-1i...n−1 中不再出现,且在 BBB 的 j...m−1j...m-1j...m−1 中不再出现
算法步骤
- 预处理:对每种颜色,记录在两个字符串中第一次和最后一次出现的位置
- DP\texttt{DP}DP 初始化:dp[0][0]=0dp[0][0] = 0dp[0][0]=0
- 状态转移:
- 对于每个状态 (i,j)(i, j)(i,j),计算当前"已经开始但未结束"的颜色数量 costcostcost
- 如果 i>0i > 0i>0:dp[i][j]=min(dp[i][j],dp[i−1][j]+cost)dp[i][j] = \min(dp[i][j], dp[i-1][j] + cost)dp[i][j]=min(dp[i][j],dp[i−1][j]+cost)
- 如果 j>0j > 0j>0:dp[i][j]=min(dp[i][j],dp[i][j−1]+cost)dp[i][j] = \min(dp[i][j], dp[i][j-1] + cost)dp[i][j]=min(dp[i][j],dp[i][j−1]+cost)
- 输出结果:dp[n][m]dp[n][m]dp[n][m] 即为答案
复杂度分析
- 状态数:O(nm)O(nm)O(nm)
- 每个状态计算 cost\texttt{cost}cost 需要 O(26)=O(1)O(26) = O(1)O(26)=O(1) 时间
- 总复杂度:O(nm)O(nm)O(nm),对于 n,m≤5000n, m \leq 5000n,m≤5000 可行
代码实现
// Color Length
// UVa ID: 1625
// Verdict: Accepted
// Submission Date: 2025-10-19
// UVa Run Time: 0.000s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 5005;
const int INF = 0x3f3f3f3f;
int dp[MAXN][MAXN];
int firstA[26], lastA[26], firstB[26], lastB[26];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) {
string A, B;
cin >> A >> B;
int n = A.length(), m = B.length();
// 初始化 first 和 last 数组
for (int c = 0; c < 26; c++) {
firstA[c] = firstB[c] = INF;
lastA[c] = lastB[c] = -1;
}
// 预处理 A 中每种颜色的第一次和最后一次出现位置
for (int i = 0; i < n; i++) {
int ch = A[i] - 'A';
if (firstA[ch] == INF) firstA[ch] = i;
lastA[ch] = i;
}
// 预处理 B 中每种颜色的第一次和最后一次出现位置
for (int i = 0; i < m; i++) {
int ch = B[i] - 'A';
if (firstB[ch] == INF) firstB[ch] = i;
lastB[ch] = i;
}
// DP 数组初始化
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
dp[i][j] = INF;
}
}
dp[0][0] = 0;
// 动态规划主循环
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (i == 0 && j == 0) continue;
// 计算 cost: 已经开始但未结束的颜色数量
int cost = 0;
for (int c = 0; c < 26; c++) {
bool started = false; // 颜色是否已经开始
bool ended = true; // 颜色是否已经结束
// 检查颜色是否已经开始
// 在 A 的前 i 个字符中出现过,或在 B 的前 j 个字符中出现过
if (i > 0 && firstA[c] != INF && firstA[c] < i) started = true;
if (j > 0 && firstB[c] != INF && firstB[c] < j) started = true;
// 检查颜色是否已经结束
// 在 A 的剩余部分或 B 的剩余部分还会出现
if (i < n && lastA[c] != -1 && lastA[c] >= i) ended = false;
if (j < m && lastB[c] != -1 && lastB[c] >= j) ended = false;
// 如果已经开始但未结束,则计入代价
if (started && !ended) cost++;
}
// 状态转移:从左边(A 串)取一个字符
if (i > 0) {
dp[i][j] = min(dp[i][j], dp[i - 1][j] + cost);
}
// 状态转移:从上边(B 串)取一个字符
if (j > 0) {
dp[i][j] = min(dp[i][j], dp[i][j - 1] + cost);
}
}
}
// 输出结果
cout << dp[n][m] << "\n";
}
return 0;
}
代码说明
- 预处理部分:记录每种颜色在两个字符串中的首尾位置,用于后续判断颜色是否开始或结束
- DP\texttt{DP}DP 状态:dp[i][j]dp[i][j]dp[i][j] 表示处理完 AAA 的前 iii 个字符和 BBB 的前 jjj 个字符时的最小总 color length\texttt{color length}color length
- 代价计算:对于每个状态,遍历所有颜色,统计已经开始但未结束的颜色数量
- 状态转移:分别考虑从 AAA 串或 BBB 串取下一个字符的情况
该算法通过巧妙的代价计算,将复杂的最优化问题转化为经典的动态规划问题,保证了在合理时间复杂度内求解。
249

被折叠的 条评论
为什么被折叠?



