Discrete Mathematics YMT211 · Fall 2026 Assoc. Prof. Ferhat Uçar

>_ Lab 4 · Week 5 · Monday, October 19, 2026

Nested Quantifiers: Checking a Social Network

Write the four shapes of a nested statement, find witnesses and counterexamples, check a social network for celebrities and unreturned follows, negate a nested statement in code, and turn a sorting specification into a test.

week
5 · Mon, Oct 19
due
Fri, Oct 23 · 23:59
assignment
lab04
points
100 (autograded)
time
75 min + finish at home
files you edit
network.py

Goal

This lab turns the week 5 notes into code. You will work with a small social network of eight fictional people and check statements with two quantifiers: does everyone follow someone, is there a celebrity whom everyone follows, is every follow returned? Along the way you will see, in your own code, why the order of ∀\forall and ∃\exists matters.

All your work is in one file, network.py. Every task is short, and later tasks reuse earlier ones, so please work from top to bottom.

Steps

1

Accept Lab 4 and open it

⏱ 3 min

Click the orange Accept lab lab04 button at the top of this page and then Accept assignment. When the checklist finishes, click Open repository, and open it with the green Code button → Codespaces → Create codespace on main.

If you prefer to work on your own computer or in another editor, that is fine as well; the section Other ways to work and submit explains how.

2

Meet the network

⏱ 5 min

Open data.py. The network is a dictionary that maps each person to the set of people they follow:

FOLLOWS = {
    "Ada":  {"Bora"},
    "Bora": {"Ada", "Can"},
    ...
}

So FOLLOWS["Bora"] is {"Ada", "Can"}, which means that Bora follows Ada and Can. Nobody follows themselves.

At the top of network.py you will find two small helper functions that are already written for you. follows(net, x, y) answers the question “does xx follow yy?”, and people(net) gives the list of everyone in the network. You will use them in almost every task.

follows(FOLLOWS, "Bora", "Can")    # True
follows(FOLLOWS, "Can", "Bora")    # False
people(FOLLOWS)                    # ['Ada', 'Bora', 'Can', ...]

Now run the checks once, before you change anything. Every check should report that it is not implemented yet.

>_ terminal
$ python check.py:| forall_exists: [1,2,3] with x+y=4 → True, [1,2] → False
  not implemented yet (still raises NotImplementedError)
...
0/18 checks passed
3

The four shapes of a nested statement

⏱ 10 min

Part A asks for four general functions. Each one receives a list D (the domain) and a two-place predicate P(x, y), a function of two elements that returns True or False, such as lambda x, y: x + y == 4.

From the notes, a nested statement is a loop inside a loop, and in Python it is a call of all or any inside another one. The outer function belongs to xx, the inner one to yy:

# ∀x ∃y P(x, y): for every x (outer all), there is some y (inner any)
all(any(P(x, y) for y in D) for x in D)

Write forall_forall, forall_exists, exists_forall and exists_exists in the same way, choosing all or any for each position. Before you run the checks, predict the answers to these two lines and then confirm them:

forall_exists([1, 2, 3], lambda x, y: x + y == 4)   # True: 1 pairs with 3, 2 with 2, 3 with 1
exists_forall([1, 2, 3], lambda x, y: x + y == 4)   # False: no single x makes every sum equal to 4

The same predicate, the same domain, opposite answers. That is the whole message of this week.

4

Witnesses and the first failure

⏱ 10 min

Knowing that “everyone follows someone” is true is useful, but a program usually needs to know whom each person follows. That is what a witness is.

witnesses(D, P) returns a dictionary that gives every xx its first witness yy, or None if xx has none. Python’s next with a default is a neat tool here: next((y for y in D if P(x, y)), None) returns the first matching yy, or None when there is none.

witnesses([1, 2, 3], lambda x, y: x + y == 4)   # {1: 3, 2: 2, 3: 1}
witnesses([1, 2], lambda x, y: x + y == 4)      # {1: None, 2: 2}

first_failure(D, P) returns the first xx that has no witness at all. That xx is exactly the counterexample to ∀x ∃y  P(x,y)\forall x\, \exists y\; P(x, y), and it is None when the statement is true.

5

Q1–Q3: following, celebrities and super-fans

⏱ 15 min

Now the network. In every question, “everyone” means “everyone else”, because nobody follows themselves. Each function receives the network as its parameter net, so always work with net, follows(net, …) and people(net), never with the global FOLLOWS.

Q1, everyone_follows_someone. The statement is ∀x ∃y  (x≠y∧F(x,y))\forall x\, \exists y\; (x \ne y \land F(x, y)). You can answer it in one line with your forall_exists, giving it people(net) and the predicate lambda x, y: x != y and follows(net, x, y).

Q2, celebrity. The statement is ∃y ∀x  (x≠y→F(x,y))\exists y\, \forall x\; (x \ne y \to F(x, y)), but this time the question wants the name of the celebrity, or None. Go through every candidate yy; a candidate is a celebrity when every other person follows them, that is, when all(x == y or follows(net, x, y) for x in P) is true. Notice how the implication x≠y→F(x,y)x \ne y \to F(x, y) became x == y or ...: an implication is false only when its hypothesis holds and its conclusion fails.

Q3, someone_follows_everyone. The mirror image of Q2: ∃x ∀y  (x≠y→F(x,y))\exists x\, \forall y\; (x \ne y \to F(x, y)), a person whose own row is full.

>_ terminal
$ python network.pyQ1 everyone follows someone : True
Q2 celebrity                : Ada
...

Look at data.py and check the answers by eye. Everyone except Ada follows Ada, so Ada is the celebrity. Because a celebrity exists, Q1 had to be true as well; this is the one direction that always works.

6

Q4–Q6: mutual follows and loners

⏱ 15 min

Q5, first_unreturned (do this one before Q4). Find the first pair (x,y)(x, y) where xx follows yy but yy does not follow xx back. Use two for loops over people(net), the outer one for xx and the inner one for yy, and return the tuple (x, y) as soon as you find such a pair. If the loops finish, return None.

Q4, all_mutual. The statement ∀x ∀y  (F(x,y)→F(y,x))\forall x\, \forall y\; (F(x, y) \to F(y, x)) is true exactly when Q5 finds no counterexample. That makes this a one-line function.

Q6, loners. A loner follows nobody and is followed by nobody: for every yy, neither F(x,y)F(x, y) nor F(y,x)F(y, x). Return the names of all loners as a list, in the order of people(net). A list comprehension with an all(...) inside it does the job.

>_ terminal
$ python network.pyQ4 all follows mutual       : False
Q5 first unreturned follow  : ('Bora', 'Can')
Q6 loners                   : []
7

Negating a nested statement

⏱ 8 min

From the notes, a negation moves inwards and flips every quantifier it passes:

¬ ∀x ∃y  P(x,y)  ≡  ∃x ∀y  ¬P(x,y)\neg\, \forall x\, \exists y\; P(x, y) \;\equiv\; \exists x\, \forall y\; \neg P(x, y)

Write not_forall_exists(D, P) as the right-hand side: call your exists_forall with a new predicate that returns the opposite of P, that is, lambda x, y: not P(x, y).

The checks compare your answer with the real negation on 60 random cases, and they also read your code. If you write not forall_exists(D, P), the value is correct but the rule is skipped, so that check will not pass.

8

A specification for sorting

⏱ 10 min

The last part puts the specification from the notes into code.

is_sorted(b) checks ∀i  (b[i]≤b[i+1])\forall i\; (b[i] \le b[i+1]) for every position ii that has a next element. keeps_values(a, b) checks ∀i ∃j  (a[i]=b[j])\forall i\, \exists j\; (a[i] = b[j]), which is a nested statement like the ones in Part A.

is_correct_sort(a, b) is the complete specification: bb must be sorted and every value must occur the same number of times in aa and in bb. The notes showed why keeps_values alone is not enough, and the checks include exactly that trap:

is_correct_sort([3, 3, 1], [1, 3, 3])   # True
is_correct_sort([3, 3, 1], [1, 3, 5])   # False: a 3 was lost and a 5 appeared from nowhere

list.count(v) tells you how many times v occurs in a list. Compare the counts for every value that appears in either list.

9

Submit

⏱ 1 min

When python check.py reports 18/18 checks passed, or when you have done as much as you can, push your work. Every push is graded out of 100.

>_ terminal
$ git add -A$ git commit -m "Lab 4"$ git push

About two minutes later your score appears under Releases on your repository page. Your last push before Friday 23:59 is the one that counts.

Other ways to work and submit

You do not have to use Codespaces or a terminal.

Work anywhere: on your own computer (download the repository with Code → Download ZIP, then run pip install pytest and python check.py), on cs50.dev, in Google Colab, or in github.dev by pressing . on your repository page.

Submit from the browser: open network.py in your repository on github.com, click the pencil icon ✎, select everything and paste your whole file, then click Commit changes. Alternatively, use Add file → Upload files and drag your network.py in. Keep the file name exactly network.py and leave data.py unchanged. Every commit is graded in the same way as a push.

Explore: the notebook

The notebook draws the network so that you can see the celebrity, shows the follow relation as a grid of rows and columns, and runs an experiment on thousands of random networks: “everyone follows someone” becomes true very quickly as people follow more, while a celebrity remains rare until almost everyone follows almost everyone. It also measures how the cost of a nested check grows with the size of the network.

Lab 4 · Explore notebook

Read it here, run it in Colab or in your cs50.dev workspace.

Challenges (optional, not graded)

  1. Followers count. Write most_followed(net), which returns the person with the most followers. Is a celebrity always the most followed person? Is the most followed person always a celebrity?
  2. Two-step reach. Write everyone_reaches(net, x), which checks ∀y  (x≠y→(F(x,y)∨∃z (F(x,z)∧F(z,y))))\forall y\; (x \ne y \to (F(x, y) \lor \exists z\, (F(x, z) \land F(z, y)))): everyone else is followed by xx directly or by someone xx follows.
  3. Order matters, by search. Find a two-place predicate and a domain of three numbers for which ∀x ∃y\forall x\, \exists y is true and ∃y ∀x\exists y\, \forall x is false, and test them with your Part A functions.

Troubleshooting