【问题标题】:How to animate algorithm steps using a recursive generator如何使用递归生成器动画算法步骤
【发布时间】:2021-11-12 13:52:58
【问题描述】:

我使用递归辅助函数构建了一个 boggle 求解器算法。 它使用 trie 数据结构来快速检查现有前缀是否有字典中的任何单词。 我想对算法的每一步进行动画处理,以显示如何在 HTML 表中找到每个单词,但无法使其正常工作。

算法如下:

const trie = '' // instantiated trie class (omitted for brevity)
const list = '' // imported dictionary (omitted for brevity)

// inputs are all the cells inside the table that hold the letters
const inputs = document.getElementsByClassName("input");
// size is the size of the table (ex. 4x4, 8x8, etc...)
const size = document.getElementById("size").valueAsNumber;

// populating the trie with every word in the dictionary
for (let item of list) {
  trie.insert(item);
}

const movements = (i, j) => [
  { row: i, column: j + 1, move: "RIGHT" },
  { row: i + 1, column: j + 1, move: "BOTTOM_RIGHT" },
  { row: i + 1, column: j, move: "BOTTOM" },
  { row: i + 1, column: j - 1, move: "BOTTOM_LEFT" },
  { row: i, column: j - 1, move: "LEFT" },
  { row: i - 1, column: j - 1, move: "TOP_LEFT" },
  { row: i - 1, column: j, move: "TOP" },
  { row: i - 1, column: j + 1, move: "TOP_RIGHT" },
];

// takes a 2D array as input (ex. [['a', 'b', 'c', 'd'], ['e', 'f', 'g', 'h']])
export const findWords = (matrix) => {
  const words = [];
  const iterate = (i, j, word, visited) => {
    if (matrix[i] && matrix[i][j]) {
      if (!visited[`${i}_${j}`]) {
        visited[`${i}_${j}`] = true;
        word += matrix[i][j];
        if (trie.find(word).length) {
          if (trie.contains(word)) {
            words.push(word);
          }
          const moves = movements(i, j);
          for (let move of moves) {
            const { row, column } = move;
            iterate(row, column, word, { ...visited });
          }
        }
      }
    }
  };
  for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
      iterate(i, j, "", {});
    }
  }
  return words;
};

可以在此处找到一个工作应用示例:https://jsfiddle.net/Msiric/24x765bn/

下面是使用生成器时的代码:

export const findWords = (matrix) => {
  const words = [];
  const iterate = function* (i, j, word, visited, color) {
    if (matrix[i] && matrix[i][j]) {
      if (!visited[`${i}_${j}`]) {
        visited[`${i}_${j}`] = true;
        word += matrix[i][j];
        // highlighting the current cell here
        inputs[j + size * i].classList.add(color);
        if (trie.find(word).length) {
          if (trie.contains(word)) {
            words.push(word);
          }
          const moves = movements(i, j);
          for (let move of moves) {
            const { row, column } = move;
            yield* iterate(
              row,
              column,
              word,
              { ...visited },
              column % 2 === 0 ? "blue" : "red"
            );
            // the cell should be unhighlighted once this cell is done 
            inputs[j + size * i].classList.remove(color);
          }
        }
      }
    }
  };
  for (let i = 0; i < matrix.length; i++) {
    for (let j = 0; j < matrix[i].length; j++) {
      iterate(i, j, "", {}, j % 2 === 0 ? "blue" : "red").next();
    }
  }
  return words;
};

这样做只会显示算法的最终状态,这不是我想要的。 我想显示每个单元格被突出显示或不突出显示,因为每个步骤正在执行并且每个单词都被找到。 我还尝试包含 setTimeout 函数来延迟辅助函数的执行,但这也不起作用。 它只是导致随机闪烁,因为单元格被无序突出显示/取消突出显示。

感谢任何帮助

【问题讨论】:

    标签: javascript algorithm recursion generator


    【解决方案1】:

    在 javascript 中,您不能在同步函数的中间渲染某些东西。任何尝试进行 DOM 更改的代码只会将这些更改排队,在运行时执行同步函数时无法绘制这些更改。

    因此,你必须让你的函数异步,就像在这个例子中一样 https://jsfiddle.net/ep783cgw/2/

    虽然我不确定您希望如何对其进行可视化,但我已经创建了一个 renderState 函数,您可以根据自己的喜好进行定义。

    // modify it as per your liking
    const renderState = (anim,i,j,move) => {
        const {row,column} = move
      anim.innerText = `${i}_${j}-${row}_${column}-${move.move}`
    }
    
    // show noticable delay in animation
    const delay = (ms) => new Promise(res => setTimeout(res, ms))
    
    // make findWords async
    const findWords = async (matrix) => {
      const words = [];
      const anim = document.querySelector('#result');
      const iterate = async (i, j, word, visited) => {
        if (matrix[i] && matrix[i][j]) {
          if (!visited[`${i}_${j}`]) {
            visited[`${i}_${j}`] = true;
            word += matrix[i][j];
            if (trie.find(word).length) {
              if (trie.contains(word)) {
                words.push(word);
              }
              const moves = movements(i, j);
              for (let move of moves) {
                const { row, column } = move;
                // render the DOM
                renderState(anim,i,j, move);
                // wait for 200ms using await. Browser will draw the DOM in this time
                await delay(200);
                // iterate again using await
                await iterate(row, column, word, { ...visited });
              }
            }
          }
        }
      };
      for (let i = 0; i < matrix.length; i++) {
        for (let j = 0; j < matrix[i].length; j++) {
          await iterate(i, j, "", {});
        }
      }
      return words;
    };
    
    // make handleSubmit async
    const handleSubmit = async (e) => {
      e.preventDefault();
      const inputs = document.querySelectorAll(".input");
      const result = document.getElementById("result");
      const size = document.getElementById("size").valueAsNumber;
      const matrix = [];
      for (let i = 0; i < size; i++) {
        const row = [];
        for (let j = 0; j < size; j++) {
          row.push(inputs[j + size * i].value.toLowerCase());
        }
        matrix.push(row);
      }
      // use await to get output of findWords
      const words = await findWords(matrix);
      result.innerHTML = words;
    };
    

    要了解更多信息,请点击这些链接

    1. Event Loop Description | MDN
    2. A stackoverflow answer on similar topic

    编辑:我刚刚注意到您没有正确使用生成器功能。

    为了更好地利用生成器函数,您应该创建一个生成器实例并在其上调用next() 以生成所有值。但是在你的代码中,你已经写了

    iterate(i, j, "", {}, j % 2 === 0 ? "blue" : "red").next();
    

    您在每次.next() 调用之前创建一个新实例。因此,iterate 生成器不会产生所有值。 因此,上面的行应该是

    const instance = iterate(i, j, "", {}, j % 2 === 0 ? "blue" : "red")
    while(!instance.next().done)
    

    您可以阅读生成器函数的示例here

    【讨论】:

      【解决方案2】:

      这是我想出的:https://jsfiddle.net/Msiric/jwag86o2/3/

      该解决方案的部分灵感来自 Scott 的建议,但由于我无法使其工作,我找到了另一种方法来指定动画运行时应执行的每个步骤。

      算法的主要变化:

      const movements = (i, j) => [
        { row: i, column: j + 1, move: "RIGHT" },
        { row: i + 1, column: j + 1, move: "BOTTOM_RIGHT" },
        { row: i + 1, column: j, move: "BOTTOM" },
        { row: i + 1, column: j - 1, move: "BOTTOM_LEFT" },
        { row: i, column: j - 1, move: "LEFT" },
        { row: i - 1, column: j - 1, move: "TOP_LEFT" },
        { row: i - 1, column: j, move: "TOP" },
        { row: i - 1, column: j + 1, move: "TOP_RIGHT" },
      ];
      
      const addStep = (() => {
        let counter = 1;
        return (i, j, matrix, isWord, action, steps) => {
          steps.push({
            x: i,
            y: j,
            c: matrix[i][j],
            isWord,
            action,
            counter,
          });
          action === "remove" ? counter-- : counter++;
        };
      })();
      
      const findWords = (matrix) => {
        const words = [];
        const map = {};
        const steps = [];
        const iterate = (i, j, word, visited) => {
          if (matrix[i] && matrix[i][j]) {
            if (!visited[`${i}_${j}`]) {
              visited[`${i}_${j}`] = true;
              word += matrix[i][j];
              addStep(i, j, matrix, false, "add", steps);
              if (trie.find(word).length) {
                if (trie.contains(word) && !map[word]) {
                  words.push(word);
                  map[word] = true;
                  steps[steps.length - 1] = {
                    ...steps[steps.length - 1],
                    isWord: true,
                  };
                }
                const moves = movements(i, j);
                for (let move of moves) {
                  const { row, column } = move;
                  iterate(row, column, word, { ...visited });
                }
                addStep(i, j, matrix, false, "remove", steps);
              } else {
                addStep(i, j, matrix, false, "remove", steps);
              }
            }
          }
        };
        for (let i = 0; i < matrix.length; i++) {
          for (let j = 0; j < matrix[i].length; j++) {
            iterate(i, j, "", {});
          }
        }
        return { words, steps };
      };
      

      这是可视化功能:

      const visualizeSteps = async (steps, inputs, spans, size) => {
        for (let step of steps) {
          const { x, y } = step;
          const selection = y + size * x;
          await delay(0);
          if (spans[selection].innerHTML === "") {
            spans[selection].innerHTML = step.counter;
          }
          inputs[selection].classList.add("highlight");
          if (step.isWord) {
            const highlights = document.getElementsByClassName("highlight");
            for (let highlight of highlights) {
              highlight.classList.add("success");
            }
            await delay(500);
            for (let highlight of highlights) {
              highlight.classList.remove("success");
            }
          } else {
            if (step.action === "remove") {
              await delay(0);
              inputs[selection].classList.remove("highlight");
              spans[selection].innerHTML = "";
            }
          }
        }
      };
      

      【讨论】:

        【解决方案3】:

        我现在要处理的代码太多了。但一般建议是不要尝试将算法与显示混合。

        在算法中,跟踪您测试的动作及其结果。完全不用担心如何显示它。

        完成后,参加考试,一次一项,突出显示网格。在这里,使用setInterval 或链式setTimeouts 都很容易,因为您只是在迭代一个尝试列表。每次您只需要取消设置当前突出显示,然后为路径上的单元格设置它,可能对路径的第一个和最后一个单元格使用不同的显示,并且当您点击合法单词时可能会有所不同(动画添加到列表中?)

        假设我们有一个部分看起来像这样的网格:

          \ x
        y  \  0   1   2   3
            +---+---+---+---+
          0 | · | · | O | · |
            +---+---+---+---+
          1 | · | O | F | · |
            +---+---+---+---+
          2 | Z | L | · | H |
            +---+---+---+---+
          3 | · | I | S | · |
            +---+---+---+---+
        

        也许每个测试都会产生类似的结果

        {
          path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, 'c': 'O'}], 
          stuck: false, 
          isWord: false
        }
        

        (假设“FOO”不在您的字典中。)

        然后,因为你没有卡住,你最终添加{x: 1, y: 2, c: 'L'},你将有isWord: true(“FOOL”)和stuck: false(因为trie说F-O-O-L下有节点)。

        {
          path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, 'c': 'O'}, 
                 {x: 1, y: 2, c: 'L'}], 
          stuck: false, 
          isWord: true
        }
        

        但是当您尝试添加 {x: 0, y: 2, c: 'Z'},而 trie 告诉您没有以 F-O-O-Z 开头的单词时,您可以记录它不是一个单词并且您被卡住了。

        {
          path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, 'c': 'O'}, 
                 {x: 0, y: 2, c: 'Z'}], 
          stuck: true, 
          isWord: false
        }
        

        最后,你会试试这个七个字母的单词:

        {
          path: [{x: 2, y: 1, c: 'F'}, {x: 2, y: 0, c: 'O'}, {x: 1, y: 1, c: 'O'}, 
                 {x: 1, y: 2, c: 'L'}, {x: 1, y: 3, c: 'I'}, {x: 2, y: 3, c: 'S'}, 
                 {x: 3, y: 2, c: 'H'}], 
          stuck: false, 
          isWord: true
        }
        

        请注意,这些可能是您算法的唯一输出,因为从这些测试中生成单词列表很简单:

          .filter (t => t .isWord) 
          .map (({path}) => path .map (n => n .c) .join (''))
        
        const uniqueWords = [... new Set (words)]
        

        重大更新

        自从我发布上述内容以来,我一直在思考这个问题。这是尝试以接近这种方式的方式进行的尝试。 (如果您使用 sn-p 的“全屏”选项,它可能会更好看。):

        // *************************
        // Main function
        // *************************
        const run = () =>
          Promise.resolve (showLoading())
            .then (getDictionary)
            .then (trie)
            .then (buildPuzzle)
            .then (tap (displayPuzzle))
            .then (solvePuzzle)
            // .then (tap (console .log))
            .then (displaySolution)
            .then (showWordCount)
        
        // *************************
        // Utility functions
        // *************************
        const tap = (fn) => (x) => (fn (x), x)
        const range = (lo, hi) => [... Array (hi - lo)] .map ((_, i) => i + lo)
        const last = (xs) => xs [xs .length - 1] 
        const titleCase = ([c, ...cs]) => c .toUpperCase() + cs.join('')
        const showLoading = () => // not really a utility function, but no place better
          document .getElementById ('word') .textContent ='Loading ...'
        
        // *************************
        // Dictionary handling -trie
        // *************************
        const getDictionary = () =>
          //fetch ('http://fourwindssoft.com/scott/words/')
          fetch ('https://norvig.com/ngrams/enable1.txt')
            .then (s => s .text ())
            .then (s => s .split ('\n'))
        
        const trie = (words) => 
          words .reduce (insertWord, {}) 
        const insertWord = (trie, [c, ...cs]) => 
          c ? {...trie, [c]: insertWord (trie [c] || {}, cs)} : {...trie, $: 1}
        const find = (trie) => ([c = '', ...cs]) =>
          trie && (cs .length == 0 ? trie [c] : find (trie[c]) (cs))
        const contains = (trie) => (word) =>
          '$' in (find (trie) (word) || {})
        
        
        // *************************
        // Board creation
        // *************************
        const buildPuzzle = (trie) => ({trie, letters: roll (dice)})
        const dice = 
           'a|a|c|i|o|t, a|b|i|l|t|y, a|b|j|m|o|qu, a|c|d|e|m|p, a|c|e|l|r|s, a|d|e|n|v|z, a|h|m|o|r|s, b|i|f|o|r|x, d|e|n|o|s|w, d|k|n|o|t|u, e|e|f|h|i|y, e|g|k|l|u|y, e|g|i|n|t|v, e|h|i|n|p|s, e|l|p|s|t|u, g|i|l|r|u|w'
           .split (', ') .map (d => d.split ('|'))
        const roll = (dice) => shuffle (dice) .map (pickOne)
        const shuffle = (xs, i = Math .floor (Math .random () * xs .length)) =>
          xs .length == 0
            ? []
            : [xs[i], ... shuffle ([... xs .slice (0, i), ... xs .slice (i + 1)])]
        const pickOne = (xs) => xs [Math .floor (Math .random () * xs .length)]
        
        
        // *************************
        // Initial display
        // *************************
        const displayPuzzle = ({letters}) =>
          letters .forEach ((l, i) => document .getElementById (`c${i}`) .textContent = titleCase (l))
        
        
        // *************************
        // Solving puzzle
        // *************************
        const solvePuzzle = ({trie, letters}) =>
          ({letters, tests: search (letters, neighbors, trie)})
        
        // see https://link.fourwindssoft.com/30 for derivation or other grid sizes
        const neighbors = 
          [[1, 4, 5], [0, 2, 4, 5, 6], [1, 3, 5, 6, 7], [2, 6, 7], [0, 1, 5, 8, 9], [0, 1, 2, 4, 6, 8, 9, 10], [1, 2, 3, 5, 7, 9, 10, 11], [2, 3, 6, 10, 11], [4, 5, 9, 12, 13], [4, 5, 6, 8, 10, 12, 13, 14], [5, 6, 7, 9, 11, 13, 14, 15], [6, 7, 10, 14, 15], [8, 9, 13], [8, 9, 10, 12, 14], [9, 10, 11, 13, 15], [10, 11, 14]]
        
        const search = (letters, neighbors, words, path = []) => 
          path .length == 0
            ? letters .flatMap ((l, i) => search (letters, neighbors, find (words) (l) || {}, [i]))
            : [
                {path, isWord : words ? '$' in words && path.length > 2 : false, stuck: !words},
                ... neighbors [last (path)] 
                      .filter ((i) => ! path .includes (i))
                      .flatMap ((i) => words
                         ? search (letters, neighbors, find (words) (letters [i]), [...path, i])     
                         : []
                      )
              ]
        
        // *************************
        // Display algorithm process
        // *************************
        const displaySolution = ({letters, tests}) => 
          showPaths (letters) (tests)
        
        const showPaths = (letters) => ([t, ...ts]) => 
          t == undefined
            ? Promise.resolve(true)
            : showPath (letters) (t) .then (() => showPaths (letters) (ts))  
        
        const showPath = (letters) => (t) => {
          document .getElementById ('word') .textContent = t.path .map (n => letters [n]) .join('')
          return highlightPath (50) (t.path)
            .then (() => {
              if (t.isWord) {
                document.getElementById ('board') .classList .add ('match')
              } else if (t.stuck) {
                document.getElementById ('board') .classList .add ('stuck')
              }
            })
            .then (delay (t.isWord ? 1500: 500) (clearHighlighting))
            .then (handleWord (t, letters))
            .then (() => {
              const classList = document.getElementById ('board') .classList 
              classList .remove ('stuck') 
              classList .remove ('match')
            })
          }
        
        const highlightPath = (t) => (path) => seq (
          path .map ((n) => delay (t) (highlightCell (n)))
        )
        
        const seq = (promGens) =>
          promGens .reduce ((c, n) => c .then (n), Promise.resolve(true))
        
        const delay = (time) => (thunk) => () => 
          new Promise ((res) => {setTimeout (() => {thunk(); res()}, time)})
        
        const highlightCell = (n) => () =>
          document .getElementById (`c${n}`) .classList .add ('highlighted')
        
        const clearHighlighting = () =>
          document .querySelectorAll ('#board td') .forEach ((node) => node .classList .remove ('highlighted'))
        
        
        const handleWord = (t, letters) => {
          if (t.isWord) {
            const text = t.path .map (p => letters [p]) .join('')
            const match = [...document .querySelectorAll ('#found li')] .find (node => node .textContent == text)
            if (match) {
              match .scrollIntoView ()
              match .classList .add ('item-used')
              setTimeout (() => match .classList .remove ('item-used'), 6000) // uggh, 6000 should match css animation!
            } else {
              const li = document .createElement ('li')
              li. appendChild (document .createTextNode(text))
              document .getElementById('found') .appendChild (li)
              li .scrollIntoView ()
              li .classList .add ('item-highlight')
              setTimeout (() => li .classList .remove ('item-highlight'), 6000)
            }
          }
          return t
        }
        
        const showWordCount = () => // ugly to use the DOM for this info, but too much refactoring to fix
          document .getElementById ('word') .textContent = `${document.querySelectorAll('#found li') .length} words`
        
        
        // *************************
        // Start everything
        // *************************
        run ()
        table {background: #999; padding: .25em; border: 1px solid black;}
        td {border: 1px solid black; padding: .25em; background: white; text-align: center; width: 1em; height: 1em;}
        td.highlighted {background: #ccc;}
        table.stuck, table.stuck td.highlighted {background: #f99;}
        table.match, table.match td.highlighted {background: #9f9;}
        pre {width: 10em; text-align: center; color: #666;}
        code {font-size: 2em;}
        .container {display: flex; flex-direction: row;}
        #results {height: 10em; width: 8em; margin-left: .5em; padding: .5em; background: #ddd; overflow: auto;}
        #results ul {list-style: none; padding: 0; margin: 0em;}
        @keyframes fadenew {from {background: #6f6} to {background: transparent;}}
        li.item-highlight {animation: fadenew 6s;}
        @keyframes fadeused {from {background: #f66} to {background: transparent;}}
        li.item-used {animation: fadeused 6s;}
        <div class="container">
          <div id="demo">
            <table id="board">
              <tr><td  id="c0">?</td><td  id="c1">?</td><td  id="c2">?</td><td  id="c3">?</td></tr>
              <tr><td  id="c4">?</td><td  id="c5">?</td><td  id="c6">?</td><td  id="c7">?</td></tr>
              <tr><td  id="c8">?</td><td  id="c9">?</td><td id="c10">?</td><td id="c11">?</td></tr>
              <tr><td id="c12">?</td><td id="c13">?</td><td id="c14">?</td><td id="c15">?</td></tr>
            </table>
            <pre><code id="word"></code></pre>
          </div>
          <div id="results">
            <ul id ="found"></ul>
          </div>
        </div>

        我确实与上面的建议略有不同。路径只是整数列表,索引到作为网格的平面字母数组。每个索引的邻居列表都是硬编码的,虽然最初在摆弄不同的网格大小时,我calculated them

        代码加载一个字典,这里是 Peter Norvig 的 enable1 列表,使用相当简单的 trie 函数将其转换为 trie。我们使用simulated dice 随机创建一个谜题,它应该与原始的 Boggle 匹配,显示它(通过整数索引到单元格 ID 的简单映射),并使用相当简单的 search 函数在其中找到我们字典中的所有单词,并且至少包含三个字母。这将为我们提供如下所示的结果数组:

        [
          //...
          {path: [9, 5, 6, 3], stuck: false, isWord: true},
          {path: [9, 5, 6, 3, 2], stuck: true, isWord: false},
          //...
        ]
        

        反对网格,

        ['L',' T',' P',' T',' O',' E',' N',' Y',' U',' T',' F',' O',' S',' H',' K',' I']
        

        代表董事会

           +---+---+---+---+
           | L | T | P | T |
           +---+---+---+---+
           | O | E | N | Y |
           +---+---+---+---+
           | U | T | F | O |
           +---+---+---+---+
           | S | H | K | I |
           +---+---+---+---+
        

        所以[9, 5, 6, 3] 代表"TENT"[9, 5, 6, 3, 2] 代表"TENTP"

        这里一个有趣的设计决定是在求解器中而不是在字典中将单词限制为三个或更多字母。它的效率略低,但我认为将算法可视化尝试一个和两个字母单词会更清晰。

        现在我们有了这个表示,我们可以通过突出显示我们尝试过的每个测试用例的路径来演示它。请注意,当 trie 不包含最新字母时,搜索器会停止,因此我们没有指数数量的路径来搜索。我假设这里的所有其他算法都做类似的事情。显示中没有特别有趣的代码。它只是一次一个地突出显示通过网格的路径,简要说明它是否是我们字典中的一个单词,如果是一个,在将它添加到列表之前检查我们是否已经找到它找到的话。所有这些都是通过简单的 DOM 操作完成的。唯一的小技巧是动画的时间安排。我们希望快速浏览我们的单词,但希望单词的动画足够快以显示实际路径。我不确定我是否在这里取得了正确的平衡,但这并不可怕。

        对此的一种可能扩展是显示我们已显示的试验的进度条,或使其成为可以停止、重新启动和逐步执行的内容。另一种非常有用的方法是用一条穿过所用方块的线来显示路径,而不仅仅是突出显示颜色。但这些是另一天的事情。

        很多代码对于网格大小是通用的,使用 getNeighbors 函数,我们可以轻松地将其余部分设为通用,除了一件事。我们在这里掷的骰子是为了模拟实际的 Boggle 骰子,而不是简单的随机选择。我不清楚如何为更大的网格更改它。


        这是一件有趣的小事。虽然我希望它可能对 OP 有所帮助,但我很高兴这样做只是为了我自己的娱乐。

        【讨论】:

          猜你喜欢
          • 2016-07-22
          • 1970-01-01
          • 2016-08-27
          • 2016-08-25
          • 2014-04-07
          • 2012-10-24
          • 2016-07-19
          • 1970-01-01
          相关资源
          最近更新 更多