diff --git "a/chapters/1-tecnicas-de-dise\303\261o.tex" "b/chapters/1-tecnicas-de-dise\303\261o.tex" index d0c478b..16ac85e 100644 --- "a/chapters/1-tecnicas-de-dise\303\261o.tex" +++ "b/chapters/1-tecnicas-de-dise\303\261o.tex" @@ -88,11 +88,11 @@ \subsection{Notación O} \subsubsection{Complejidades comunes} \begin{itemize} - \item Si un algoritmo es\BigO{\log{n}}, se dice \textbf{logarítmico}. - \item Si un algoritmo es\BigO{n}, se dice \textbf{lineal}. - \item Si un algoritmo es\BigO{n^2}, se dice \textbf{cuadrático}. - \item Si un algoritmo es\BigO{n^3}, se dice \textbf{cúbico}. - \item Si un algoritmo es\BigO{n^k}, se dice \textbf{polinomial}. + \item Si un algoritmo es \BigO{\log{n}}, se dice \textbf{logarítmico}. + \item Si un algoritmo es \BigO{n}, se dice \textbf{lineal}. + \item Si un algoritmo es \BigO{n^2}, se dice \textbf{cuadrático}. + \item Si un algoritmo es \BigO{n^3}, se dice \textbf{cúbico}. + \item Si un algoritmo es \BigO{n^k}, se dice \textbf{polinomial}. \item Si un algoritmo es $\BigO{k^n}\ (k > 1)$, se dice \textbf{exponencial}. \end{itemize} @@ -239,7 +239,7 @@ \subsubsection{Ejemplo: Cálculo de coeficientes binomiales} \End \end{codebox} -Este método tiene una complejidad de\BigOmega{\binom{n}{k}}, y evita calcular factoriales, pero podría ser más eficiente, ya que al ejecutarlo se repiten llamadas con los mismos parámetros. +Este método tiene una complejidad de \BigOmega{\binom{n}{k}}, y evita calcular factoriales, pero podría ser más eficiente, ya que al ejecutarlo se repiten llamadas con los mismos parámetros. \begin{figure}[H] \centering \includegraphics[width=0.8\textwidth]{combinatorio_call_tree.jpg} @@ -263,7 +263,7 @@ \subsubsection{Ejemplo: Cálculo de coeficientes binomiales} \End \end{codebox} -La complejidad de este método es\BigO{nk}, y $\BigO{nk} \subseteq \BigO{n^2}$, ya que $k \leq n$. Además, se puede implementar con una complejidad espacial de\BigO{k} almacenando solo la fila actual y la anterior de la tabla en el ciclo. +La complejidad de este método es \BigO{nk}, y $\BigO{nk} \subseteq \BigO{n^2}$, ya que $k \leq n$. Además, se puede implementar con una complejidad espacial de \BigO{k} almacenando solo la fila actual y la anterior de la tabla en el ciclo. \subsection{Principio de optimalidad de Bellman} \label{optimalidad-bellman} @@ -297,7 +297,7 @@ \subsubsection{Ejemplo: Problema de la mochila} Esto contempla, para cada $k$ dos posibilidades: o bien el objeto de índice $k$ está en la solución óptima, y entonces $m(k, D) = b_k + m(k - 1, D - p_k)$, o bien no, en cuyo caso $m(k, D) = m(k - 1, D)$. -Si esta función se implementa directamente en un algoritmo de PD (ya sea top-down o bottom-up) utilizando una matriz como estructura de memoización, tanto la complejidad temporal como la espacial son\BigO{nC}. Esta complejidad es \textit{pseudopolinomial}: está acotada por un polinomio, pero este incluye valores númericos del input, no solo el tamaño del mismo. +Si esta función se implementa directamente en un algoritmo de PD (ya sea top-down o bottom-up) utilizando una matriz como estructura de memoización, tanto la complejidad temporal como la espacial son \BigO{nC}. Esta complejidad es \textit{pseudopolinomial}: está acotada por un polinomio, pero este incluye valores númericos del input, no solo el tamaño del mismo. \subsubsection{Solución óptima} \label{reconstruccion-solucion} @@ -363,7 +363,7 @@ \subsubsection{Ejemplo: Problema de la mochila} \item ...maximice $\frac{b_i}{p_i}$ (la ``densidad''). \end{enumerate} -Se puede demostrar que, si se corre el algoritmo goloso $2$ veces, una con el primer criterio y otra con el segundo, alguno de los resultados tiene un valor de al menos la mitad de la solución óptima. Esto hace al procedimiento un algoritmo $\frac{1}{2}$-aproximado, y se puede implementar en tiempo\BigO{n\log{n}} si se ordenan los elementos previamente (es aún más eficiente usar una colas de prioridad implementadas con heap). +Se puede demostrar que, si se corre el algoritmo goloso $2$ veces, una con el primer criterio y otra con el segundo, alguno de los resultados tiene un valor de al menos la mitad de la solución óptima. Esto hace al procedimiento un algoritmo $\frac{1}{2}$-aproximado, y se puede implementar en tiempo \BigO{n\log{n}} si se ordenan los elementos previamente (es aún más eficiente usar una colas de prioridad implementadas con heap). Por otro lado, si cambia el problema, permitiendo poner una \underline{fracción} de cada elemento en la mochila, el algoritmo goloso que utiliza el tercer criterio devuelve soluciones óptimas. @@ -402,7 +402,7 @@ \subsubsection{Ejemplo: Tiempo de espera total en un sistema} Se puede plantear el siguiente algoritmo goloso: En cada paso, atender al cliente pendiente que tenga el menor tiempo de atención. La idea detrás de ese criterio es que el tiempo de los clientes que son atendidos primero tendrá que ser esperado por todos los demás, así que lo ideal es que sea el mínimo. Formalmente, la solución $I = (i_1, ..., i_n)$ es una que cumple $t_{i_j} \leq t_{i_{j+1}}$ para $j = 1, ..., n - 1$. -En este caso, la solución que proporciona el algoritmo resulta ser óptima. Por otro lado, la complejidad temporal es\BigO{n\log{n}}, ya que el procedimiento es equivalente a ordenar a los clientes por tiempo de espera. +En este caso, la solución que proporciona el algoritmo resulta ser óptima. Por otro lado, la complejidad temporal es \BigO{n\log{n}}, ya que el procedimiento es equivalente a ordenar a los clientes por tiempo de espera. \section{Algoritmos Probabilísticos} diff --git a/chapters/2-intro-teoria-de-grafos.tex b/chapters/2-intro-teoria-de-grafos.tex index 2b7a91f..a128518 100644 --- a/chapters/2-intro-teoria-de-grafos.tex +++ b/chapters/2-intro-teoria-de-grafos.tex @@ -177,7 +177,7 @@ \subsubsection{Matriz de adyacencia} La matriz es simétrica para grafos, pero no necesariamente para digrafos. -La estructura permite comprobar si dos vértices son adyacentes en tiempo constante. Sin embargo, construirla a partir de una lista de adyacencia es una operación de complejidad cuadrática, y la estructura es muy rígida (para agregar un vértice se debe armar una nueva matriz). Además, la complejidad espacial es también\BigO{|V|^2}, lo cual es problemático para guardar grafos ralos\footnote{Un grafo \textit{ralo} es uno con ``pocas'' aristas.}. +La estructura permite comprobar si dos vértices son adyacentes en tiempo constante. Sin embargo, construirla a partir de una lista de adyacencia es una operación de complejidad cuadrática, y la estructura es muy rígida (para agregar un vértice se debe armar una nueva matriz). Además, la complejidad espacial es también \BigO{|V|^2}, lo cual es problemático para guardar grafos ralos\footnote{Un grafo \textit{ralo} es uno con ``pocas'' aristas.}. \subsubsection{Matriz de incidencia} @@ -440,7 +440,7 @@ \subsection{DFS} \li $\id{fin}[v] \gets \id{contador}$ \end{codebox} -El tiempo de ejecución del algoritmo, al igual que BFS, es lineal\footnote{La linealidad de estas complejidades se refiere a que, como los grafos se pasan como listas de adyacencia, el tamaño de la entrada es $|E|$. Si se considerara la cantidad de vértices, una complejidad de\BigO{|E|} sería cuadrática, ya que $|E| \in \BigO{|V|^2}$.}: hay una llamada por cada nodo, y cada llamada tiene un tiempo de ejecución proporcional al grado del nodo, así que la complejidad es $\BigO{|V| + |E|}$. +El tiempo de ejecución del algoritmo, al igual que BFS, es lineal\footnote{La linealidad de estas complejidades se refiere a que, como los grafos se pasan como listas de adyacencia, el tamaño de la entrada es $|E|$. Si se considerara la cantidad de vértices, una complejidad de \BigO{|E|} sería cuadrática, ya que $|E| \in \BigO{|V|^2}$.}: hay una llamada por cada nodo, y cada llamada tiene un tiempo de ejecución proporcional al grado del nodo, así que la complejidad es $\BigO{|V| + |E|}$. Al terminar, DFS no solo devuelve el árbol generado $T$, sino que también un par de arreglos \id{principio} y \id{fin}. El primero guarda el orden en el que se empieza a explorar el subárbol de cada nodo (también llamado \textit{pre-order}), mientras que el segundo guarda el orden en el que se termina dicha exploración (también llamado \textit{post-order}). Estos valores son muy útiles para analizar la estructura del árbol. @@ -533,7 +533,7 @@ \subsection{Algoritmo} Como el valor de \id{fin}[$v$] se asigna después de haber explorado por todos los vértices $u$ alcanzables desde $v$, se cumple que $\id{fin}[u] < \id{fin}[v]$. Por ende, si se ordenan los vértices por valor \id{fin} decreciente, los vértices que se pueden alcanzar desde otros aparecerán después. \end{proof} -Para implementar este algoritmo, no es necesario ordenar directamente los vértices a través de \id{fin} (lo tendría complejidad\BigO{|V|\log{|V|}}), sino que basta con modificar DFS: después de terminar de explorar el subárbol de cada vértice, se lo agrega al principio de una secuencia. Esto produce un ordenamiento topológico de $D$. +Para implementar este algoritmo, no es necesario ordenar directamente los vértices a través de \id{fin} (lo tendría complejidad \BigO{|V|\log{|V|}}), sino que basta con modificar DFS: después de terminar de explorar el subárbol de cada vértice, se lo agrega al principio de una secuencia. Esto produce un ordenamiento topológico de $D$. \section{Algoritmo de Kosaraju} diff --git a/chapters/6-np-completitud.tex b/chapters/6-np-completitud.tex index 1ee5a09..5d5b770 100644 --- a/chapters/6-np-completitud.tex +++ b/chapters/6-np-completitud.tex @@ -57,7 +57,7 @@ \subsection{Máquina de Turing Determinística} Esas son las características generales de cualquier MTD. Para una máquina particular, se tiene: \begin{itemize} - \item Un \textit{alfabeto} de \textit{símbolos} finito $\Sigma$ y un símbolo especial $\ast$ llamado ``\textit{blanco}''. Se define $\Gamma = \Sigma \cup \{\ast\}$. La celda puede tomar valores en $\Gamma$. + \item Un \textit{alfabeto} de \textit{símbolos} finito $\Sigma$ y un símbolo especial $\ast$ llamado ``\textit{blanco}''. Se define $\Gamma = \Sigma \cup \{\ast\}$. Las celdas puede tomar valores en $\Gamma$. \item Un conjunto finito $Q$ de \textit{estados}. \item Un \textit{estado inicial} $q_0 \in Q$. \item Un conjunto de \textit{estados finales} $Q_f \subseteq Q$ (en el caso de problemas de optimización $Q_f = \{q_{\text{sí}}, q_{\text{no}}\}$). @@ -77,7 +77,7 @@ \subsubsection{Resolución y Complejidad} La máquina \textit{resuelve} el problema $\Pi$ si para toda instancia $I \in I_{\Pi}$ esta alcanza un estado final y es el correcto. La \textit{complejidad} de una MTD está dada por la cantidad de movimientos de la cabeza que se realizan entre el estado inicial y el final, en función del tamaño de entrada: -$$T_M(n) = \max\{m \mid M \text{ realiza $m$ movimientos para la entrada $I \in I_{@P}, |I| = n$}\}$$ +$$T_M(n) = \max\{m \mid M \text{ realiza $m$ movimientos para la entrada $I \in I_{\Pi}, |I| = n$}\}$$ Una MTD $M$ es \textit{polinomial} para $\Pi$ cuando $T_M(n) \in \BigO{n^k}$ para algún $k$. @@ -112,7 +112,7 @@ \subsection{La clase NP} Si $\Pi$ es un problema de decisión que pertenece a la clase NP, entonces $\Pi$ puede ser resuelto por un algoritmo determinístico en tiempo exponencial con respecto al tamaño de la entrada. \end{theorem*} \begin{proof} - Para resolver $\Pi$, se puede simular la ejecución de la MTND que lo resuelve: cada vez que hay ambigüedad con respecto a la instrucción a aplicar se exploran todos los ``súbarboles de ejecución''. Esto implica una complejidad exponencial: si hay a lo sumo $A$ estados distintos para cada punto de no-determinismo, se realizan \BigO{A^{T_M(n)}}, y $T_M(n)$ es una función exponencial (porque $\Pi \in \text{NP}$). + Para resolver $\Pi$, se puede simular la ejecución de la MTND que lo resuelve: cada vez que hay ambigüedad con respecto a la instrucción a aplicar se exploran todos los ``súbarboles de ejecución''. Esto implica una complejidad exponencial: si hay a lo sumo $A$ estados distintos para cada punto de no-determinismo, se realizan \BigO{A^{T_M(n)}}, y $T_M(n)$ es una función polinomial (porque $\Pi \in \text{NP}$). \end{proof} \subsubsection{P vs. NP}