Python - [p-cycles] ** небольшой код для большой работы

Итак, сегодня я продемонстрирую как была решена задача нахождения p-cycles (п-циклов) в WDM сетях (магистральных, оптических).
План таков:
-- для начала определимся что такое эти p-cycles, и каков алгоритм их нахождения, после подробно рассмотрим код на Питоне, который по-своему решает данную задачу. Заметка эта длинная, не для слабонервных и, возможно, будет создаваться в несколько присестов. Итак, поехали, под кат.
Статья требует базового знания Python. Но можно насладиться и без этакого знания.


------ Часть 1: P-cycles. ------

Всё наш рассуждение будет отталкиваться от данного рисунка:
p-cycle

Итак, мне суждено было заняться разработкой алгоритма построения п-циклов (модное название, которое сейчас в неких Российских кругах выдаётся за нечто особое), по сути, как я понял, p-cycle - это сокращение от protection cycle (цикл защиты). То есть, любой цикл на графе, иллюстрирующим топологию рассматриваемой сети, можно назвать p-cycle. Ха, тут я расслабился, построю любой цикл - и назову его "П")) Но стало мне интересно, какой же смысл вкладывают в это слово спецы?

Итак, в WDM ячеистых сетях, которые перетаскивают огромные потоки данных, и выполненные на основе дорогостоющего оптоволокна необходимо создать резервные мощности. Вот для такой задачи и созданы p-циклы. Дело в том, что при обрыве линий связи очень сложно переконфигурировать всю систему, это довольно долго - строить новые маршруты, поэтому нужна система которая практически мгновенно отреагирует. И тут, строим по принципу FDDI защитное кольцо. На рисунке красные узлы и красные соединения как раз и показывают наш защитный цикл. И если произойдёт обрыв (это также показано на рисунке), то достаточно просто замкнуть на себя (выходы на входы - loopback) конечные узлы, между которыми был обрыв. И тогда при обрые, если узел 1 пошлёт сообщение узлу 7 через узел 5, то сообщение дойдёт до узла 5, там "развернётся" пойдёт по кольцу обратно и дойдёт до узла 7, при этом нам никак не пришлось переконфигурировать маршруты на всех узлах - для глобальных сетей это удобно - Правда?
Итак, самый наверное малоэффективный, но и самый надёжный p-cycle - это Гамильтонов цикл ( цикл, содержащий все вершины ровно один раз), но это дорого. Что же делать? А давайте некоторые узлы обойдём. Эти узлы на рисунке синенькие. Ведь, скажем, если у узла 6 оборвётся линия связи с узлом 7, то остаётся связь с узлом 2, что нас вполне устраивает, а узел 2 состоит в нашем цикле. Вот на таких соображениях и основаны разные пути оптимизации p-cycle. По этому поводу есть много публикаций и диссертаций. Я не берусь за рассказ и реализацию многих методов которые я встречал. Моей задачей было построить что-то более эффективное чем гамильтонов цикл, и без всяких критериев оптимизации, об этом я и расскажу далее.

------ Часть 2: Алгоритм поиска p-cycle-ов------
Смотрим на наш рисунок, в предлагаемом мною алгоритме для начала нужно выделить случайный отправной узел - пусть 1. После чего создаём список всех смежных с ним узлов, и выбираем первый попавшийся узел, который имеет общий смежный узел с исходным узлом. Этот общий помечается как трансграничный, а выбранный, становится новым исходным и всё повторяется - это в двух словах весь алгоритм.

-------Часть 3: реализация на Python ----------
Сначала приведу весь код - не пугайтесь если он непонятен и без подробных комментариев, они будут чуть дальше, а пока тот код, который лежит у меня))) Просто для начала окиньте всё взглядом.

"""
This module find p-cycles in Graph
"""
import random


#-----------------------------------------------------------------------------
def takeTestMatrix(n=10):
	GMatrix = [[(random.randint(0, 1) if j != i else 0)
				for j in range(i + 1)] for i in range(n)]
	for i in range(n):
		for j in range(i + 1, n):
			GMatrix[i].append(GMatrix[j][i])
	return GMatrix
#-----------------------------------------------------------------------------


#-----------------------------------------------------------------------------
def MakeP_Cycle(GMatrix):
	"""functioin to search p_cycle in graf
	parametr GMatrix - target graf - it should be matrix nXn
	function returns list of nodes witch contains p_cycle or take interrapt"""
	N = len(GMatrix)
	FreeNodesSet = {i for i in range(N)}
	TransNodedSet = set()
	p_cycle = []
	#take first node
	FirstNode = random.randint(0, N - 1)
	FreeNodesSet.remove(FirstNode)
	p_cycle.append(FirstNode)

	#-----------------------------------------
	def EndingFind(currentNode):
		"""
		check, if current node have direct link to first node
		- then return ending p-cycle,
		else it check if node have link to first node throught transnode
		- return p-cycle,
		else return FIND_ERROR interrupt
		"""
		if GMatrix[currentNode][FirstNode] == 1:
			p_cycle.append(FirstNode)
			return
		buf1 = {node for node in range(N) if GMatrix[FirstNode][node] == 1}
		buf2 = {node for node in range(N) if GMatrix[currentNode][node] == 1}
		if buf1 & buf2:
			buf = buf1 & buf2
			currentNode = [node for node in buf if node in TransNodedSet]
			if currentNode:
				p_cycle.append(currentNode[0])
				p_cycle.append(FirstNode)
				TransNodedSet.remove(currentNode[0])
				return
			else:
				p_cycle.append('ERROR')
		return

	#-----------------------------------------
	def addNextNode(currentNode):
		"""find and add to p_cycle next node"""
		print('call addNextNode')
		CurrLikeNodes = [node for node in range(N) if
						(node in FreeNodesSet) & (GMatrix[currentNode][node]
						== 1)]
		if len(CurrLikeNodes) == 0:
			EndingFind(currentNode)
			return
		for i in range(len(CurrLikeNodes)):
			takenTransNode = CurrLikeNodes[i]  # take node wich can be transnode
			#find nodes has direct link to takentransnode and sit in CurrLikeNodes list:
			buf = [node for node in CurrLikeNodes if (node !=
					takenTransNode) & (GMatrix[node][takenTransNode] == 1)]
			if buf:
				FreeNodesSet.remove(takenTransNode)
				TransNodedSet.add(takenTransNode)
				currentNode = random.choice(buf)
				FreeNodesSet.remove(currentNode)
				p_cycle.append(currentNode)
				print('--debug: ', currentNode)
				break
		else:
			currentNode = random.choice(CurrLikeNodes)
			FreeNodesSet.remove(currentNode)
			p_cycle.append(currentNode)
			print('--debug: ', currentNode)
		addNextNode(currentNode)
	#-----------------------------------------
	addNextNode(FirstNode)
	print('--debug: ', FreeNodesSet)
	print('--debug: ', TransNodedSet)
	print('--debug: p-cycle  ', p_cycle)
	return (p_cycle, FreeNodesSet, TransNodedSet)
#------------------------------------------------------------------------------


if __name__ == '__main__':

	#initialize test graf
	n = 10
	GMatrix = takeTestMatrix(n)
	#end init GMatrix

	#ptint test matrix:
	for i in range(n):
		print(GMatrix[i])

	#test func
	#testM = [[0,1,0,0,1],[1,0,1,0,1],[0,1,0,1,0],[0,0,1,0,1],[1,1,0,1,0]]
	p_c = MakeP_Cycle(GMatrix)
	print('result:')
	print(p_c)

Итак, начнём с самого первого и простого кусочка - это генерация тестовой матрицы. Дело в том, что код работает с матрицой смежности, а так как у меня невзвешанный двунаправленный граф, то надо сгенерировать симметричную матрицу смежности с нулями на главное диагонали.
Итак, вот эта функция:

#-----------------------------------------------------------------------------
def takeTestMatrix(n=10):
	GMatrix = [[(random.randint(0, 1) if j != i else 0)
				for j in range(i + 1)] for i in range(n)]  #этим одним выражением формируем нижнетреугольную матрицу
	for i in range(n):                               #а эти циклы служат для дублирования элементов и формирования
		for j in range(i + 1, n):                      # полной квадратной матрицы
			GMatrix[i].append(GMatrix[j][i])        #просто добавляя элементы
	return GMatrix
#-----------------------------------------------------------------------------

Рассмотрим чуть подробнее выражение генерации нижнетреугольной матрицы, оно мне нравится за лаконичность и показыает удобство использование генераторов списков:

	GMatrix = [[(random.randint(0, 1) if j != i else 0)
				for j in range(i + 1)] for i in range(n)]

Здесь два вложенных генератора списков, внешний генерирует список списков, и перебирает строки от 0 до n, а внутренний генерирует уже внутренние списки - как бы строки, из случайных числе, выбраных из двух вариков - 1 или 0, при этом проверяется, если это диагональ, то ставится 0.

Далее, сама функция нахождения p-cycl-ов:

#-----------------------------------------------------------------------------
def MakeP_Cycle(GMatrix):
	"""functioin to search p_cycle in graf
	parametr GMatrix - target graf - it should be matrix nXn
	function returns list of nodes witch contains p_cycle or take interrapt"""
	N = len(GMatrix)                               #храним количество узлов
	FreeNodesSet = {i for i in range(N)}   #создаём и инициализируем с помощью генератора множеств множество
                                                                      #неиспользованных узлов
	TransNodedSet = set()                          #множество трансграничных узлов
	p_cycle = []                                           #список, п-цикл
	#take first node
	FirstNode = random.randint(0, N - 1)   #выбираем случайный стартовый узел
	FreeNodesSet.remove(FirstNode)         #соотвественно изменяем наши множества
	p_cycle.append(FirstNode)

	#-----------------------------------------
	def EndingFind(currentNode):                #функция окончания поиска, пока можно её порсто порсмотреть
		"""
		check, if current node have direct link to first node
		- then return ending p-cycle,
		else it check if node have link to first node throught transnode
		- return p-cycle,
		else return FIND_ERROR interrupt
		"""
		if GMatrix[currentNode][FirstNode] == 1:
			p_cycle.append(FirstNode)
			return
		buf1 = {node for node in range(N) if GMatrix[FirstNode][node] == 1}
		buf2 = {node for node in range(N) if GMatrix[currentNode][node] == 1}
		if buf1 & buf2:
			buf = buf1 & buf2
			currentNode = [node for node in buf if node in TransNodedSet]
			if currentNode:
				p_cycle.append(currentNode[0])
				p_cycle.append(FirstNode)
				TransNodedSet.remove(currentNode[0])
				return
			else:
				p_cycle.append('ERROR')
		return

	#-----------------------------------------
	def addNextNode(currentNode):  # самая главная рекурсивная функция, её объвление, обсудим позже
		"""find and add to p_cycle next node"""
		print('call addNextNode')
		CurrLikeNodes = [node for node in range(N) if
						(node in FreeNodesSet) & (GMatrix[currentNode][node]
						== 1)]
		if len(CurrLikeNodes) == 0:
			EndingFind(currentNode)
			return
		for i in range(len(CurrLikeNodes)):
			takenTransNode = CurrLikeNodes[i]  # take node wich can be transnode
			#find nodes has direct link to takentransnode and sit in CurrLikeNodes list:
			buf = [node for node in CurrLikeNodes if (node !=
					takenTransNode) & (GMatrix[node][takenTransNode] == 1)]
			if buf:
				FreeNodesSet.remove(takenTransNode)
				TransNodedSet.add(takenTransNode)
				currentNode = random.choice(buf)
				FreeNodesSet.remove(currentNode)
				p_cycle.append(currentNode)
				print('--debug: ', currentNode)
				break
		else:
			currentNode = random.choice(CurrLikeNodes)
			FreeNodesSet.remove(currentNode)
			p_cycle.append(currentNode)
			print('--debug: ', currentNode)
		addNextNode(currentNode)
	#-----------------------------------------
	addNextNode(FirstNode) # вызов главной рекурсивной функции, что мы выше объявили, для первого узла.
	print('--debug: ', FreeNodesSet)
	print('--debug: ', TransNodedSet)
	print('--debug: p-cycle  ', p_cycle)
	return (p_cycle, FreeNodesSet, TransNodedSet)
#------------------------------------------------------------------------------

Итак, в общем ознакомившесь с кодом алгоритма приступим к разбору основной рекурсивной функции:
Она ищет следующий подходящий узел для включения в п-цикл. Листинг ниже.

  • Сначала она создаёт список узлов смежных с исходным (исходный передаётся в аргументе) - это список CurrLikeNodes. Посмотрите, опеть сделано одним генератором списков - одна строчка, на С++ убил бы много тараканов на клавиатуре))) Тут просто, перебираем все номера узлов и включаем их в список если эти номера есть в множестве свободных (незадействованных) узлов, а также они смежны с исходным
  • Потом, учитываем что список дальнейших подходящих узлов может быть пуст и тогда нам нужно вызвать процедуру корректного завершени работы алгоритма. Но не в этом соль, потом помечаем один первый узел как трансграничный и смотим, а правда ли это, если ли ещё узлы которые смежны как с нашим исходным так и с помеченныйм (опять генерируем список), и если этот список не пуст, то преобразуем наши множества и случайным образом выбираем следующий узел из списка подходящих.
    -- Обратите внимание, что если мы нашли трансграничные узлы, то цикл for завершится с помощью команды break - это значит, что следующий блок else, относящийся к for не будет выполняться, а если мы не найдём трансграничных узлов, то в этом случае цикл завершится щтатно и блок else выполнится. Таким образом без всяких флагов и проверок, лаконично определили. бы ли ли найдены трансграничные узлы и соотвествено отреагировали.
  • Потом вызываем нашу рекурентную функцию уже для свежевыбранного исходного узла.

А теперь листинг данной функции:

#-----------------------------------------
	def addNextNode(currentNode):
		"""find and add to p_cycle next node"""
		print('call addNextNode')
		CurrLikeNodes = [node for node in range(N) if                
						(node in FreeNodesSet) & (GMatrix[currentNode][node]
						== 1)]                        # формируем список смежных узлов исходному
		if len(CurrLikeNodes) == 0:                     #если список удовлетворяемых узлов пуст
			EndingFind(currentNode)                 #вызываем функцию завершения (о ней потом)
			return
		for i in range(len(CurrLikeNodes)):          #ищем трансграничные узлы
			takenTransNode = CurrLikeNodes[i]  # take node wich can be transnode
			#find nodes has direct link to takentransnode and sit in CurrLikeNodes list:
			buf = [node for node in CurrLikeNodes if (node !=
					takenTransNode) & (GMatrix[node][takenTransNode] == 1)] #смотрим список узлов, которые делают
                                                                       #вышевыбранный трансграничным
			if buf:                                      #если список не пуст значит: 
				FreeNodesSet.remove(takenTransNode)              #выбранный трансграничным узел действительно такой
                                                                       #вырезаем его из множества свободных
				TransNodedSet.add(takenTransNode)                   #помещаем в множество трансграничных
				currentNode = random.choice(buf)                       #случайно выбираем следующий исходный узел
				FreeNodesSet.remove(currentNode)                     #ну и добавляем в п-цикл с вырезание из свободных
				p_cycle.append(currentNode)
				print('--debug: ', currentNode)
				break
		else:          #этот else относится к циклу
			currentNode = random.choice(CurrLikeNodes)         #если не нашли трансграничных, тупо берём из списка смеж.
			FreeNodesSet.remove(currentNode)
			p_cycle.append(currentNode)
			print('--debug: ', currentNode)
		addNextNode(currentNode)                        #рекурсивный вызов
	#-----------------------------------------

Функция завершения не представляет особого интереса, она просто выполняет проверку того, что п-цикл получился замкнут и замыкает его, либо ищет как его замкнуть использую трансграничные узлы))) Вот реализация поиска узла, через которого можно замкнуть п-цикл довольно интересна, она создаёт множества и оперирует ими, и код получился очень маленький, а по сути он совершает большую работу. Для реализации этого на C++ я бы убил не просто тучу жуков на клавиатуре, а целое полчище)))
--
Just Fun!!!