UVa 1625 Color Length

题目理解

我们有两条车道,每条车道上的车辆颜色用字符串表示。我们需要将这两条车道的车辆合并成一条车道,合并时需要保持各自车道内原有顺序,但可以任意交叉(类似归并排序的合并过程)。

对于合并后的序列,每种颜色 ccccolor 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...n1],第二个字符串为 B[0...m−1]B[0...m-1]B[0...m1]

定义 dp[i][j]dp[i][j]dp[i][j] 表示已经取了 iii 个字符来自 AAAjjj 个字符来自 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 个字符中出现
  • 结束条件:该颜色在 AAAi...n−1i...n-1i...n1 中不再出现,且在 BBBj...m−1j...m-1j...m1 中不再出现

算法步骤

  1. 预处理:对每种颜色,记录在两个字符串中第一次和最后一次出现的位置
  2. DP\texttt{DP}DP 初始化dp[0][0]=0dp[0][0] = 0dp[0][0]=0
  3. 状态转移
    • 对于每个状态 (i,j)(i, j)(i,j),计算当前"已经开始但未结束"的颜色数量 costcostcost
    • 如果 i>0i > 0i>0dp[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[i1][j]+cost)
    • 如果 j>0j > 0j>0dp[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][j1]+cost)
  4. 输出结果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,m5000 可行

代码实现

// 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 串取下一个字符的情况

该算法通过巧妙的代价计算,将复杂的最优化问题转化为经典的动态规划问题,保证了在合理时间复杂度内求解。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值