Skip to content

Commit a16f5a0

Browse files
committed
Final adjustments
1 parent 7055e58 commit a16f5a0

6 files changed

Lines changed: 141 additions & 51 deletions

File tree

4/.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
graphs
2-
tests
1+
graphs/*
2+
tests/*

4/generate_graphs.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66

77
def model(x, a, b):
8-
return a * (x ** 2) + b * (x ** 2) * np.log(x)
8+
return a * (x ** 3) + b * (x ** 2) * np.log(x)
99

1010

1111
if __name__ == "__main__":
@@ -27,12 +27,10 @@ def model(x, a, b):
2727
ss_tot = np.sum((y_mean - np.mean(y_mean)) ** 2)
2828
r_squared = 1 - ss_res / ss_tot
2929

30-
rmse = np.sqrt(np.mean((y_mean - y_fit) ** 2))
31-
3230
plt.scatter(df["n"], df["time"], label="Observações individuais")
3331

3432
plt.errorbar(x_mean, y_mean, yerr=y_std, fmt="o", color="black",
35-
capsize=4, label="Média ± desvio‑padrão")
33+
capsize=4, label="Média ± desvio‑padrão (ms)")
3634

3735
x_fit = np.linspace(x_mean.min(), x_mean.max(), 300)
3836
y_fit = model(x_fit, a_fit, b_fit)
@@ -43,3 +41,55 @@ def model(x, a, b):
4341
plt.legend()
4442

4543
plt.savefig("graphs/n_time.eps")
44+
plt.clf()
45+
46+
x = df["n"]
47+
y = df["loop_iters"]
48+
plt.scatter(x, y, label="Observações individuais")
49+
50+
coeffs = np.polyfit(x, y, 2)
51+
poly = np.poly1d(coeffs)
52+
x_fit = np.linspace(min(x), max(x), 50)
53+
y_fit = poly(x_fit)
54+
plt.plot(x_fit, y_fit, color="red")
55+
56+
plt.xlabel("Tamanho da matriz")
57+
plt.ylabel("Iterações no loop principal")
58+
plt.legend()
59+
60+
plt.savefig("graphs/main_loop.eps")
61+
plt.clf()
62+
63+
x = df["n"]
64+
y = df["relaxations"]
65+
plt.scatter(x, y, label="Observações individuais")
66+
67+
coeffs = np.polyfit(x, y, 2)
68+
poly = np.poly1d(coeffs)
69+
x_fit = np.linspace(min(x), max(x), 50)
70+
y_fit = poly(x_fit)
71+
plt.plot(x_fit, y_fit, color="red", label="Regressão polinomial de 2º grau")
72+
73+
plt.xlabel("Tamanho da matriz")
74+
plt.ylabel("Relaxações")
75+
plt.legend()
76+
77+
plt.savefig("graphs/relaxations.eps")
78+
plt.clf()
79+
80+
x = df["n"]
81+
y = df["heap_ops"]
82+
plt.scatter(x, y, label="Observações individuais")
83+
84+
coeffs = np.polyfit(x, y, 2)
85+
poly = np.poly1d(coeffs)
86+
x_fit = np.linspace(min(x), max(x), 50)
87+
y_fit = poly(x_fit)
88+
plt.plot(x_fit, y_fit, color="red", label="Regressão polinomial de 2º grau")
89+
90+
plt.xlabel("Tamanho da matriz")
91+
plt.ylabel("Operações do heap")
92+
plt.legend()
93+
94+
plt.savefig("graphs/heap_ops.eps")
95+
plt.clf()

4/generator.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
#include <random>
21
#include <iostream>
2+
#include <random>
3+
#include <string>
34

45
using namespace std;
56

4/hungarian_algorithm.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ vector<int64_t> hungarian(const mdspan<Weight, dextents<size_t, 2>> C) {
5353
greater<pair<int32_t, size_t>>>
5454
pq;
5555

56-
// Connect every job to the dummy worker
56+
// Connect every worker to the current job
5757
for (size_t v = 0; v < W; ++v) {
5858
auto edge = C[j_cur, v] - h[v];
5959
dist[v] = edge;
@@ -139,7 +139,8 @@ int main() {
139139
}
140140
}
141141

142-
auto ans = hungarian(costs);
142+
const auto ans = hungarian(costs);
143+
143144
cout << ans[ans.size() - 1] << '\n';
144145

145146
return 0;

4/report.tex

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
\usepackage{graphicx}
1010
\usepackage{fontspec}
1111
\usepackage{float}
12-
%\usepackage{listings}
12+
\usepackage{listings}
1313
\setlength{\parskip}{10pt}
1414
\setmainfont{texgyreheros}[
1515
UprightFont = *-regular,
@@ -18,6 +18,10 @@
1818
BoldItalicFont = *-bolditalic,
1919
Extension = .otf
2020
]
21+
\lstset{
22+
language=C++,
23+
keepspaces=true
24+
}
2125

2226
\title{Laboratório 4 — Emparelhamentos}
2327
\author{Eduardo Menges Mattje}
@@ -26,15 +30,15 @@
2630
\maketitle
2731

2832
\section{Introdução}
29-
3033
O algoritmo húngaro resolve problemas de emparelhamento para grafos bi-partidos ao buscar repetidamente por caminhos aumentantes de forma a tornar valores potenciais iguais à atribuição que minimiza (ou maximiza) a soma dos emparelhamentos.
3134
A forma de encontrar os caminhos aumentantes escolhida para esta implementação foi a de Johnson, que utiliza Dijkstra para encontrar os menores caminhos.
3235

3336
\section{Detalhes de implementação}
37+
\label{sec:implementation-details}
3438

35-
O algoritmo é iterativo, atribuindo um trabalho a um trabalhador a cada iteração.
39+
O algoritmo é iterativo, atribuindo um trabalho a um trabalhador a cada iteração, chegando, portanto, na solução somente na última iteração.
3640

37-
Há um trabalhador extra, que se liga a todos os trabalhos no começo das iterações e inicializa a pilha (implementada com std::priority\_queue) do Dijkstra.
41+
Há um trabalhador extra, que se liga a todos os trabalhos no começo das iterações e inicializa a pilha (implementada com \lstinline|std::priority_queue|) do Dijkstra.
3842
Então, para cada outro trabalhador, empilha com as distâncias atualizadas considerando seus potenciais (valores do dual) e a relaxação em relação ao trabalhador atual, somente se essa for menor do que a distância já existente.
3943

4044
O Dijkstra se encerra ao encontrar o primeiro trabalhador que não possui trabalho associado.
@@ -44,30 +48,79 @@ \section{Detalhes de implementação}
4448

4549
\section{Ambiente de teste}
4650

47-
A máquina de teste possui Linux Mint 22.1, processador Intel i5-12450H e 16GB de memória RAM.
48-
O compilador utilizado foi o GCC, versão 14.2, com todas as otimizações padrões habilitadas, e a implementação da biblioteca padrão do C++ é a libstdc++ versão 14.
51+
A máquina de teste possui Windows 11, um processador AMD Ryzen 5 7600X e 32 GB de memória RAM. O compilador utilizado foi o Clang, versão 19.1.5, com todas as otimizações padrões habilitadas e utilizando a especificação do C++ 23.
4952

5053
\section{Plano de teste}
5154

52-
O gerador utilizado para gerar os grafos toma uma entrada $n$ e gera uma matriz $N \times N$ com valores no intervalo $[0, n \cdot n]$.
55+
O gerador utilizado para gerar os grafos toma uma entrada $n$ e gera uma matriz de tamanho $n \times n$ com valores uniformemente distribuídos no intervalo $[0, n \cdot n]$.
56+
57+
Foram gerados $10$ testes para cada $n$ pertencente ao intervalo $[1000, 20000]$, incrementando $1000$ a cada passo. Para cada teste, mensurou-se:
58+
59+
\begin{itemize}
5360

54-
Foram gerados $10$ testes para cada $n$ pertencente ao intervalo $[1000, 20000]$, incrementando $1000$ a cada passo. Para cada teste, mensurou-se o tempo de execução.
61+
\item o tempo de execução;
62+
\item o número de iterações do laço principal;
63+
\item a quantidade de operações do heap;
64+
\item a quantidade de relaxações realizadas.
65+
66+
\end{itemize}
5567

5668
\section{Resultados}
5769

70+
\subsection{Tempo de execução}
71+
5872
\begin{figure}[H]
5973
\centering
6074
\caption{Tempo de execução em função de $n$}
6175
\label{fig:n_time}
6276
\includegraphics[width=0.8\linewidth]{graphs/n_time}
6377
\end{figure}
6478

65-
Utilizando o algoritmo de Johnson para caminhos aumentantes, o algoritmo húngaro tem como complexidade $O(n(m+n \log n))$.
66-
Como o gerador gera matrizes quadradas, $n = m$, logo a complexidade é $O(n^2 + n^2 \log n)$.
67-
Uma regressão não linear com coeficientes $a$ e $b$ para esses termos cabe aos dados, conforme apontado na figura \ref{fig:n_time}.
79+
Utilizando o algoritmo de Johnson para caminhos aumentantes, o algoritmo húngaro tem como complexidade $O(n(m + n \log n))$.
80+
Como o gerador gera matrizes quadradas, $m = n^2$, logo a complexidade esperada é $O(n^3 + n^2 \log n)$.
81+
Uma regressão para a equação $an^3 + bn^2 \log n$ cabe aos dados, conforme apontado na figura \ref{fig:n_time}, tendo o coeficiente $R^2 = 0,9987$ muito próximo de 1.
82+
83+
\subsection{Laço principal}
84+
85+
\begin{figure}[H]
86+
\centering
87+
\caption{Iterações no laço principal em relação a $n$}
88+
\label{fig:main_loop}
89+
\includegraphics[width=0.8\linewidth]{graphs/main_loop}
90+
\end{figure}
91+
92+
Devido à implementação iterativa do algoritmo, o crescimento das iterações no laço principal ocorre de maneira linear a $n$, conforme esperado.
93+
94+
\subsection{Operações no heap}
95+
\label{sec:heap_ops}
96+
97+
\begin{figure}[H]
98+
\centering
99+
\caption{Operações no heap em função de $n$}
100+
\label{fig:heap_ops}
101+
\includegraphics[width=0.8\linewidth]{graphs/heap_ops}
102+
\end{figure}
103+
104+
Conforme discutido em \ref{sec:implementation-details}, o Dijkstra para ao encontrar o primeiro trabalhador livre, e na $k$-ésima iteração, há $n - k + 1$ trabalhadores livres.
105+
Desta forma, o número esperado de operações \lstinline|pop()| até encontrarmos o primeiro trabalhador livre é $\frac{n}{n - k + 1}$, o que, nas primeiras 50\% das iterações é $<= 2$, e mesmo nos 10\% finais o valor é $<= 10$, o que seria um tempo constante $\Theta(1)$.
106+
107+
Como percorremos todos os outros $n$ vizinhos no processo de relaxação, e como cada relaxação pode gerar um \lstinline|push()| no heap, temos $O(n)$ operações no heap.
108+
Por fim, o laço principal do algoritmo faz $O(n^2)$ operações no heap, conforme observado nos dados coletados.
109+
110+
\subsection{Relaxações}
111+
112+
\begin{figure}[H]
113+
\centering
114+
\caption{Relaxações bem-sucedidas em função de $n$}
115+
\label{fig:relaxations}
116+
\includegraphics[width=0.8\linewidth]{graphs/relaxations}
117+
\end{figure}
118+
119+
As relaxações são contadas somente quando bem-sucedidas ou seja, se realmente melhoram a distância, e só ocorrem caso o primeiro \lstinline|pop()| não ocorra com um trabalhador livre. Seguindo um raciocínio parecido com o item \ref{sec:heap_ops}, sabemos que a probabilidade de isso ocorrer é baixa, e cada vez que ocorre são realizadas no máximo $n$ relaxações.
120+
Deste modo, $O(1) * O(n) = O(n)$, e contando as iterações do laço principal temos $O(n^2)$.
68121

69122
\section{Conclusão}
70123

71-
Com essa mensuração, conseguimos observar que a complexidade teórica é respeitada.
124+
Com essa mensuração e os testes com matrizes quadradas, conseguimos observar que a complexidade teórica é respeitada para o caso médio, o que é reforçado pelos testes de regressão realizados e a probabilidade de emparelhamento dos trabalhadores livres.
72125

73126
\end{document}

4/test.sh

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,32 @@
11
#!/usr/bin/env bash
2-
generator="/home/menges/repos/advanced_algorithms/cmake-build-gcc/bin/generator"
3-
target="/home/menges/repos/advanced_algorithms/cmake-build-gcc/bin/hungarian_algorithm"
2+
generator="../cmake-build-release/bin/Release/generator.exe"
3+
target="../cmake-build-release/bin/Release/hungarian_algorithm.exe"
44

55
# Your commands here
66

77
sizes=(
8-
1000
9-
2000
10-
3000
11-
4000
12-
5000
13-
6000
14-
7000
15-
8000
16-
9000
17-
10000
18-
11000
19-
12000
20-
13000
21-
14000
22-
15000
23-
16000
24-
17000
25-
18000
26-
19000
27-
20000
8+
21000
9+
22000
10+
23000
11+
24000
12+
25000
13+
26000
14+
27000
15+
28000
16+
29000
17+
30000
2818
)
2919

30-
echo "n,time,result"
20+
echo "n,time,loop_iters,relaxations,heap_ops,result"
3121

3222
for i in {1..10}; do
3323
for size in "${sizes[@]}"; do
3424
(
35-
$generator $size >"tests/test_${size}"
36-
37-
start=$(date +%s%N)
25+
$generator "$size" >"tests/test_${size}"
3826

3927
result=$($target <"tests/test_${size}")
4028

41-
end=$(date +%s%N)
42-
elapsed=$(((end - start) / 1000000))
43-
44-
echo "$size,$elapsed,$result"
29+
echo "$size,$result"
4530
) &
4631
done
4732
wait

0 commit comments

Comments
 (0)