题目概述
有两个孩子在一个 H×WH \times WH×W 的网格中行走,网格中的每个格子包含一个字符(ASCII\texttt{ASCII}ASCII 码在 333333 到 127127127 之间)。两个孩子每一步都可以向北、东、西、南四个方向移动。第一个孩子走了 NNN 步,第二个孩子走了 MMM 步,且满足 0≤N≤M≤200000 \leq N \leq M \leq 200000≤N≤M≤20000。
记录每个孩子走过的所有字符,得到两个字符串 SAS_ASA 和 SBS_BSB。我们需要从两个字符串中删除尽可能少的字符,使得删除后的两个新字符串完全相同。
输入格式
第一行包含一个整数 ttt(1≤t≤151 \leq t \leq 151≤t≤15),表示测试用例的数量。每个测试用例包含以下部分:
- 两个整数 HHH 和 WWW(1≤H,W≤201 \leq H, W \leq 201≤H,W≤20)
- 接下来 HHH 行,每行包含 WWW 个字符,表示网格内容
- 三个整数 NNN、X0X_0X0 和 Y0Y_0Y0,表示第一个孩子从 (X0,Y0)(X_0, Y_0)(X0,Y0) 出发,走 NNN 步(1≤X0≤H1 \leq X_0 \leq H1≤X0≤H, 1≤Y0≤W1 \leq Y_0 \leq W1≤Y0≤W,XXX 从北向南增加,YYY 从西向东增加)
- 一个长度为 NNN 的字符串,由字符 N\texttt{N}N、E\texttt{E}E、W\texttt{W}W、S\texttt{S}S 组成,分别表示北、东、西、南
- 第二个孩子的信息格式相同,包含 MMM、X1X_1X1、Y1Y_1Y1 和一个长度为 MMM 的移动序列字符串
注意:
- 行走序列保证合法,不会走出网格边界
- NNN 或 MMM 可能为 000,此时对应的移动序列字符串为空行
输出格式
对于每个测试用例,输出用例编号和两个整数 XAX_AXA 和 XBX_BXB,分别表示从 SAS_ASA 和 SBS_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}ABCDG(SAS_ASA)
第二个孩子从 (3,1)(3,1)(3,1) 出发,走 NES\texttt{NES}NES,经过的字符为 ADEB\texttt{ADEB}ADEB(SBS_BSB)
最长公共子序列可以是 AB\texttt{AB}AB 或 AD\texttt{AD}AD,长度为 222
因此需要从 SAS_ASA 中删除 5−2=35-2=35−2=3 个字符,从 SBS_BSB 中删除 4−2=24-2=24−2=2 个字符
问题分析与解题思路
核心问题转化
题目要求从两个字符串中删除尽可能少的字符,使得剩下的字符串相同。这等价于求两个字符串的最长公共子序列(LCS\texttt{LCS}LCS)的长度。
设:
- SAS_ASA 的长度为 lenAlen_AlenA
- SBS_BSB 的长度为 lenBlen_BlenB
- LCS\texttt{LCS}LCS 长度为 lcslcslcs
则需要删除的字符数为:
- 从 SAS_ASA 中删除:lenA−lcslen_A - lcslenA−lcs
- 从 SBS_BSB 中删除:lenB−lcslen_B - lcslenB−lcs
因此,问题的核心转化为高效计算两个字符串的 LCS\texttt{LCS}LCS 长度。
数据规模分析
- 0≤N≤M≤200000 \leq N \leq M \leq 200000≤N≤M≤20000,因此字符串长度最大为 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)logn+∣Σ∣logn)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 算法可以获得更好的性能。
解题步骤
-
构建网格矩阵:读取 H×WH \times WH×W 的字符网格
-
生成路径字符串:
- 从起点 (X0,Y0)(X_0, Y_0)(X0,Y0) 开始,将起点字符加入字符串
- 按照移动序列逐步移动,将经过的每个格子字符加入字符串
- 注意:坐标需要从 1−1-1−based\texttt{based}based 转换为 0−0-0−based\texttt{based}based
- 特别注意:当 N=0N=0N=0 或 M=0M=0M=0 时,移动序列为空,需要使用 getline\texttt{getline}getline 正确读取空行
-
计算 LCS\texttt{LCS}LCS 长度:
- 方法一:使用滚动数组的动态规划
- 方法二:使用 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法
-
计算结果:
- 删除字符数 = 字符串长度 - LCS\texttt{LCS}LCS 长度
-
输出结果:按照格式输出
关键注意事项
-
空移动序列的处理:当 N=0N=0N=0 或 M=0M=0M=0 时,移动序列字符串为空行,必须使用 getline\texttt{getline}getline 而非 cin >>\texttt{cin >>}cin >> 来读取
-
坐标转换:输入使用 111-based 坐标,而数组使用 000-based 索引,需要减 111 转换
-
包含起点字符:无论移动步数多少,起点字符总是包含在路径字符串中
-
边界保证:题目保证移动序列不会越界,无需进行边界检查
代码实现
方法一:经典动态规划(滚动数组优化)
// 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}DP | O(nm)O(nm)O(nm) | O(min(n,m))O(\min(n,m))O(min(n,m)) | 小规模数据,实现简单 |
| Hunt-Szymanski | O((n+m)logn+∣Σ∣logn)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
总结
本题的关键在于:
- 将"删除最少字符使字符串相同"问题转化为 LCS\texttt{LCS}LCS 问题
- 正确处理空移动序列的输入
- 根据数据规模选择合适的 LCS\texttt{LCS}LCS 算法
使用 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法可以高效处理最大规模数据,而经典动态规划算法在小规模数据上实现更简单。两种方法都正确,但 Hunt-Szymanski\texttt{Hunt-Szymanski}Hunt-Szymanski 算法在性能上更有优势。
1006

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



