BFS五字母单词链

问题描述

我需要有关BFS字链的作业帮助。 单词链基于五个字母的单词,当单词x中的最后四个字母位于单词y中时,两个单词会连接在一起。例如,攀爬和飞艇连接在一起是因为攀登中的l,i,m和b在飞艇这个词中。

建议使用Sedgewick算法第4版中的定向BFS或对其进行修改。可以在这里找到代码https://algs4.cs.princeton.edu/40graphs/,并使用以下代码来阅读 一个数据文件到列表单词:

BufferedReader r =
    new BufferedReader(new InputStreamReader(new FileInputStream(fnam)));
ArrayList<String> words = new ArrayList<String>();
while (true) {
    String word = r.readLine();
    if (word == null) { break; }
    assert word.length() == 5;  // inputcheck,if you run with assertions
    words.add(word);
}

和以下代码文件中读取测试用例:

BufferedReader r = 
    new BufferedReader(new InputStreamReader(new FileInputStream(fnam)));
while (true) {
    String line = r.readLine();
    if (line == null) { break; }
    assert line.length() == 11; // inputcheck,if you run with assertions
    String start = line.substring(0,5);
    String goal = line.substring(6,11);
    // ... search path from start to goal here
}

数据文件中的单词是:

their
moist
other
blimp
limps
about
there
pismo
abcde
bcdez
zcdea
bcdef
fzcde

使用测试用例文件时...

other there
other their
their other
blimp moist
limps limps
moist limps
abcde zcdea

...输出应为每个单词对之间的边数,如果单词之间没有路径,则输出为-1。

1
1
-1
3
0
-1
2

我刚开始使用图形,并且不确定如何使用Sedgewick的BFS并对其进行修改以读取测试用例文件。任何帮助表示赞赏。

解决方法

假设 n 是数据集中的单词数。

首先,我们需要根据给定的条件为上述所有单词建立一个邻接表,即当且仅当{{的最后四个字母在xy之间存在边1}} 出现在 x 中。构建这个邻接表是一个 O(n^2 * w) 操作,其中 w 是数据集中每个单词的平均大小。

其次,我们要做的就是对测试数据进行传统的 BFS。

这是 y 函数:

main

这是根据给定条件构建邻接表的函数:

    public static void main(String[] args) throws IOException {
        // get words from dataset
        List<String> words = readData();
        // get the word pairs to test
        List<List<String>> testData = getTestData();
        // form an adjacency list
        Map<String,List<String>> adj = getAdjacencyList(words);
        
        // for each test,do a traditional BFS
        for (List<String> test : testData) {
            System.out.println(bfs(adj,test));
        }
    }

这里是 BFS:

    public static Map<String,List<String>> getAdjacencyList(List<String> words) {
        Map<String,List<String>> adj = new HashMap<>();
        for (int i = 0; i < words.size(); ++i) {
            String word = words.get(i);
            adj.put(word,adj.getOrDefault(word,new ArrayList<>()));
            for (int j = 0; j < words.size(); ++j) {
                if (i == j) continue;
                int count = 0;
                String other = words.get(j);
                for (int k = 1; k < 5; ++k) {
                    count += other.indexOf(word.charAt(k)) != -1 ? 1 : 0;
                }
                // if the condition is satisfied,there exists an edge from `word` to `other`
                if (count >= 4)
                    adj.get(word).add(other);
            }
        }

        return adj;
    }
,

@The Room 提供了一个很好的答案,但我想建议对邻接列表构建部分进行简单的修改,因为所提供的构建列表的方法的复杂度为 O(n^2),这将导致性能不佳用于大型输入文件。

简单地,您可以将每个单词的 4 个字符的所有可能的排序模式插入到带有单词 ID(例如索引)的哈希映射中。

C++ 代码示例:

map<string,vector<int> >mappings ;

for(int i = 0 ; i < words.size();  i++){
    string word = words[i].substr(0,4) ; 
    sort(word.begin(),word.end()); 
    mappings[word].push_back(i); 
    for(int j = 0 ; j < 4 ; j++){
        word = words[i].substr(0,4) ; 
        word[j] = words[i][4]; 
        sort(word.begin(),word.end()); 
        mappings[word].push_back(i);
    }
}

现在你有一个词索引向量,你知道它们和任何以向量键的相同 4 个字符结尾的词之间必须有一条边。

然后您可以简单地构建图形,只需注意不要创建自循环(避免使用节点和自身创建边)。

代码示例:

// Building the graph with complexity of O(n * log(no. of edges))
const int N = 100000; // Just and example 
vector<int>graph[N]; 
for(int i = 0 ; i < words.size(); i++){
    string tmp = words[i].substr(1,4); 
    sort(tmp.begin(),tmp.end()); 
    for(int j = 0 ; j < mappings[tmp].size(); j++){
        if (j == mappings[tmp][j])
            continue; 
            
        graph[i].push_back(mappings[tmp][j]);
    }
}

最后,您可以遍历测试文件,获取开始和目标索引(读取文件时,将每个单词存储为具有索引值的键),然后应用 bfs 函数计算边数为在@The Room 的回答中描述

我只是想为那些可能需要解决具有大量输入的类似问题的人建议这个答案,这将降低构建从 O(N^2) 到 O(N * log(no.边)),其中 N 是单词数。

,

我的方法略有不同,我将在下面讨论的问题也有细微的差别:

首先我们创建一个邻接列表:(@Volpe95 对此有一个很好的优化)。 使用节点映射,其中单词是关键。

Map<String,Node> nodes = new HashMap<>();

        List<String> words = new DataHelper().loadWords("src/main/wordsInput.dat");
        System.out.println(words);

        for (int i = 0; i < words.size(); i++) {
            String l = words.get(i);
            nodes.put(l,new Node(l));
        }

        for(Map.Entry<String,Node> l: nodes.entrySet()) {
            for(Map.Entry<String,Node> r:nodes.entrySet()) {
                if (l.equals(r)) continue;
                if (isLinkPair(l.getKey(),r.getKey())) {
                    Node t = nodes.get(l.getKey());
                    System.out.println(t);
                    t.addChild(nodes.get(r.getKey()));
                }
            }

        }

IsLinkPair 检查是否可以在可能的子单词中找到单词的最后四个字母。

private static boolean isLinkPair(String l,String r) {
        // last 4 chars only
        for (int i = 1; i < l.length(); i++) {
            if(r.indexOf(l.charAt(i)) == -1){
                return false;
            }
        }
        return true;
    }

一个节点存储每个单词和子节点以及edgeTo,用于计算每个节点存储其父节点的最短路径。此子父级将始终位于最短路径上。 (Sedgewick 将这些数据存储在单独的数组中,但通常更容易将这些数据分组到一个类中,因为它使代码更易于理解)

(为了清晰和等于省略了 Getters Setters 等)

public class Node {
    private Set<Node> children;
    private String word;

    private Node edgeTo;

    private int visited;

    public Node(String word) {
        children = new HashSet<>();
        this.word = word;
        edgeTo = null;
    }
}

基于 Sedgewick 的 BFS 算法,依次搜索每个节点、其直接子节点和它们的子节点,依此类推。它每次都在远离原点的地方寻找。注意这里使用了一个队列,它是由 Java 中的 LinkedList 实现的。

private boolean bfs(Map<String,Node> map,Node source,Node target) {
        if(source == null || target == null) return false;
        if(source.equals(target))return true;
        Queue<Node> queue = new LinkedList<>();
        source.setVisited();
        queue.add(source);
        while(!queue.isEmpty()) {
            Node v = queue.poll();
            for (Node c : v.getChildren()) {
                if(c.getVisited()==0){
                    System.out.println("visiting " + c);
                    c.setVisited();
                    c.setEdgeTo(v);
                    if(c.equals(target)) {
                        return true;
                    }
                    queue.add(c);
                }
            }
        }

        return false;
    }

注意 v 是父级,c 是它的子级。 setEdgeTo 用于设置孩子的父母。

最后我们检查结果,其中 source 和 target 分别是源词和目标词:

BreadthFirstPaths bfs = new BreadthFirstPaths(nodes,source,target);
int shortestPath = bfs.getShortestPath(nodes,target);

那么我上面提到的细微差别呢?最短路径计算是必要的 zcdea 有两个父节点 fzcde 和 bcdez,你需要一个在最短路径上。要使用子节点的 edgeTo,找到它的父节点并重复,直到路径如下所示。由于 bfs 从原点向外搜索的方式,这种子父关系将始终处于最短路径上。

// get edgeTo on target (the parent),find this node and get its parent
    // continue until the shortest path is walked or no path is found
    public int getShortestPath(Map<String,String source,String target) {
        Node node = map.get(target);
        int pathLength = 0;
        do {
            if(node == null || pathLength > map.size()) return NOPATH;
            if(node.equals(map.get(source))) return pathLength;
            node = map.get(node.getWord()).getEdgeTo();
            pathLength++;
        } while (true);
    }

总是需要考虑和优化空间时间复杂度的权衡。