diff --git a/hungarian_method.py b/hungarian_method.py new file mode 100644 index 0000000..be7348e --- /dev/null +++ b/hungarian_method.py @@ -0,0 +1,291 @@ +import sys +from typing import NewType, Sequence, Tuple +Matrix = NewType('Matrix', Sequence[Sequence[int]]) + +class Hungarian: + + def __init__(self): + self.mat = None + self.row_covered = [] + self.col_covered = [] + self.starred = None + self.n = 0 + self.Z0_r = 0 + self.Z0_c = 0 + self.series = None + + def solve(self, cost_matrix: Matrix): + self.mat = cost_matrix + self.n = len(self.mat) + self.row_covered = [False for i in range(self.n)] + self.col_covered = [False for i in range(self.n)] + self.Z0_r = 0 + self.Z0_c = 0 + self.series = [[0 for j in range(2)] for j in range(self.n*2)] + self.starred = [[0 for j in range(self.n)] for j in range(self.n)] + + done = False + step = 1 + + steps = { 1 : self.step1, + 2 : self.step2, + 3 : self.step3, + 4 : self.step4, + 5 : self.step5, + 6 : self.step6 + } + + while not done: + try: + func = steps[step] + step = func() + except: + done = True + + results = [] + for i in range(self.n): + for j in range(self.n): + if self.starred[i][j] == 1: + results += [(i, j)] + + return results + + def step1(self): + """ + For each row of the matrix, find the smallest element and + subtract it from every element in its row. Go to Step 2. + """ + n = self.n + for i in range(n): + vals = [x for x in self.mat[i]] + minval = min(vals) + for j in range(n): + self.mat[i][j] -= minval + return 2 + + def step2(self): + """ + Find a zero (Z) in the resulting matrix. If there is no starred + zero in its row or column, star Z. Repeat for each element in the + matrix. Go to Step 3. + """ + for i in range(self.n): + for j in range(self.n): + if (self.mat[i][j] == 0) and \ + (not self.col_covered[j]) and \ + (not self.row_covered[i]): + self.starred[i][j] = 1 + self.col_covered[j] = True + self.row_covered[i] = True + break + + self.__clear_covers() + return 3 + + def step3(self): + """ + Cover each column containing a starred zero. If K columns are + covered, the starred zeros describe a complete set of unique + assignments. In this case, Go to DONE, otherwise, Go to Step 4. + """ + n = self.n + count = 0 + for i in range(n): + for j in range(n): + if self.starred[i][j] == 1 and not self.col_covered[j]: + self.col_covered[j] = True + count += 1 + + if count >= n: + step = 7 # done + else: + step = 4 + + return step + + def step4(self): + """ + Find a noncovered zero and prime it. If there is no starred zero + in the row containing this primed zero, Go to Step 5. Otherwise, + cover this row and uncover the column containing the starred + zero. Continue in this manner until there are no uncovered zeros + left. Save the smallest uncovered value and Go to Step 6. + """ + step = 0 + done = False + row = 0 + col = 0 + star_col = -1 + while not done: + (row, col) = self.__find_a_zero(row, col) + if row < 0: + done = True + step = 6 + else: + self.starred[row][col] = 2 + star_col = self.__find_star_in_row(row) + if star_col >= 0: + col = star_col + self.row_covered[row] = True + self.col_covered[col] = False + else: + done = True + self.Z0_r = row + self.Z0_c = col + step = 5 + + return step + + def step5(self): + """ + Construct a series of alternating primed and starred zeros as + follows. Let Z0 represent the uncovered primed zero found in Step 4. + Let Z1 denote the starred zero in the column of Z0 (if any). + Let Z2 denote the primed zero in the row of Z1 (there will always + be one). Continue until the series terminates at a primed zero + that has no starred zero in its column. Unstar each starred zero + of the series, star each primed zero of the series, erase all + primes and uncover every line in the matrix. Return to Step 3 + """ + count = 0 + series = self.series + series[count][0] = self.Z0_r + series[count][1] = self.Z0_c + done = False + while not done: + row = self.__find_star_in_col(series[count][1]) + if row >= 0: + count += 1 + series[count][0] = row + series[count][1] = series[count-1][1] + else: + done = True + + if not done: + col = self.__find_prime_in_row(series[count][0]) + count += 1 + series[count][0] = series[count-1][0] + series[count][1] = col + + self.__convert_series(series, count) + self.__clear_covers() + self.__erase_primes() + return 3 + + def step6(self): + """ + Add the value found in Step 4 to every element of each covered + row, and subtract it from every element of each uncovered column. + Return to Step 4 without altering any stars, primes, or covered + lines. + """ + minval = self.__find_smallest() + for i in range(self.n): + for j in range(self.n): + if self.row_covered[i]: + self.mat[i][j] += minval + if not self.col_covered[j]: + self.mat[i][j] -= minval + return 4 + + def __find_smallest(self): + """Find the smallest uncovered value in the matrix.""" + minval = sys.maxsize + for i in range(self.n): + for j in range(self.n): + if (not self.row_covered[i]) and (not self.col_covered[j]): + if minval > self.mat[i][j]: + minval = self.mat[i][j] + return minval + + def __find_a_zero(self, i0: int = 0, j0: int = 0): + """Find the first uncovered element with value 0""" + row = -1 + col = -1 + i = i0 + n = self.n + done = False + + while not done: + j = j0 + while True: + if (self.mat[i][j] == 0) and \ + (not self.row_covered[i]) and \ + (not self.col_covered[j]): + row = i + col = j + done = True + j = (j + 1) % n + if j == j0: + break + i = (i + 1) % n + if i == i0: + done = True + + return (row, col) + + def __find_star_in_row(self, row: Sequence[int]): + """ + Find the first starred element in the specified row. Returns + the column index, or -1 if no starred element was found. + """ + col = -1 + for j in range(self.n): + if self.starred[row][j] == 1: + col = j + break + + return col + + def __find_star_in_col(self, col: Sequence[int]): + """ + Find the first starred element in the specified col. Returns + the row index, or -1 if no starred element was found. + """ + row = -1 + for i in range(self.n): + if self.starred[i][col] == 1: + row = i + break + + return row + + def __find_prime_in_row(self, row): + """ + Find the first prime element in the specified row. Returns + the column index, or -1 if no starred element was found. + """ + col = -1 + for j in range(self.n): + if self.starred[row][j] == 2: + col = j + break + + return col + + + def __convert_series(self, + series: Matrix, + count: int): + """ + Unstar each starred zero + of the series, star each primed zero of the series + """ + for i in range(count+1): + if self.starred[series[i][0]][series[i][1]] == 1: + self.starred[series[i][0]][series[i][1]] = 0 + else: + self.starred[series[i][0]][series[i][1]] = 1 + + + def __clear_covers(self): + for i in range(self.n): + self.row_covered[i] = False + self.col_covered[i] = False + + + def __erase_primes(self): + for i in range(self.n): + for j in range(self.n): + if self.starred[i][j] == 2: + self.starred[i][j] = 0 diff --git a/main.py b/main.py new file mode 100644 index 0000000..a1c8046 --- /dev/null +++ b/main.py @@ -0,0 +1,70 @@ +import tkinter as tk +from tkinter import ttk +import hungarian_method as hm +from matrix_input import * + + +class main(tk.Frame): + def __init__(self, parent): + tk.Frame.__init__(self, parent) + self.win1 = tk.Toplevel() + self.win1.geometry("350x50") + self.win1.attributes('-topmost', 'true') + btn = ttk.Button(self.win1, text="Create", command=self.win1_submit) + self.e1 = tk.Entry(self.win1, width = 5) + self.lbl = tk.Label(self.win1, text = "Enter the size of the matrix: ") + self.lbl.grid(row=1, column=1) + self.e1.grid(row=1, column=2) + btn.grid(row=1, column=3, padx=10, pady=2) + + + def win1_submit(self): + self.table = MatrixInput(self, int(self.e1.get()), int(self.e1.get())) + self.solve = ttk.Button(self, text="Solve", width=6, command=self.on_submit) + self.parser = ttk.Button(self, text="Parser", width=6, command=self.on_parser) + self.resultLabel = tk.Label(self, text = "", font=("Helvetica", 13), padx=30) + self.table.grid(row=1, column=1, padx=30, pady=20) + self.solve.grid(row=1, column= 2) + self.parser.grid(row=1, column= 3, padx=20) + self.resultLabel.grid(row=5,column= 1) + self.grid_columnconfigure(index=0, weight=1) + self.grid_rowconfigure(index=2, weight=1) + self.win1.destroy() + + def on_submit(self): + solver = hm.Hungarian() + sol = solver.solve(self.table.get()) + self.resultLabel["text"] = 'The Assignment is: \n' + for i in range(solver.n): + self.resultLabel["text"] += ("Assignee #"+str(sol[i][0]+1)+" --> Task #"+str(sol[i][1]+1)+"\n") + + + def on_parser(self): + self.win2 = tk.Toplevel() + self.win2.attributes('-topmost', 'true') + self.tb = tk.Text(self.win2, height=10, width=30) + self.parse = ttk.Button(self.win2, text="Parse", width=5, command=self.on_parse) + self.tb.grid(row=1, column=1, padx=30, pady=30) + self.parse.grid(row=1, column=2, padx=30) + self.win2.grid_columnconfigure(index=0, weight=1) + self.win2.grid_rowconfigure(index=2, weight=1) + + + def on_parse(self): + matStr = self.tb.get("1.0",'end-1c').split("--") + row = 0 + col = 0 + for j in range(len(matStr)): + elements = matStr[j].split("-") + for i in elements: + self.table._entry[(row, col)].insert(0, str(i)) + col =col+1 + row = row + 1 + col = 0 + + +root = tk.Tk() +root.title("Hungarian Method") +#root.geometry("400x400") +main(root).pack(side="top", fill="both", expand=True) +root.mainloop() \ No newline at end of file diff --git a/matrix_input.py b/matrix_input.py new file mode 100644 index 0000000..0c5bbc0 --- /dev/null +++ b/matrix_input.py @@ -0,0 +1,42 @@ +import tkinter as tk +from tkinter import ttk + +class MatrixInput(tk.Frame): + def __init__(self, parent, rows, columns): + tk.Frame.__init__(self, parent) + + self._entry = {} + self.rows = rows + self.columns = columns + vcmd = (self.register(self._validate), "%P") + for row in range(self.rows): + for column in range(self.columns): + index = (row, column) + e = ttk.Entry(self, validate="key", width=5, validatecommand=vcmd) + e.grid(row=row, column=column, stick="nsew", padx=3, pady=3) + self._entry[index] = e + for column in range(self.columns): + self.grid_columnconfigure(column, weight=1) + self.grid_rowconfigure(rows, weight=1) + + def get(self): + mat = [] + for row in range(self.rows): + current_row = [] + for column in range(self.columns): + index = (row, column) + current_row.append(int(self._entry[index].get())) + mat.append(current_row) + return mat + + def _validate(self, P): + + if P.strip() == "": + return True + + try: + f = int(P) + except ValueError: + self.bell() + return False + return True \ No newline at end of file