2026-02-22:删除子字符串后不同的终点。用go语言,给你一个只包含字符 U、D、L、R 的字符串 s,用来表示在无限的二维格点上每一步的方向(U 向上,D 向下,L 向左,R 向右)。另有一个正整数 k。
你需要从 s 中取出并删掉一段长度为 k 的连续字符(只能删一次)。随后从起点 (0,0) 出发,按删掉这段之后剩余的字符顺序执行移动。
问:在允许任意选择删掉位置的情况下,可能得到的不同终点坐标共有多少个?
1 <= s.length <= 100000。
s 只包含 'U'、'D'、'L' 和 'R'。
1 <= k <= s.length。
输入:s = "LUL", k = 1。
输出:2。
解释:
移除长度为 1 的子字符串后,s 可以是 "UL"、"LL" 或 "LU"。执行这些移动后,最终坐标将分别是 (-1, 1)、(-2, 0) 和 (-1, 1)。有两个不同的点 (-1, 1) 和 (-2, 0),因此答案是 2。
题目来自力扣3694。
一、解题过程分步解析
要理解这段代码的逻辑,首先需要把“删除一段长度为k的子串后走剩余路径的终点”这个问题,转化为更易计算的数学等价问题,以下是详细步骤:
步骤1:核心问题的数学转化(关键思路)
题目要求“删除长度为k的连续子串,走剩余字符得到终点”,我们可以把路径拆分为三部分:
- 原路径:
[0...n-1](n是字符串长度) - 若删除的是
[i...i+k-1]这段子串,剩余路径就是[0...i-1] + [i+k...n-1]
从坐标变化的角度,剩余路径的终点 = 前i步的总位移 + 从i+k到末尾的总位移。 而代码中用了更巧妙的等价计算: 总位移(走完全部字符)= 前i步位移 + 被删除的k步位移 + 后段(i+k到末尾)位移 → 剩余路径终点 = 总位移 - 被删除的k步位移
这个转化是核心:不用模拟删除后的路径,只需计算“总位移减去任意一段长度为k的连续子串的位移”,结果就是删除该子串后的终点。
步骤2:初始化方向映射和基础变量
- 方向映射(dirs):定义了每个方向字符对应的坐标变化:
- 'L':x减1(向左),y不变 → (-1, 0)
- 'R':x加1(向右),y不变 → (1, 0)
- 'D':y减1(向下),x不变 → (0, -1)
- 'U':y加1(向上),x不变 → (0, 1)
- 初始位移(p):用
pair{0,0}表示初始坐标(起点),对应“删除第一段长度为k的子串(即前k个字符)”时的被删除位移(初始为0,因为还没计算任何字符)。 - 集合(set):用于存储所有不同的终点坐标,初始时加入第一个情况的结果(删除前k个字符时,剩余路径的终点 = 总位移 - 初始p(0,0),但代码里先初始化set为{p: {}},后续会修正这个逻辑的执行顺序)。
步骤3:滑动窗口计算所有k长度子串的位移
代码用滑动窗口(长度为k)遍历所有可能的k长度子串,计算每个子串的总位移:
- 窗口范围:从第0
k-1位,逐步滑动到第n-kn-1位(共n-k+1个窗口)。 - 滑动规则:
- 初始窗口(0~k-1):p的初始值是(0,0),对应该窗口的位移(代码里先把这个窗口的位移对应的终点存入set)。
- 窗口右移一位(比如从i-k
i-1滑到i-k+1i):- 移出字符:窗口最左边的字符(s[i-k]),需要减去该字符对应的位移(dirs[out].x/dirs[out].y)。
- 移入字符:窗口新加入的字符(s[i]),需要加上该字符对应的位移(dirs[in].x/dirs[in].y)。
- 此时p的值就是“当前窗口(长度为k)的总位移”。
- 每个窗口的p计算完成后,将p存入set(set会自动去重,因为map的key是pair,重复的坐标不会被重复存储)。
步骤4:结合示例验证(s="LUL", k=1)
以题目示例为例,拆解执行过程:
- 总长度n=3,k=1,窗口数量=3(删除第0位、第1位、第2位)。
- 初始化:p=(0,0),set={(0,0): {}}。
- 遍历i从1开始(k=1,i < 3):
- i=1(对应删除第1位字符):
- in=s[1]='U',out=s[0]='L'。
- p.x = 0 + dirs['U'].x - dirs['L'].x = 0 + 0 - (-1) = 1。
- p.y = 0 + dirs['U'].y - dirs['L'].y = 0 + 1 - 0 = 1。
- p=(1,1),存入set → set={(0,0), (1,1)}。
- i=2(对应删除第2位字符):
- in=s[2]='L',out=s[1]='U'。
- p.x = 1 + dirs['L'].x - dirs['U'].x = 1 + (-1) - 0 = 0。
- p.y = 1 + dirs['L'].y - dirs['U'].y = 1 + 0 - 1 = 0。
- p=(0,0),存入set(无新增)→ set仍为{(0,0), (1,1)}。
- i=1(对应删除第1位字符):
- 最终set的大小是2,与题目输出一致(注:代码中p实际是“被删除子串的位移”,而“剩余路径终点=总位移-p”,但因为总位移是固定值,“总位移-p”的去重结果和p的去重结果数量相同,所以代码直接统计p的去重数量即可)。
补充:总位移的等价性验证
以示例s="LUL"为例,总位移计算:
- 走完全部字符:L(x=-1,y=0)→ U(x=-1,y=1)→ L(x=-2,y=1)→ 总位移(-2,1)。
- 删除第0位(L):剩余路径是U+L → 位移(0,1)+(-1,0)=(-1,1) → 总位移(-2,1) - 被删除位移(-1,0) = (-1,1)。
- 删除第1位(U):剩余路径是L+L → 位移(-1,0)+(-1,0)=(-2,0) → 总位移(-2,1) - 被删除位移(0,1) = (-2,0)。
- 删除第2位(L):剩余路径是L+U → 位移(-1,0)+(0,1)=(-1,1) → 总位移(-2,1) - 被删除位移(-1,0) = (-1,1)。 可见“剩余路径终点=总位移-被删除子串位移”成立,因此统计“被删除子串位移”的去重数量,就是最终答案。
二、时间复杂度与空间复杂度分析
1. 时间复杂度
- 滑动窗口遍历:遍历字符串中从k到len(s)-1的所有字符,共
len(s)-k次循环,每次循环仅做常数次操作(加减坐标、存入map)→ O(n)(n是字符串长度)。 - map的插入操作:Go中map的插入平均时间复杂度是O(1),最坏O(n),但实际工程中可认为是O(1)。
- 总时间复杂度:O(n)(n≤1e5时,该复杂度是高效的,符合题目数据范围要求)。
2. 额外空间复杂度
- 方向映射dirs:固定存储4个键值对,空间O(1)。
- 集合set:最多存储
n-k+1个不同的pair(最坏情况所有k长度子串的位移都不同)→ O(n)。 - 其他变量(p、循环变量等):O(1)。
- 总额外空间复杂度:O(n)。
三、总结
- 核心思路:将“删除k长度子串后的终点”转化为“总位移 - 被删除子串的位移”,避免模拟删除后的路径,大幅降低计算量。
- 实现方式:用滑动窗口高效计算所有k长度子串的位移,利用map(set)自动去重,最终统计map的大小即为答案。
- 复杂度:时间复杂度O(n),空间复杂度O(n),能高效处理n=1e5的输入规模。
Go完整代码如下:
package main
import (
"fmt"
)
type pair struct{ x, y int }
var dirs = []pair{'L': {-1, 0}, 'R': {1, 0}, 'D': {0, -1}, 'U': {0, 1}}
func distinctPoints(s string, k int) int {
p := pair{}
set := map[pair]struct{}{p: {}} // 第一个窗口
for i := k; i < len(s); i++ {
in, out := s[i], s[i-k]
p.x += dirs[in].x - dirs[out].x
p.y += dirs[in].y - dirs[out].y
set[p] = struct{}{}
}
return len(set)
}
func main() {
s := "LUL"
k := 1
result := distinctPoints(s, k)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-
def distinct_points(s: str, k: int) -> int:
# 定义方向映射
dirs = {'L': (-1, 0), 'R': (1, 0), 'D': (0, -1), 'U': (0, 1)}
# 初始化位置和集合
x, y = 0, 0
points = {(x, y)} # 第一个窗口
for i in range(k, len(s)):
in_char, out_char = s[i], s[i - k]
# 更新位置:加上新字符的移动,减去移出窗口的字符的移动
dx_in, dy_in = dirs[in_char]
dx_out, dy_out = dirs[out_char]
x += dx_in - dx_out
y += dy_in - dy_out
points.add((x, y))
return len(points)
def main():
s = "LUL"
k = 1
result = distinct_points(s, k)
print(result)
if __name__ == "__main__":
main()
C++完整代码如下:
#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>
struct Pair {
int x, y;
// 为了能作为unordered_set的key,需要定义相等比较和哈希函数
bool operator==(const Pair& other) const {
return x == other.x && y == other.y;
}
};
// 为Pair类型提供哈希函数
namespace std {
template<>
struct hash<Pair> {
size_t operator()(const Pair& p) const {
return hash<int>()(p.x) ^ (hash<int>()(p.y) << 1);
}
};
}
int distinctPoints(const std::string& s, int k) {
// 定义方向映射
std::unordered_map<char, Pair> dirs = {
{'L', {-1, 0}},
{'R', {1, 0}},
{'D', {0, -1}},
{'U', {0, 1}}
};
Pair p = {0, 0};
std::unordered_set<Pair> points;
points.insert(p); // 第一个窗口
for (int i = k; i < s.length(); i++) {
char in = s[i];
char out = s[i - k];
p.x += dirs[in].x - dirs[out].x;
p.y += dirs[in].y - dirs[out].y;
points.insert(p);
}
return points.size();
}
int main() {
std::string s = "LUL";
int k = 1;
int result = distinctPoints(s, k);
std::cout << result << std::endl;
return 0;
}