\documentclass[a4paper]{article}
\usepackage{amsmath, amssymb, amsfonts}
\usepackage[italian]{babel}
\usepackage[utf8]{inputenc}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage[a4paper, total={6in, 8in}]{geometry}
\usepackage{minted}
\usepackage{mathpazo}
\usepackage{fancyhdr}
\usepackage{amsthm}
\usepackage{tikz}
\usetikzlibrary{automata,positioning}

% Definizione degli ambienti
\newtheorem{theorem}{Teorema}
\newtheorem{definition}{Definizione}
\newtheorem{example}{Esempio}
\newtheorem{lemma}{Lemma}
\newtheorem{proposition}{Proposizione}

\hypersetup{
    pdftitle={Lezione di Informatica Teorica},
    pdfauthor={Appunti da Trascrizione AI}
}

\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{\textit{Lezione di Informatica Teorica}}
\fancyfoot[C]{\thepage}

\title{Lezione di Informatica Teorica}
\author{Appunti da Trascrizione Automatica}
\date{\today}

\begin{document}
\maketitle
\tableofcontents
\newpage

\section{Introduzione e Recap}

Questa lezione riprende e approfondisce i concetti introdotti precedentemente, con un focus sulla formalizzazione dei problemi e l'introduzione di un modello di calcolo più potente: la Macchina di Turing.

\subsection{Problemi e Linguaggi}
Abbiamo studiato la complessità non degli algoritmi, ma dei \textbf{problemi}. La differenza sostanziale è che qui formalizziamo i problemi stessi.

Esistono due tipi principali di problemi:
\begin{itemize}
    \item \textbf{Problemi di Ricerca}: Le risposte possono essere varie.
    \item \textbf{Problemi di Decisione}: La risposta è sempre un booleano (Sì/No).
\end{itemize}

I problemi di decisione possono riguardare vari contesti (matrici, grafi, immagini, testo, ecc.). Per semplificare l'analisi, si scelgono problemi che possono essere formalizzati in modo uniforme. Abbiamo scelto di studiare i \textbf{problemi di decidere linguaggi}.

\begin{definition}[Decidere un linguaggio]
    Decidere un linguaggio significa verificare se una data stringa appartiene o meno a un determinato linguaggio.
    \begin{itemize}
        \item \textbf{Input}: Una stringa.
        \item \textbf{Linguaggio}: Fa parte della definizione del problema, non dell'input.
        \item \textbf{Output}: Sì/No (la stringa appartiene al linguaggio?).
    \end{itemize}
\end{definition}

\textbf{Codifica dei Problemi in Linguaggi}:
È possibile ricodificare problemi di decisione arbitrari in problemi di decisione di linguaggi.
\begin{example}[Problema dei Grafi Totalmente Connessi]
    \textbf{Problema}: Dato un grafo, stabilire se è totalmente connesso.
    \begin{itemize}
        \item Questo è un problema di decisione (risposta Sì/No).
        \item \textbf{Codifica in un linguaggio}: Possiamo inventare un alfabeto $\Sigma$ e una codifica tale per cui le stringhe che fanno parte del linguaggio $L_{GC}$ sono solo quelle stringhe che, secondo la nostra codifica, rappresentano grafi totalmente connessi.
        \item Una stringa in input che non codifica un grafo (secondo la nostra codifica) non appartiene a $L_{GC}$.
        \item Le stringhe che codificano grafi non totalmente connessi non appartengono a $L_{GC}$.
    \end{itemize}
    In questo modo, decidere se una stringa appartiene a $L_{GC}$ equivale a decidere se il grafo codificato è totalmente connesso.
\end{example}

Questa codifica può essere estesa anche ai problemi di ricerca:
\begin{itemize}
    \item Un problema di ricerca è una relazione binaria tra stringhe (input-output).
    \item Per codificare un problema di ricerca in un linguaggio, il linguaggio conterrebbe stringhe che codificano le coppie (input, output) valide per quel problema. Decidere l'appartenenza di una stringa a tale linguaggio implicherebbe, in qualche modo, il calcolo della soluzione.
\end{itemize}
Per semplicità, in questo corso ci concentreremo sui problemi di decisione e sulla loro formalizzazione come problemi di decisione di linguaggi.

\subsection{Automi a Stati Finiti (Recap)}
Per determinare se un linguaggio è decidibile (intuitivamente, se esiste un algoritmo che lo risolve), abbiamo introdotto i modelli di calcolo. Inizialmente, abbiamo visto gli \textbf{Automi a Stati Finiti (FA)}.

\begin{itemize}
    \item \textbf{DFA (Deterministic Finite Automata)}: Per ogni stato e simbolo in input, esiste una sola transizione possibile.
    \item \textbf{NFA (Non-Deterministic Finite Automata)}: Per ogni stato e simbolo in input, possono esserci più transizioni possibili. Un NFA accetta una stringa se esiste \emph{almeno un percorso} di computazione che porta a uno stato accettante consumando tutto l'input.
\end{itemize}

\textbf{Potere Computazionale degli FA}:
Gli NFA e i DFA hanno lo stesso potere computazionale. Ogni NFA può essere convertito in un DFA equivalente. Il tempo di esecuzione è lineare rispetto alla lunghezza dell'input.

\begin{example}[Linguaggio $L = \{a^n b^n \mid n \ge 0\}$]
    Questo linguaggio, che rappresenta stringhe con $n$ 'a' seguite da $n$ 'b' (es. \texttt{aabb}, \texttt{aaabbb}), \textbf{non può essere riconosciuto da un automa a stati finiti}.
    \begin{itemize}
        \item \textbf{Intuito}: Un FA non ha memoria sufficiente per "contare" le 'a' e confrontarle con le 'b'. Ha un numero finito di stati, quindi non può memorizzare un numero arbitrario $n$.
    \end{itemize}
    Questo implica la necessità di un modello di calcolo più potente.
\end{example}

\subsection{Linguaggi Regolari ed Espressioni Regolari}
Gli automi a stati finiti (DFA/NFA) sono in grado di riconoscere tutti e soli i \textbf{linguaggi regolari}.

\begin{definition}[Linguaggio Regolare ed Espressioni Regolari]
    Sia $\Sigma$ un alfabeto. Un linguaggio regolare su $\Sigma$ è un linguaggio le cui stringhe possono essere descritte da \textbf{espressioni regolari}.
    Le regole per costruire espressioni regolari sono:
    \begin{itemize}
        \item Se $\alpha \in \Sigma$, allora $\alpha$ è un'espressione regolare (es. $0$, $1$).
        \item Se $\alpha$ e $\beta$ sono espressioni regolari, allora anche la loro \textbf{concatenazione} $\alpha\beta$ è un'espressione regolare (es. $00$, $0111$).
        \item Se $\alpha$ e $\beta$ sono espressioni regolari, allora anche la loro \textbf{disgiunzione} $\alpha \lor \beta$ (spesso scritta $\alpha + \beta$ o $\alpha | \beta$) è un'espressione regolare. Rappresenta le stringhe che sono generate da $\alpha$ oppure da $\beta$ (es. $0 | 11$ accetta $0$ o $11$).
        \item Se $\alpha$ è un'espressione regolare, allora $\alpha^*$ (Kleene Star) è un'espressione regolare. Rappresenta una concatenazione di zero o più occorrenze di $\alpha$ (es. $(10)*$ accetta $\epsilon$, $10$, $1010$, ...).
        \item A volte si usa $\alpha^+$ (Kleene Plus) per indicare una concatenazione di una o più occorrenze di $\alpha$ (es. $(10)+$ accetta $10$, $1010$, ... ma non $\epsilon$).
    \end{itemize}
\end{definition}

\textbf{Proprietà}: I linguaggi riconoscibili dagli automi a stati finiti sono esattamente i linguaggi regolari. Il linguaggio $\{a^n b^n \mid n \ge 0\}$ \textbf{non è un linguaggio regolare}, motivo per cui gli FA non possono riconoscerlo.

\section{Macchine di Turing (TM)}
Per risolvere problemi più complessi come $\{a^n b^n \mid n \ge 0\}$, è necessario un modello di calcolo più potente: la Macchina di Turing.

\subsection{Introduzione e Concetti Base}
La Macchina di Turing è un modello astratto di calcolo ideato da Alan Turing negli anni '30 per formalizzare il concetto di "calcolabilità".

\textbf{Differenze chiave rispetto agli Automi a Stati Finiti}:
\begin{itemize}
    \item \textbf{Nastro Infinito}: La TM opera su un nastro infinito in entrambe le direzioni, diviso in celle.
    \item \textbf{Simbolo Blank ($\texttt{B}$)}: Le celle vuote del nastro contengono un simbolo speciale (blank) per delimitare la stringa di input.
    \item \textbf{Testina di Lettura/Scrittura}: La TM ha una testina che può non solo leggere un simbolo, ma anche \emph{scrivere} (sovrascrivere) un simbolo sulla cella corrente.
    \item \textbf{Movimento Bidirezionale della Testina}: La testina può spostarsi a destra ($\texttt{R}$) o a sinistra ($\texttt{L}$) lungo il nastro.
\end{itemize}
Queste capacità conferiscono alla TM una "memoria" infinita e la capacità di manipolare i dati sul nastro, superando le limitazioni degli FA.

\textbf{Funzionamento}:
Una TM è un automa con un numero finito di stati. Ogni passo della computazione è determinato dallo stato corrente e dal simbolo letto dalla testina. La TM esegue le seguenti azioni:
\begin{enumerate}
    \item Transisce a un nuovo stato.
    \item Scrive un simbolo sulla cella corrente (sovrascrivendo quello precedente).
    \item Sposta la testina a destra o a sinistra.
\end{enumerate}

\subsection{Definizione Formale della Macchina di Turing}
\begin{definition}[Macchina di Turing]
    Una Macchina di Turing (TM) $M$ è una 7-tupla:
    $M = \langle \Sigma, \Gamma, B, Q, q_0, F, \delta \rangle$
    dove:
    \begin{itemize}
        \item $\Sigma$: è l'alfabeto di input (insieme finito di simboli che possono apparire nella stringa di input).
        \item $\Gamma$: è l'alfabeto di nastro (insieme finito di simboli che possono essere scritti sul nastro). Deve essere $\Sigma \subseteq \Gamma$.
        \item $B \in \Gamma \setminus \Sigma$: è il simbolo di blank, non è parte dell'input e indica una cella vuota.
        \item $Q$: è l'insieme finito degli stati interni della macchina.
        \item $q_0 \in Q$: è lo stato iniziale.
        \item $F \subseteq Q$: è l'insieme degli stati accettanti (o finali).
        \item $\delta: Q \times \Gamma \to Q \times \Gamma \times \{L, R\}$: è la funzione di transizione (parziale). Data una coppia (stato corrente, simbolo letto), restituisce una tripla (nuovo stato, simbolo da scrivere, direzione del movimento della testina).
    \end{itemize}
\end{definition}

\subsection{Esempio: Macchina di Turing per $L = \{a^n b^n \mid n \ge 0\}$}
\textbf{Strategia intuitiva}:
L'idea è "barrare" una 'a' dal lato sinistro e una 'b' dal lato destro ad ogni passo, tornando indietro e avanti fino a quando tutte le 'a' e 'b' sono state "consumate". Se il numero di 'a' e 'b' è lo stesso, il nastro sarà vuoto alla fine.

\begin{figure}[h!]
    \centering
    \begin{tikzpicture}[
        shorten >=1pt, node distance=2cm, on grid, auto,
        state/.style={rectangle, draw, minimum width=2.5cm, minimum height=0.7cm},
        head/.style={circle, draw, minimum size=0.5cm, fill=gray!20}
    ]
    \node[state] (tape) at (0,0) {};
    \draw (tape.north west) -- (tape.south west) node[below=0.2cm] {$\dots$};
    \draw (tape.north east) -- (tape.south east) node[below=0.2cm] {$\dots$};
    \foreach \x/\y in {-2.5/B, -1.5/B, -0.5/A, 0.5/A, 1.5/B, 2.5/B} {
        \draw (\x,0.35) rectangle (\x+1,-0.35) node[pos=.5] {\y};
    }
    \node[head] (head) at (0,1.2) {$\delta$};
    \draw[->] (head) -- (0,0.35);
    \node[below=0.5cm of tape] (blank_sym) {$\texttt{B}$ (Blank)};
    \end{tikzpicture}
    \caption{Rappresentazione del nastro della Macchina di Turing con testina.}
\end{figure}

\begin{figure}[h!]
    \centering
    \begin{tikzpicture}[shorten >=1pt,node distance=2.5cm,on grid,auto]
        \node[state,initial] (q0)   {$q_0$};
        \node[state] (q1) [right of=q0] {$q_1$};
        \node[state] (q2) [right of=q1] {$q_2$};
        \node[state] (q3) [below of=q0] {$q_3$};
        \node[state,accepting] (q4) [below of=q1] {$q_4$};

        \path[->]
        (q0) edge [bend left] node {A/B, R} (q1) % First A to blank
        (q1) edge [loop above] node {A/A, R} (q1) % Skip A's
        (q1) edge [loop below] node {B/B, R} (q1) % Skip B's
        (q1) edge [bend left] node {B/B, L} (q2) % Found Blank, go left to find last B (Correction based on lecture: should be Blank/Blank, L)
        (q2) edge [loop below] node {B/B, L} (q2) % Skip B's backwards
        (q2) edge [bend right] node {A/B, L} (q3) % Found A, replace with Blank, go left (Correction based on lecture: should be B/Blank, L)
        (q3) edge [loop above] node {A/A, L} (q3) % Skip A's backwards
        (q3) edge [loop below] node {B/B, L} (q3) % Skip B's backwards
        (q3) edge [bend left] node {B/B, R} (q0) % Found Blank, go right to q0 (Correction based on lecture: should be Blank/Blank, R)
        (q0) edge [bend right] node {B/B, R} (q4); % If all A's and B's are matched, hit blank and accept (Correction based on lecture: should be Blank/Blank, R)
    \end{tikzpicture}
    \caption{Diagramma di stati per la Macchina di Turing che riconosce $L = \{a^n b^n \mid n \ge 0\}$.\\ \small{\textbf{Nota}}: Le etichette degli archi sono nel formato: \texttt{SimboloLetto/SimboloScritto, DirezioneMovimento}.}
\end{figure}

\textbf{Traccia del funzionamento (con correzioni al diagramma basate sulla discussione):}
1.  \textbf{$q_0$ (Stato Iniziale)}:
    \begin{itemize}
        \item Se legge \texttt{A}: Scrive \texttt{B} (blank), sposta a destra ($\texttt{R}$), va in $q_1$. (Cancella la prima 'a').
        \item Se legge \texttt{B} o \texttt{B} (blank, dopo aver cancellato tutto): Significa che non ci sono più 'a' da cancellare, quindi verifica se anche le 'b' sono finite. Se legge \texttt{B} e sposta a destra, va in $q_4$ (stato accettante).
    \end{itemize}
2.  \textbf{$q_1$ (Cerca l'ultima 'b')}:
    \begin{itemize}
        \item Se legge \texttt{A}: Scrive \texttt{A}, sposta a destra ($\texttt{R}$), rimane in $q_1$. (Salta le 'a' rimanenti).
        \item Se legge \texttt{B}: Scrive \texttt{B}, sposta a destra ($\texttt{R}$), rimane in $q_1$. (Salta le 'b' fino alla fine).
        \item Se legge \texttt{B} (blank): Scrive \texttt{B}, sposta a sinistra ($\texttt{L}$), va in $q_2$. (Ha raggiunto la fine della stringa, torna indietro per trovare l'ultima 'b').
    \end{itemize}
3.  \textbf{$q_2$ (Cancella l'ultima 'b')}:
    \begin{itemize}
        \item Se legge \texttt{B}: Scrive \texttt{B} (blank), sposta a sinistra ($\texttt{L}$), va in $q_3$. (Cancella l'ultima 'b' e inizia a tornare all'inizio della stringa).
    \end{itemize}
4.  \textbf{$q_3$ (Torna all'inizio)}:
    \begin{itemize}
        \item Se legge \texttt{A}: Scrive \texttt{A}, sposta a sinistra ($\texttt{L}$), rimane in $q_3$. (Salta le 'a' rimanenti, andando a sinistra).
        \item Se legge \texttt{B}: Scrive \texttt{B}, sposta a sinistra ($\texttt{L}$), rimane in $q_3$. (Salta le 'b' rimanenti, andando a sinistra).
        \item Se legge \texttt{B} (blank): Scrive \texttt{B}, sposta a destra ($\texttt{R}$), va in $q_0$. (Ha raggiunto l'inizio della stringa, torna a $q_0$ per la prossima iterazione).
    \end{itemize}
5.  \textbf{$q_4$ (Stato Accettante)}:
    \begin{itemize}
        \item Questo è uno stato accettante. Se la TM raggiunge $q_4$, la stringa è accettata.
    \end{itemize}

\textbf{Gestione di stringhe "strane" (e.g., \texttt{ABaAB})}:
La macchina come progettata dovrebbe rifiutare stringhe che non seguono il pattern $a^*b^*$.
\begin{itemize}
    \item Se $q_0$ legge \texttt{B} all'inizio, si muove a $q_4$, quindi accetterebbe la stringa vuota $\epsilon$. Ma se è $B$ e non $\epsilon$, si muoverebbe a $q_4$ e accetterebbe una stringa di soli $B$s (e.g., $B$). Questo è un dettaglio che richiede un'attenzione specifica sul caso base $\epsilon$ o stringhe di soli $B$s.
    \item Se in $q_1$ (dopo aver cancellato la prima 'a' e spostato a destra) la macchina incontra un'altra 'a' dopo una 'b', non ci sono transizioni definite per questa sequenza, e la macchina si blocca, rifiutando la stringa.
\end{itemize}

\subsection{Computazione di una Macchina di Turing}
Per formalizzare il comportamento di una TM, si introduce il concetto di \textbf{configurazione}.

\begin{definition}[Configurazione di una TM]
    Una \textbf{configurazione} di una TM $M$ è una fotografia dello stato corrente di esecuzione della macchina. È rappresentata da una stringa che include:
    \begin{itemize}
        \item La parte non-blank del nastro.
        \item Lo stato corrente della TM.
        \item La posizione della testina sul nastro.
    \end{itemize}
    La notazione comune è $uqv$, dove $u$ è la stringa sul nastro a sinistra della testina, $q$ è lo stato corrente, e $v$ è la stringa sul nastro a destra della testina (incluso il simbolo letto dalla testina, che è il primo simbolo di $v$). Si omettono i blank a meno che non siano rilevanti per la posizione della testina.
    \begin{example}
        \begin{itemize}
            \item $A q_1 BB$: La stringa sul nastro è $\texttt{ABB}$, la TM è nello stato $q_1$, e la testina sta leggendo il primo $\texttt{B}$.
            \item $ABB q_1 \texttt{B}$: La stringa sul nastro è $\texttt{ABB}$, la TM è nello stato $q_1$, e la testina sta leggendo il primo blank a destra della stringa.
        \end{itemize}
    \end{example}
\end{definition}

\begin{definition}[Successore Legale di una Configurazione]
    Date due configurazioni $C_1$ e $C_2$ per una TM $M$, diciamo che $C_2$ è un \textbf{successore legale} (o raggiungibile in un passo) di $C_1$ rispetto a $M$, e scriviamo $C_1 \xrightarrow{M} C_2$, se $C_2$ è la configurazione che $M$ raggiunge partendo da $C_1$ ed eseguendo un solo passo secondo la sua funzione di transizione $\delta$.
    \begin{example}
        Per la TM di $a^n b^n$, se $C_1 = q_0 AABB$, allora $C_2 = B q_1 ABB$ (dopo aver cancellato la prima A e mosso a destra).
    \end{example}
\end{definition}

\begin{definition}[Configurazione Iniziale]
    La \textbf{configurazione iniziale} di una TM $M$ su una stringa di input $w = w_1 w_2 \dots w_n$ è $q_0 w_1 w_2 \dots w_n$. Si assume che la testina sia sul primo simbolo di $w$.
\end{definition}

\begin{definition}[Configurazione Finale]
    Una \textbf{configurazione finale} è una configurazione $C$ per la quale la funzione di transizione $\delta$ non è definita per la combinazione (stato di $C$, simbolo letto in $C$). In altre parole, la macchina si blocca.
\end{definition}

\begin{definition}[Configurazione Accettante]
    Una \textbf{configurazione accettante} è una configurazione finale il cui stato corrente appartiene all'insieme degli stati accettanti $F$.
\end{definition}

\begin{definition}[Configurazione Rifiutante]
    Una \textbf{configurazione rifiutante} è una configurazione finale il cui stato corrente \textbf{non} appartiene all'insieme degli stati accettanti $F$.
\end{definition}

\begin{definition}[Computazione Parziale]
    Una \textbf{computazione parziale} di una TM $M$ è una sequenza di configurazioni $C_1, C_2, \dots, C_k$ tale che $C_i \xrightarrow{M} C_{i+1}$ per ogni $1 \le i < k$.
\end{definition}

\begin{definition}[Computazione (Completa)]
    Una \textbf{computazione} di una TM $M$ su una stringa di input $w$ è una computazione parziale $C_1, C_2, \dots, C_k$ tale che:
    \begin{itemize}
        \item $C_1$ è la configurazione iniziale di $M$ su $w$.
        \item $C_k$ è una configurazione finale.
    \end{itemize}
\end{definition}

\begin{definition}[Computazione Accettante]
    Una \textbf{computazione accettante} di una TM $M$ su una stringa $w$ è una computazione $C_1, \dots, C_k$ dove $C_k$ è una configurazione accettante.
\end{definition}

\begin{definition}[Linguaggio di una Macchina di Turing $L(M)$]
    Il \textbf{linguaggio di una Macchina di Turing $M$}, denotato $L(M)$, è l'insieme di tutte le stringhe $w$ tali che la computazione di $M$ su $w$ è accettante.
    $L(M) = \{w \mid M \text{ accetta } w\}$
    Se una TM non si ferma mai su una stringa $w$, allora $w$ non appartiene a $L(M)$. Se si ferma in uno stato non accettante, $w$ non appartiene a $L(M)$.
\end{definition}

\subsection{Accettazione vs. Decisione di un Linguaggio}

La differenza tra "accettare" e "decidere" un linguaggio da parte di una Macchina di Turing è fondamentale in Teoria della Computabilità.

\begin{definition}[Macchina di Turing che Decide un Linguaggio]
    Una Macchina di Turing $M$ \textbf{decide} un linguaggio $L$ se e solo se per ogni stringa $w \in \Sigma^*$:
    \begin{itemize}
        \item Se $w \in L$, allora $M$ si arresta (termina) e accetta $w$ (ovvero, la computazione termina in una configurazione accettante).
        \item Se $w \notin L$, allora $M$ si arresta (termina) e rifiuta $w$ (ovvero, la computazione termina in una configurazione rifiutante).
    \end{itemize}
    In questo caso, si dice che $L$ è un \textbf{linguaggio decidibile}. Una macchina che decide garantisce una risposta (Sì o No) in tempo finito per ogni input.
\end{definition}

\begin{definition}[Macchina di Turing che Accetta un Linguaggio]
    Una Macchina di Turing $M$ \textbf{accetta} un linguaggio $L$ se e solo se per ogni stringa $w \in \Sigma^*$:
    \begin{itemize}
        \item Se $w \in L$, allora $M$ si arresta e accetta $w$.
        \item Se $w \notin L$, allora $M$ \textbf{non accetta} $w$. Ciò significa che $M$ potrebbe arrestarsi e rifiutare $w$, \emph{oppure potrebbe non arrestarsi mai (loop indefinitamente)}.
    \end{itemize}
    In questo caso, si dice che $L$ è un \textbf{linguaggio accettabile} (o ricorsivamente enumerabile).
\end{definition}

\textbf{Implicazioni}:
\begin{itemize}
    \item La classe dei linguaggi decidibili è un sottoinsieme stretto della classe dei linguaggi accettabili.
    \item Se un linguaggio è decidibile, allora è anche accettabile.
    \item Esistono linguaggi che sono accettabili ma non decidibili. Per questi linguaggi, se la risposta è "Sì", la macchina terminerà e lo dirà. Ma se la risposta è "No", la macchina potrebbe entrare in un loop infinito e non fornire mai una risposta, lasciando l'utente in attesa indefinita. Questo è un problema fondamentale in Informatica Teorica.
\end{itemize}

\end{document}