LeetCode 127 ---- 单词接龙【C语言】
本文写作目的:
锻炼自己的写作能力
分享解题思路(水平有限,作为学习后输出的一种方式)
本题解法会用到DFS、字典树等概念,本文不解释这些概念什么意思,怎么实现。
题目
解题思路
本题并没有直接一次做出来,因此,本部分为自己学习Leetcode官方题解的内容。
-
理解题意:题目给定一个开始单词、一个词典和一个结束单词,每次变化单词中的一个字母,直到单词变化为结束单词为止,要求每次变化的单词必须在词典中,求最短的变化路径长度。
-
理解题目给的例子:
给定开始单词:"hit"
给定词典:["hot","dot","dog","lot","log","cog"]
给定结束单词:"cog"
变化过程:"hit" -> "hot" -> "dot" -> "dog" -> "cog"
最短变化路径长度:5
-
题目特别的说明:题目给定字典不包含重复单词,题目给定的字典中的单词长度一样,单词都是小写字母组成
-
分析暴力解法:
编程题(非数学题)通常是遍历能搞定的题目,这类题目适合计算机,各种算法即是各种优化暴力解法的方法。
因此,首先思考暴力解法。
暴力解法就是从开始单词起,每次都遍历一遍词典,找出满足变化一个字母后相同的单词,重复这个步骤,直到找到结束词为止。如上图所示,红色节点cog为结束单词,从蓝色节点hit下来至少有3条路径满足要求,红色路径为最短路径。
从上面的暴力解法可以看出,这道题其实是图路径遍历搜索的问题。
这个问题有几个比较关键的子问题,解决了子问题,题目也就解决了。
-
数据的组成方式
-
遍历方式
-
判断两个字母是否仅相差一个字母的方式
-
-
广度优先搜索 + 优化建图
首先,从暴力解法可以推出本题通过广度优先搜索,遍历到第一个结束单词,此时的路径长度就是题目的解。这样遍历方式的问题已经解决。
下一步,输入数据以什么方式组织起来,方便搜索。我们可以定义一个二维数组graph[id1][i],其中id1表示一个单词,i表示该单词连接的第i个节点,graph[id1][i]表示id1代表的单词连接的第i个节点的id是多少。
广度优先搜索的过程中,每次要比较上一个节点的单词和词典中的节点单词是否满足相差一个字母的要求,是比较费时的。因此,提出了一种优化建图的方法。
优化建图是在两个节点之间插入一个虚拟节点,虚拟节点满足与相连的节点只差一个字母的要求。如果两个节点都能连接到虚拟节点上,则认为这两个节点满足要求。
-
-
如上图所示,hit可以转变为*it, h*t, hi*,hot可以转变成*ot, h*t, ho*,都可以转变成h*t,因此hit和hot满足相差一个字母的要求。注意插入了虚拟节点,求解最终的路径的时候需要减去虚拟节点。
这样三个问题都已经解决,上面解释得还比较抽象,下面贴出此题的代码,已经构成的graph。
/* 1、字典树保存单词 2、插入虚拟单词,减少匹配次数 3、构建图 4、dfs遍历图 */ //定义一个足够大的字典树 struct Trie { int ch[27]; int val; //单词的尾巴标记代表字典中的单词id } trie[50001]; //size表示trie的大小 //nodeNum表示trie插入的word个数 int size, nodeNum; //插入节点到字典树上 void insert(char* word, int num) { int len = strlen(word); int add = 0; int x; for (int i = 0; i < len; i++) { x = word[i] - '`'; if (trie[add].ch[x] == 0) { trie[add].ch[x] = ++size; memset(trie[size].ch, 0, sizeof(trie[size].ch)); trie[size].val = -1; } add = trie[add].ch[x]; } trie[add].val = num; } //从字典树中查找对应单词的id int find(char* word) { int len = strlen(word); int add = 0; int x; for (int i = 0; i < len; i++) { x = word[i] - '`'; if (trie[add].ch[x] == 0) { return -1; } add = trie[add].ch[x]; } return trie[add].val; } /* graph构建思路: 数据结构:graph[id1][index] = id2 表示 id1连的第index个节点是id2 edgeSize[id1] = count 表示graph中id1连接了count条边 根据数据结构,向graph中添加单词 */ graph[30001][26]; edgeSize[30001]; //向字典树中添加单词,并返回id int addWord(char* word) { int id = find(word); if (id == -1) { insert(word, nodeNum++); id = find(word); } return id; /* if (find(word) == -1) { insert(word, nodeNum++); } return find(word); */ } //向graph中加边 void addEdge(char* word) { int len = strlen(word); int id1 = addWord(word); int id2; char tmp; //遍历插入虚拟节点 for (int i = 0; i < len; i++) { tmp = word[i]; word[i] = '`'; id2 = addWord(word); graph[id1][edgeSize[id1]++] = id2; graph[id2][edgeSize[id2]++] = id1; word[i] = tmp; } } int ladderLength(char * beginWord, char * endWord, char ** wordList, int wordListSize){ size = nodeNum = 0; memset(trie[size].ch, 0, sizeof(trie[size].ch)); trie[size].val = -1; memset(edgeSize, 0, sizeof(edgeSize)); for (int i = 0; i < wordListSize; i++) { addEdge(wordList[i]); } addEdge(beginWord); int id1 = find(beginWord); int id2 = find(endWord); if (id2 == -1) { return 0; } int dis[nodeNum]; memset(dis, -1, nodeNum * sizeof(int)); dis[id1] = 0; int que[nodeNum]; int left = 0; int right = 0; que[right++] = id1; int tmpid; while (left < right) { tmpid = que[left++]; for (int i = 0; i < edgeSize[tmpid]; i++) { if (dis[graph[tmpid][i]] == -1) { dis[graph[tmpid][i]] = dis[tmpid] + 1; if (graph[tmpid][i] == id2) { return dis[graph[tmpid][i]] / 2 + 1; } que[right++] = graph[tmpid][i]; } } } return 0; }
-
双向广度优先搜索
暂未学习~~~