Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import random
import time
import tkinter as tk
from tkinter import messagebox, ttk

EDGE_DENSITY = 0.3
MAX_N = 300
MAX_M = 10
MAX_ESTIMATED_EXPANSIONS = 2_000_000
MAX_DISPLAY_CHAINS = 50


def center_window(root, width, height):
root.update_idletasks()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
x = max((screen_width - width) // 2, 0)
y = max((screen_height - height) // 2, 0)
root.geometry(f"{width}x{height}+{x}+{y}")


def validate_inputs(n, m, d1, d2):
if n < 2:
return "n must be at least 2."
if m < 2:
return "m must be at least 2."
if m > n:
return "m cannot be greater than n."
if n > MAX_N:
return f"n exceeds limit {MAX_N}."
if m > MAX_M:
return f"m exceeds limit {MAX_M}."
if d1 < 0 or d2 < 0:
return "d1 and d2 must be non-negative."
if d1 > d2:
return "d1 must be less than or equal to d2."
return None


def generate_random_debts(n, d1, d2):
adj = [[] for _ in range(n)]
weights = [{} for _ in range(n)]
if n <= 1:
return adj, weights

max_degree = max(1, int((n - 1) * EDGE_DENSITY))
all_nodes = list(range(n))
for i in range(n):
candidates = all_nodes[:i] + all_nodes[i + 1 :]
k = random.randint(1, max_degree)
neighbors = random.sample(candidates, k)
for j in neighbors:
amount = random.randint(d1, d2)
adj[i].append((j, amount))
weights[i][j] = amount

incoming = [0] * n
for i in range(n):
for j, _ in adj[i]:
incoming[j] += 1
for node in range(n):
if incoming[node] == 0:
src = random.choice([i for i in range(n) if i != node])
if node not in weights[src]:
amount = random.randint(d1, d2)
adj[src].append((node, amount))
weights[src][node] = amount
incoming[node] += 1

return adj, weights


def estimate_expansions(n, avg_out_degree, m):
if m <= 1 or avg_out_degree <= 0:
return 0
return n * (avg_out_degree ** (m - 1))


def find_cycles_length_m(adj, weights, m, display_limit):
n = len(adj)
count = 0
display = []
visited = [False] * n
path = []

for start in range(n):
visited[start] = True
path[:] = [start]

def dfs(current, depth, min_so_far):
nonlocal count
if depth == m:
if start in weights[current]:
min_amount = min(min_so_far, weights[current][start])
count += 1
if len(display) < display_limit:
display.append((path[:] + [start], min_amount))
return

for neighbor, amount in adj[current]:
if neighbor <= start or visited[neighbor]:
continue
visited[neighbor] = True
path.append(neighbor)
dfs(neighbor, depth + 1, min(min_so_far, amount))
path.pop()
visited[neighbor] = False

dfs(start, 1, float("inf"))
visited[start] = False

return count, display


class DebtChainFinderApp:
def __init__(self, root):
self.root = root
self.root.title("Closed Debt Chains Finder")
center_window(self.root, 940, 680)

style = ttk.Style(self.root)
if "clam" in style.theme_names():
style.theme_use("clam")

main_frame = ttk.Frame(self.root, padding=10)
main_frame.grid(row=0, column=0, sticky="nsew")
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)

input_frame = ttk.LabelFrame(main_frame, text="Parameters", padding=10)
input_frame.grid(row=0, column=0, sticky="ew")
input_frame.columnconfigure(1, weight=1)

self.n_var = tk.StringVar(value="30")
self.m_var = tk.StringVar(value="4")
self.d1_var = tk.StringVar(value="10")
self.d2_var = tk.StringVar(value="100")

ttk.Label(input_frame, text="n (companies):").grid(
row=0, column=0, sticky="w", padx=4, pady=4
)
ttk.Entry(input_frame, textvariable=self.n_var, width=12).grid(
row=0, column=1, sticky="w", padx=4, pady=4
)

ttk.Label(input_frame, text="m (chain length):").grid(
row=0, column=2, sticky="w", padx=4, pady=4
)
ttk.Entry(input_frame, textvariable=self.m_var, width=12).grid(
row=0, column=3, sticky="w", padx=4, pady=4
)

ttk.Label(input_frame, text="d1 (min debt):").grid(
row=1, column=0, sticky="w", padx=4, pady=4
)
ttk.Entry(input_frame, textvariable=self.d1_var, width=12).grid(
row=1, column=1, sticky="w", padx=4, pady=4
)

ttk.Label(input_frame, text="d2 (max debt):").grid(
row=1, column=2, sticky="w", padx=4, pady=4
)
ttk.Entry(input_frame, textvariable=self.d2_var, width=12).grid(
row=1, column=3, sticky="w", padx=4, pady=4
)

self.run_button = ttk.Button(
input_frame, text="Generate and Find Chains", command=self.on_run
)
self.run_button.grid(row=0, column=4, rowspan=2, padx=8, pady=4, sticky="ns")

limits_text = (
f"System limits: n <= {MAX_N}, m <= {MAX_M}, "
f"estimated expansions <= {MAX_ESTIMATED_EXPANSIONS:,}"
)
ttk.Label(main_frame, text=limits_text).grid(
row=1, column=0, sticky="w", padx=4, pady=6
)

self.summary_var = tk.StringVar(value="Chains found: - | Time: -")
ttk.Label(main_frame, textvariable=self.summary_var).grid(
row=2, column=0, sticky="w", padx=4, pady=2
)

self.warning_var = tk.StringVar(value="")
self.warning_label = tk.Label(main_frame, textvariable=self.warning_var, fg="red")
self.warning_label.grid(row=3, column=0, sticky="w", padx=4, pady=2)

output_frame = ttk.LabelFrame(main_frame, text="Results", padding=6)
output_frame.grid(row=4, column=0, sticky="nsew", pady=(8, 0))
main_frame.rowconfigure(4, weight=1)
output_frame.rowconfigure(0, weight=1)
output_frame.columnconfigure(0, weight=1)

self.results_text = tk.Text(output_frame, height=20, wrap="word")
self.results_text.grid(row=0, column=0, sticky="nsew")
scrollbar = ttk.Scrollbar(
output_frame, orient="vertical", command=self.results_text.yview
)
scrollbar.grid(row=0, column=1, sticky="ns")
self.results_text.configure(yscrollcommand=scrollbar.set)

def _update_results(self, text):
self.results_text.configure(state="normal")
self.results_text.delete("1.0", tk.END)
self.results_text.insert(tk.END, text)
self.results_text.configure(state="disabled")

def _show_error(self, message):
self.warning_var.set(message)
messagebox.showerror("Input error", message)

def on_run(self):
self.warning_var.set("")
try:
n = int(self.n_var.get())
m = int(self.m_var.get())
d1 = int(self.d1_var.get())
d2 = int(self.d2_var.get())
except ValueError:
self._show_error("All inputs must be integers.")
return

error = validate_inputs(n, m, d1, d2)
if error:
self._show_error(error)
return

self.summary_var.set("Working...")
self._update_results("")
self.root.update_idletasks()

start_time = time.perf_counter()
adj, weights = generate_random_debts(n, d1, d2)
edge_count = sum(len(edges) for edges in adj)
avg_out_degree = edge_count / n if n else 0.0
estimate = estimate_expansions(n, avg_out_degree, m)

if estimate > MAX_ESTIMATED_EXPANSIONS:
elapsed = time.perf_counter() - start_time
message = (
f"Estimated search size {estimate:,.0f} exceeds the system limit "
f"{MAX_ESTIMATED_EXPANSIONS:,}. Reduce n or m."
)
self.warning_var.set(message)
messagebox.showwarning("Limitations exceeded", message)
self.summary_var.set(f"Aborted due to limitations | Time: {elapsed:.4f}s")
return

count, display = find_cycles_length_m(adj, weights, m, MAX_DISPLAY_CHAINS)
elapsed = time.perf_counter() - start_time

lines = [
f"Generated graph: {n} companies, {edge_count} debts, "
f"avg out-degree {avg_out_degree:.2f}",
f"Cycle length m: {m}",
]
if count == 0:
lines.append("No closed chains found.")
else:
lines.append(f"Showing {len(display)} of {count} chains:")
for idx, (cycle, min_amount) in enumerate(display, 1):
formatted = " -> ".join(f"C{node + 1}" for node in cycle)
lines.append(
f"{idx:02d}. {formatted} | min compensation: {min_amount}"
)

self._update_results("\n".join(lines))
self.summary_var.set(f"Chains found: {count} | Time: {elapsed:.4f}s")


def main():
root = tk.Tk()
app = DebtChainFinderApp(root)
root.mainloop()


if __name__ == "__main__":
main()