การค้นหารอบทั้งหมดในกราฟกำกับ


198

ฉันจะค้นหา (วนซ้ำ) รอบทั้งหมดในกราฟที่มีทิศทางจาก / ไปยังโหนดที่ระบุได้อย่างไร

ตัวอย่างเช่นฉันต้องการสิ่งนี้:

A->B->A
A->B->C->A

แต่ไม่ใช่: B-> C-> B


1
ฉันทำการบ้าน me.utexas.edu/~bard/IP/Handouts/cycles.pdfไม่ได้ว่ามันไม่ได้เป็นคำถามที่ถูกต้อง :)
ShuggyCoUk

5
โปรดทราบว่านี่เป็นอย่างน้อย NP Hard อาจเป็นไปได้ PSPACE ฉันต้องคิดเกี่ยวกับมัน แต่มันเร็วเกินไปในตอนเช้าสำหรับทฤษฎีความซับซ้อน B-)
Brian Postow

2
หากกราฟอินพุทของคุณมี v จุดยอดและขอบ e แสดงว่ามี 2 ^ (e - v +1) -1 รอบที่แตกต่างกัน (แม้ว่าไม่ใช่ทั้งหมดอาจเป็นรอบที่เรียบง่าย) ค่อนข้างมาก - คุณอาจไม่ต้องการเขียนทั้งหมดอย่างชัดเจน นอกจากนี้เนื่องจากขนาดเอาต์พุตเป็นเลขชี้กำลังความซับซ้อนของอัลกอริทึมจึงไม่สามารถเป็นพหุนามได้ ฉันคิดว่ายังคงไม่มีคำตอบสำหรับคำถามนี้
CygnusX1

1
ตัวเลือกที่ดีที่สุดของฉันสำหรับฉันคือ: personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/GraphAlgor/ …
Melsi

คำตอบ:


105

ฉันพบหน้านี้ในการค้นหาของฉันและเนื่องจากรอบไม่เหมือนกับส่วนประกอบที่เชื่อมต่ออย่างมากฉันจึงทำการค้นหาและในที่สุดฉันก็พบว่าอัลกอริทึมที่มีประสิทธิภาพซึ่งแสดงรายการทั้งหมด (ระดับประถมศึกษา) ของกราฟกำกับ มันมาจาก Donald B. Johnson และกระดาษสามารถพบได้ในลิงค์ต่อไปนี้:

http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF

การใช้งานจาวาสามารถพบได้ใน:

http://normalisiert.de/code/java/elementaryCycles.zip

การสาธิตทางคณิตศาสตร์ของอัลกอริทึมของจอห์นสันสามารถพบได้ที่นี่สามารถดาวน์โหลดการใช้งานได้จากด้านขวา ( "ดาวน์โหลดรหัสผู้แต่ง" )

หมายเหตุ: จริงๆแล้วมีอัลกอริทึมมากมายสำหรับปัญหานี้ บางส่วนของพวกเขาอยู่ในบทความนี้:

http://dx.doi.org/10.1137/0205007

อ้างอิงจากบทความอัลกอรึทึมของจอห์นสันนั้นเร็วที่สุด


1
ฉันพบว่ามันยุ่งยากในการติดตั้งจากกระดาษและท้ายที่สุด aglorithm นี้ยังต้องใช้ Tarjan และจาวาโค้ดก็น่าเกลียดเหมือนกัน :(
Gleno

7
@Gleno ดีถ้าคุณหมายความว่าคุณสามารถใช้ Tarjan เพื่อหารอบทั้งหมดในกราฟแทนที่จะใช้ส่วนที่เหลือคุณผิด ที่นี่คุณสามารถเห็นความแตกต่างระหว่างส่วนประกอบที่เชื่อมต่ออย่างมากกับวงจรทั้งหมด (วงจร cd cd และ gh จะไม่ถูกส่งคืนโดย alg ของ Tarjan) (@ batbrat คำตอบของความสับสนของคุณยังถูกซ่อนอยู่ที่นี่: รอบที่เป็นไปได้ทั้งหมด alg ดังนั้นความซับซ้อนอาจน้อยกว่าเลขชี้กำลัง) Java-Code น่าจะดีกว่านี้ แต่มันช่วยฉันในการลงมือทำจากกระดาษ
eminsenay

4
คำตอบนี้ดีกว่าคำตอบที่เลือก ฉันพยายามอย่างหนึ่งพยายามหาวิธีที่จะทำให้วงจรง่าย ๆ ทั้งหมดจากส่วนประกอบที่เชื่อมต่ออย่างยิ่ง ปรากฎว่าสิ่งนี้ไม่สำคัญ กระดาษโดย Johnson มีอัลกอริทึมที่ดี แต่เป็นการยากที่จะลุย ฉันดูการใช้งานจาวาและรีดเองใน Matlab รหัสที่มีอยู่ในgist.github.com/1260153
codehippo

5
@moteutsch: บางทีฉันอาจจะพลาดบางสิ่งบางอย่าง แต่ตามกระดาษจอห์นสัน (และแหล่งอื่น ๆ ) รอบเป็นประถมถ้าไม่มีจุดสุดยอด (นอกเหนือจากจุดเริ่มต้น / เสร็จสิ้น) ปรากฏมากกว่าหนึ่งครั้ง ตามคำนิยามนั้นแล้วA->B->C->Aประถมไม่ใช่หรือ?
psmears

9
หมายเหตุสำหรับทุกคนที่ใช้ python สำหรับสิ่งนี้: อัลกอริทึมของ Johnson ได้รับการติดตั้งsimple_cycleใน networkx
Joel

35

การค้นหาความลึกครั้งแรกด้วยการย้อนรอยควรทำงานที่นี่ เก็บอาร์เรย์ของค่าบูลีนเพื่อติดตามว่าคุณเคยเข้าชมโหนดมาก่อนหรือไม่ หากคุณไม่มีโหนดใหม่ให้ไปที่ (โดยไม่ต้องกดปุ่มที่คุณเคยไปแล้ว) ให้ลองย้อนกลับและลองสาขาอื่น

DFS นั้นง่ายต่อการนำไปใช้หากคุณมีรายการ adjacency เพื่อแสดงกราฟ ตัวอย่างเช่น adj [A] = {B, C} ระบุว่า B และ C เป็นลูกของ A

ตัวอย่างเช่นหลอกรหัสด้านล่าง "start" เป็นโหนดที่คุณเริ่มต้น

dfs(adj,node,visited):  
  if (visited[node]):  
    if (node == start):  
      "found a path"  
    return;  
  visited[node]=YES;  
  for child in adj[node]:  
    dfs(adj,child,visited)
  visited[node]=NO;

เรียกใช้ฟังก์ชันข้างต้นด้วยโหนดเริ่มต้น:

visited = {}
dfs(adj,start,visited)

2
ขอบคุณ ฉันชอบวิธีการนี้กับคนอื่น ๆ ที่กล่าวถึงในที่นี้เนื่องจากเป็นเรื่องง่าย (r) ที่จะเข้าใจและมีความซับซ้อนของเวลาที่สมเหตุสมผลแม้ว่าอาจจะไม่เหมาะสมก็ตาม
redcalx

1
สิ่งนี้จะหารอบทั้งหมดได้อย่างไร
พายุสมอง

3
if (node == start): - สิ่งที่อยู่node and startในสายแรก
พายุสมอง

2
@ user1988876 สิ่งนี้จะปรากฏขึ้นเพื่อค้นหารอบทั้งหมดที่เกี่ยวข้องกับจุดสุดยอดที่กำหนด (ซึ่งจะเป็นstart) มันเริ่มต้นที่จุดสุดยอดนั้นและทำ DFS จนกว่ามันจะกลับไปที่จุดยอดนั้นอีกครั้งจากนั้นก็รู้ว่ามันพบวัฏจักร แต่มันไม่ได้ออกรอบจริง ๆ เพียงแค่นับพวกมัน (แต่การดัดแปลงเพื่อทำอย่างนั้นแทนที่จะไม่ยากเกินไป)
แบร์นฮาร์ดบาร์

1
@ user1988876 ทีนี้มันก็แค่พิมพ์ "พบเส้นทาง" จำนวนครั้งเท่ากับจำนวนรอบที่ค้นพบ (ซึ่งสามารถแทนที่ได้อย่างง่ายดายด้วยการนับ) startใช่มันจะตรวจสอบรอบเฉพาะจาก visited[node]=NO;คุณไม่จำเป็นจริงๆที่จะล้างธงเยือนเป็นแต่ละธงเยี่ยมชมจะถูกลบเนื่องจาก แต่โปรดจำไว้ว่าหากคุณมีวัฏจักรA->B->C->Aคุณจะตรวจจับได้ 3 ครั้งซึ่งstartสามารถเป็น 3 อย่างใดอย่างหนึ่ง แนวคิดหนึ่งที่จะป้องกันไม่ให้เกิดขึ้นคือมีอาร์เรย์ที่เข้าเยี่ยมชมอีกจุดหนึ่งซึ่งทุกโหนดที่เคยเป็นstartโหนดในบางจุดถูกตั้งค่าไว้
แบร์นฮาร์ดบาร์

23

ก่อนอื่น - คุณไม่ต้องการลองหาวัฏจักรจริงอย่างแท้จริงเพราะถ้ามี 1 ก็จะมีจำนวนอนันต์ ตัวอย่างเช่น ABA, ABABA เป็นต้นหรืออาจเป็นไปได้ที่จะรวมกัน 2 รอบเป็น 8 รอบเหมือน ฯลฯ ฯลฯ ... แนวทางที่มีความหมายคือการมองหาวัฏจักรธรรมดา ๆ ที่เรียกว่าวัฏจักรธรรมดา - สิ่งที่ไม่ข้ามตัวเองยกเว้น ในจุดเริ่มต้น / สิ้นสุด จากนั้นถ้าคุณต้องการคุณสามารถสร้างชุดของรอบง่าย

หนึ่งในอัลกอริธึมพื้นฐานสำหรับการค้นหาวัฏจักรง่าย ๆ ทั้งหมดในกราฟกำกับคือ: ทำการสำรวจเส้นทางแรกที่มีความลึกของเส้นทางที่เรียบง่ายทั้งหมด (ที่ไม่ข้ามตัวเอง) ในกราฟ ทุกครั้งที่โหนดปัจจุบันมีตัวตายตัวแทนบนสแต็กจะพบวัฏจักรอย่างง่าย ประกอบด้วยองค์ประกอบบนสแต็กเริ่มต้นด้วยตัวตายตัวแทนที่ระบุและลงท้ายด้วยส่วนบนสุดของสแต็ก การสำรวจเส้นทางแรกที่มีความลึกของเส้นทางแบบง่าย ๆ ทั้งหมดจะคล้ายกับการค้นหาความลึกครั้งแรก แต่คุณไม่ได้ทำเครื่องหมาย / บันทึกการเยี่ยมชมโหนดอื่น ๆ นอกเหนือจากที่อยู่ในสแต็กเป็นจุดหยุด

อัลกอริทึมแรงเดรัจฉานด้านบนไม่มีประสิทธิภาพอย่างมากและนอกจากนั้นยังสร้างสำเนาหลายรอบ อย่างไรก็ตามมันเป็นจุดเริ่มต้นของอัลกอริธึมเชิงปฏิบัติหลายอย่างที่ใช้การปรับปรุงต่าง ๆ เพื่อปรับปรุงประสิทธิภาพและหลีกเลี่ยงการทำซ้ำวงจร ฉันรู้สึกประหลาดใจที่รู้เมื่อไม่นานมานี้ว่าอัลกอริธึมเหล่านี้ไม่พร้อมใช้งานในตำราและบนเว็บ ดังนั้นผมจึงได้บางวิจัยและดำเนินการ 4 ขั้นตอนวิธีการดังกล่าวและ 1 อัลกอริทึมสำหรับรอบในกราฟไม่มีทิศทางในโอเพนซอร์ส Java ห้องสมุดที่นี่: http://code.google.com/p/niographs/

BTW เนื่องจากฉันพูดถึงกราฟที่ไม่ได้บอกทิศทาง: อัลกอริทึมสำหรับสิ่งนั้นแตกต่างกัน สร้างต้นไม้ที่ทอดแล้วขอบที่ไม่ได้เป็นส่วนหนึ่งของต้นไม้เป็นวัฏจักรที่เรียบง่ายพร้อมกับขอบบางส่วนในต้นไม้ รอบพบวิธีนี้ในรูปแบบฐานที่เรียกว่า สามารถพบวัฏจักรธรรมดาทั้งหมดได้โดยรวมวงจรฐานตั้งแต่ 2 รอบขึ้นไป ดูรายละเอียดเพิ่มเติมเช่นนี้: http://dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf


เป็นตัวอย่างวิธีการใช้jgraphtที่ใช้ในhttp://code.google.com/p/niographs/คุณสามารถรับตัวอย่างจากgithub.com/jgrapht/jgrapht/wiki/DirectedGraphDemo
Vishrant

19

ทางเลือกที่ง่ายที่สุดผมพบว่าการแก้ปัญหานี้ได้รับการใช้ lib networkxหลามที่เรียกว่า

มันใช้อัลกอริทึมของจอห์นสันที่กล่าวถึงในคำตอบที่ดีที่สุดของคำถามนี้ แต่มันค่อนข้างง่ายที่จะดำเนินการ

ในระยะสั้นคุณต้องต่อไปนี้:

import networkx as nx
import matplotlib.pyplot as plt

# Create Directed Graph
G=nx.DiGraph()

# Add a list of nodes:
G.add_nodes_from(["a","b","c","d","e"])

# Add a list of edges:
G.add_edges_from([("a","b"),("b","c"), ("c","a"), ("b","d"), ("d","e"), ("e","a")])

#Return a list of cycles described as a list o nodes
list(nx.simple_cycles(G))

คำตอบ: [['a', 'b', 'd', 'e'], ['a', 'b', 'c']]

ป้อนคำอธิบายรูปภาพที่นี่


1
นอกจากนี้คุณยังสามารถควบคุมพจนานุกรมไปยังกราฟเครือข่าย:nx.DiGraph({'a': ['b'], 'b': ['c','d'], 'c': ['a'], 'd': ['e'], 'e':['a']})
Luke Miles

ฉันจะระบุจุดเริ่มต้นได้อย่างไร
nosense

5

เพื่อชี้แจง:

  1. คอมโพเนนต์ที่เชื่อมต่ออย่างแน่นหนาจะค้นหากราฟย่อยทั้งหมดที่มีรอบอย่างน้อยหนึ่งรอบไม่ใช่รอบที่เป็นไปได้ทั้งหมดในกราฟ เช่นถ้าคุณใช้ส่วนประกอบที่เชื่อมต่ออย่างยิ่งทั้งหมดและยุบ / กลุ่ม / รวมแต่ละองค์ประกอบเข้าด้วยกันเป็นหนึ่งโหนด (เช่นโหนดต่อองค์ประกอบ) คุณจะได้รับต้นไม้ที่ไม่มีวงจร (DAG จริง) แต่ละองค์ประกอบ (ซึ่งโดยทั่วไปจะมีกราฟย่อยที่มีอย่างน้อยหนึ่งรอบในนั้น) สามารถมีรอบที่เป็นไปได้มากมายภายในดังนั้น SCC จะไม่พบรอบที่เป็นไปได้ทั้งหมดมันจะค้นหากลุ่มที่เป็นไปได้ทั้งหมดที่มีรอบอย่างน้อยหนึ่งรอบ พวกเขาแล้วกราฟจะไม่มีรอบ

  2. เพื่อหาวัฏจักรอย่างง่ายทั้งหมดในกราฟดังที่คนอื่น ๆ กล่าวถึงอัลกอริทึมของจอห์นสันเป็นตัวเลือก


3

ฉันได้รับนี่เป็นคำถามสัมภาษณ์หนึ่งครั้งฉันสงสัยว่าสิ่งนี้ได้เกิดขึ้นกับคุณและคุณมาที่นี่เพื่อขอความช่วยเหลือ แบ่งปัญหาออกเป็นสามคำถามและง่ายขึ้น

  1. คุณจะกำหนดเส้นทางที่ถูกต้องต่อไปได้อย่างไร
  2. คุณจะทราบได้อย่างไรว่ามีการใช้จุด
  3. คุณจะหลีกเลี่ยงการข้ามผ่านจุดเดิมได้อย่างไร

ปัญหา 1) ใช้รูปแบบตัววนซ้ำเพื่อกำหนดวิธีการวนซ้ำผลลัพธ์เส้นทาง สถานที่ที่ดีในการวางตรรกะเพื่อให้ได้เส้นทางถัดไปน่าจะเป็น "moveNext" ของตัววนซ้ำของคุณ เพื่อหาเส้นทางที่ถูกต้องมันขึ้นอยู่กับโครงสร้างข้อมูลของคุณ สำหรับฉันมันเป็นตาราง sql ที่เต็มไปด้วยความเป็นไปได้ของเส้นทางที่ถูกต้องดังนั้นฉันจึงต้องสร้างแบบสอบถามเพื่อให้ได้ปลายทางที่ถูกต้องตามแหล่งที่มา

ปัญหาที่ 2) ผลักดันแต่ละโหนดเมื่อคุณพบพวกมันลงในคอลเลกชั่นที่คุณได้รับซึ่งหมายความว่าคุณสามารถดูว่าคุณ "ย้อนกลับ" สองจุดผ่านจุดที่ง่ายมากโดยการซักถามคอลเลกชันที่คุณกำลังสร้าง

ปัญหา 3) หาก ณ จุดใดที่คุณเห็นว่าคุณกำลังเพิ่มกลับเป็นสองเท่าคุณสามารถเปิดสิ่งต่าง ๆ ออกจากคอลเลกชันและ "สำรอง" จากนั้นลองพยายาม "ก้าวไปข้างหน้า" อีกครั้ง

แฮ็ค: ถ้าคุณใช้ Sql Server 2008 มี "ลำดับชั้น" ใหม่บางอย่างที่คุณสามารถใช้เพื่อแก้ไขได้อย่างรวดเร็วหากคุณจัดโครงสร้างข้อมูลของคุณในทรี


3

ตัวแปรที่ใช้ DFS ที่มีขอบด้านหลังจะพบวัฏจักรจริง ๆ แต่ในหลาย ๆ กรณีมันจะไม่เป็นวัฏจักรขั้นต่ำสุด โดยทั่วไป DFS จะให้คุณตั้งค่าสถานะว่ามีรอบ แต่มันไม่ดีพอที่จะหารอบจริง ตัวอย่างเช่นลองนึกภาพ 5 รอบที่ต่างกันที่แชร์สองขอบ ไม่มีวิธีง่ายๆในการระบุรอบโดยใช้เพียง DFS (รวมถึงตัวแปรย้อนรอย)

อัลกอริธึมของ Johnson นั้นมอบวัฏจักรเรียบง่ายที่เป็นเอกลักษณ์และมีความซับซ้อนของเวลาและพื้นที่

แต่ถ้าคุณต้องการหารอบน้อยที่สุด (หมายความว่าอาจมีมากกว่าหนึ่งรอบผ่านจุดสุดยอดใด ๆ และเราสนใจที่จะหารอบน้อยที่สุด) และกราฟของคุณไม่ใหญ่มากคุณสามารถลองใช้วิธีง่าย ๆ ด้านล่าง มันง่ายมาก แต่ค่อนข้างช้าเมื่อเทียบกับของจอห์นสัน

ดังนั้นหนึ่งในอย่างวิธีที่ง่ายที่สุดที่จะหารอบน้อยที่สุดคือการใช้อัลกอริทึมของฟลอยด์เพื่อหาเส้นทางที่น้อยที่สุดระหว่างจุดทั้งหมดที่ใช้ถ้อยคำเมทริกซ์ อัลกอริธึมนี้ไม่มีที่ใดใกล้เคียงกับของ Johnson แต่มันง่ายมากและลูปด้านในของมันแน่นจนกราฟขนาดเล็ก (<= 50-100 โหนด) มันสมเหตุสมผลอย่างยิ่งที่จะใช้มัน ความซับซ้อนของเวลาคือ O (n ^ 3), ความซับซ้อนของพื้นที่ O (n ^ 2) หากคุณใช้การติดตามผู้ปกครองและ O (1) หากคุณไม่ต้องการ ก่อนอื่นเรามาหาคำตอบของคำถามว่ามีวัฏจักรอยู่หรือเปล่า อัลกอริทึมนั้นตายง่าย ด้านล่างเป็นตัวอย่างข้อมูลใน Scala

  val NO_EDGE = Integer.MAX_VALUE / 2

  def shortestPath(weights: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        weights(i)(j) = throughK
      }
    }
  }

แต่เดิมอัลกอริทึมนี้ทำงานบนกราฟถ่วงน้ำหนักเพื่อหาเส้นทางที่สั้นที่สุดระหว่างโหนดทั้งหมดคู่ เพื่อให้ทำงานได้อย่างถูกต้องคุณต้องระบุ 1 หากมีขอบกำกับระหว่างโหนดหรือ NO_EDGE เป็นอย่างอื่น หลังจากรันอัลกอริทึมแล้วคุณสามารถตรวจสอบเส้นทแยงมุมหลักได้หากมีค่าน้อยกว่า NO_EDGE กว่าโหนดนี้มีส่วนร่วมในรอบความยาวเท่ากับค่า โหนดอื่น ๆ ในวัฏจักรเดียวกันจะมีค่าเท่ากัน (บนเส้นทแยงมุมหลัก)

ในการสร้างวงจรใหม่เราจำเป็นต้องใช้อัลกอริธึมเวอร์ชันดัดแปลงเล็กน้อยกับการติดตามผู้ปกครอง

  def shortestPath(weights: Array[Array[Int]], parents: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        parents(i)(j) = k
        weights(i)(j) = throughK
      }
    }
  }

ผู้ปกครองเมทริกซ์เริ่มแรกควรมีดัชนีจุดสุดยอดแหล่งที่มาในเซลล์ขอบถ้ามีขอบระหว่างจุดยอดและ -1 อย่างอื่น หลังจากฟังก์ชันคืนค่าขอบแต่ละอันคุณจะมีการอ้างอิงไปยังโหนดพาเรนต์ในแผนผังพา ธ ที่สั้นที่สุด แล้วมันก็กู้คืนรอบจริงได้ง่าย

สรุปเรามีโปรแกรมดังต่อไปนี้เพื่อค้นหารอบน้อยที่สุด

  val NO_EDGE = Integer.MAX_VALUE / 2;

  def shortestPathWithParentTracking(
         weights: Array[Array[Int]],
         parents: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        parents(i)(j) = parents(i)(k)
        weights(i)(j) = throughK
      }
    }
  }

  def recoverCycles(
         cycleNodes: Seq[Int], 
         parents: Array[Array[Int]]): Set[Seq[Int]] = {
    val res = new mutable.HashSet[Seq[Int]]()
    for (node <- cycleNodes) {
      var cycle = new mutable.ArrayBuffer[Int]()
      cycle += node
      var other = parents(node)(node)
      do {
        cycle += other
        other = parents(other)(node)
      } while(other != node)
      res += cycle.sorted
    }
    res.toSet
  }

และวิธีการหลักขนาดเล็กเพียงเพื่อทดสอบผลลัพธ์

  def main(args: Array[String]): Unit = {
    val n = 3
    val weights = Array(Array(NO_EDGE, 1, NO_EDGE), Array(NO_EDGE, NO_EDGE, 1), Array(1, NO_EDGE, NO_EDGE))
    val parents = Array(Array(-1, 1, -1), Array(-1, -1, 2), Array(0, -1, -1))
    shortestPathWithParentTracking(weights, parents)
    val cycleNodes = parents.indices.filter(i => parents(i)(i) < NO_EDGE)
    val cycles: Set[Seq[Int]] = recoverCycles(cycleNodes, parents)
    println("The following minimal cycle found:")
    cycles.foreach(c => println(c.mkString))
    println(s"Total: ${cycles.size} cycle found")
  }

และผลลัพธ์คือ

The following minimal cycle found:
012
Total: 1 cycle found

2

ในกรณีของกราฟที่ไม่ได้บอกทิศทางกระดาษที่ตีพิมพ์เมื่อเร็ว ๆ นี้ ( รายการที่เหมาะสมของวัฏจักรและ st-path ในกราฟที่ไม่ได้บอกทิศทาง ) เสนอวิธีแก้ปัญหาที่เหมาะสมที่สุดแบบ asymptotically คุณสามารถอ่านได้ที่นี่http://arxiv.org/abs/1205.2766หรือที่นี่http://dl.acm.org/citation.cfm?id=2627951 ฉันรู้ว่ามันไม่ได้ตอบคำถามของคุณ แต่เนื่องจากชื่อของ คำถามของคุณไม่ได้กล่าวถึงทิศทาง แต่อาจเป็นประโยชน์สำหรับการค้นหาโดย Google


1

เริ่มต้นที่โหนด X และตรวจสอบโหนดชายด์ทั้งหมด (พาเรนต์และโหนดชายน์เทียบเท่ากันหากไม่ถูกกำหนดทิศทาง) ทำเครื่องหมายโหนดลูกเหล่านั้นว่าเป็นลูกของ X จากโหนดลูกใด ๆ เช่นนั้นทำเครื่องหมายว่าเป็นลูกของการเป็นลูกของ A, X 'โดยที่ X' ถูกทำเครื่องหมายว่าอยู่ห่างออกไป 2 ก้าว) หากคุณกด X ในภายหลังและทำเครื่องหมายว่าเป็นลูกของ X '' นั่นหมายความว่า X อยู่ในรอบ 3 โหนด การย้อนรอยไปสู่ผู้ปกครองนั้นเป็นเรื่องง่าย (ตามที่เป็นอยู่อัลกอริทึมไม่สนับสนุนสิ่งนี้ดังนั้นคุณจะพบว่าผู้ปกครองคนใดที่มี X ')

หมายเหตุ: หากกราฟไม่ได้ถูกบอกทิศทางหรือมีขอบสองทิศทางอัลกอริทึมนี้จะซับซ้อนมากขึ้นโดยสมมติว่าคุณไม่ต้องการข้ามขอบเดียวกันสองรอบสำหรับรอบ


1

หากสิ่งที่คุณต้องการคือการหาวงจรระดับประถมศึกษาทั้งหมดในกราฟคุณสามารถใช้อัลกอริทึม EC โดย JAMES C. TIERNAN ที่พบบนกระดาษตั้งแต่ปี 1970

เดิมมากขั้นตอนวิธี EC เป็นฉันจัดการเพื่อใช้มันใน PHP (ความหวังมีความผิดพลาดไม่ได้แสดงให้เห็นด้านล่าง) มันสามารถหาลูปได้เช่นกันหากมี วงจรในการนำไปใช้นี้ (ที่พยายามโคลนต้นฉบับ) เป็นองค์ประกอบที่ไม่ใช่ศูนย์ ศูนย์ที่นี่หมายถึงการไม่มีตัวตน (เป็นโมฆะอย่างที่เรารู้)

นอกเหนือจากด้านล่างแล้วยังมีการใช้งานอื่น ๆ ที่ให้อัลกอริธึมอิสระมากขึ้นซึ่งหมายความว่าโหนดสามารถเริ่มจากที่ใดก็ได้แม้จากจำนวนลบเช่น -4, -3, -2, .. เป็นต้น

ในทั้งสองกรณีจำเป็นต้องใช้โหนดนั้นเรียงลำดับ

คุณอาจต้องศึกษาบทความต้นฉบับอัลกอริทึมวงจรเบื้องต้นของเจมส์ซีเทียร์นัน

<?php
echo  "<pre><br><br>";

$G = array(
        1=>array(1,2,3),
        2=>array(1,2,3),
        3=>array(1,2,3)
);


define('N',key(array_slice($G, -1, 1, true)));
$P = array(1=>0,2=>0,3=>0,4=>0,5=>0);
$H = array(1=>$P, 2=>$P, 3=>$P, 4=>$P, 5=>$P );
$k = 1;
$P[$k] = key($G);
$Circ = array();


#[Path Extension]
EC2_Path_Extension:
foreach($G[$P[$k]] as $j => $child ){
    if( $child>$P[1] and in_array($child, $P)===false and in_array($child, $H[$P[$k]])===false ){
    $k++;
    $P[$k] = $child;
    goto EC2_Path_Extension;
}   }

#[EC3 Circuit Confirmation]
if( in_array($P[1], $G[$P[$k]])===true ){//if PATH[1] is not child of PATH[current] then don't have a cycle
    $Circ[] = $P;
}

#[EC4 Vertex Closure]
if($k===1){
    goto EC5_Advance_Initial_Vertex;
}
//afou den ksana theoreitai einai asfales na svisoume
for( $m=1; $m<=N; $m++){//H[P[k], m] <- O, m = 1, 2, . . . , N
    if( $H[$P[$k-1]][$m]===0 ){
        $H[$P[$k-1]][$m]=$P[$k];
        break(1);
    }
}
for( $m=1; $m<=N; $m++ ){//H[P[k], m] <- O, m = 1, 2, . . . , N
    $H[$P[$k]][$m]=0;
}
$P[$k]=0;
$k--;
goto EC2_Path_Extension;

#[EC5 Advance Initial Vertex]
EC5_Advance_Initial_Vertex:
if($P[1] === N){
    goto EC6_Terminate;
}
$P[1]++;
$k=1;
$H=array(
        1=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        2=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        3=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        4=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        5=>array(1=>0,2=>0,3=>0,4=>0,5=>0)
);
goto EC2_Path_Extension;

#[EC5 Advance Initial Vertex]
EC6_Terminate:
print_r($Circ);
?>

แล้วนี่คือการใช้งานอื่น ๆ ที่เป็นอิสระมากขึ้นของกราฟโดยไม่ต้อง goto และไม่มีค่าอาร์เรย์ แต่จะใช้คีย์อาร์เรย์, เส้นทาง, กราฟและวงจรจะถูกเก็บไว้เป็นคีย์อาร์เรย์ (ใช้ค่าอาร์เรย์หากคุณต้องการเพียงแค่เปลี่ยนค่าที่ต้องการ บรรทัด) กราฟตัวอย่างเริ่มต้นจาก -4 เพื่อแสดงความเป็นอิสระ

<?php

$G = array(
        -4=>array(-4=>true,-3=>true,-2=>true),
        -3=>array(-4=>true,-3=>true,-2=>true),
        -2=>array(-4=>true,-3=>true,-2=>true)
);


$C = array();


EC($G,$C);
echo "<pre>";
print_r($C);
function EC($G, &$C){

    $CNST_not_closed =  false;                          // this flag indicates no closure
    $CNST_closed        = true;                         // this flag indicates closure
    // define the state where there is no closures for some node
    $tmp_first_node  =  key($G);                        // first node = first key
    $tmp_last_node  =   $tmp_first_node-1+count($G);    // last node  = last  key
    $CNST_closure_reset = array();
    for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
        $CNST_closure_reset[$k] = $CNST_not_closed;
    }
    // define the state where there is no closure for all nodes
    for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
        $H[$k] = $CNST_closure_reset;   // Key in the closure arrays represent nodes
    }
    unset($tmp_first_node);
    unset($tmp_last_node);


    # Start algorithm
    foreach($G as $init_node => $children){#[Jump to initial node set]
        #[Initial Node Set]
        $P = array();                   // declare at starup, remove the old $init_node from path on loop
        $P[$init_node]=true;            // the first key in P is always the new initial node
        $k=$init_node;                  // update the current node
                                        // On loop H[old_init_node] is not cleared cause is never checked again
        do{#Path 1,3,7,4 jump here to extend father 7
            do{#Path from 1,3,8,5 became 2,4,8,5,6 jump here to extend child 6
                $new_expansion = false;
                foreach( $G[$k] as $child => $foo ){#Consider each child of 7 or 6
                    if( $child>$init_node and isset($P[$child])===false and $H[$k][$child]===$CNST_not_closed ){
                        $P[$child]=true;    // add this child to the path
                        $k = $child;        // update the current node
                        $new_expansion=true;// set the flag for expanding the child of k
                        break(1);           // we are done, one child at a time
            }   }   }while(($new_expansion===true));// Do while a new child has been added to the path

            # If the first node is child of the last we have a circuit
            if( isset($G[$k][$init_node])===true ){
                $C[] = $P;  // Leaving this out of closure will catch loops to
            }

            # Closure
            if($k>$init_node){                  //if k>init_node then alwaya count(P)>1, so proceed to closure
                $new_expansion=true;            // $new_expansion is never true, set true to expand father of k
                unset($P[$k]);                  // remove k from path
                end($P); $k_father = key($P);   // get father of k
                $H[$k_father][$k]=$CNST_closed; // mark k as closed
                $H[$k] = $CNST_closure_reset;   // reset k closure
                $k = $k_father;                 // update k
        }   } while($new_expansion===true);//if we don't wnter the if block m has the old k$k_father_old = $k;
        // Advance Initial Vertex Context
    }//foreach initial


}//function

?>

ฉันวิเคราะห์และบันทึก EC แล้ว แต่น่าเสียดายที่เอกสารเป็นภาษากรีก


1

มีสองขั้นตอน (อัลกอริทึม) ที่เกี่ยวข้องในการค้นหารอบทั้งหมดใน DAG

ขั้นตอนแรกคือการใช้อัลกอริทึมของ Tarjan เพื่อค้นหาชุดของส่วนประกอบที่เชื่อมต่ออย่างยิ่ง

  1. เริ่มจากจุดสุดยอดใดก็ได้
  2. DFS จากจุดสุดยอดนั้น สำหรับแต่ละโหนด x ให้เก็บสองตัวเลข dfs_index [x] และ dfs_lowval [x] dfs_index [x] เก็บเมื่อโหนดนั้นถูกเยี่ยมชมในขณะที่ dfs_lowval [x] = min (dfs_low [k]) โดยที่ k คือลูกทั้งหมดของ x ที่ไม่ใช่พาเรนต์โดยตรงของ x ในแผนผัง dfs-spanning
  3. โหนดทั้งหมดที่มี dfs_lowval [x] เดียวกันอยู่ในองค์ประกอบที่เชื่อมต่อกันอย่างยิ่งเดียวกัน

ขั้นตอนที่สองคือการค้นหารอบ (เส้นทาง) ภายในส่วนประกอบที่เชื่อมต่อ คำแนะนำของฉันคือใช้อัลกอริทึมของ Hierholzer รุ่นที่แก้ไข

ความคิดคือ:

  1. เลือกจุดสุดยอดเริ่มต้น v และตามรอยขอบจากจุดยอดนั้นจนกว่าคุณจะกลับไปที่ v คุณไม่สามารถติดที่จุดยอดใด ๆ ที่ไม่ใช่ v เนื่องจากระดับจุดยอดทั้งหมดทำให้แน่ใจว่าเมื่อเส้นทางเข้าสู่อีกทางหนึ่ง จุดยอด w จะต้องมีขอบที่ไม่ได้ใช้เหลืออยู่ การเดินทางที่เกิดขึ้นในลักษณะนี้เป็นการทัวร์แบบปิด แต่อาจไม่ครอบคลุมจุดยอดและขอบทั้งหมดของกราฟเริ่มต้น
  2. ตราบใดที่มีจุดยอด v ที่เป็นของทัวร์ปัจจุบัน แต่มีขอบที่อยู่ติดกันไม่ใช่ส่วนหนึ่งของทัวร์ให้เริ่มเส้นทางใหม่จาก v ตามขอบที่ไม่ได้ใช้จนกว่าคุณจะกลับไปที่ v และเข้าร่วมทัวร์ที่เกิดขึ้นในลักษณะนี้ ทัวร์ครั้งก่อน

นี่คือลิงค์สำหรับการนำ Java ไปใช้กับกรณีทดสอบ:

http://stones333.blogspot.com/2013/12/find-cycles-in-directed-graph-dag.html


16
วงจรมีอยู่ใน DAG (Directed Acyclic Graph) ได้อย่างไร
sky_coder123

นี่ไม่พบรอบทั้งหมด
Vishwa Ratna


0

ฉันสะดุดอัลกอริทึมต่อไปนี้ซึ่งดูเหมือนว่าจะมีประสิทธิภาพมากกว่าอัลกอริทึมของจอห์นสัน (อย่างน้อยสำหรับกราฟขนาดใหญ่) อย่างไรก็ตามฉันไม่แน่ใจเกี่ยวกับประสิทธิภาพของมันเทียบกับอัลกอริทึมของ Tarjan
นอกจากนี้ฉันตรวจสอบเพียงสามเหลี่ยม หากสนใจโปรดดูที่ "ขั้นตอนวิธีการขึ้นทะเบียนและ Subgraph" โดย Norishige Chiba และ Takao Nishizeki ( http://dx.doi.org/10.1137/0214017 )


0

โซลูชัน Javascript โดยใช้ disjoint ตั้งค่ารายการที่ลิงก์ สามารถอัปเกรดเป็น disjoint ตั้งค่าฟอเรสต์สำหรับเวลาที่เร็วขึ้น

var input = '5\nYYNNN\nYYYNN\nNYYNN\nNNNYN\nNNNNY'
console.log(input);
//above solution should be 3 because the components are
//{0,1,2}, because {0,1} and {1,2} therefore {0,1,2}
//{3}
//{4}

//MIT license, authored by Ling Qing Meng

//'4\nYYNN\nYYYN\nNYYN\nNNNY'

//Read Input, preformatting
var reformat = input.split(/\n/);
var N = reformat[0];
var adjMatrix = [];
for (var i = 1; i < reformat.length; i++) {
    adjMatrix.push(reformat[i]);
}

//for (each person x from 1 to N) CREATE-SET(x)
var sets = [];
for (var i = 0; i < N; i++) {
    var s = new LinkedList();
    s.add(i);
    sets.push(s);
}

//populate friend potentials using combinatorics, then filters
var people =  [];
var friends = [];
for (var i = 0; i < N; i++) {
    people.push(i);
}
var potentialFriends = k_combinations(people,2);
for (var i = 0; i < potentialFriends.length; i++){
    if (isFriend(adjMatrix,potentialFriends[i]) === 'Y'){
        friends.push(potentialFriends[i]);
    }
}


//for (each pair of friends (x y) ) if (FIND-SET(x) != FIND-SET(y)) MERGE-SETS(x, y)
for (var i = 0; i < friends.length; i++) {
    var x = friends[i][0];
    var y = friends[i][1];
    if (FindSet(x) != FindSet(y)) {
        sets.push(MergeSet(x,y));
    }
}


for (var i = 0; i < sets.length; i++) {
    //sets[i].traverse();
}
console.log('How many distinct connected components?',sets.length);



//Linked List data structures neccesary for above to work
function Node(){
    this.data = null;
    this.next = null;
}

function LinkedList(){
    this.head = null;
    this.tail = null;
    this.size = 0;

    // Add node to the end
    this.add = function(data){
        var node = new Node();
        node.data = data;
        if (this.head == null){
            this.head = node;
            this.tail = node;
        } else {
            this.tail.next = node;
            this.tail = node;
        }
        this.size++;
    };


    this.contains = function(data) {
        if (this.head.data === data) 
            return this;
        var next = this.head.next;
        while (next !== null) {
            if (next.data === data) {
                return this;
            }
            next = next.next;
        }
        return null;
    };

    this.traverse = function() {
        var current = this.head;
        var toPrint = '';
        while (current !== null) {
            //callback.call(this, current); put callback as an argument to top function
            toPrint += current.data.toString() + ' ';
            current = current.next; 
        }
        console.log('list data: ',toPrint);
    }

    this.merge = function(list) {
        var current = this.head;
        var next = current.next;
        while (next !== null) {
            current = next;
            next = next.next;
        }
        current.next = list.head;
        this.size += list.size;
        return this;
    };

    this.reverse = function() {
      if (this.head == null) 
        return;
      if (this.head.next == null) 
        return;

      var currentNode = this.head;
      var nextNode = this.head.next;
      var prevNode = this.head;
      this.head.next = null;
      while (nextNode != null) {
        currentNode = nextNode;
        nextNode = currentNode.next;
        currentNode.next = prevNode;
        prevNode = currentNode;
      }
      this.head = currentNode;
      return this;
    }


}


/**
 * GENERAL HELPER FUNCTIONS
 */

function FindSet(x) {
    for (var i = 0; i < sets.length; i++){
        if (sets[i].contains(x) != null) {
            return sets[i].contains(x);
        }
    }
    return null;
}

function MergeSet(x,y) {
    var listA,listB;
    for (var i = 0; i < sets.length; i++){
        if (sets[i].contains(x) != null) {
            listA = sets[i].contains(x);
            sets.splice(i,1);
        }
    }
    for (var i = 0; i < sets.length; i++) {
        if (sets[i].contains(y) != null) {
            listB = sets[i].contains(y);
            sets.splice(i,1);
        }
    }
    var res = MergeLists(listA,listB);
    return res;

}


function MergeLists(listA, listB) {
    var listC = new LinkedList();
    listA.merge(listB);
    listC = listA;
    return listC;
}

//access matrix by i,j -> returns 'Y' or 'N'
function isFriend(matrix, pair){
    return matrix[pair[0]].charAt(pair[1]);
}

function k_combinations(set, k) {
    var i, j, combs, head, tailcombs;
    if (k > set.length || k <= 0) {
        return [];
    }
    if (k == set.length) {
        return [set];
    }
    if (k == 1) {
        combs = [];
        for (i = 0; i < set.length; i++) {
            combs.push([set[i]]);
        }
        return combs;
    }
    // Assert {1 < k < set.length}
    combs = [];
    for (i = 0; i < set.length - k + 1; i++) {
        head = set.slice(i, i+1);
        tailcombs = k_combinations(set.slice(i + 1), k - 1);
        for (j = 0; j < tailcombs.length; j++) {
            combs.push(head.concat(tailcombs[j]));
        }
    }
    return combs;
}

0

DFS จากโหนดเริ่มต้นติดตามเส้นทาง DFS ระหว่างการสำรวจเส้นทางและบันทึกเส้นทางหากคุณพบขอบจากโหนด v ในเส้นทางไปยัง s (v, s) เป็นแบ็คเอนด์ในทรี DFS และดังนั้นจึงแสดงถึงวงจรที่มี s


ดี แต่นี่ไม่ใช่สิ่งที่ OP ต้องการ: ค้นหาทุกรอบมีโอกาสน้อยที่สุด
Sean L

0

เกี่ยวกับคำถามของคุณเกี่ยวกับวงจรการเปลี่ยนแปลงอ่านเพิ่มเติมได้ที่นี่: https://www.codechef.com/problems/PCYCLE

คุณสามารถลองใช้รหัสนี้ (ป้อนขนาดและหมายเลขหลัก):

# include<cstdio>
using namespace std;

int main()
{
    int n;
    scanf("%d",&n);

    int num[1000];
    int visited[1000]={0};
    int vindex[2000];
    for(int i=1;i<=n;i++)
        scanf("%d",&num[i]);

    int t_visited=0;
    int cycles=0;
    int start=0, index;

    while(t_visited < n)
    {
        for(int i=1;i<=n;i++)
        {
            if(visited[i]==0)
            {
                vindex[start]=i;
                visited[i]=1;
                t_visited++;
                index=start;
                break;
            }
        }
        while(true)
        {
            index++;
            vindex[index]=num[vindex[index-1]];

            if(vindex[index]==vindex[start])
                break;
            visited[vindex[index]]=1;
            t_visited++;
        }
        vindex[++index]=0;
        start=index+1;
        cycles++;
    }

    printf("%d\n",cycles,vindex[0]);

    for(int i=0;i<(n+2*cycles);i++)
    {
        if(vindex[i]==0)
            printf("\n");
        else
            printf("%d ",vindex[i]);
    }
}

0

รุ่น DFS c ++ สำหรับโค้ดหลอกในคำตอบของชั้นสอง:

void findCircleUnit(int start, int v, bool* visited, vector<int>& path) {
    if(visited[v]) {
        if(v == start) {
            for(auto c : path)
                cout << c << " ";
            cout << endl;
            return;
        }
        else 
            return;
    }
    visited[v] = true;
    path.push_back(v);
    for(auto i : G[v])
        findCircleUnit(start, i, visited, path);
    visited[v] = false;
    path.pop_back();
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.