[路飞] 等式方程的可满足性

163 阅读2分钟

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」。

记录 1 道算法题

等式方程的可满足性

leetcode-cn.com/problems/sa…


思路

这道题的要求是给定一个等式 'a==b'。这个等式的特点是只有4位,也就是说除去中间的 == 或者 != 之外,等式两边是一个字母,他们可能是同一个字母,也可能不是同一个字母。

所以我们可以得出范围是26个英文字母。

如果在多个等式中,我们可以给出现的字母各赋值一个数字,使每一个等式都成立。只要有一个等式不符合就返回 false。都符合就返回 true。

例子:

    a!=b, b==c  我们可以提供 a = 任何数,b 和 c 是同一个数就可以。返回 true
    a==b, b!=a  我们无法提供一个数让 a==b 同时 b!=a。返回false

我们可以想象成帮派合并,相等的是同一个帮派,当有一个字母他在帮派里,又不等于帮派的时候,就是 false,其余情况都是 true。

我们可以分类成多个相等的阵营,多个不相等的阵营。这两种阵营之间应该是不相互交叉的。相等是一个很大的阵营,因为与阵营内的字母相等就会被吸纳进这个阵营,但可以有多组没有交集的相等。比如: a==b, c==d

不相等是跟相等没有交集的,无论分成了多少个相等阵营和不相等阵营,相等阵营跟不相等阵营就是水火不容。

提到合并,很容易想到并查集。并查集就是解决集合的合并问题,也能很清晰的知道两个字母是不是在一个集合里面。

实现

那我们先准备一个数组,来存放字母之间的关系。同时因为我们得到的是字母,所以我们还需要做字母和关系数组下标的映射。利用 JavaScript 的特点,我们动态的扩增关系数组。

    const map = {} // 映射
    const parent = [] // 关系数组
    
    for(let i = 0; i < arr.length; i++) {
        const item = arr[i]
        
        // 等式两边的字母
        const a = item[0]
        const b = item[3]
        
        if (map[a] == undefined) {
            create(map, parent, a)
        }
        if (map[b] == undefined) {
            create(map, parent, b)
        }
    }

这个 create 方法实现了扩增关系数组。在 parent 最后添加上新增位置的下标。并在 map 做映射。

    function (map, parent, a) {
        const i = parent.length
        map[a] = parent.push(i) - 1
    }

然后我们要开始处理等式。我们采取的策略是等号时给字母进行合并。不等号时给字母进行检查,如果他们是一个集合就返回 false。如果不是一个集合就不做任何操作。因为 create 的时候已经分配了另一个集合。

但是只有不等号的时候才返回 false,总感觉考虑少了。

如果是在不等号的时候检查集合,已经合并过了后才检查得出来。那假如先检查了不等于分配了另一个集合。但下一个又等于,就进行合并。但实际上不应该合并。所以在等于的分支也要进行检查。

但是检查起来很麻烦,因为 create 默认分配了不同的集合。所以我们不知道他的默认的不同还是不等于的不同。

所以采取的策略是,先遍历一遍只处理等号,目的是建立字母之间的联系。然后再遍历一遍只处理不等号,做检验。这样就解决了等号时的检验问题。

所以完整代码如下:

    function equationsPossible(equations) {
        const map = {}
        const parent = []
        // 第一遍建立联系,只检查等号
        for (let i = 0; i < equations.length; i++) {
          const item = equations[i]
          const a = item[0]
          const b = item[3]
          const equal = item[1] === '='

          if (map[a] == undefined) {
            create(map, parent, a)
          }
          if (map[b] == undefined) {
            create(map, parent, b)
          }

          if (equal) {
            const j = map[a]
            const k = map[b]
            if (a !== b) {
              // 找到下标,然后往前合并
              const n1 = find(parent, j)
              const n2 = find(parent, k)
              if (n1 < n2) {
                parent[n2] = n1
              } else if (n1 > n2) {
                parent[n1] = n2
              }
            }
          }
        }
        // 第二遍只检查不等号
        for (let i = 0; i < equations.length; i++) {
          const item = equations[i]
          const a = item[0]
          const b = item[3]
          const equal = item[1] === '='

          if (!equal) {
            const j = map[a]
            const k = map[b]
            // 如果不是相同字母就检查是不是同一个集合
            if (a !== b) {
              const n1 = find(parent, j)
              const n2 = find(parent, k)
              if (n1 === n2) {
                return false
              }
            } else {
              // 同一个字母自己返回 false 了
              return false
            }
          }
        }
        // 全部正确
        return true
   }

   function create(map, parent, a) {
        const i = parent.length
        map[a] = parent.push(i) - 1
   }
    
   // 并查集的经典查找方法,一直读下标,直到下标相等
   function find(parent, i) {
        if (parent[i] !== i) {
          parent[i] = find(parent, parent[i])
        }

        return parent[i]
   }

结束