Coursera Algorithm Ⅱ week1 WordNet

全部代码 https://github.com/Joshmomel/Princeton_Algorithms/tree/wordnet/src

背景

这次作业需要先构造一个wordnet, 然后在此基础上找出两组次的最短距离

wordnet是单词指向上位词的一个网络

wordnet

SAP

需求

根据FAQ 的Possible progress steps, 我们先要完成SAP. 也就是输入一个Digraph, 点v, 点w, 就算出最短共同祖先 (shortest ancestral path), 以及最短距离.

在这里插入图片描述


比如左图, 点3跟10的最短共同祖先就是1, 他们的距离是4
比如右图, 点1跟点5的最短共同祖先就是0, 他们的距离是2

算法分析

  1. 对点v做一次BFS, 使用mapV记录下经过的点以及其距离, {vertices: distance}
  2. 设置两个variable, node以及count, 初始值为-1, 用于储存下面的最短距离以及祖先用
  3. 对点w做一次BFS, 如果点n同时也经过mapV, 那么计算一下点v跟w的距离, 如果比count少, 则跟新count, 并且把node指向n
 BreadthFirstDirectedPaths bfsFromV = new BreadthFirstDirectedPaths(G, v);
 BreadthFirstDirectedPaths bfsFromW = new BreadthFirstDirectedPaths(G, w);

 HashMap<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < G.V(); i++) {
      if (bfsFromW.hasPathTo(i)) {
        map.put(i, bfsFromW.distTo(i));
      }
    }

    int node = -1;
    int count = -1;
    for (int i = 0; i < G.V(); i++) {
      var find = map.get(i);
      if (find != null && bfsFromV.hasPathTo(i)) {
        var total = find + bfsFromV.distTo(i);
        if (count == -1 || total < count) {
          count = total;
          node = i;
        }
      }
    }

上面的count就是lenght, node就是ancestor

性能提升

当然, 除了算法这个SAP还需要考虑在call length(int v, int w), ancestor(int v, int w) 怎么不用重复调用BFS, 毕竟调用一次就能同时算出length以及ancestor 我的方法是通过map存起来

具体来说, 构建一个VwPair类

private static class VwPair {
    int v;
    int w;

    public VwPair(int v, int w) {
      this.v = v;
      this.w = w;
    }
}

构建一个VwData类

 private static class VwData {
   int length;
   int ancestor;

   public VwData(int length, int ancestor) {
     this.length = length;
     this.ancestor = ancestor;
   }
 }

然后map是

private final HashMap<VwPair, VwData> db = new HashMap<>();

流程就是
ancestor(int v, int w) -> getSAP(v, w) -> 如果db没有 -> setSAP()
length(int v, int w) -> getSAP(v, w) -> 如果db没有 -> setSAP()

那么如果先ancestor(int v, int w), 则在length(int v, int w)的时候就db已经有记录了, 直接getSAP(v, w)中会, 不需要setSAP()

完整代码可以参考这里

Wordnet

需求

Wordnet就是把输入的文件转成Digraph的格式, 其中输入包括了synsets以及hypernyms

synsets: 词的id, 词, 词的定义

在这里插入图片描述


hypernyms: 词id, 上位词

在这里插入图片描述

要构成Wordnet的Graph是这样的

在这里插入图片描述


基本思路就是通过hypernyms构造Graph, 然后通过synsets找对应id的词

当然还要注意构造出来的Digraph的root不要有outdegree

代码

 private int v = 0;
 private final Digraph G;
 private final SAP sap;
 private final Map<String, List<Integer>> nounMap = new HashMap<>();
 private final Map<Integer, String> idMap = new HashMap<>();
 
 public WordNet(String synsets, String hypernyms) {
    readSynsets(synsets);

    G = new Digraph(this.v);

    buildDigraph(hypernyms);

    if (!isRootedDAG()) {
      throw new IllegalArgumentException();
    }

    this.sap = new SAP(this.G);
  }

readSynsets 就是读取Synsets并存在nounMap中,要注意一个id是可以对应多个词的, 所以是Map<String, List>
isRootedDAG 就是看是否有一个outdegree为1的node就行
buildDigraph 就是读取hypernyms,然后调用Gigraph的addEdge方法

  private void readSynsets(String synsets) {
    In in = new In(synsets);
    while (!in.isEmpty()) {
      String s = in.readLine();
      var fields = s.split(",");
      var nouns = fields[1].split(" ");
      int id = Integer.parseInt(fields[0]);

      List<String> nounList = new ArrayList<>();
      Collections.addAll(nounList, nouns);

      for (String noun : nounList) {
        List<Integer> idList = nounMap.get(noun);
        if (idList == null) {
          ArrayList<Integer> ids = new ArrayList<>();
          ids.add(id);
          nounMap.put(noun, ids);
        } else {
          idList.add(id);
        }
      }
      idMap.put(Integer.valueOf(fields[0]), fields[1]);
      v += 1;
    }
  }

  private boolean isRootedDAG() {
    int count = 0;
    for (int i = 0; i < G.V(); i++) {
      if (G.outdegree(i) == 0)
        count++;
    }
    return count == 1;
  }


  private void buildDigraph(String hypernyms) {
    In in = new In(hypernyms);
    while (!in.isEmpty()) {
      String s = in.readLine();
      var fields = s.split(",");
      for (int i = 1; i < fields.length; i++) {
        G.addEdge(Integer.parseInt(fields[0]), Integer.parseInt(fields[i]));
      }
    }
  }

其它方法基本上调用SAP即可

    public Iterable<String> nouns() {
    return nounMap.keySet();
  }

  // is the word a WordNet noun?
  public boolean isNoun(String word) {
    if (word == null) {
      throw new IllegalArgumentException();
    }
    return nounMap.get(word) != null;
  }

  // distance between nounA and nounB (defined below)
  public int distance(String nounA, String nounB) {
    if (!isNoun(nounA) || !isNoun(nounB)) {
      throw new IllegalArgumentException();
    }

    return this.sap.length(nounMap.get(nounA), nounMap.get(nounB));
  }

  // a synset (second field of synsets.txt) that is the common ancestor of nounA and nounB
  // in a shortest ancestral path (defined below)
  public String sap(String nounA, String nounB) {
    if (!isNoun(nounA) || !isNoun(nounB)) {
      throw new IllegalArgumentException();
    }

    int id = this.sap.ancestor(nounMap.get(nounA), nounMap.get(nounB));

    return idMap.get(id);
  }

完整代码可以参考这里

Outcast

需求

作业已经把算法给了, 基本上就是照着写就行. 需要做的就是计算每个词跟其他词的距离di,找出di中最大的距离

在这里插入图片描述

代码

  // given an array of WordNet nouns, return an outcast
  public String outcast(String[] nouns) {

    String outcast = null;
    int maxDistance = -2;

    for (String noun : nouns) {
      int distance = 0;
      for (String s : nouns) {
        distance += wordNet.distance(noun, s);
      }

      if (maxDistance == -2 || distance > maxDistance) {
        maxDistance = distance;
        outcast = noun;
      }
    }
    return outcast;
  }

完整代码可以参考这里

总结

这个作业主要难度在于SAP的类,我花了很多时间思考这么计算最短共同祖先 (shortest ancestral path), 后来发现其实Algorithm书上的作业给了参考!就是做两次BFS, 后来研究了一下algs4中的BreadthFirstDirectedPaths是怎么用的, 基本就做出来了。但是后面需要拿100%适当的抛出Exception也是花了点时间。 总的来说这个作业还是很有意思的,通过基本的算法实现了一个看着高大上的问题 - 找词的相似度

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...