Introduction to Discrete Mathematics

Relations and Functions

Notes By Pr. El Hadiq Zouhair

1 / 14

Overview

Python / SymPy — imports (run once) from itertools import product from sympy import symbols, Function, Lambda, solve, log, exp, sqrt, oo, S, Interval x, y = symbols('x y', real=True)
2 / 14

Relations

A relation $R$ between sets $A$ and $B$ is any subset of the Cartesian product:

$R \subseteq A \times B$

Each ordered pair $(a, b) \in R$ means "$a$ is related to $b$" (often written $aRb$).

Python — relation as a set of tuples A = {1, 2, 3} B = {"x", "y", "z"} R = {(1, "x"), (1, "y"), (2, "y"), (2, "z"), (3, "x")} # Is R a relation from A to B? print(R.issubset({(a, b) for a in A for b in B})) # True
Python — relation as a dict (multi-valued) from collections import defaultdict R_dict = defaultdict(set) for a, b in R: R_dict[a].add(b) print(dict(R_dict)) # {1: {'x','y'}, 2: {'y','z'}, 3: {'x'}}
3 / 14

Graphing

Relations can be visualized as the links between the input set and the output set — a bipartite graph.

1 2 3 x y z

Bipartite graph for $R = \{(1,x),(1,y),(2,y),(2,z),(3,x)\}$

Python — networkx bipartite drawing # pip install networkx matplotlib import networkx as nx, matplotlib.pyplot as plt R = [(1,"x"),(1,"y"),(2,"y"),(2,"z"),(3,"x")] G = nx.DiGraph(); G.add_edges_from(R) left = {1, 2, 3} pos = {**{n: (0, -i) for i, n in enumerate(sorted(left))}, **{n: (1, -i) for i, n in enumerate(["x","y","z"])}} nx.draw(G, pos, with_labels=True, node_color="#fff", edgecolors="#2c3e50", edge_color="#3a6fb3", arrows=True) plt.show()
4 / 14

Functions

A relation $R \subseteq A \times B$ is called many-to-one iff each $a \in A$ has a unique $b \in B$ with $(a,b) \in R$. We then write $f(a) = b$.

A function is defined as a many-to-one relation.

Python — test whether a relation is a function def is_function(R, A): """Each a in A must have exactly one image in R.""" counts = {a: 0 for a in A} for a, b in R: counts[a] += 1 return all(c == 1 for c in counts.values()) R1 = {(1,"x"),(2,"y"),(3,"x")} # function R2 = {(1,"x"),(1,"y"),(2,"z")} # not a function (1 has two images) print(is_function(R1, {1,2,3})) # True print(is_function(R2, {1,2})) # False
Python — functions as code def f(x): # named function return x ** 2 + 1 g = lambda x: 2*x + 3 # anonymous (lambda) print(f(3), g(3)) # 10, 9
5 / 14

Valid function?

For a graph to be a valid function, every element of the domain must have exactly one outgoing arrow.

1 2 3 4 5 6 x y z

✓ Valid function — each of 1..6 has one image

1 2 3 4 5 6 x y z

✗ Not a function — 3 maps to two images

Vertical line test (for real-valued graphs): a curve is a function iff every vertical line intersects it at most once. A spiral fails this test; the upper half of a circle passes it.

6 / 14

Domain and Range

Consider a function $f : A \to B$.

Python — finite function f = {1:"a", 2:"b", 3:"a"} domain = set(f.keys()) # {1, 2, 3} ran = set(f.values()) # {'a', 'b'} print(domain, ran)
SymPy — symbolic function from sympy import symbols, sqrt, log, S, Interval, solveset x = symbols('x', real=True) # domain of sqrt(x): [0, ∞) print(solveset(sqrt(x) - x, x, domain=S.Reals)) print(Interval(0, S.Infinity))
SymPy — domain / range with solveset from sympy import Symbol, log, S, calculus x = Symbol('x', real=True) # domain of log(x) : (0, ∞) print(calculus.util.continuous_domain(log(x), x, S.Reals)) # range of sin(x) : [-1, 1] from sympy import sin print(calculus.util.function_range(sin(x), x, S.Reals))
7 / 14

Injection (one-to-one)

A function $f$ is injective iff $f(x_1) \neq f(x_2)$ whenever $x_1 \neq x_2$   (equivalently $f(x_1)=f(x_2) \Rightarrow x_1=x_2$).

Python — check injectivity def is_injective(f_dict): return len(set(f_dict.values())) == len(f_dict) print(is_injective({1:"v", 2:"x", 3:"w", 4:"z"})) # True print(is_injective({1:"x", 2:"x", 3:"y"})) # False
Python — symbolic check via SymPy from sympy import symbols, solve, Eq x1, x2 = symbols('x1 x2', real=True) # f(x) = 3x + 1 — solve f(x1) = f(x2) sols = solve(Eq(3*x1+1, 3*x2+1), x1) print(sols) # [x2] → only when x1 = x2, so injective

Horizontal line test: a real-valued function $f$ is injective iff every horizontal line crosses its graph at most once.

8 / 14

Surjection (onto)

A function $f : A \to B$ is surjective iff each $b \in B$ has at least one $a \in A$ with $f(a) = b$. Equivalently, $\text{range}(f) = B$.

Python — check surjectivity def is_surjective(f_dict, codomain): return set(f_dict.values()) == set(codomain) f = {1:"x", 2:"x", 3:"y", 4:"z", 5:"y", 6:"x"} print(is_surjective(f, {"x","y","z"})) # True
Python — common quick facts | Function | Injective | Surjective | Bijective | |-------------------|-----------|------------|-----------| | f(x) = 3x + 1 | yes | yes | yes | | f(x) = x² on ℝ | no | no | no | | f(x) = x² on ℝ⁺ | yes | yes | yes | | f(x) = eˣ | yes | no | no |
9 / 14

Bijection & Invertibility

A function $f$ is bijective (one-to-one correspondence) iff it is both injective and surjective.

A bijective function is invertible. For an invertible $f(x)$, the inverse function is denoted $f^{-1}(x)$, and satisfies $\;f^{-1}(f(x)) = x = f(f^{-1}(x))$.

Python — invert a finite bijection def invert(f_dict): if len(set(f_dict.values())) != len(f_dict): raise ValueError("not injective — no inverse") return {v: k for k, v in f_dict.items()} f = {1:"x", 2:"y", 3:"z"} print(invert(f)) # {'x':1, 'y':2, 'z':3}
SymPy — symbolic inverse from sympy import symbols, log, exp, solve, Eq x, y = symbols('x y', real=True) # Invert log(x): y = log(x) → x = e^y sol = solve(Eq(y, log(x)), x) print(sol) # [exp(y)]
10 / 14

Example — invertible?

Is $f(x) = x^2$ invertible on $\mathbb{R}$?  No — it is not injective (e.g. $f(-2) = f(2) = 4$). The horizontal line $y = 2$ crosses the parabola twice.

Is $f(x) = x^3$ invertible on $\mathbb{R}$?  Yes — strictly increasing, so injective and surjective on $\mathbb{R}$. Its inverse is $f^{-1}(x) = x^{1/3}$.

Python — numerical horizontal-line test import numpy as np def horiz_line_test(f, xs, c): """Count intersections of y=c with f over xs (by sign changes).""" ys = np.array([f(x) - c for x in xs]) return int(np.sum(np.sign(ys[:-1]) != np.sign(ys[1:]))) xs = np.linspace(-2, 2, 1001) print(horiz_line_test(lambda x: x**2, xs, 2.0)) # 2 → not injective print(horiz_line_test(lambda x: x**3, xs, 2.0)) # 1 → injective
SymPy — invert x³ from sympy import symbols, solve, Eq, Rational x, y = symbols('x y', real=True) print(solve(Eq(y, x**3), x)) # [y**(1/3)]
11 / 14

Composition

The composition of two functions is the nesting that forms a single new function: $(f \circ g)(x) = f(g(x))$.

Python — compose plain functions def compose(f, g): return lambda x: f(g(x)) f = lambda x: x + 1 g = lambda x: x * 2 h = compose(f, g) # h(x) = f(g(x)) = 2x + 1 print(h(3)) # 7
SymPy — symbolic composition from sympy import symbols, Lambda, log, exp x = symbols('x') f = Lambda(x, log(x)) g = Lambda(x, exp(x)) print(f(g(x))) # x print(g(f(x))) # x (perfect inverses)

Composition is the core mechanic of functional programming: in Python, functools.reduce, decorators, and map/filter/compose chains all rely on it.

Python — chain many functions with reduce from functools import reduce def pipe(*fs): return lambda x: reduce(lambda v, f: f(v), fs, x) # pipe(f, g, h) means h(g(f(x))) print(pipe(lambda x: x+1, lambda x: x*2, lambda x: x-3)(5)) # (5+1)*2-3 = 9
12 / 14

Example — $(f^{-1} \circ g^{-1})(x)$

Consider $f(x) = \dfrac{-x}{-1+x}$   and   $g(x) = \log(x)$.

Find the domain and range of $h(x) = (f^{-1} \circ g^{-1})(x)$.

SymPy — full computation from sympy import symbols, log, exp, solve, Eq, simplify, Lambda from sympy.calculus.util import continuous_domain, function_range from sympy import S x, y = symbols('x y', real=True) # 1) Invert f and g symbolically. f_inv = solve(Eq(y, -x/(-1+x)), x)[0] # → y/(1+y) g_inv = solve(Eq(y, log(x)), x)[0] # → exp(y) # 2) Build h(x) = f_inv(g_inv(x)) by substitution. h_expr = f_inv.subs(y, g_inv.subs(y, x)) h_expr = simplify(h_expr) print(h_expr) # exp(x)/(1 + exp(x)) # 3) Domain and range over the reals. print(continuous_domain(h_expr, x, S.Reals)) # (-oo, oo) print(function_range (h_expr, x, S.Reals)) # Interval.open(0, 1)
Result h(x) = e^x / (1 + e^x) # the logistic / sigmoid function domain : ℝ range : (0, 1)
13 / 14

Summary & Quick Reference

Concept Wolfram Python (built-in) SymPy
Function definition f[x_]:= …def f(x): …Lambda(x, …)
Domain FunctionDomainvia filtercontinuous_domain
Range FunctionRangevia imagefunction_range
Injective? FunctionInjectivelen(set(f.values()))via solve(f(x1)=f(x2))
Surjective? FunctionSurjectiveset(f.values()) == codomainvia function_range
Inverse InverseFunction{v:k for k,v in f.items()}solve(Eq(y, f(x)), x)
Composition f @ glambda x: f(g(x))f(g(x)) with Lambda
14 / 14