UVa 10949 Kids in a Grid

题目概述

有两个孩子在一个 H×WH \times WH×W 的网格中行走,网格中的每个格子包含一个字符(ASCII\texttt{ASCII}ASCII 码在 333333127127127 之间)。两个孩子每一步都可以向北、东、西、南四个方向移动。第一个孩子走了 NNN 步,第二个孩子走了 MMM 步,且满足 0≤N≤M≤200000 \leq N \leq M \leq 200000NM20000

记录每个孩子走过的所有字符,得到两个字符串 SAS_ASASBS_BSB。我们需要从两个字符串中删除尽可能少的字符,使得删除后的两个新字符串完全相同。

输入格式

第一行包含一个整数 ttt1≤t≤151 \leq t \leq 151t15),表示测试用例的数量。每个测试用例包含以下部分:

  1. 两个整数 HHHWWW1≤H,W≤201 \leq H, W \leq 201H,W20
  2. 接下来 HHH 行,每行包含 WWW 个字符,表示网格内容
  3. 三个整数 NNNX0X_0X0Y0Y_0Y0,表示第一个孩子从 (X0,Y0)(X_0, Y_0)(X0,Y0) 出发,走 NNN 步(1≤X0≤H1 \leq X_0 \leq H1X0H, 1≤Y0≤W1 \leq Y_0 \leq W1Y0WXXX 从北向南增加,YYY 从西向东增加)
  4. 一个长度为 NNN 的字符串,由字符 N\texttt{N}NE\texttt{E}EW\texttt{W}WS\texttt{S}S 组成,分别表示北、东、西、南
  5. 第二个孩子的信息格式相同,包含 MMMX1X_1X1Y1Y_1Y1 和一个长度为 MMM 的移动序列字符串

注意:

  • 行走序列保证合法,不会走出网格边界
  • NNNMMM 可能为 000,此时对应的移动序列字符串为空行

输出格式

对于每个测试用例,输出用例编号和两个整数 XAX_AXAXBX_BXB,分别表示从 SAS_ASASBS_BSB 中需要删除的字符数。

样例分析

样例输入

2

3 4
ABCD
DEFG
ABCD
4 1 1
EEES
3 3 1
NES

3 4
ABCD
DEFG
ABCD
4 1 1
EEES
3 3 1
NES

样例输出

Case 1: 3 2
Case 2: 3 2

解释:
第一个孩子从 (1,1)(1,1)(1,1) 出发,走 EEES\texttt{EEES}EEES,经过的字符为 ABCDG\texttt{ABCDG}ABCDGSAS_ASA
第二个孩子从 (3,1)(3,1)(3,1) 出发,走 NES\texttt{NES}NES,经过的字符为 ADEB\texttt{ADEB}ADEBSBS_BSB
最长公共子序列可以是 AB\texttt{AB}ABAD\texttt{AD}AD,长度为 222
因此需要从 SAS_ASA 中删除 5−2=35-2=352=3 个字符,从 SBS_BSB 中删除 4−2=24-2=242=2 个字符


问题分析与解题思路

核心问题转化

题目要求从两个字符串中删除尽可能少的字符,使得剩下的字符串相同。这等价于求两个字符串的最长公共子序列LCS\texttt{LCS}LCS)的长度。

设:

  • SAS_ASA 的长度为 lenAlen_AlenA
  • SBS_BSB 的长度为 lenBlen_BlenB
  • LCS\texttt{LCS}LCS 长度为 lcslcslcs

则需要删除的字符数为:

  • SAS_ASA 中删除:lenA−lcslen_A - lcslenAlcs
  • SBS_BSB 中删除:lenB−lcslen_B - lcslenBlcs

因此,问题的核心转化为高效计算两个字符串的 LCS\texttt{LCS}LCS 长度。

数据规模分析

  • 0≤N≤M≤200000 \leq N \leq M \leq 200000NM20000,因此字符串长度最大为 200012000120001(包含起点字符)
  • 测试用例最多 151515
  • 最坏情况下:15×20000×20000=6×10915 \times 20000 \times 20000 = 6 \times 10^915×20000×20000=6×109 次比较,使用 O(nm)O(nm)O(nm) 的动态规划算法可能超时

算法选择

1. 经典动态规划(DP\texttt{DP}DP)算法
  • 时间复杂度:O(nm)O(nm)O(nm)
  • 空间复杂度:O(min⁡(n,m))O(\min(n,m))O(min(n,m))(使用滚动数组优化)
  • 优点:实现简单,代码清晰
  • 缺点:对于最大数据规模,可能达到时间限制边缘
2. Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法
  • 时间复杂度:O((n+m)log⁡n+∣Σ∣log⁡n)O((n+m) \log n + |\Sigma| \log n)O((n+m)logn+∣Σ∣logn),其中 ∣Σ∣|\Sigma|∣Σ∣ 是字符集大小
  • 空间复杂度:O(n+m+∣Σ∣)O(n+m+|\Sigma|)O(n+m+∣Σ∣)
  • 优点:对于字符集较小的情况非常高效(本题字符集大小为 959595
  • 原理:将 LCS\texttt{LCS}LCS 问题转化为最长递增子序列(LIS\texttt{LIS}LIS)问题

由于题目字符集有限(ASCII\texttt{ASCII}ASCII 333333-127127127),使用 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法可以获得更好的性能。

解题步骤

  1. 构建网格矩阵:读取 H×WH \times WH×W 的字符网格

  2. 生成路径字符串

    • 从起点 (X0,Y0)(X_0, Y_0)(X0,Y0) 开始,将起点字符加入字符串
    • 按照移动序列逐步移动,将经过的每个格子字符加入字符串
    • 注意:坐标需要从 1−1-1based\texttt{based}based 转换为 0−0-0based\texttt{based}based
    • 特别注意:当 N=0N=0N=0M=0M=0M=0 时,移动序列为空,需要使用 getline\texttt{getline}getline 正确读取空行
  3. 计算 LCS\texttt{LCS}LCS 长度

    • 方法一:使用滚动数组的动态规划
    • 方法二:使用 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法
  4. 计算结果

    • 删除字符数 = 字符串长度 - LCS\texttt{LCS}LCS 长度
  5. 输出结果:按照格式输出

关键注意事项

  1. 空移动序列的处理:当 N=0N=0N=0M=0M=0M=0 时,移动序列字符串为空行,必须使用 getline\texttt{getline}getline 而非 cin >>\texttt{cin >>}cin >> 来读取

  2. 坐标转换:输入使用 111-based 坐标,而数组使用 000-based 索引,需要减 111 转换

  3. 包含起点字符:无论移动步数多少,起点字符总是包含在路径字符串中

  4. 边界保证:题目保证移动序列不会越界,无需进行边界检查


代码实现

方法一:经典动态规划(滚动数组优化)

// Kids in a Grid
// UVa ID: 10949
// Verdict: Accepted
// Submission Date: 2025-12-02
// UVa Run Time: 1.580s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

const int MAX_LEN = 20005;
int dp[2][MAX_LEN];  // 滚动数组用于 LCS 长度计算

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    cin >> t;
    string dummy;
    getline(cin, dummy);  // 读取第一行末尾的换行符
    for (int caseNo = 1; caseNo <= t; ++caseNo) {
        int h, w;
        cin >> h >> w;
        getline(cin, dummy);  // 读取 h w 后的换行符
        vector<string> grid(h);
        for (int i = 0; i < h; ++i) getline(cin, grid[i]);

        // 处理第一个孩子
        int n, x0, y0;
        cin >> n >> x0 >> y0;
        getline(cin, dummy);  // 读取 n x0 y0 后的换行符
        string movesA;
        getline(cin, movesA);
        string sA = "";
        int x = x0 - 1, y = y0 - 1;
        sA += grid[x][y];
        for (char ch : movesA) {
            if (ch == 'N') x--;
            else if (ch == 'S') x++;
            else if (ch == 'E') y++;
            else if (ch == 'W') y--;
            sA += grid[x][y];
        }

        // 处理第二个孩子
        int m, x1, y1;
        cin >> m >> x1 >> y1;
        getline(cin, dummy);  // 读取 m x1 y1 后的换行符
        string movesB;
        getline(cin, movesB);
        string sB = "";
        x = x1 - 1, y = y1 - 1;
        sB += grid[x][y];
        for (char ch : movesB) {
            if (ch == 'N') x--;
            else if (ch == 'S') x++;
            else if (ch == 'E') y++;
            else if (ch == 'W') y--;
            sB += grid[x][y];
        }

        int lenA = sA.size(), lenB = sB.size();

        // 初始化第一行
        for (int j = 0; j <= lenB; ++j) dp[0][j] = 0;

        // 滚动数组计算 LCS 长度
        for (int i = 1; i <= lenA; ++i) {
            int cur = i % 2, prev = 1 - cur;
            dp[cur][0] = 0;
            for (int j = 1; j <= lenB; ++j) {
                if (sA[i - 1] == sB[j - 1])
                    dp[cur][j] = dp[prev][j - 1] + 1;
                else
                    dp[cur][j] = max(dp[prev][j], dp[cur][j - 1]);
            }
        }

        int lcsLen = dp[lenA % 2][lenB];
        int deleteA = lenA - lcsLen;
        int deleteB = lenB - lcsLen;

        cout << "Case " << caseNo << ": " << deleteA << " " << deleteB << "\n";
    }
    return 0;
}

方法二:Hunt-Szymanski 算法(更高效)

// Kids in a Grid
// UVa ID: 10949
// Verdict: Accepted
// Submission Date: 2025-12-02
// UVa Run Time: 0.910s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

const int MAX_LEN = 20005;
const int CHAR_SET = 128;  // ASCII 范围

// Hunt-Szymanski 算法求 LCS
int huntSzymanskiLCS(const string& a, const string& b) {
    int n = a.size(), m = b.size();
    if (n > m) return huntSzymanskiLCS(b, a);  // 确保 n <= m
    
    // 为每个字符记录在 b 中出现的所有位置(逆序)
    vector<vector<int>> pos(CHAR_SET);
    for (int j = m - 1; j >= 0; --j) {
        pos[(unsigned char)b[j]].push_back(j);
    }
    
    // dp[i] 表示长度为 i 的 LCS 的最后一个字符在 b 中的最小位置
    vector<int> dp(n + 1, INT_MAX);
    dp[0] = -1;
    
    for (int i = 0; i < n; ++i) {
        unsigned char c = a[i];
        // 对每个字符在 b 中的位置进行二分查找
        for (int j : pos[c]) {
            // 在 dp 中找到第一个 >= j 的位置
            int k = upper_bound(dp.begin(), dp.end(), j - 1) - dp.begin();
            if (dp[k] > j) dp[k] = j;
        }
    }
    
    // 找到最大的 i 使得 dp[i] < INF
    for (int i = n; i >= 0; --i) 
        if (dp[i] != INT_MAX) return i;
    return 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int t;
    cin >> t;
    string dummy;
    getline(cin, dummy);
    for (int caseNo = 1; caseNo <= t; ++caseNo) {
        int h, w;
        cin >> h >> w;
        getline(cin, dummy);
        vector<string> grid(h);
        for (int i = 0; i < h; ++i) getline(cin, grid[i]);
        
        // 处理第一个孩子
        int n, x0, y0;
        cin >> n >> x0 >> y0;
        getline(cin, dummy);
        string movesA;
        getline(cin, movesA);
        string sA = "";
        int x = x0 - 1, y = y0 - 1;
        sA += grid[x][y];
        for (char ch : movesA) {
            if (ch == 'N') x--;
            else if (ch == 'S') x++;
            else if (ch == 'E') y++;
            else if (ch == 'W') y--;
            sA += grid[x][y];
        }
        
        // 处理第二个孩子
        int m, x1, y1;
        cin >> m >> x1 >> y1;
        getline(cin, dummy);
        string movesB;
        getline(cin, movesB);
        string sB = "";
        x = x1 - 1, y = y1 - 1;
        sB += grid[x][y];
        for (char ch : movesB) {
            if (ch == 'N') x--;
            else if (ch == 'S') x++;
            else if (ch == 'E') y++;
            else if (ch == 'W') y--;
            sB += grid[x][y];
        }
        
        int lcsLen = huntSzymanskiLCS(sA, sB);
        int deleteA = sA.size() - lcsLen;
        int deleteB = sB.size() - lcsLen;
        
        cout << "Case " << caseNo << ": " << deleteA << " " << deleteB << "\n";
    }
    return 0;
}

性能对比

算法时间复杂度空间复杂度适用场景
经典 DP\texttt{DP}DPO(nm)O(nm)O(nm)O(min⁡(n,m))O(\min(n,m))O(min(n,m))小规模数据,实现简单
Hunt-SzymanskiO((n+m)log⁡n+∣Σ∣log⁡n)O((n+m) \log n + \vert \Sigma \vert \log n)O((n+m)logn+∣Σ∣logn)O(n+m+∣Σ∣)O(n+m+\vert \Sigma \vert)O(n+m+∣Σ∣)字符集较小,大规模数据

对于本题:

  • 字符集大小 ∣Σ∣=95|\Sigma| = 95∣Σ∣=95
  • 最大字符串长度 200012000120001
  • Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法更优,预计运行时间可降低到 100100100-200200200 ms\texttt{ms}ms

总结

本题的关键在于:

  1. 将"删除最少字符使字符串相同"问题转化为 LCS\texttt{LCS}LCS 问题
  2. 正确处理空移动序列的输入
  3. 根据数据规模选择合适的 LCS\texttt{LCS}LCS 算法

使用 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法可以高效处理最大规模数据,而经典动态规划算法在小规模数据上实现更简单。两种方法都正确,但 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法在性能上更有优势。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值