【问题标题】:BFS five letter word chainBFS 五个字母词链
【发布时间】:2020-10-26 10:03:53
【问题描述】:

我需要一些关于 BFS 词链作业的帮助。 单词链是基于五个字母的单词,当单词 x 的最后四个字母在单词 y 中时,两个单词被连接起来。例如爬升和飞艇是相连的,因为爬升中的 l、i、m 和 b 在 blimp 中。

建议使用 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 并对其进行修改以读取测试用例文件。任何帮助表示赞赏。

【问题讨论】:

  • Java 是首选语言吗?还是单纯的算法无关紧要?
  • @DanielHao 我对其他语言不太熟悉,所以我更喜欢 Java,但即使是其他语言也可能会有所帮助。
  • 你能发布你目前的代码吗?
  • 只是为了理解,所以服务bfs算法,你不知道如何将文件传递给算法?对吗?

标签: java algorithm graph-algorithm


【解决方案1】:

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

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

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

这是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));
        }
    }

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

    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;
    }

这是 BFS:

    public static int bfs(Map<String, List<String>> adj, List<String> test) {
        Queue<String> q = new LinkedList<>();
        Set<String> visited = new HashSet<>(); // to keep track of the visited words, since the graph is not necessarily a DAG
        String start = test.get(0);
        String end = test.get(1);
        // if `start` and `end` words are equal
        if (start.equals(end))
            return 0;

        q.add(start);
        visited.add(start);
        int count = 0;
        while (!q.isEmpty()) {
            count++;
            int size = q.size();
            for (int i = 0; i < size; ++i) {
                String word = q.poll();
                for (String val : adj.get(word)) {
                    if (val.equals(end))
                        return count; // return the number of edges
                    if (!visited.contains(val)) // only add the words which aren't visited yet.
                        q.add(val);
                }
            }
        }
        return -1; // if there isn't any edge
    }

【讨论】:

  • 感谢您的建议,虽然我不确定如何读取 testdata 以将其添加到 List> testData。
  • 这并没有给出 abcde zcdea 的最短路径,看看我下面的答案,看看怎么做
【解决方案2】:

@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 是单词数。

【讨论】:

    【解决方案3】:

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

    首先我们创建一个邻接列表:(@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,source,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,Node> map, 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);
        }
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-14
      • 1970-01-01
      相关资源
      最近更新 更多