用有向无环图解决casbin循环继承的问题

背景

casbin学习记录一文中,
我提到了casbin多重继承的功能.
这里多重继承是说, 比如对某个资源R1, 在: grouping policy: g2 = _, _
设计下
如下的实际数据表示: r1同时属于r2和r3这两个分组

g2, r1, r2
g2, r1, r3,

问题提出

那么我们很自然就会有一个问题, 如果循环了怎么办?
比如:

g2, r1, r2
g2, r2, r3,
g2, r3, r1

上面的分组关系就是一个环, 很明显, 现实生活中是不可能有这种关系的, 而且这种数据也没有意义.

所以我们要避免这种情况.

问题分析

其实仔细想想, 这种多继承的关系, 其实就是一个有向图, 那问题就变成了, 如果保证有向图无环?

问题解决

这是个经典的有向图检测环的算法, 我是这样做的:

  1. 在存储新policy(g2, r1, r2就是一个policy)之前, 先取出已有的policy, 记录exist_policy,
    其结构为: [][2]string
  2. 将要存储的, 记为input_policy, 其结构为: [][2]string
  3. 将exist_policy和input_policy认为是有向图的许多条边, 构建一个有向图.
  4. 对于input_policy的点进行检测(因为如果有环, input_policy的某些点肯定在环上).
  5. 判断有没有环

代码

下面是我写的有向图判断环的代码, 有一些方法和判断环无关的, 可不用管

package algo

import (
	"bytes"
	"container/list"
	"fmt"
	"strings"
)

// 有向图
type dGraph struct {
	v int // 顶点数
	e int // 边数

	// 存储顶点之间关系的结构, 邻接表
	m map[string][]string

	// "谁指向它", 如edgeTo["2"] = "1", 表示1指向2
	// 直观理解为: "2"的"前驱"节点是"1"
	edgeTo map[string]string

	// 是否在某次递归的栈里
	onStask map[string]bool

	// 从key出发的环
	// 如: 1 => [1, 2, 3, 1]
	circles map[string][]string

	// 遍历过程中已经访问过的点标记下
	visited map[string]bool
}

// m: 已经有的k,v表示的图, m为空表示创建一个空的图
func NewGraph() *dGraph {
	return &dGraph{
		m:       make(map[string][]string),
		circles: make(map[string][]string),
		visited: make(map[string]bool),
		onStask: make(map[string]bool),
		edgeTo:  make(map[string]string),
	}
}

func (g *dGraph) Destroy() {
	g.m = nil
	g.edgeTo = nil
	g.onStask = nil
	g.circles = nil
	g.visited = nil
}

// 数据不变, 记录的状态重置
func (g *dGraph) ResetStatus() {
	g.circles = make(map[string][]string)
	g.visited = make(map[string]bool)
	g.onStask = make(map[string]bool)
	g.edgeTo = make(map[string]string)
}

// 顶点数
func (g *dGraph) V() int {
	slice := make([]string, 0)
	for k, v := range g.m {
		slice = append(slice, k)
		slice = append(slice, v...)
	}

	marked := make(map[string]bool)
	sum := 0
	for _, v := range slice {
		if !marked[v] {
			marked[v] = true
			sum++
		}
	}

	return sum
}

// 边数
func (g *dGraph) E() int {
	return g.e
}

// 添加一条边(v => w)
func (g *dGraph) AddEdge(v, w string) {
	// 不能自成环
	if v == w {
		return
	}

	// 不接收空串
	if v == "" || w == "" {
		return
	}

	for _, u := range g.m[v] {
		// w已经是v的邻接点了, 不要重复添加
		if u == w {
			return
		}
	}

	if len(g.m[v]) == 0 { // v还没有邻接点
		g.m[v] = make([]string, 0)
	}
	g.m[v] = append(g.m[v], w)

	// 只要添加节点成功, 边数都是增加1
	g.e++
}

// 返回顶点v的邻接点
func (g *dGraph) Adj(v string) []string {
	return g.m[v]
}

// 以顶点v为起点进行深度优先遍历
// stopOnErr: 遍历过程中遇到错误(由visitor的返回值决定是否出错)立即停止遍历
func (g *dGraph) Dfs(v string, visitor func(v string) error, stopOnErr bool) {
	var err error

	err = visitor(v)
	g.visited[v] = true
	if err != nil && stopOnErr {
		return
	}

	adjs := g.Adj(v)
	for _, w := range adjs {
		if !g.visited[w] {
			g.Dfs(w, visitor, stopOnErr)
		}
	}
}

// 检测从v出发是否有环
func (g *dGraph) dfs4Circle(v string) {
	g.onStask[v] = true // v在此次递归的栈里
	g.visited[v] = true

	// 如果从某顶点出发有多个环, 只记录一个
	if len(g.circles[v]) > 0 {
		return
	}

	adjs := g.Adj(v)
	for _, w := range adjs {
		if !g.visited[w] {
			g.edgeTo[w] = v
			g.dfs4Circle(w)
		} else if g.onStask[w] {

			stack := list.New()
			// 如果图中无环的话, w(v的邻接点)在dfs过程中是不可能在onStask中的, 所以到这里说明成环
			for x := v; x != w && len(x) > 0; x = g.edgeTo[x] {
				stack.PushFront(x)
			}
			stack.PushFront(w)
			stack.PushFront(v)

			circle := make([]string, 0, stack.Len())
			for e := stack.Front(); e != nil; e = e.Next() { // 出栈
				circle = append(circle, e.Value.(string))
			}

			g.circles[w] = circle
			return
		}
	}

	// 某个点再无邻接点, 本次递归退出(针对v的深度优先结束)
	// 重置 "在栈状态"
	g.onStask[v] = false
}

// 从v出发的环
func (g *dGraph) Circle(v string) []string {
	g.ResetStatus()
	g.dfs4Circle(v)
	return g.circles[v]
}

// 图中的所有环
func (g *dGraph) CirclesInGraph() map[string][]string {
	g.ResetStatus()
	visited := make(map[string]bool)

	for k := range g.m {
		if !visited[k] {
			g.dfs4Circle(k)
			visited[k] = true
		}
	}

	// TODO: 环没有去重, 如: 1,2,3,1和2,3,1,2是一个环
	return g.circles
}

// 按照邻接表的顺序打印图, 由于是map存储的起始顶点, 所以起始顶点的顺序不定
func (g *dGraph) String() string {
	buffer := bytes.NewBufferString("")
	for k, v := range g.m {
		if _, err := buffer.WriteString(fmt.Sprintf("%s => [%s]\n", k, strings.Join(v, ","))); err != nil {
			buffer.WriteString(fmt.Sprintf("write %s failed", v))
			continue
		}
	}
	return buffer.String()
}

测试代码

package algo

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestGraph(t *testing.T) {
	g := NewGraph()
	if len(g.CirclesInGraph()) > 0 {
		t.Fatalf("CirclesInGraph error")
	}

	g.AddEdge("1", "2")
	g.AddEdge("2", "3")
	g.AddEdge("3", "4")
	g.AddEdge("4", "5")
	g.AddEdge("4", "6")
	g.AddEdge("8", "9")

	// 测试重复数据
	g.AddEdge("8", "9")

	t.Logf(`g: 
%s`, g.String())

	path := make([]string, 0)
	g.Dfs("1", func(v string) error {
		path = append(path, v)
		return nil
	}, false)

	g.ResetStatus()

	expected := "1,2,3,4,5,6"
	actual := strings.Join(path, ",")
	if actual != expected {
		t.Fatalf("expected: %v, actual: %v", expected, actual)
	}

	assert.Equal(t, 6, g.E(), "error edge num")
	assert.Equal(t, 8, g.V(), "error vertex num")

	// 检测环: 1,2,3,4,5,1
	g.AddEdge("5", "1")
	t.Logf(`g: 
%s`, g.String())

	t.Log(g.Circle("1"))
	t.Log(g.CirclesInGraph())

	// again
	g.Destroy()
	g = NewGraph()
	g.AddEdge("1", "2")
	g.AddEdge("2", "3")
	g.AddEdge("2", "5")
	g.AddEdge("3", "4")

	g.AddEdge("4", "1")
	if circle := g.Circle("4"); len(circle) == 0 {
		t.Errorf("can not check circle for %v", "4")
	}

	if circle := g.Circle("1"); len(circle) == 0 {
		t.Errorf("can not check circle for %v", "1")
	}

	// 两次输出结果可能不一样, 如1,2,3,1和2,3,1,2. 由于起点是map的key, 所以会有这种现象
	t.Log(g.CirclesInGraph())
	t.Log(g.CirclesInGraph())
}

欢迎补充指正!

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读