2026-02-22:删除子字符串后不同的终点。用go语言,给你一个只包含字符 U、D、L、R 的字符串 s,用来表示在无限的二维格点上每一步的方向(U 向上,

0 阅读8分钟

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:初始化方向映射和基础变量

  1. 方向映射(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)
  2. 初始位移(p):用pair{0,0}表示初始坐标(起点),对应“删除第一段长度为k的子串(即前k个字符)”时的被删除位移(初始为0,因为还没计算任何字符)。
  3. 集合(set):用于存储所有不同的终点坐标,初始时加入第一个情况的结果(删除前k个字符时,剩余路径的终点 = 总位移 - 初始p(0,0),但代码里先初始化set为{p: {}},后续会修正这个逻辑的执行顺序)。

步骤3:滑动窗口计算所有k长度子串的位移

代码用滑动窗口(长度为k)遍历所有可能的k长度子串,计算每个子串的总位移:

  • 窗口范围:从第0k-1位,逐步滑动到第n-kn-1位(共n-k+1个窗口)。
  • 滑动规则:
    1. 初始窗口(0~k-1):p的初始值是(0,0),对应该窗口的位移(代码里先把这个窗口的位移对应的终点存入set)。
    2. 窗口右移一位(比如从i-ki-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)

以题目示例为例,拆解执行过程:

  1. 总长度n=3,k=1,窗口数量=3(删除第0位、第1位、第2位)。
  2. 初始化:p=(0,0),set={(0,0): {}}。
  3. 遍历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)}。
  4. 最终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)

三、总结

  1. 核心思路:将“删除k长度子串后的终点”转化为“总位移 - 被删除子串的位移”,避免模拟删除后的路径,大幅降低计算量。
  2. 实现方式:用滑动窗口高效计算所有k长度子串的位移,利用map(set)自动去重,最终统计map的大小即为答案。
  3. 复杂度:时间复杂度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;
}

在这里插入图片描述