寻找欧拉之旅

时间:2012-09-16 14:52:11

标签: python algorithm graph discrete-mathematics

我正在尝试解决Udacity上的问题,描述如下:

# Find Eulerian Tour
#
# Write a function that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]

我提出了以下解决方案,虽然不像某些递归算法那样优雅,但似乎在我的测试用例中起作用。

def find_eulerian_tour(graph):
    tour = []

    start_vertex = graph[0][0]
    tour.append(start_vertex)

    while len(graph) > 0:
        current_vertex = tour[len(tour) - 1]
        for edge in graph:
            if current_vertex in edge:
                if edge[0] == current_vertex:
                    current_vertex = edge[1]
                elif edge[1] == current_vertex:
                    current_vertex = edge[0]
                else:
                    # Edit to account for case no tour is possible
                    return False

                graph.remove(edge)
                tour.append(current_vertex)
                break
    return tour

graph = [(1, 2), (2, 3), (3, 1)]
print find_eulerian_tour(graph)

>> [1, 2, 3, 1]

然而,在提交时,我被评分者拒绝了。我做错了什么?我看不出任何错误。

10 个答案:

答案 0 :(得分:9)

以下是您的算法失败的有效案例:

graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]

利用print的力量了解graphcurrent_vertex会发生什么。

另一个提示:向下移动else以使其属于for,并在for循环未被破坏时执行。就像现在一样,它永远不会被执行。 在校正之后,算法仍然失败了。

当然,算法仍然失败。

当然,算法仍然失败。

请不要评论说明代码不起作用。它没有。算法仍然失败,即使下面的代码完成了OP的考虑。关键是要表明OP的算法是错误的,OP无法确定。为此,需要正确实现OP的算法(见下文)。错误算法的正确实现仍然不是正确的解决方案。

我很抱歉通过编写所有这些冗长的解释来使这个答案变得更糟,但是人们继续抱怨代码不起作用(当然,重点是表明它是错误的)。他们也赞成这个答案,可能是因为他们希望能够将代码复制为解决方案。但这不是重点,重点是向OP表明他的算法存在错误。

以下代码未找到欧洲之旅。寻找其他地方复制代码以传递你的分配!

def find_eulerian_tour(graph):
    tour = []

    current_vertex = graph[0][0]
    tour.append(current_vertex)

    while len(graph) > 0:
        print(graph, current_vertex)
        for edge in graph:
            if current_vertex in edge:
                if edge[0] == current_vertex:
                    current_vertex = edge[1]
                else:
                    current_vertex = edge[0]

                graph.remove(edge)
                tour.append(current_vertex)
                break
        else:
            # Edit to account for case no tour is possible
            return False
    return tour

graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
print(find_eulerian_tour(graph))

输出:

[(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)] 1
[(2, 3), (3, 1), (3, 4), (4, 3)] 2
[(3, 1), (3, 4), (4, 3)] 3
[(3, 4), (4, 3)] 1
False

答案 1 :(得分:3)

我也在同一个讲座中,WolframH的回答并不适合我。这是我的解决方案(已被评分者接受):

将所有可能的next node推入堆(search),然后在录制时搜索每一个。

def next_node(edge, current):
    return edge[0] if current == edge[1] else edge[1]

def remove_edge(raw_list, discard):
    return [item for item in raw_list if item != discard]

def find_eulerian_tour(graph):
    search = [[[], graph[0][0], graph]]
    while search:
        path, node, unexplore = search.pop()
        path += [node]

        if not unexplore:
            return path

        for edge in unexplore:
            if node in edge:
                search += [[path, next_node(edge, node), remove_edge(unexplore, edge)]]

if __name__ == '__main__':
    graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
    print find_eulerian_tour(graph)
  

[1,3,4,3,2,1]

答案 2 :(得分:2)

以下是您的算法无法处理的情况:4个顶点上的完整图形。坚持print tour,你得到:

>>> cg4 = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
>>> find_eulerian_tour(cg4)
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 0]
[0, 1, 2, 0, 3]
[0, 1, 2, 0, 3, 1]
[0, 1, 2, 0, 3, 1]
[0, 1, 2, 0, 3, 1]
[etc.]

我会让你发现你的方法存在问题 - 你可以轻松谷歌进行完整的实施,所以既然你没有,我假设你想要自己搞清楚的乐趣。 :^)

编辑:

Hmmph。我承认我认为这只是一个错过的失败案例。无论如何,@ WolframH打败了我一个更新的例子,但你也可以查看 5 顶点上的完整图表,代码在哪里

[0, 1, 2, 0, 3, 1, 4, 0]

并错过边缘(2,3),(2,4)和(3,4)。

答案 3 :(得分:2)

使用简单的递归,问题可以比上述解决方案轻松解决。

def find_eulerian_tour(graph):
    tour=[]
    find_tour(graph[0][0],graph,tour)
    return tour
def find_tour(u,E,tour): 
  for (a,b) in E:
    if a==u:
        E.remove((a,b))
        find_tour(b,E,tour)
    elif b==u:
        E.remove((a,b))
        find_tour(a,E,tour)
  tour.insert(0,u)

此代码适用于元组的任何输入列表,并返回巡视列表。 请发送建议和更改(如果有)。 谢谢 @WolframH:如果图中存在任何循环并且输入元组只是为了使代码失败,则代码不起作用。

答案 4 :(得分:1)

我在Udacity上经历了同样的课程。我从维基百科阅读后实现了Hierholzer的算法。以下是算法https://en.wikipedia.org/wiki/Eulerian_path

的链接

以下是我的代码。毫无疑问,它被分级者接受了(在做了一些Python3到Python2的更改之后)。 :)

#!/usr/bin/env python3
# Find Eulerian Tour
#
# Write a program that takes in a graph
# represented as a list of tuples
# and return a list of nodes that
# you would follow on an Eulerian Tour
#
# For example, if the input graph was
# [(1, 2), (2, 3), (3, 1)]
# A possible Eulerian tour would be [1, 2, 3, 1]

def get_a_tour():
    '''This function returns a possible tour in the current graph and removes the edges included in that tour, from the graph.'''
    global graph

    nodes_degree = {}       # Creating a {node: degree} dictionary for current graph.
    for edge in graph:
        a, b = edge[0], edge[1]
        nodes_degree[a] = nodes_degree.get(a, 0) + 1
        nodes_degree[b] = nodes_degree.get(b, 0) + 1

    tour =[]        # Finding a tour in the current graph.
    loop = enumerate(nodes_degree)
    while True:
        try:
            l = loop.__next__()
            index = l[0]
            node = l[1]
            degree = nodes_degree[node]
            try:
                if (tour[-1], node) in graph or (node, tour[-1]) in graph:
                    tour.append(node)
                    try:
                        graph.remove((tour[-2], tour[-1]))
                        nodes_degree[tour[-1]] -= 1     # Updating degree of nodes in the graph, not required but for the sake of completeness.
                        nodes_degree[tour[-2]] -= 1     # Can also be used to check the correctness of program. In the end all degrees must zero.
                    except ValueError:
                        graph.remove((tour[-1], tour[-2]))
                        nodes_degree[tour[-1]] -= 1
                        nodes_degree[tour[-2]] -= 1
            except IndexError:
                tour.append(node)
        except StopIteration:
            loop = enumerate(nodes_degree)

        if len(tour) > 2:
            if tour[0] == tour[-1]:
                return tour

def get_eulerian_tour():
    '''This function returns a Eulerian Tour for the input graph.'''
    global graph
    tour = get_a_tour()

    if graph:   # If stuck at the beginning, finding additional tour in the graph.
        loop = enumerate(tour[: -1])
        l = loop.__next__()
        i = l[0]
        node = l[1]
        try:
            while True:
                if node in list(zip(*graph))[0] or node in list(zip(*graph))[1]:
                    t = get_a_tour()    # Retreivng the additional tour
                    j = t.index(node)
                    tour = tour[ : i] + t[j:-1] + t[ :j+1] + tour[i+1: ]        # Joining the two tours.
                    if not graph:       # Found Eulerian Tour
                        return tour     # Returning the Eulerian Tour
                    loop = enumerate(tour[: -1])        # Still stuck? Looping back to search for another tour.
                l = loop.__next__()
                i = l[0]
                node = l[1]
        except StopIteration:   # Oops! seems like the vertices in the current tour cannot connect to rest of the edges in the graph.
            print("Your graph doesn't seem to be connected")
            exit()
    else:       # Found the Eulerian Tour in the very first call. Lucky Enough!
        return tour

# Sample inputs
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6)]
# graph = [(1, 2), (1, 3), (2, 3)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (9, 10), (10, 11), (11, 9)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (2, 7), (7, 8), (8, 2)]
# graph = [(1, 2), (1, 3), (2, 3), (2, 4), (2, 6), (3, 4), (3, 5), (4, 5), (4, 6), (1, 5), (5, 6), (1, 6)]
# graph = [(1, 2), (2, 3), (3, 1), (3, 4), (4, 3)]
# graph = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]
# graph = [(2, 6), (4, 2), (5, 4), (6, 5), (6, 8), (7, 9), (8, 7), (9, 6)]

# creating a {node: degree} dictionary
nodes_degree = {}
for edge in graph:
    a, b = edge[0], edge[1]
    nodes_degree[a] = nodes_degree.get(a, 0) + 1
    nodes_degree[b] = nodes_degree.get(b, 0) + 1

#checking degree
degrees = nodes_degree.values() # remember it return a view
for degree in degrees:
    if degree % 2:
        print("Your graph have one or more nodes with odd degrees. Hence an Eulerian Tour is impossible.")
        exit()

#finding Eulerian Tour
tour = get_eulerian_tour()
print(tour)

希望这有帮助。

答案 5 :(得分:0)

虽然代码对于无向图的代码失败,但是对于有向图的代码运行得非常好。然而,它仍然无法解决Udacity方面的问题,但可以将其视为相同版本的较低版本。请不要介意使用不当的Python,因为我还不熟悉该语言。

在底部添加了两个相当好的复杂程度的测试场景。

initialNode = ''
nglength = 0
in_graphLength = 0
normalizedGraph = list()
path = []
node_dict = {}
mod_flag = ''

def find_eulerian_tour(graph):
    global in_graphLength
    in_graphLength = len(graph)
    graph = normalize_graph(graph,[],-1,len(graph))
    print (path)
    return path
def normalize_graph(graph,nG,remNode,length):
    counter = 0
    global path
    global initialNode
    global in_graphLength
    global nglength
    global normalizedGraph
    localGraph = list()
    path = []
    pathList = []
    if(remNode != -1):
        normalizedGraph = nG
    baseNode = 0
    if(len(normalizedGraph) != 0):
        ini1, ini2 = normalizedGraph[0]
        initialNode = ini1
        a1,b1 = normalizedGraph[len(normalizedGraph) - 1]
        baseNode = b1
        if(remNode != -2):
            graph.pop(remNode)
    if(remNode == -1):
        a,b = graph[0]
        baseNode = b
        normalizedGraph.append(graph[0])
        initialNode = a
        nglength = 1
        graph.pop(0)
    i = 0
    if(len(graph) != 0):
        for n1, n2 in graph:
            i = i + 1
            if(n1 == baseNode):
                localGraph = graph[:]
                if(isJunction((n1,n2),localGraph, nglength)):
                    graph.pop(i-1)
                    graph.append((n1,n2))
                    normalize_graph(graph, normalizedGraph, -2,in_graphLength)
                    break
                else:
                    normalizedGraph.append((n1, n2))
                    nglength = nglength + 1
                    normalize_graph(graph, normalizedGraph, i - 1,in_graphLength)
                    break

    else:
        if( counter == 0):
            counter = counter + 1
            a0, b0 = normalizedGraph[0]
            for n1, n2 in normalizedGraph:
                path.append(n1)
            path.append(a0)
            path = path
            return path

def isJunction((n1,n2), graph, nglength):
    global node_dict
    count = 0
    if(len(graph) > 1):
        for a1, a2 in graph:
            if (n1 == a1):
                count = count + 1
        if (count > 1):
            if(str(n1) not in node_dict):
                key = str(n1)
                node_dict[key] = count
            else:
                return handle_degree(n1)
            return modification_needed((n1, n2), graph, nglength)
        else:
            return False
    else:
        return False

def handle_degree(n1):
    global node_dict
    key = str(n1)
    if(node_dict.get(key) == 2):
        return False

def modification_needed((n1,n2),graph, tmplength):
    i = 0
    global mod_flag
    if( n2 == initialNode):
        return True
    if(len(graph) > 1):
        for b1, b2 in graph:
            if(n2 == b1):
                i = i + 1
                tmplength = tmplength + 1
                if (b1,b2) in normalizedGraph:
                    mod_flag = True
                    continue
                if(tmplength < in_graphLength and b2 == initialNode):
                    mod_flag = True
                    continue
                else:
                    graph.pop(i-1)
                    modification_needed((b1,b2),graph,tmplength)
    return mod_flag


#find_eulerian_tour([(1,2),(2,6),(7,2),(6,1),(2,3),(3,5),(3,4),(4,5),(5,7),(7,3),(5,6),(6,7)])
#find_eulerian_tour([(0,4),(1,0),(4,2),(4,8),(2,5),(9,5),(8,9),(5,4),(5,1),(7,1),(3,7),(1,6),(6,3)])

答案 6 :(得分:0)

Here is Gregor Ulm网页上的原始代码并且有效。

def find_eulerian_tour(graph):

def freqencies():
    # save all nodes of edges to my_list
    # e.g. [3,4,5,1,2,2,3,5]
    my_list = [x for (x, y) in graph]
    # get the max num of nodes-->create a list
    # set all to 0
    # for i in range(5) = 0 1 2 3 4
    # so range("5" +1) means
    # len=6, result=[0,0,0,0,0,0]
    # so that the index = the number itself
    result = [0 for i in range(max(my_list) + 1)]
    # nodes in my_list, increment
    # e.g. [0,1,2,2,1,2] 
    # 3appears 2times.
    for i in my_list:
        result[i] += 1
    return result
    # this is Frequencies of each nodes.

def find_node(tour):
    for i in tour:
        if freq[i] != 0:
            return i
    return -1

def helper(tour, next):
    find_path(tour, next)
    u = find_node(tour)
    while sum(freq) != 0:     
        sub = find_path([], u)
        # get the sub_path
        # add them together
        # when draw to u, turn to sub, and then come back to go on the original tour path
        # [:a], start to a; [a+1:] a+1 to end
        tour = tour[:tour.index(u)] + sub + tour[tour.index(u) + 1:]  
        u = find_node(tour)
    return tour

def find_path(tour, next):
    for (x, y) in graph:
        if x == next:
            # from "double-graph"
            # pop out the current one and its respondent one
            # actually it means we delete this edge
            current = graph.pop(graph.index((x,y)))
            graph.pop(graph.index((current[1], current[0])))
            # now add this "next" node into the tour
            tour.append(current[0])
            # decrement in frequency
            freq[current[0]] -= 1
            freq[current[1]] -= 1
            return find_path(tour, current[1])
    # if this "next" node is not connected to any other nodes
    # single one
    tour.append(next)
    return tour             

# in graph, all edges get reversed one and be added to graph
# can call it "double-graph"  
# it helps to calculate the frequency in find_path
# actually we can regard frequency as degrees for each node       
graph += [(y, x) for (x, y) in graph]
freq = freqencies()   
# set graph[0][0] as starting point
return helper([], graph[0][0])

graph = [(1, 2), (2, 3), (3, 1)]
print find_eulerian_tour(graph)

答案 7 :(得分:0)

此解决方案针对 O(V + E)复杂度进行了优化,即图中边缘和顶点的线性

对于那些直接希望查看代码的人:https://github.com/cubohan/py-algos/blob/master/eulerian_tour.py

请注意,代码主要违反了可读性和DRY设计,但在阅读说明后,您可以轻松地制作出自己的版本。

# eulerian_tour.py by cubohan
# circa 2017
#
# Problem statement: Given a list of edges, output a list of vertices followed in an eulerian tour
#
# complexity analysis: O(E + V) LINEAR


def find_eulerian_tour(graph):
    edges = graph
    graph = {}
    degree = {}
    start = edges[0][0]
    count_e = 0
    for e in edges:
        if not e[0] in graph:
            graph[e[0]] = {}
        if not e[0] in degree:
            degree[e[0]] = 0
        if not e[1] in graph:
            graph[e[1]] = {}
        if not e[1] in degree:
            degree[e[1]] = 0
        graph[e[0]][e[1]] = 1
        graph[e[1]][e[0]] = 1
        degree[e[0]] += 1
        degree[e[1]] += 1
        count_e += 1
    max_d = 0
    this_ = 0
    for v, d in degree.items():
        if not d%2 == 0:
            # Eulerian tour not possible as odd degree found!
            return False 
        if d>max_d:
            this_ = v
            max_d = d
    visited_e = {}
    def is_visited(i, j):
        key = str(sorted([i,j]))
        if key in visited_e:
            return True
        else:
            visited_e[key] = True
            return False
    start = this_
    route = [start]
    indexof = {}
    indexof[start] = 0
    while count_e>0:
        flag = False
        for to_v in graph[this_]:
            if not is_visited(to_v, this_):
                route.append([to_v])
                indexof[to_v] = len(route)-1
                degree[to_v] -= 1
                if degree[to_v] == 0:
                    del degree[to_v]
                degree[this_] -= 1
                if degree[this_] == 0:
                    del degree[this_]
                this_ = to_v
                flag = True
                count_e -= 1
                break
        if not flag:
            break
    for key, v in degree.items():
        if v <=0:
            continue
        try:
            ind = indexof[key]
        except Exception as e:
            continue
        this_ = key
        while count_e>0:
            flag = False
            for to_v in graph[this_]:
                if not is_visited(to_v, this_):
                    route[ind].append(to_v)
                    degree[to_v] -= 1
                    degree[this_] -= 1
                    this_ = to_v
                    flag = True
                    count_e -= 1
                    break
            if not flag:
                break
    route_ref = []
    for r in route:
        if type(r) == list:
            for _r in r:
                route_ref.append(_r)
        else:
            route_ref.append(r)
    return route_ref

if __name__ == "__main__":
    print find_eulerian_tour([(0, 1), (1, 5), (1, 7), (4, 5),(4, 8), (1, 6), (3, 7), (5, 9),(2, 4), (0, 4), (2, 5), (3, 6), (8, 9)])

**首先,问题可以分为以下几个子任务:**

  1. 将图表构建为比边缘列表更友好的结构,以便于处理,即(邻接列表)

  2. 找出每个顶点的度数,首先检查是否可以进行欧拉游览(是否只有偶数度?还有,将这些值存储在dict中,顶点为key =&gt;以便稍后使用)

  3. 建立欧拉之旅

  4. 我的解决方案背后的概念很简单。

    1. 您选择具有最高度数的顶点作为起点并将其设置为当前顶点。 (注意:您在计算每个顶点的度数的同时完成此操作。您将所有这些度数存储在字典中。)

    2. 您在路径列表中插入当前顶点,这是您的答案(注意:还要在路径列表中创建顶点及其索引的字典。稍后将使用。)

    3. 如果当前顶点尚未访问,则访问当前顶点的第一个边缘。 (注意:保留了访问边的字典,该字典的关键是构成边的顶点对的排序元组。访问边后,通过将其插入字典来标记它被访问。)

      < / LI>
    4. 你保持当前顶点和被访问顶点的剩余度数(这将在以后证明是有用的)(注意:你只需要在每次选择之前从你生成的度数的dict中减去1)边缘)

    5. 您将当前顶点切换到您决定访问的边缘另一端的顶点。

    6. 重复步骤2-5,直到找不到当前顶点中未访问的边缘。 (注意:这意味着你已经返回到起始顶点)

    7. 现在考虑一下:注意任何未访问的边/顶点将构成主图中具有与主图相同属性的子图,即可以从子图中的任何顶点开始和结束时进行欧拉巡视顶点。

      通过在这些子图中进行欧拉游览,可以访问所有未经检查的边缘。您只需要将这些子游览与第一次游览合并。

      下一步:

      1. 循环遍历图形的所有顶点,并在与主要巡视列出的相同过程中构建一个子层,当且仅当此顶点的缩小程度为非零

      2. 这些游览将与先前计算的路线列表合并的方式是您替换您考虑从路径列表中使用子颜色输出列表开始子地点并稍后展平的顶点的位置此路线列表

      3. 我们还没有完成!上面有什么问题?

        当你获得一个非零度顶点时会发生什么,这个顶点是否已经被访问并且没有出现在路线列表中?!

        警告: 这是一个例外情况。

        您可能会遇到之前未访问过的顶点,因此它们不会出现在主路径列表中。

        IGNORE这些循环!您已经访问过的具有非零降阶的顶点之一是保证在您将从这些顶点开始创建的这些顶点。

        怎么可能?!!

        从代码链接中给出的测试用例中绘制一个图表,您就会明白。追踪你的算法在这个过程的每一步都在做什么。画出来!图像是理解和单词O(n2)的log(N)复杂度。

        啊,请注意,只有当输入列表中的所有边形成单个图形而不是两个单独的脱节图时,此保证才会成立。

答案 8 :(得分:0)

您可以模仿BFS算法的行为并捎带它。

注意:我没有尝试使用链接列表编写答案,因为链接列表需要定义2个类(一个用于定义节点及其行为,另一个用于定义整个链接列表及其行为)。但是,为了提高(追加)​​和(删除)行为的效率,您应该使用链接列表而不是数组:

def find_eulerian_tour(graph):
    nodes = set()

    for i in graph:
        if not i[0] in nodes:
            nodes.add(i[0])
        if not i[1] in nodes:
            nodes.add(i[1])

    tour = []
    tempstack = []
    graphtemp = []

    current_vertex = graph[0][0]
    tour.append(current_vertex)
    tempstack.append(current_vertex)
    last_edge = ()
    count = 0

    while len(set(tempstack)) + 1 < len(nodes) or (count == 0 or tour[0] != tour[len(tour) - 1]):
        count += 1
        flag = False
        for edge in graph:
           if current_vertex in edge and edge != last_edge:
                if current_vertex == edge[0]:
                    current_vertex = edge[1]
                else:
                    current_vertex = edge[0]
                last_edge = edge
                graphtemp.append(edge)
                graph.remove(edge)
                tour.append(current_vertex)
                tempstack.append(current_vertex)
                flag = True
                break

        if flag == False:
            tour.remove(current_vertex)
            current_vertex = tempstack[0]
            tempstack.remove(tempstack[0])
            graph.append(graphtemp[0])
            graphtemp.remove(graphtemp[0])


    return tour


print find_eulerian_tour([(1,2), (2,3), (3,1)])
print(find_eulerian_tour([(0, 1), (1, 5), (1, 7), (4, 5), (4, 8), (1, 6), (3, 7), (5, 9), (2, 4), (0, 4), (2, 5), (3, 6), (8, 9)]))
print(find_eulerian_tour([(1, 13), (1, 6), (6, 11), (3, 13), (8, 13), (0, 6), (8, 9),(5, 9), (2, 6), (6, 10), (7, 9), (1, 12), (4, 12), (5, 14), (0, 1),  (2, 3), (4, 11), (6, 9), (7, 14),  (10, 13)]))
print(find_eulerian_tour([(8, 16), (8, 18), (16, 17), (18, 19), (3, 17), (13, 17), (5, 13),(3, 4), (0, 18), (3, 14), (11, 14), (1, 8), (1, 9), (4, 12), (2, 19),(1, 10), (7, 9), (13, 15), (6, 12), (0, 1), (2, 11), (3, 18), (5, 6), (7, 15), (8, 13), (10, 17)]))

答案 9 :(得分:0)

如果我们这样做怎么办? (刚检查过,它通过了udacity测试!!)

spring-configuration-metadata.json