题目:
给定一个无向图 graph,当这个图为二分图时返回 true。
如果我们能将一个图的节点集合分割成两个独立的子集 A 和 B,并使图中的每一条边的两个节点一个来自 A 集合,一个来自 B 集合,我们就将这个图称为二分图。
graph 将会以邻接表方式给出,graph[i]表示图中与节点 i 相连的所有节点。每个节点都是一个在 0 到 graph.length-1 之间的整数。这图中没有自环和平行边: graph[i] 中不存在 i,并且 graph[i]中没有重复的值。
示例:
- 示例 1
输入: [[1,3], [0,2], [1,3], [0,2]]
输出: true
解释:
无向图如下:
0----1
| |
| |
3----2
我们可以将节点分成两组: {0, 2} 和 {1, 3}。
- 示例 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
}
博客: 小书童博客
公号: 坑人的小书童
