一天一大 leet(判断二分图)难度:中等-Day20200716

时间:2022-07-25
本文章向大家介绍一天一大 leet(判断二分图)难度:中等-Day20200716,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目:

给定一个无向图 graph,当这个图为二分图时返回 true。

如果我们能将一个图的节点集合分割成两个独立的子集 A 和 B,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,我们就将这个图称为二分图。

graph 将会以邻接表方式给出,graph[i]表示图中与节点 i 相连的所有节点。每个节点都是一个在 0 到 graph.length-1 之间的整数。这图中没有自环和平行边:graph[i] 中不存在 i,并且 graph[i]中没有重复的值。

示例:

  1. 示例 1
输入: [[1,3], [0,2], [1,3], [0,2]]
输出: true
解释:
无向图如下:
0----1
|    |
|    |
3----2
我们可以将节点分成两组: {0, 2} 和 {1, 3}。
  1. 示例 2
输入: [[1,2,3], [0,2], [0,1,3], [0,2]]
输出: false
解释:
无向图如下:
0----1
|   |
|   |
3----2
我们不能将节点分割成两个独立的子集。

注意

  • graph 的长度范围为 [1, 100]。
  • graph[i] 中的元素的范围为 [0, graph.length - 1]。
  • graph[i] 不会包含 i 或者有重复的值。
  • 图是无向的: 如果 j 在 graph[i]里边, 那么 i 也会在 graph[j]里边。

抛砖引玉

  • 理解下题意:

位置

节点

A

B

0

[1,3]

[0]

[1,3]

1

[0,2]

[0,0]

[1,3]

2

[1,3]

[0,0,2]

[1,3,1,1,3]

3

[0,2]

[0,0,0,2]

[1,3,1,1,3]

结果

-

[0,2]

[1,3]

逻辑

  • A 填充索引
  • b 填充值
  • A 填充前判断该索引 i 在 B 中存在吗:
    • A 中存在 graph[i]中的值,则将 graph[i]填充 A,i 填充 B
    • A 中不存在 graph[i]中的值,则将 graph[i]填充 B,i 填充 A AB 均为去重填充
    • 不存在,
    • 存在,将改值 graph[i]填充到 A,A 为去重填充
  • 按照以上规则,发现当 graph[i]中的值在 A 中出现或者在 B 中出现收受影响的并不是改组值,需要对单个值进行处理

换种思路:

  • 有连接的数据一定不在一组中则分别不同的组
  • 那从 i=0 遍历所有点,对确定在连接线两端的数组分组
  • graph[i]会有多个值,那把他们都分到与 i 不同的组
  • 在遍历 graph[i]时会遇到与 i 相同的节点,为了避免重复遍历则声明 dp 作为记录,存放过的节点不再操作

注意

  • A 优先填充索引
  • 填充过的数据在遍历索引时不能重复填充,避免默认值与逻辑值冲突
  • 一个元素填充过 A 之后又在遍历中填充 B 则说明无法生成二分图 返回 false

实现

  • 按节点遍历,使用递归填充其索引 i 对应的值 graph[i]
  • 递归参数:索引,填充到的数组标记
  • 递归的终止条件:
    • 已填充过,即在 dp 中出现过
/**
 * @param {number[][]} graph
 * @return {boolean}
 */
var isBipartite = function (graph) {
  let _result = false,
    dp = new Map(),
    A = new Map(),
    B = new Map()

  for (let i = 0; i < graph.length; i++) {
    // 跳过已经存存放的节点,如不跳过已存放节点,那默认填充A会与已填充元素冲突
    if (!dp.has(i)) {
      pushItem(i, 'A')
    }
  }

  function pushItem(i, flag) {
    // 已经存存放的节点确认是否冲突
    if (dp.has(i)) {
      _result = _result || flag !== dp.get(i)
      return
    }
    // 存入A组
    if (flag === 'A') {
      A.set(i)
      dp.set(i, 'A')
    } else {
      // 存入B组
      B.set(i)
      dp.set(i, 'B')
    }
    // graph[i]的所有元素都不能与i在同一个分组
    for (let j = 0; j < graph[i].length; j++) {
      pushItem(graph[i][j], flag === 'A' ? 'B' : 'A')
    }
  }
  return !_result
}

优化

上面最终生成了 A,B 两个组,但是这两个组其实并没有参与判断,则优化删除 A,B 两个组对象

/**
 * @param {number[][]} graph
 * @return {boolean}
 */
var isBipartite = function (graph) {
  let _result = false,
    dp = new Map()

  for (let i = 0; i < graph.length; i++) {
    // 跳过已经存存放的节点,如不跳过已存放节点,那默认填充A会与已填充元素冲突
    if (!dp.has(i)) {
      pushItem(i, 'A')
    }
  }

  function pushItem(i, flag) {
    // 已经存存放的节点确认是否冲突
    if (dp.has(i)) {
      _result = _result || flag !== dp.get(i)
      return
    }
    dp.set(i, flag)
    // graph[i]的所有元素都不能与i在同一个分组
    for (let j = 0; j < graph[i].length; j++) {
      pushItem(graph[i][j], flag === 'A' ? 'B' : 'A')
    }
  }
  return !_result
}

其他解法

  • 声明一个存储对象 dp 记录每个元素的分组 A 组标记 1,B 组标记-1
  • 声明一个 queue(存放索引时存放一个,存放索引对应的值是存放多个)
  • 遍历 graph 将其索引 i 放入 queue,在从其取出带上标记存放到 dp
  • 放入 graph[i]对应的值到 queue 依次取出带上标记存放到 dp
  • 每次 queue 从取出元素时切换标记
/**
 * @param {number[][]} graph
 * @return {boolean}
 */
var isBipartite = function (graph) {
  let dp = new Map()
  for (let i = 0; i < graph.length; i++) {
    if (dp.has(i)) continue
    let queue = [i]
    dp.set(i, 1)
    while (queue.length) {
      let j = queue.shift()
      let A = dp.get(j),
        B = -A
      for (let k = 0; k < graph[j].length; k++) {
        let item = graph[j][k]
        if (!dp.has(item)) {
          dp.set(item, B)
          queue.push(item)
        } else if (dp.get(item) != B) {
          return false
        }
      }
    }
  }
  return true
}