{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "fb6dde13",
      "metadata": {
        "id": "fb6dde13"
      },
      "source": [
        "# QBronze 1.5 — Algoritmo de búsqueda de Grover\n",
        "\n",
        "Este cuaderno desarrolla los ejercicios principales sobre el algoritmo de búsqueda de Grover.\n",
        "\n",
        "El problema abstracto es:\n",
        "\n",
        "$$\n",
        "\\text{Dado un espacio de }N\\text{ elementos, encontrar un elemento marcado.}\n",
        "$$\n",
        "\n",
        "Si\n",
        "\n",
        "$$\n",
        "N=2^n,\n",
        "$$\n",
        "\n",
        "se necesitan $n$ qubits para representar los elementos. Grover ofrece una mejora cuadrática: el número de consultas al oráculo escala como\n",
        "\n",
        "$$\n",
        "O(\\sqrt N),\n",
        "$$\n",
        "\n",
        "no como $O(N)$. No es una mejora exponencial.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f37aca05",
      "metadata": {
        "id": "f37aca05"
      },
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "from math import sqrt, pi, cos, sin, log2, floor\n",
        "from collections import Counter\n",
        "\n",
        "np.set_printoptions(precision=6, suppress=True)\n",
        "\n",
        "ket0 = np.array([1.0, 0.0])\n",
        "ket1 = np.array([0.0, 1.0])\n",
        "\n",
        "I = np.eye(2)\n",
        "X = np.array([[0.0, 1.0], [1.0, 0.0]])\n",
        "Z = np.array([[1.0, 0.0], [0.0, -1.0]])\n",
        "H = (1 / sqrt(2)) * np.array([[1.0, 1.0], [1.0, -1.0]])\n",
        "\n",
        "def tensor(*vectors_or_matrices):\n",
        "    \"\"\"Producto tensorial en el orden escrito: A ⊗ B ⊗ C.\"\"\"\n",
        "    result = np.array([1.0])\n",
        "    for item in vectors_or_matrices:\n",
        "        result = np.kron(result, item)\n",
        "    return result\n",
        "\n",
        "def norm2(state):\n",
        "    \"\"\"Norma al cuadrado: suma |amplitud|^2.\"\"\"\n",
        "    return float(np.vdot(state, state).real)\n",
        "\n",
        "def probs(state):\n",
        "    \"\"\"Probabilidades de medición en la base computacional.\"\"\"\n",
        "    return np.abs(state) ** 2\n",
        "\n",
        "def basis_state(bitstring):\n",
        "    \"\"\"Vector base |bitstring⟩ en orden lógico de izquierda a derecha.\"\"\"\n",
        "    n = len(bitstring)\n",
        "    index = int(bitstring, 2)\n",
        "    v = np.zeros(2**n)\n",
        "    v[index] = 1.0\n",
        "    return v\n",
        "\n",
        "def bitstrings(n):\n",
        "    return [format(i, f\"0{n}b\") for i in range(2**n)]\n",
        "\n",
        "def show_state(state, n=None, tol=1e-10):\n",
        "    \"\"\"Representación textual compacta de un vector de estado.\"\"\"\n",
        "    if n is None:\n",
        "        n = int(round(log2(len(state))))\n",
        "    terms = []\n",
        "    for amp, bits in zip(state, bitstrings(n)):\n",
        "        if abs(amp) > tol:\n",
        "            terms.append(f\"{amp:+.6g}|{bits}⟩\")\n",
        "    return \" \".join(terms) if terms else \"0\"\n",
        "\n",
        "def sample_counts(state, shots=1024, seed=7, qiskit_order=False):\n",
        "    \"\"\"Muestreo de una distribución de medición. Si qiskit_order=True, invierte las etiquetas.\"\"\"\n",
        "    rng = np.random.default_rng(seed)\n",
        "    p = probs(state)\n",
        "    n = int(round(log2(len(state))))\n",
        "    outcomes = rng.choice(2**n, p=p/p.sum(), size=shots)\n",
        "    labels = []\n",
        "    for i in outcomes:\n",
        "        bits = format(i, f\"0{n}b\")\n",
        "        labels.append(bits[::-1] if qiskit_order else bits)\n",
        "    return dict(sorted(Counter(labels).items()))\n",
        "\n",
        "def is_valid_quantum_state(v, tol=1e-9):\n",
        "    return abs(norm2(np.asarray(v, dtype=float)) - 1.0) < tol\n",
        "\n",
        "def uniform_state(N):\n",
        "    return np.ones(N) / sqrt(N)\n",
        "\n",
        "def phase_oracle(state, marked_indices):\n",
        "    \"\"\"Invierte el signo de los elementos marcados.\"\"\"\n",
        "    out = state.copy()\n",
        "    for idx in marked_indices:\n",
        "        out[idx] *= -1\n",
        "    return out\n",
        "\n",
        "def diffusion(state):\n",
        "    \"\"\"Inversión sobre el promedio: a_i -> 2*promedio - a_i.\"\"\"\n",
        "    mean = np.mean(state)\n",
        "    return 2*mean - state\n",
        "\n",
        "def grover_iteration(state, marked_indices):\n",
        "    return diffusion(phase_oracle(state, marked_indices))\n",
        "\n",
        "def label_to_index(label):\n",
        "    return int(label, 2)\n",
        "\n",
        "def index_to_label(index, n):\n",
        "    return format(index, f\"0{n}b\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "01a1060e",
      "metadata": {
        "id": "01a1060e"
      },
      "source": [
        "## 1. Qué sí afirma Grover\n",
        "\n",
        "Afirmaciones correctas:\n",
        "\n",
        "- Grover da una mejora cuadrática para búsqueda no estructurada:\n",
        "\n",
        "$$\n",
        "O(N)\\longrightarrow O(\\sqrt N).\n",
        "$$\n",
        "\n",
        "Afirmaciones incorrectas:\n",
        "\n",
        "- No da una mejora exponencial.\n",
        "- No mejora indefinidamente al aumentar el número de iteraciones. Después del punto óptimo, la probabilidad del elemento marcado puede volver a disminuir.\n",
        "- No se necesita conocer internamente qué elemento está marcado; se usa un oráculo que implementa la marca.\n",
        "\n",
        "La idea central es alternar dos operaciones:\n",
        "\n",
        "1. Oráculo: cambia el signo de los elementos marcados.\n",
        "2. Difusión: inversión de amplitudes alrededor del promedio."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "add53f81",
      "metadata": {
        "id": "add53f81"
      },
      "outputs": [],
      "source": [
        "N = 32\n",
        "print(\"Búsqueda clásica no estructurada: O(N) =\", N)\n",
        "print(\"Escala de Grover: O(sqrt(N)) ≈\", sqrt(N))\n",
        "print(\"Grover es cuadrático, no exponencial.\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "75e3824a",
      "metadata": {
        "id": "75e3824a"
      },
      "source": [
        "## 2. Phase kickback: marcar sin revelar el elemento marcado\n",
        "\n",
        "El oráculo booleano se puede escribir como\n",
        "\n",
        "$$\n",
        "B_f|x\\rangle|b\\rangle=|x\\rangle|b\\oplus f(x)\\rangle.\n",
        "$$\n",
        "\n",
        "Si el segundo registro se prepara como\n",
        "\n",
        "$$\n",
        "|-\\rangle=\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}},\n",
        "$$\n",
        "\n",
        "entonces ocurre:\n",
        "\n",
        "$$\n",
        "B_f|x\\rangle|-\\rangle=(-1)^{f(x)}|x\\rangle|-\\rangle.\n",
        "$$\n",
        "\n",
        "Así, si $f(x)=1$, el estado $|x\\rangle$ recibe un signo negativo. Si $f(x)=0$, queda igual. Esto permite marcar el elemento sin medirlo ni conocerlo explícitamente dentro del algoritmo."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "588484d4",
      "metadata": {
        "id": "588484d4"
      },
      "outputs": [],
      "source": [
        "# Ejemplo con N=4 y elemento marcado |01>.\n",
        "n = 2\n",
        "N = 2**n\n",
        "state = uniform_state(N)\n",
        "marked = [label_to_index(\"01\")]\n",
        "state_after_oracle = phase_oracle(state, marked)\n",
        "\n",
        "print(\"Estado inicial uniforme:\")\n",
        "for bits, amp in zip(bitstrings(n), state):\n",
        "    print(bits, amp)\n",
        "\n",
        "print(\"\\nDespués del oráculo que marca |01>:\")\n",
        "for bits, amp in zip(bitstrings(n), state_after_oracle):\n",
        "    print(bits, amp)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9fb88a04",
      "metadata": {
        "id": "9fb88a04"
      },
      "source": [
        "## 3. Número de qubits para representar 32 elementos\n",
        "\n",
        "Si el espacio de búsqueda tiene $32$ elementos, buscamos $n$ tal que\n",
        "\n",
        "$$\n",
        "2^n=32.\n",
        "$$\n",
        "\n",
        "Como\n",
        "\n",
        "$$\n",
        "32=2^5,\n",
        "$$\n",
        "\n",
        "se necesitan 5 qubits para representar los elementos. No se cuentan aquí qubits auxiliares."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "9f1a0a90",
      "metadata": {
        "id": "9f1a0a90"
      },
      "outputs": [],
      "source": [
        "N = 32\n",
        "n = int(log2(N))\n",
        "print(\"N =\", N)\n",
        "print(\"n = log2(N) =\", n)\n",
        "print(\"Verificación: 2^n =\", 2**n)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5c221f5a",
      "metadata": {
        "id": "5c221f5a"
      },
      "source": [
        "## 4. Crear superposición uniforme de 32 elementos\n",
        "\n",
        "Para $n=5$ qubits, el estado inicial es\n",
        "\n",
        "$$\n",
        "|00000\\rangle.\n",
        "$$\n",
        "\n",
        "Aplicar $H$ a cada qubit produce\n",
        "\n",
        "$$\n",
        "H^{\\otimes 5}|00000\\rangle\n",
        "=\n",
        "\\frac{1}{\\sqrt{32}}\\sum_{x=0}^{31}|x\\rangle.\n",
        "$$\n",
        "\n",
        "En código de circuito, conceptualmente:\n",
        "\n",
        "```python\n",
        "for i in range(5):\n",
        "    mycircuit.h(qreg[i])\n",
        "```\n",
        "\n",
        "Por tanto, en el fragmento\n",
        "\n",
        "```python\n",
        "for i in range(a):\n",
        "    mycircuit.b(qreg[i])\n",
        "```\n",
        "\n",
        "se usa\n",
        "\n",
        "$$\n",
        "a=5,\n",
        "\\qquad\n",
        "b=h.\n",
        "$$"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "5438cec5",
      "metadata": {
        "id": "5438cec5"
      },
      "outputs": [],
      "source": [
        "N = 32\n",
        "state = uniform_state(N)\n",
        "print(\"amplitud esperada de cada elemento:\", 1/sqrt(N))\n",
        "print(\"norma^2:\", norm2(state))\n",
        "print(\"primeros 8 elementos:\")\n",
        "for i in range(8):\n",
        "    print(index_to_label(i, 5), state[i])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "34c94fc5",
      "metadata": {
        "id": "34c94fc5"
      },
      "source": [
        "## 5. Preparar el ancilla en $|-\\rangle$\n",
        "\n",
        "Grover suele usar un qubit auxiliar en el estado\n",
        "\n",
        "$$\n",
        "|-\\rangle=\\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}.\n",
        "$$\n",
        "\n",
        "Desde $|0\\rangle$, se prepara así:\n",
        "\n",
        "$$\n",
        "|0\\rangle \\xrightarrow{X} |1\\rangle\n",
        "\\quad\\text{y}\\quad\n",
        "|1\\rangle \\xrightarrow{H} \\frac{|0\\rangle-|1\\rangle}{\\sqrt{2}}.\n",
        "$$\n",
        "\n",
        "Por tanto, se aplica primero $X$ y luego $H$."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "36345b19",
      "metadata": {
        "id": "36345b19"
      },
      "outputs": [],
      "source": [
        "ancilla = ket0.copy()\n",
        "ancilla = X @ ancilla\n",
        "ancilla = H @ ancilla\n",
        "print(\"ancilla:\", show_state(ancilla, n=1))\n",
        "print(\"vector:\", ancilla)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "76794fdc",
      "metadata": {
        "id": "76794fdc"
      },
      "source": [
        "## 6. Identificar el elemento marcado por el signo negativo\n",
        "\n",
        "Para 4 elementos, el estado uniforme es\n",
        "\n",
        "$$\n",
        "\\frac{|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle}{2}.\n",
        "$$\n",
        "\n",
        "Después de la fase de consulta, si el estado es\n",
        "\n",
        "$$\n",
        "\\frac{|00\\rangle-|01\\rangle+|10\\rangle+|11\\rangle}{2},\n",
        "$$\n",
        "\n",
        "el único término con signo negativo es $|01\\rangle$. Por tanto, el elemento marcado es\n",
        "\n",
        "$$\n",
        "|01\\rangle.\n",
        "$$"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "372e59c3",
      "metadata": {
        "id": "372e59c3"
      },
      "outputs": [],
      "source": [
        "state = np.array([1/2, -1/2, 1/2, 1/2])\n",
        "labels = bitstrings(2)\n",
        "for bits, amp in zip(labels, state):\n",
        "    print(bits, amp)\n",
        "marked = [bits for bits, amp in zip(labels, state) if amp < 0]\n",
        "print(\"elemento marcado por signo negativo:\", marked)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "17c0bb07",
      "metadata": {
        "id": "17c0bb07"
      },
      "source": [
        "## 7. Inversión sobre el promedio\n",
        "\n",
        "La fase de difusión de Grover se interpreta como inversión sobre el promedio.\n",
        "\n",
        "Si las amplitudes son\n",
        "\n",
        "$$\n",
        "a_0,a_1,\\ldots,a_{N-1},\n",
        "$$\n",
        "\n",
        "y el promedio es\n",
        "\n",
        "$$\n",
        "\\bar a=\\frac1N\\sum_i a_i,\n",
        "$$\n",
        "\n",
        "entonces cada amplitud se transforma como\n",
        "\n",
        "$$\n",
        "a_i' = 2\\bar a - a_i.\n",
        "$$\n",
        "\n",
        "Geométricamente, esto es una reflexión respecto al estado de superposición uniforme."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0a2a4f77",
      "metadata": {
        "id": "0a2a4f77"
      },
      "outputs": [],
      "source": [
        "amplitudes = np.array([0.5, -0.5, 0.5, 0.5])\n",
        "mean = np.mean(amplitudes)\n",
        "reflected = diffusion(amplitudes)\n",
        "print(\"amplitudes:\", amplitudes)\n",
        "print(\"promedio:\", mean)\n",
        "print(\"después de inversión sobre el promedio:\", reflected)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c5aa0b3d",
      "metadata": {
        "id": "c5aa0b3d"
      },
      "source": [
        "## 8. Una iteración completa para 4 elementos\n",
        "\n",
        "Tomemos $N=4$ y elemento marcado $|01\\rangle$.\n",
        "\n",
        "Estado inicial:\n",
        "\n",
        "$$\n",
        "|s\\rangle=\\frac{|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle}{2}.\n",
        "$$\n",
        "\n",
        "Después del oráculo:\n",
        "\n",
        "$$\n",
        "\\frac{|00\\rangle-|01\\rangle+|10\\rangle+|11\\rangle}{2}.\n",
        "$$\n",
        "\n",
        "El promedio de las amplitudes es\n",
        "\n",
        "$$\n",
        "\\bar a=\\frac{1/2-1/2+1/2+1/2}{4}=\\frac14.\n",
        "$$\n",
        "\n",
        "Aplicamos\n",
        "\n",
        "$$\n",
        "a_i'=2\\bar a-a_i.\n",
        "$$\n",
        "\n",
        "Para el marcado:\n",
        "\n",
        "$$\n",
        "a'_{01}=2\\cdot\\frac14-\\left(-\\frac12\\right)=1.\n",
        "$$\n",
        "\n",
        "Para un no marcado:\n",
        "\n",
        "$$\n",
        "a'=2\\cdot\\frac14-\\frac12=0.\n",
        "$$\n",
        "\n",
        "Resultado:\n",
        "\n",
        "$$\n",
        "|01\\rangle.\n",
        "$$\n",
        "\n",
        "Con 4 elementos y 1 marcado, una iteración basta para concentrar toda la probabilidad en el elemento correcto."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0f9299dc",
      "metadata": {
        "id": "0f9299dc"
      },
      "outputs": [],
      "source": [
        "n = 2\n",
        "N = 4\n",
        "state = uniform_state(N)\n",
        "marked = [label_to_index(\"01\")]\n",
        "print(\"inicial:\", show_state(state, n=n))\n",
        "\n",
        "state = phase_oracle(state, marked)\n",
        "print(\"después del oráculo:\", show_state(state, n=n))\n",
        "\n",
        "state = diffusion(state)\n",
        "print(\"después de difusión:\", show_state(state, n=n))\n",
        "print(\"probabilidades:\")\n",
        "for bits, prob in zip(bitstrings(n), probs(state)):\n",
        "    print(bits, prob)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ba9bf6d6",
      "metadata": {
        "id": "ba9bf6d6"
      },
      "source": [
        "## 9. Más iteraciones no siempre significan mejor resultado\n",
        "\n",
        "Grover es una rotación en un subespacio bidimensional: el eje de elementos marcados y el eje de elementos no marcados. Si se itera demasiado, el estado puede pasar de largo el punto óptimo.\n",
        "\n",
        "Para un marcado entre $N$ elementos, una aproximación común para el número de iteraciones es\n",
        "\n",
        "$$\n",
        "k\\approx \\left\\lfloor \\frac{\\pi}{4}\\sqrt N\\right\\rfloor.\n",
        "$$\n",
        "\n",
        "Para $N=32$:\n",
        "\n",
        "$$\n",
        "\\frac{\\pi}{4}\\sqrt{32}\\approx 4.44.\n",
        "$$\n",
        "\n",
        "Se esperan alrededor de 4 iteraciones, dependiendo de la convención de redondeo y del número de soluciones marcadas."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "e4ceb7ad",
      "metadata": {
        "id": "e4ceb7ad"
      },
      "outputs": [],
      "source": [
        "N = 32\n",
        "estimate = (pi/4) * sqrt(N)\n",
        "print(\"estimación continua:\", estimate)\n",
        "print(\"floor:\", floor(estimate))\n",
        "print(\"round:\", round(estimate))\n",
        "\n",
        "# Simulación para mostrar que la probabilidad sube y luego baja.\n",
        "n = 5\n",
        "marked = [17]\n",
        "state = uniform_state(N)\n",
        "for k in range(10):\n",
        "    p_marked = probs(state)[marked[0]]\n",
        "    print(f\"iteración {k}: P(marcado) = {p_marked:.6f}\")\n",
        "    state = grover_iteration(state, marked)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3cbbc5b4",
      "metadata": {
        "id": "3cbbc5b4"
      },
      "source": [
        "## 10. Simular Grover para 32 elementos\n",
        "\n",
        "Elegimos arbitrariamente el elemento marcado 17. En binario de 5 bits:\n",
        "\n",
        "$$\n",
        "17=10001_2.\n",
        "$$\n",
        "\n",
        "La simulación muestra cómo la probabilidad del elemento marcado aumenta durante las primeras iteraciones."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "01b2c3cd",
      "metadata": {
        "id": "01b2c3cd"
      },
      "outputs": [],
      "source": [
        "N = 32\n",
        "n = 5\n",
        "marked_index = 17\n",
        "marked_label = index_to_label(marked_index, n)\n",
        "state = uniform_state(N)\n",
        "\n",
        "print(\"elemento marcado:\", marked_index, \"=\", marked_label)\n",
        "for k in range(6):\n",
        "    probs_now = probs(state)\n",
        "    most_likely = int(np.argmax(probs_now))\n",
        "    print(f\"k={k}: P(marcado)={probs_now[marked_index]:.6f}, más probable={index_to_label(most_likely,n)}\")\n",
        "    state = grover_iteration(state, [marked_index])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55d35715",
      "metadata": {
        "id": "55d35715"
      },
      "source": [
        "## 11. Oráculo como caja negra\n",
        "\n",
        "El algoritmo no necesita conocer internamente cómo se implementa el oráculo. Sólo requiere que el oráculo produzca el efecto correcto:\n",
        "\n",
        "$$\n",
        "|x\\rangle\\mapsto (-1)^{f(x)}|x\\rangle.\n",
        "$$\n",
        "\n",
        "Si $f(x)=1$, el signo cambia. Si $f(x)=0$, el signo no cambia.\n",
        "\n",
        "Este es el mecanismo que permite que el elemento marcado sea distinguido por interferencia de amplitudes, no por inspección clásica directa."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c8e39428",
      "metadata": {
        "id": "c8e39428"
      },
      "outputs": [],
      "source": [
        "def black_box_f(x):\n",
        "    # La implementación interna podría estar oculta.\n",
        "    # Aquí la escribimos sólo para simular el comportamiento.\n",
        "    return 1 if x == 17 else 0\n",
        "\n",
        "def black_box_phase_oracle(state):\n",
        "    out = state.copy()\n",
        "    for x in range(len(out)):\n",
        "        if black_box_f(x) == 1:\n",
        "            out[x] *= -1\n",
        "    return out\n",
        "\n",
        "state = uniform_state(32)\n",
        "after = black_box_phase_oracle(state)\n",
        "negative_indices = [i for i, amp in enumerate(after) if amp < 0]\n",
        "print(\"índices con signo negativo:\", negative_indices)\n",
        "print(\"etiqueta binaria:\", [index_to_label(i, 5) for i in negative_indices])"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a42724b7",
      "metadata": {
        "id": "a42724b7"
      },
      "source": [
        "## 12. Resumen operativo del módulo\n",
        "\n",
        "Reglas clave:\n",
        "\n",
        "$$\n",
        "N=2^n \\quad\\Longrightarrow\\quad n=\\log_2(N)\\text{ qubits}.\n",
        "$$\n",
        "\n",
        "$$\n",
        "H^{\\otimes n}|0\\cdots0\\rangle=\\frac1{\\sqrt N}\\sum_{x=0}^{N-1}|x\\rangle.\n",
        "$$\n",
        "\n",
        "$$\n",
        "B_f|x\\rangle|-\\rangle=(-1)^{f(x)}|x\\rangle|-\\rangle.\n",
        "$$\n",
        "\n",
        "$$\n",
        "\\text{difusión: } a_i' = 2\\bar a-a_i.\n",
        "$$\n",
        "\n",
        "Grover alterna oráculo y difusión para amplificar la probabilidad del elemento marcado."
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.x"
    },
    "title": "QBronze 1.5 — Grover",
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}