当前位置: 首页 > news >正文

字符串字典树-依依的瓶中信

问题描述

依依是一个住在海边小镇的女孩,她的朋友们分散在世界的各个角落。他们有一个特殊的传递信息的方式,那就是通过海洋传递瓶中信。每个瓶中信里,都装着一串由小写英文字母组成的信息,代表一个友情的密码。

这个夏天,依依在海滩上捡到了 N 个瓶中信,每个瓶中信里都有一条由小写英文字符组成的信息,这些信息分别来自她的 N 个朋友。我们记第 ii 个朋友的信息为 Si,其中 i=1,2,...,N。

为了找出与自己最有缘分的朋友,依依决定比较这些信息的相似度。这里的"相似度"指的是两条信息从头开始,最长能够匹配的字符数量。

注意,依依并不想比较一条信息与它自身的相似度。

现在,依依希望你能帮助她找出对于每条信息 Si,哪条信息与其最相似,即从开头开始,最长能连续匹配的字符的数量是多少。

输入格式

输入的第一行包含一个整数 NN(1≤N≤104)。

接下来的 NN 行,每行包含一个由小写字符构成的字符串 SiSi​,表示小蓝的一个朋友在信封里刻写的信息。保证

输出格式

输出共 N行,对于每条信息Si​,输出一个整数,表示与 Si​ 最接近的信息的最长公共前缀的长度。

样例输入

3
abc
ab
bc

 

样例输出

2
2
0

解题代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
string s[N];//存储n个信息s
int tire[N][27],idx=2,cnt[N];

void insert(string a){  //生成一个tire树,把所有s都插入到树中
  int n=a.length()-1;//在 C++ 的 std::string 中,a[i] 不会自动包含 '\0'。
  int x=1;
  for(int i=1;i<=n;i++){
    if(!tire[x][a[i]-'a']) tire[x][a[i]-'a']=idx++; //如果不存在的话就创建这个节点
    x=tire[x][a[i]-'a'];  //这个是让x指向当前节点
    cnt[x]++;
    //让每个走过当前节点都用cnt记录下来并自加
    //这个是关键,当cnt[x]>1的时候说明这个节点至少有两个字符串是走过这里的
  }
}

int check(string a){
  int n=a.length()-1;
  int x=1;
  int ans=0;
  for(int i=1;i<=n;i++){
    x=tire[x][a[i]-'a'];
    if(cnt[x]!=1){//当cnt[x]不等于1的时候说明此时的s[i]肯定有一个s的前s[t]和s[i]的前i是相同的
      ans=i;//当存在s[t]和s[i]相同的时候就让ans=i,i是相同的字符的个数
    }
    else break;//如果cnt[x]==1说明没有与之匹配的串了,直接退出返回当前的ans就行
  }
  return ans;
}
int main(){
  int n;
  cin>>n;
  for(int i=1;i<=n;i++){
    cin>>s[i];
    s[i]='0'+s[i];
    insert(s[i]);
  }
  for(int i=1;i<=n;i++){
    cout<<check(s[i])<<"\n";
  }
  return 0;
}

解题思路:

  1. 字典树(Trie)

    • 使用字典树存储所有字符串的前缀信息。

    • 字典树的每个节点表示一个字符,从根节点到某个节点的路径表示一个前缀。

  2. 统计前缀出现次数

    • 在插入字符串时,记录每个节点被访问的次数(cnt[x]++)。

    • 如果某个节点的 cnt[x] > 1,说明至少有两个字符串共享这个前缀。

  3. 查询最长公共前缀

    • 对于每个字符串,从根节点开始遍历字典树,找到最深的满足 cnt[x] > 1 的节点。

    • 这个节点的深度就是该字符串与其他字符串的最长公共前缀长度。


为什么将 cnt[x]++ 写在 for 循环里面?

在字典树算法中,cnt[x]++ 的作用是记录每个节点被访问的次数。以下是详细解释:


1. cnt[x]++ 的作用

  • cnt[x]:表示从根节点到节点 xx 的路径所表示的前缀被多少个字符串共享。

  • cnt[x]++:每当一个字符串经过节点 x 时,cnt[x] 增加 1。


2. 为什么写在 for 循环里面?

  • 插入字符串时

    • 在插入字符串的过程中,每经过一个节点 x,都需要更新 cnt[x]

    • 这是因为每个节点都代表一个前缀,cnt[x] 记录了有多少个字符串共享这个前缀。

  • 查询最长公共前缀时

    • 在查询过程中,cnt[x] 用于判断当前前缀是否被多个字符串共享。

    • 如果 cnt[x] > 1,说明当前前缀被至少两个字符串共享。


3. 示例分析

假设有以下字符串:

abc
ab
a

插入过程

  1. 插入 abc

    • 经过节点 acnt[a] = 1

    • 经过节点 bcnt[ab] = 1

    • 经过节点 ccnt[abc] = 1

  2. 插入 ab

    • 经过节点 acnt[a] = 2

    • 经过节点 bcnt[ab] = 2

  3. 插入 a

    • 经过节点 acnt[a] = 3

查询过程

  1. 查询 abc

    • 节点 acnt[a] = 3 > 1,匹配长度 = 1。

    • 节点 bcnt[ab] = 2 > 1,匹配长度 = 2。

    • 节点 ccnt[abc] = 1,停止。

    • 最长公共前缀长度 = 2。

  2. 查询 ab

    • 节点 acnt[a] = 3 > 1,匹配长度 = 1。

    • 节点 bcnt[ab] = 2 > 1,匹配长度 = 2。

    • 最长公共前缀长度 = 2。

  3. 查询 a

    • 节点 acnt[a] = 3 > 1,匹配长度 = 1。

    • 最长公共前缀长度 = 1。

代码说明:

第一种实现

void insert(string a) {
  int n = a.length() - 1;
  int x = 1;
  for (int i = 1; i <= n; i++) {
    if (!tire[x][a[i] - 'a']) tire[x][a[i] - 'a'] = idx++;
    x = tire[x][a[i] - 'a'];
    cnt[x]++;
  }
}

特点:

  1. 字符串下标从 1 开始

    • 字符串 a 的下标从 1 开始,a[0] 被忽略。

    • 这是因为在主函数中,字符串被修改为 s[i] = '0' + s[i],即在字符串前面添加了一个字符 '0',使得有效字符从下标 1 开始。

  2. 循环范围

    • 循环从 i = 1 到 i = n,其中 n = a.length() - 1

    • 这样可以正确处理字符串的有效部分。


第二种实现

void insert(string a) {
  int x = 1;
  for (int i = 0; a[i]; i++) {
    if (!tire[x][a[i] - 'a']) tire[x][a[i] - 'a'] = idx++;
    x = tire[x][a[i] - 'a'];
    cnt[x]++;
  }
}

特点

  1. 字符串下标从 0 开始

    • 字符串 a 的下标从 0 开始,a[0] 是第一个字符。

    • 这与主函数中对字符串的处理方式不一致。

  2. 循环范围

    • 循环从 i = 0 开始,直到 a[i] 为 '\0'(字符串结束符)。

    • 这种写法适用于标准的 C 风格字符串(以 '\0' 结尾),但在 C++ 的 std::string 中,a[i] 不会自动包含 '\0'

为什么不能使用第二种实现?

  1. 字符串下标不一致

    • 在主函数中,字符串被修改为 s[i] = '0' + s[i],即在字符串前面添加了一个字符 '0',使得有效字符从下标 1 开始。

    • 如果使用第二种实现,a[0] 会被错误地当作有效字符处理,导致 Trie 树插入错误。

  2. 循环范围问题

    • 第二种实现假设字符串以 '\0' 结尾,但在 C++ 的 std::string 中,a[i] 不会自动包含 '\0'

    • 如果字符串中没有 '\0',循环会越界访问,导致未定义行为。

  3. 逻辑错误

    • 第二种实现无法正确处理主函数中对字符串的修改(添加了前缀字符 '0')。

    • 这会导致 Trie 树中插入的字符串与实际字符串不一致,影响后续的查询操作。

字典树(Trie)来解决问题,但它们的应用场景具体实现有所不同。以下是详细对比:


题目 1:最长公共前缀问题(依依的瓶中信)

问题描述

  • 给定 N 个字符串,对于每个字符串 Si​,找到与其他字符串的最长公共前缀的长度。

代码特点

  • cnt[x]++ 写在 for 循环里面

    • 在插入字符串时,每经过一个节点 x,都会更新 cnt[x]

    • 目的是记录每个节点被多少个字符串共享,从而在查询时判断当前前缀是否被多个字符串共享。

核心逻辑

  • 插入时

    • 每经过一个节点 x,cnt[x]++,表示当前前缀被一个字符串共享。

  • 查询时

    • 遍历字符串的每个字符,找到最深的满足 cnt[x] > 1 的节点,其深度就是最长公共前缀的长度。


题目 2:判断是否有重复字符串

问题描述

  • 给定 N 个字符串,判断是否存在两个相同的字符串。

代码特点

  • cnt[x]++ 写在 for 循环外面

    • 在插入字符串时,cnt[x]++ 只在字符串的末尾节点执行。

    • 目的是记录每个字符串的末尾节点被访问的次数。

核心逻辑

  • 插入时

    • 在字符串的末尾节点 x,cnt[x]++,表示当前字符串被插入了一次。

  • 查询时

    • 遍历字符串的每个字符,找到末尾节点 x。

    • 如果 cnt[x] > 1,说明当前字符串被插入了多次,即存在重复字符串。


为什么 cnt[x]++ 的位置不同?

题目 1:最长公共前缀问题

  • 目标:统计每个前缀被多少个字符串共享。

  • 实现

    • 在插入时,每经过一个节点 x,cnt[x]++,表示当前前缀被一个字符串共享。

    • 这样可以在查询时,通过 cnt[x] 判断当前前缀是否被多个字符串共享。

题目 2:判断是否有重复字符串

  • 目标:判断是否存在两个相同的字符串。

  • 实现

    • 在插入时,只在字符串的末尾节点 x,cnt[x]++,表示当前字符串被插入了一次。

    • 这样可以在查询时,通过 cnt[x] 判断当前字符串是否被插入了多次。

总结

题目目标cnt[x]++ 位置作用
最长公共前缀问题找到与其他字符串的最长公共前缀for 循环里面记录每个前缀被多少个字符串共享
判断是否有重复字符串判断是否存在两个相同的字符串for 循环外面记录每个字符串的末尾节点被访问的次数

通过调整 cnt[x]++ 的位置,可以灵活地实现不同的功能。

 

 

 

 

相关文章:

  • 深度洞察!树莓集团南京产业园再布局的核心逻辑
  • 网络运维学习笔记(DeepSeek优化版) 013网工初级(HCIA-Datacom与CCNA-EI)ACL访问控制列表
  • 项目中同时使用Redis(lettuce)和Redisson的报错
  • 服务器带宽堵塞会对网站访问产生哪些影响?
  • 打破界限!家电行业3D数字化营销,线上线下无缝对接
  • Yashan DB 体系结构
  • 初识云计算
  • 【FastAPI】 AI场景快速学习指南
  • JS采集数据爬虫-Fetch API 和 XMLHttpRequest 有什么区别?
  • 影刀 RPA 实战开发阶段总结
  • Linux--如何安装rockyLinux9虚拟机
  • 3D匹配算法简述
  • OpenCV计算摄影学(19)非真实感渲染(Non-Photorealistic Rendering, NPR)
  • Vue入门常见指令
  • 【通义万相】蓝耘智算 | 开源视频生成新纪元:通义万相2.1模型部署与测评
  • 通过CycleGAN把不成对的可见光数据转换为红外数据
  • Debian系统grub新增启动项
  • 【Leetcode 每日一题 - 补卡】2588. 统计美丽子数组数目
  • XPath 定位复杂元素的最佳实践
  • 查看k8s集群的资源使用情况
  • 免费空间测试网站/黑帽seo365t技术
  • 营销型网站有那些网站/江北seo综合优化外包
  • 哪里有网站开发团队/一键优化下载
  • 一个服务器怎么做两个网站/广州头条新闻最新
  • 网站备案名称的影响/关键词排名怎么做上首页
  • 杭州seo网站推广/提升seo排名