Warning: file_get_contents(/data/phpspider/zhask/data//catemap/9/opencv/3.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
String Ukkonen';纯英语中的s后缀树算法_String_Algorithm_Data Structures_Language Agnostic_Suffix Tree - Fatal编程技术网

String Ukkonen';纯英语中的s后缀树算法

String Ukkonen';纯英语中的s后缀树算法,string,algorithm,data-structures,language-agnostic,suffix-tree,String,Algorithm,Data Structures,Language Agnostic,Suffix Tree,我现在觉得有点难受。我花了几天的时间试图完全理解后缀树的构造,但由于我没有数学背景,许多解释让我难以理解,因为它们开始过度使用数学符号。我发现的最接近于一个好的解释是,但他掩盖了许多要点,算法的某些方面仍然不清楚 我敢肯定,在StackOverflow上对这个算法的逐步解释对除我之外的许多人来说都是非常宝贵的 以下是Ukkonen关于算法的论文供参考: 到目前为止,我的基本理解是: 我需要遍历给定字符串T的每个前缀P 我需要遍历前缀P中的每个后缀S,并将其添加到树中 要将后缀S添加到树中,我需

我现在觉得有点难受。我花了几天的时间试图完全理解后缀树的构造,但由于我没有数学背景,许多解释让我难以理解,因为它们开始过度使用数学符号。我发现的最接近于一个好的解释是,但他掩盖了许多要点,算法的某些方面仍然不清楚

我敢肯定,在StackOverflow上对这个算法的逐步解释对除我之外的许多人来说都是非常宝贵的

以下是Ukkonen关于算法的论文供参考:

到目前为止,我的基本理解是:

  • 我需要遍历给定字符串T的每个前缀P
  • 我需要遍历前缀P中的每个后缀S,并将其添加到树中
  • 要将后缀S添加到树中,我需要迭代S中的每个字符,迭代包括沿着以S中的相同字符集C开头的现有分支,并在到达后缀中的不同字符时潜在地将边拆分为后代节点,或者如果没有匹配的边缘可以走下去。当没有找到匹配的边为C向下走时,将为C创建一个新的叶边
正如大多数解释所指出的,基本算法似乎是O(n2),因为我们需要一步一步地遍历所有前缀,然后我们需要一步一步地遍历每个前缀的每个后缀。Ukkonen的算法显然是独一无二的,因为他使用了后缀指针技术,尽管我认为这是我难以理解的

我也很难理解:

  • 指定、使用和更改“活动点”的确切时间和方式
  • 算法的规范化方面发生了什么
  • 为什么我看到的实现需要“修复”它们正在使用的边界变量

以下是完整的C#源代码。它不仅工作正常,而且支持自动规范化,并呈现输出的文本图形。源代码和示例输出位于:


更新2017-11-04

多年后,我发现了后缀树的新用途,并在JavaScript中实现了该算法。要点如下。它应该是无bug的。从同一位置将其转储到js文件中,
npm install chalk
,然后使用node.js运行以查看一些彩色输出。在相同的要点中有一个精简版本,没有任何调试代码


我的直觉如下:

在主循环的k次迭代之后,您构建了一个后缀树,其中包含从前k个字符开始的完整字符串的所有后缀

在开始处,这意味着后缀树包含一个表示整个字符串的根节点(这是唯一从0开始的后缀)

在len(string)迭代之后,您有一个包含所有后缀的后缀树

在循环过程中,关键点是活动点。我猜这代表后缀树中最深的一点,对应于字符串前k个字符的适当后缀。(我认为适当意味着后缀不能是整个字符串。)

例如,假设您看到了字符“abcabc”。活动点表示树中与后缀“abc”对应的点

激活点由(原点、第一个、最后一个)表示。 这意味着您当前处于树中的某个点,您可以从节点原点开始,然后输入字符串[first:last]中的字符

添加新角色时,可以查看活动点是否仍在现有树中。如果是这样,那么你就完了。 否则,您需要在活动点的后缀树中添加一个新节点,回退到下一个最短匹配,然后再次检查

注1: 后缀指针为每个节点提供指向下一个最短匹配的链接

注2: 添加新节点和回退时,为新节点添加新的后缀指针。 此后缀指针的目标将是缩短的活动点处的节点。 该节点将已经存在,或者在该回退循环的下一次迭代中创建

注3:规范化部分只需节省检查活动点的时间。 例如,假设您始终使用origin=0,并且只更改了first和last。 要检查活动点,每次都必须沿着所有中间节点跟随后缀树。 通过只记录与最后一个节点的距离来缓存跟随此路径的结果是有意义的

您能给出一个代码示例来说明“修复”边界变量的含义吗


健康警告:我还发现该算法特别难以理解,因此请注意,这种直觉可能在所有重要细节中都是不正确的…

以下是一种试图描述Ukkonen算法的尝试,首先显示当字符串很简单(即不包含任何重复字符)时该算法的功能,然后将其扩展到完整的算法

首先是一些初步陈述。

  • 我们正在构建的,基本上就像一个搜索trie。就这样 是根节点,从中引出的边将导致新节点,以及 更多的边缘从这些边缘出来,等等

  • 但是:与搜索trie不同,边缘标签不是单一的 人物。相反,每条边都使用一对整数进行标记
    [从,到]
    。这些是指向文本的指针。从这个意义上说,每个 edge携带任意长度的字符串标签,但只接受O(1) 空格(两个指针)

  • 基本原则 我想首先演示如何创建 特别是简单字符串,一个没有重复字符的字符串:abc
    abcabxabcd
    
        require 'pry'
    
    
    class Edge
        attr_accessor :data , :edges , :suffix_link
        def initialize data
            @data = data
            @edges = []
            @suffix_link = nil
        end
    
        def find_edge element
            self.edges.each do |edge|
                return edge if edge.data.start_with? element
            end
            return nil
        end
    end
    
    class SuffixTrees
        attr_accessor :root , :active_point , :remainder , :pending_prefixes , :last_split_edge , :remainder
    
        def initialize
            @root = Edge.new nil
            @active_point = { active_node: @root , active_edge: nil , active_length: 0}
            @remainder = 0
            @pending_prefixes = []
            @last_split_edge = nil
            @remainder = 1
        end
    
        def build string
            string.split("").each_with_index do |element , index|
    
    
                add_to_edges @root , element        
    
                update_pending_prefix element                           
                add_pending_elements_to_tree element
                active_length = @active_point[:active_length]
    
                # if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data[0..active_length-1] ==  @active_point[:active_edge].data[active_length..@active_point[:active_edge].data.length-1])
                #   @active_point[:active_edge].data = @active_point[:active_edge].data[0..active_length-1]
                #   @active_point[:active_edge].edges << Edge.new(@active_point[:active_edge].data)
                # end
    
                if(@active_point[:active_edge] && @active_point[:active_edge].data && @active_point[:active_edge].data.length == @active_point[:active_length]  )
                    @active_point[:active_node] =  @active_point[:active_edge]
                    @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0])
                    @active_point[:active_length] = 0
                end
            end
        end
    
        def add_pending_elements_to_tree element
    
            to_be_deleted = []
            update_active_length = false
            # binding.pry
            if( @active_point[:active_node].find_edge(element[0]) != nil)
                @active_point[:active_length] = @active_point[:active_length] + 1               
                @active_point[:active_edge] = @active_point[:active_node].find_edge(element[0]) if @active_point[:active_edge] == nil
                @remainder = @remainder + 1
                return
            end
    
    
    
            @pending_prefixes.each_with_index do |pending_prefix , index|
    
                # binding.pry           
    
                if @active_point[:active_edge] == nil and @active_point[:active_node].find_edge(element[0]) == nil
    
                    @active_point[:active_node].edges << Edge.new(element)
    
                else
    
                    @active_point[:active_edge] = node.find_edge(element[0]) if @active_point[:active_edge]  == nil
    
                    data = @active_point[:active_edge].data
                    data = data.split("")               
    
                    location = @active_point[:active_length]
    
    
                    # binding.pry
                    if(data[0..location].join == pending_prefix or @active_point[:active_node].find_edge(element) != nil )                  
    
    
                    else #tree split    
                        split_edge data , index , element
                    end
    
                end
            end 
        end
    
    
    
        def update_pending_prefix element
            if @active_point[:active_edge] == nil
                @pending_prefixes = [element]
                return
    
            end
    
            @pending_prefixes = []
    
            length = @active_point[:active_edge].data.length
            data = @active_point[:active_edge].data
            @remainder.times do |ctr|
                    @pending_prefixes << data[-(ctr+1)..data.length-1]
            end
    
            @pending_prefixes.reverse!
    
        end
    
        def split_edge data , index , element
            location = @active_point[:active_length]
            old_edges = []
            internal_node = (@active_point[:active_edge].edges != nil)
    
            if (internal_node)
                old_edges = @active_point[:active_edge].edges 
                @active_point[:active_edge].edges = []
            end
    
            @active_point[:active_edge].data = data[0..location-1].join                 
            @active_point[:active_edge].edges << Edge.new(data[location..data.size].join)
    
    
            if internal_node
                @active_point[:active_edge].edges << Edge.new(element)
            else
                @active_point[:active_edge].edges << Edge.new(data.last)        
            end
    
            if internal_node
                @active_point[:active_edge].edges[0].edges = old_edges
            end
    
    
            #setup the suffix link
            if @last_split_edge != nil and @last_split_edge.data.end_with?@active_point[:active_edge].data 
    
                @last_split_edge.suffix_link = @active_point[:active_edge] 
            end
    
            @last_split_edge = @active_point[:active_edge]
    
            update_active_point index
    
        end
    
    
        def update_active_point index
            if(@active_point[:active_node] == @root)
                @active_point[:active_length] = @active_point[:active_length] - 1
                @remainder = @remainder - 1
                @active_point[:active_edge] = @active_point[:active_node].find_edge(@pending_prefixes.first[index+1])
            else
                if @active_point[:active_node].suffix_link != nil
                    @active_point[:active_node] = @active_point[:active_node].suffix_link               
                else
                    @active_point[:active_node] = @root
                end 
                @active_point[:active_edge] = @active_point[:active_node].find_edge(@active_point[:active_edge].data[0])
                @remainder = @remainder - 1     
            end
        end
    
        def add_to_edges root , element     
            return if root == nil
            root.data = root.data + element if(root.data and root.edges.size == 0)
            root.edges.each do |edge|
                add_to_edges edge , element
            end
        end
    end
    
    suffix_tree = SuffixTrees.new
    suffix_tree.build("abcabxabcd")
    binding.pry
    
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ST {
    
      public class Node {
        private final int id;
        private final Map<Character, Edge> edges;
        private Node slink;
    
        public Node(final int id) {
            this.id = id;
            this.edges = new HashMap<>();
        }
    
        public void setSlink(final Node slink) {
            this.slink = slink;
        }
    
        public Map<Character, Edge> getEdges() {
            return this.edges;
        }
    
        public Node getSlink() {
            return this.slink;
        }
    
        public String toString(final String word) {
            return new StringBuilder()
                    .append("{")
                    .append("\"id\"")
                    .append(":")
                    .append(this.id)
                    .append(",")
                    .append("\"slink\"")
                    .append(":")
                    .append(this.slink != null ? this.slink.id : null)
                    .append(",")
                    .append("\"edges\"")
                    .append(":")
                    .append(edgesToString(word))
                    .append("}")
                    .toString();
        }
    
        private StringBuilder edgesToString(final String word) {
            final StringBuilder edgesStringBuilder = new StringBuilder();
            edgesStringBuilder.append("{");
            for(final Map.Entry<Character, Edge> entry : this.edges.entrySet()) {
                edgesStringBuilder.append("\"")
                        .append(entry.getKey())
                        .append("\"")
                        .append(":")
                        .append(entry.getValue().toString(word))
                        .append(",");
            }
            if(!this.edges.isEmpty()) {
                edgesStringBuilder.deleteCharAt(edgesStringBuilder.length() - 1);
            }
            edgesStringBuilder.append("}");
            return edgesStringBuilder;
        }
    
        public boolean contains(final String word, final String suffix) {
            return !suffix.isEmpty()
                    && this.edges.containsKey(suffix.charAt(0))
                    && this.edges.get(suffix.charAt(0)).contains(word, suffix);
        }
      }
    
      public class Edge {
        private final int from;
        private final int to;
        private final Node next;
    
        public Edge(final int from, final int to, final Node next) {
            this.from = from;
            this.to = to;
            this.next = next;
        }
    
        public int getFrom() {
            return this.from;
        }
    
        public int getTo() {
            return this.to;
        }
    
        public Node getNext() {
            return this.next;
        }
    
        public int getLength() {
            return this.to - this.from;
        }
    
        public String toString(final String word) {
            return new StringBuilder()
                    .append("{")
                    .append("\"content\"")
                    .append(":")
                    .append("\"")
                    .append(word.substring(this.from, this.to))
                    .append("\"")
                    .append(",")
                    .append("\"next\"")
                    .append(":")
                    .append(this.next != null ? this.next.toString(word) : null)
                    .append("}")
                    .toString();
        }
    
        public boolean contains(final String word, final String suffix) {
            if(this.next == null) {
                return word.substring(this.from, this.to).equals(suffix);
            }
            return suffix.startsWith(word.substring(this.from,
                    this.to)) && this.next.contains(word, suffix.substring(this.to - this.from));
        }
      }
    
      public class ActivePoint {
        private final Node activeNode;
        private final Character activeEdgeFirstCharacter;
        private final int activeLength;
    
        public ActivePoint(final Node activeNode,
                           final Character activeEdgeFirstCharacter,
                           final int activeLength) {
            this.activeNode = activeNode;
            this.activeEdgeFirstCharacter = activeEdgeFirstCharacter;
            this.activeLength = activeLength;
        }
    
        private Edge getActiveEdge() {
            return this.activeNode.getEdges().get(this.activeEdgeFirstCharacter);
        }
    
        public boolean pointsToActiveNode() {
            return this.activeLength == 0;
        }
    
        public boolean activeNodeIs(final Node node) {
            return this.activeNode == node;
        }
    
        public boolean activeNodeHasEdgeStartingWith(final char character) {
            return this.activeNode.getEdges().containsKey(character);
        }
    
        public boolean activeNodeHasSlink() {
            return this.activeNode.getSlink() != null;
        }
    
        public boolean pointsToOnActiveEdge(final String word, final char character) {
            return word.charAt(this.getActiveEdge().getFrom() + this.activeLength) == character;
        }
    
        public boolean pointsToTheEndOfActiveEdge() {
            return this.getActiveEdge().getLength() == this.activeLength;
        }
    
        public boolean pointsAfterTheEndOfActiveEdge() {
            return this.getActiveEdge().getLength() < this.activeLength;
        }
    
        public ActivePoint moveToEdgeStartingWithAndByOne(final char character) {
            return new ActivePoint(this.activeNode, character, 1);
        }
    
        public ActivePoint moveToNextNodeOfActiveEdge() {
            return new ActivePoint(this.getActiveEdge().getNext(), null, 0);
        }
    
        public ActivePoint moveToSlink() {
            return new ActivePoint(this.activeNode.getSlink(),
                    this.activeEdgeFirstCharacter,
                    this.activeLength);
        }
    
        public ActivePoint moveTo(final Node node) {
            return new ActivePoint(node, this.activeEdgeFirstCharacter, this.activeLength);
        }
    
        public ActivePoint moveByOneCharacter() {
            return new ActivePoint(this.activeNode,
                    this.activeEdgeFirstCharacter,
                    this.activeLength + 1);
        }
    
        public ActivePoint moveToEdgeStartingWithAndByActiveLengthMinusOne(final Node node,
                                                                           final char character) {
            return new ActivePoint(node, character, this.activeLength - 1);
        }
    
        public ActivePoint moveToNextNodeOfActiveEdge(final String word, final int index) {
            return new ActivePoint(this.getActiveEdge().getNext(),
                    word.charAt(index - this.activeLength + this.getActiveEdge().getLength()),
                    this.activeLength - this.getActiveEdge().getLength());
        }
    
        public void addEdgeToActiveNode(final char character, final Edge edge) {
            this.activeNode.getEdges().put(character, edge);
        }
    
        public void splitActiveEdge(final String word,
                                    final Node nodeToAdd,
                                    final int index,
                                    final char character) {
            final Edge activeEdgeToSplit = this.getActiveEdge();
            final Edge splittedEdge = new Edge(activeEdgeToSplit.getFrom(),
                    activeEdgeToSplit.getFrom() + this.activeLength,
                    nodeToAdd);
            nodeToAdd.getEdges().put(word.charAt(activeEdgeToSplit.getFrom() + this.activeLength),
                    new Edge(activeEdgeToSplit.getFrom() + this.activeLength,
                            activeEdgeToSplit.getTo(),
                            activeEdgeToSplit.getNext()));
            nodeToAdd.getEdges().put(character, new Edge(index, word.length(), null));
            this.activeNode.getEdges().put(this.activeEdgeFirstCharacter, splittedEdge);
        }
    
        public Node setSlinkTo(final Node previouslyAddedNodeOrAddedEdgeNode,
                               final Node node) {
            if(previouslyAddedNodeOrAddedEdgeNode != null) {
                previouslyAddedNodeOrAddedEdgeNode.setSlink(node);
            }
            return node;
        }
    
        public Node setSlinkToActiveNode(final Node previouslyAddedNodeOrAddedEdgeNode) {
            return setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, this.activeNode);
        }
      }
    
      private static int idGenerator;
    
      private final String word;
      private final Node root;
      private ActivePoint activePoint;
      private int remainder;
    
      public ST(final String word) {
        this.word = word;
        this.root = new Node(idGenerator++);
        this.activePoint = new ActivePoint(this.root, null, 0);
        this.remainder = 0;
        build();
      }
    
      private void build() {
        for(int i = 0; i < this.word.length(); i++) {
            add(i, this.word.charAt(i));
        }
      }
    
      private void add(final int index, final char character) {
        this.remainder++;
        boolean characterFoundInTheTree = false;
        Node previouslyAddedNodeOrAddedEdgeNode = null;
        while(!characterFoundInTheTree && this.remainder > 0) {
            if(this.activePoint.pointsToActiveNode()) {
                if(this.activePoint.activeNodeHasEdgeStartingWith(character)) {
                    activeNodeHasEdgeStartingWithCharacter(character, previouslyAddedNodeOrAddedEdgeNode);
                    characterFoundInTheTree = true;
                }
                else {
                    if(this.activePoint.activeNodeIs(this.root)) {
                        rootNodeHasNotEdgeStartingWithCharacter(index, character);
                    }
                    else {
                        previouslyAddedNodeOrAddedEdgeNode = internalNodeHasNotEdgeStartingWithCharacter(index,
                                character, previouslyAddedNodeOrAddedEdgeNode);
                    }
                }
            }
            else {
                if(this.activePoint.pointsToOnActiveEdge(this.word, character)) {
                    activeEdgeHasCharacter();
                    characterFoundInTheTree = true;
                }
                else {
                    if(this.activePoint.activeNodeIs(this.root)) {
                        previouslyAddedNodeOrAddedEdgeNode = edgeFromRootNodeHasNotCharacter(index,
                                character,
                                previouslyAddedNodeOrAddedEdgeNode);
                    }
                    else {
                        previouslyAddedNodeOrAddedEdgeNode = edgeFromInternalNodeHasNotCharacter(index,
                                character,
                                previouslyAddedNodeOrAddedEdgeNode);
                    }
                }
            }
        }
      }
    
      private void activeNodeHasEdgeStartingWithCharacter(final char character,
                                                        final Node previouslyAddedNodeOrAddedEdgeNode) {
        this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
        this.activePoint = this.activePoint.moveToEdgeStartingWithAndByOne(character);
        if(this.activePoint.pointsToTheEndOfActiveEdge()) {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
        }
      }
    
      private void rootNodeHasNotEdgeStartingWithCharacter(final int index, final char character) {
        this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
        this.activePoint = this.activePoint.moveTo(this.root);
        this.remainder--;
        assert this.remainder == 0;
      }
    
      private Node internalNodeHasNotEdgeStartingWithCharacter(final int index,
                                                             final char character,
                                                             Node previouslyAddedNodeOrAddedEdgeNode) {
        this.activePoint.addEdgeToActiveNode(character, new Edge(index, this.word.length(), null));
        previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkToActiveNode(previouslyAddedNodeOrAddedEdgeNode);
        if(this.activePoint.activeNodeHasSlink()) {
            this.activePoint = this.activePoint.moveToSlink();
        }
        else {
            this.activePoint = this.activePoint.moveTo(this.root);
        }
        this.remainder--;
        return previouslyAddedNodeOrAddedEdgeNode;
      }
    
      private void activeEdgeHasCharacter() {
        this.activePoint = this.activePoint.moveByOneCharacter();
        if(this.activePoint.pointsToTheEndOfActiveEdge()) {
            this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
        }
      }
    
      private Node edgeFromRootNodeHasNotCharacter(final int index,
                                                 final char character,
                                                 Node previouslyAddedNodeOrAddedEdgeNode) {
        final Node newNode = new Node(idGenerator++);
        this.activePoint.splitActiveEdge(this.word, newNode, index, character);
        previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
        this.activePoint = this.activePoint.moveToEdgeStartingWithAndByActiveLengthMinusOne(this.root,
                this.word.charAt(index - this.remainder + 2));
        this.activePoint = walkDown(index);
        this.remainder--;
        return previouslyAddedNodeOrAddedEdgeNode;
      }
    
      private Node edgeFromInternalNodeHasNotCharacter(final int index,
                                                     final char character,
                                                     Node previouslyAddedNodeOrAddedEdgeNode) {
        final Node newNode = new Node(idGenerator++);
        this.activePoint.splitActiveEdge(this.word, newNode, index, character);
        previouslyAddedNodeOrAddedEdgeNode = this.activePoint.setSlinkTo(previouslyAddedNodeOrAddedEdgeNode, newNode);
        if(this.activePoint.activeNodeHasSlink()) {
            this.activePoint = this.activePoint.moveToSlink();
        }
        else {
            this.activePoint = this.activePoint.moveTo(this.root);
        }
        this.activePoint = walkDown(index);
        this.remainder--;
        return previouslyAddedNodeOrAddedEdgeNode;
      }
    
      private ActivePoint walkDown(final int index) {
        while(!this.activePoint.pointsToActiveNode()
                && (this.activePoint.pointsToTheEndOfActiveEdge() || this.activePoint.pointsAfterTheEndOfActiveEdge())) {
            if(this.activePoint.pointsAfterTheEndOfActiveEdge()) {
                this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge(this.word, index);
            }
            else {
                this.activePoint = this.activePoint.moveToNextNodeOfActiveEdge();
            }
        }
        return this.activePoint;
      }
    
      public String toString(final String word) {
        return this.root.toString(word);
      }
    
      public boolean contains(final String suffix) {
        return this.root.contains(this.word, suffix);
      }
    
      public static void main(final String[] args) {
        final String[] words = {
                "abcabcabc$",
                "abc$",
                "abcabxabcd$",
                "abcabxabda$",
                "abcabxad$",
                "aabaaabb$",
                "aababcabcd$",
                "ababcabcd$",
                "abccba$",
                "mississipi$",
                "abacabadabacabae$",
                "abcabcd$",
                "00132220$"
        };
        Arrays.stream(words).forEach(word -> {
            System.out.println("Building suffix tree for word: " + word);
            final ST suffixTree = new ST(word);
            System.out.println("Suffix tree: " + suffixTree.toString(word));
            for(int i = 0; i < word.length() - 1; i++) {
                assert suffixTree.contains(word.substring(i)) : word.substring(i);
            }
        });
      }
    }