Extra Lab Exercise

Graphs and Social Networks
(Adjacency Matrix)

Objectives

  • To explore an application of graphs
  • To get some practice with graph problems
  • To perform complexity analysis on graph algorithms
  • To implement some basic features of social networks

Admin

This lab is not marked and there is no submission for it.

Background

In lectures, we learned that a graph is a collection of vertices and edges between them. This very abstract definition allows for many real-world scenarios and systems to be modelled by graphs - for example, maps, social networks, and the web. In this lab, we will explore an application of graphs in a simple social network app called Friendbook.

Friendbook

Friendbook is a very simple social network app with the following features:

  • People can sign up with their name. For simplicity, people are identified by their names, so two people cannot have the same name.
  • People can friend other people (i.e., add them as friends). Friending goes both ways, so if you add someone as a friend, you become their friend as well.
  • People can unfriend their friends (i.e., remove them from their friends list). This also goes both ways.
  • People can see a count of how many friends they have.
  • People can see a list of their friends.
  • People can see a list of the mutual friends that they share with someone else.
  • People can receive friend recommendations. Friendbook has two different methods of generating recommendations:
    1. The first method only recommends friends of friends, and ranks friend recommendations in order of the number of mutual friends, so people who you share more mutual friends with will be recommended first.
    2. The second method recommends friends of friends first, and then friends of friends of friends next, and then friends of friends of friends of friends, and so on. Anyone who can be reached by following friendship links can be recommended.

Names as Vertices

All of the graph implementations we have seen so far have used integer vertices numbered from \( 0 \) to \( n - 1 \), where \( n \) is the number of vertices. This is convenient, as vertex numbers can double as indices into the adjacency matrix or adjacency list. But in Friendbook, the vertices are people (names), so how do we represent this internally?

It turns out we don't need to do that much more work. If we give each person an integer ID between \( 0 \) and \( n - 1 \) and store a mapping between names and IDs, then we can continue to use the graph representations that we are familiar with. A simple way to implement this mapping would be to store all the names in an array, and let the ID of each person be the index containing their name in the array. The first person in the array would have an ID of \( 0 \), the second person in the array would have an ID of \( 1 \), and so on. If we wanted to answer a question involving one or more people, we can scan this array to determine their ID, and then use this ID to query the matrix/list. For example, suppose this is our internal representation:

Now suppose we wanted to find out if Harry and Draco are friends. First, we need to find the vertex numbers associated with Harry and Draco, so we perform a linear scan of the array of names (called labels), and find that Harry is associated with a vertex number of \( 0 \), and Draco is associated with a vertex number of \( 3 \). Entry \( (0, 3) \) of the adjacency matrix contains a \( 0 \), so we can conclude that Harry and Draco are not friends.

Unfortunately, this translation between names and vertex numbers adds quite a bit of overhead to our graph operations. Converting from vertex numbers to names is easy, as we can go straight to the relevant index in the array (\( O(1) \)), but converting from names to vertex numbers requires a linear scan of the array, which is \( O(n) \). So what was once an \( O(1) \) operation (checking if an edge exists) is now an \( O(n) \) operation. We can improve the efficiency of the name to vertex number conversion by using a data structure that allows for efficient searching, such as a binary search tree.

Setting Up

Create a directory for this lab, change into it, and run the following command:

unzip /web/cs2521/24T1/labs/week14/downloads/files.zip

If you're working at home, download files.zip by clicking on the above link and then run the unzip command on the downloaded file.

If you've done the above correctly, you should now have the following files:

Makefile
a set of dependencies used to control compilation
Fb.c
an incomplete implementation of the Friendbook ADT
Fb.h
the interface for the Friendbook ADT
List.c
a complete implementation of the List ADT
List.h
the interface for the List ADT
Map.c
a complete implementation of the Map ADT
Map.h
the interface for the Map ADT
Queue.c
a complete implementation of the Queue ADT
Queue.h
the interface for the Queue ADT
runFb.c
a program that provides a command-line interface to the Friendbook ADT
analysis.txt
a template for you to fill in your answers for Task 4

Once you've got these files, the first thing to do is to run the command

make

This will compile the initial version of the files, and produce the ./runFb executable.

File Walkthrough

runFb.c

runFb.c provides a command-line interface to the Friendbook ADT. It creates a Friendbook instance, and then accepts commands to interact with it. Here is an example session with the program:

./runFb
Friendbook v1.0
Enter ? to see the list of commands.
> ?
Commands:
    + <name>             add a new person
    l                    list the names of all people
    f <name1> <name2>    friend two people
    u <name1> <name2>    unfriend two people
    s <name1> <name2>    get the friendship status of two people
    n <name>             get the number of friends a person has
    m <name1> <name2>    list all mutual friends of two people
    r <name>             get friend recommendations for a person based on mutual friends
    R <name>             get friend recommendations for a person based on friendship closeness
    ?                    show this message
    q                    quit

> + Harry
Harry was successfully added to Friendbook!
> + Ron
Ron was successfully added to Friendbook!
> + Hermione
Hermione was successfully added to Friendbook!
> f Harry Ron
Successfully friended Harry and Ron!
> f Ron Hermione
Successfully friended Ron and Hermione!
> s Harry Ron
Harry and Ron are friends.
> u Harry Ron
Could not unfriend Harry and Ron - they are not friends.
> s Harry Ron
Harry and Ron are friends.
> s Harry Hermione
Harry and Hermione are not friends.
> m Harry Hermione
Harry and Hermione's mutual friends:

> r Harry
> R Harry
> q

Note that the program currently does not correctly unfriend people. It also does not compute mutual friends or generate friend recommendations. In the above example, Ron should be a mutual friend of Harry and Hermione, and Harry should receive Hermione as a friend recommendation. Your task will be to implement these operations.

Fb.c

Fb.c implements the Friendbook ADT. Most of the functions are complete, however, it would be helpful to read through these functions to get a good idea of how they manipulate and obtain information from the graph representation, how they create and return lists of names, and how they convert people's names to vertex numbers. You should also read the definition of struct fb and make sure you understand the purpose of each field.

List.h

List.h defines the interface to the List ADT. Some operations require a list of names to be returned to the user, and the List ADT is used for this purpose. To see how to create a list and add names to the list, you should read some of the already-completed functions in the Friendbook ADT.

Map.h

Map.h defines the interface to the Map ADT, which is used to map people's names to IDs. An important thing to note is that the Map ADT is not strictly necessary - it is only used for efficiency reasons. If we didn't have access to the Map ADT and wanted to know the ID of a particular person, we could simply scan the names array until we found the index containing that person's name, and their ID would be that index.

Queue.h

Queue.h defines the interface to the Queue ADT. The Queue ADT is currently not used.

Task 1

Implement the FbUnfriend() function in Fb.c, which takes the names of two people, and unfriends them if they are friends. The function should return true if the people were friends and were successfully unfriended, and false if the two people were not friends (so they could not be unfriended).

When you think you are done, use the make command to recompile the runFb program and then run it to test your code.

Task 2

Implement the FbMutualFriends() function in Fb.c, which takes the names of two people, and returns a list of all their mutual friends. A person is a mutual friend of two people if that person is friends with both of those people. To illustrate this, here is an example:

In the example, Harry and Hermione have three mutual friends: Neville, Ron and Luna. Draco and Vincent have one mutual friend: Gregory. Harry and Draco have no mutual friends.

When you think you are done, use the make command to recompile the runFb program and then develop some scenarios to test your code. You can even use the example from above.

Task 3

Implement the FbFriendRecs1() function in Fb.c, which takes the name of a person and generates and prints friend recommendations for them. This function should only recommend people who are friends of friends of the person. In other words, it should only recommend people who share at least one mutual friend with the person. Obviously, it should not recommend someone who is already the person's friend.

The recommendations should be printed in descending order on the number of mutual friends shared, since someone with more mutual friends is more likely to be known by the person, and is therefore more likely to be added as a friend. If two people share the same number of mutual friends, they may be printed in any order. The number of mutual friends should also be displayed next to each recommendation.

For example, consider the following scenario:

If FbFriendRecs1() is called with the name "Harry", the function should produce the following output:

Harry's friend recommendations
	Neville                3 mutual friends
	Lavender               2 mutual friends
	Draco                  1 mutual friends

Explanation: Neville should be recommended first as he shares three mutual friends with Harry: Luna, Ron and Hermione. Lavender should be recommended next as she shares two mutual friends with Harry: Ron and Hermione. Draco should be recommended last as he shares just one mutual friend with Harry: Hermione. (Note: There is no typo on the last line - it is left as "friends" for simplicity's sake.)

When you think you are done, use the make command to recompile the runFb program and then develop some scenarios to test your code. You can even use the example from above.

Task 4

Congratulations on completing the above functions! Now, it's time to do some complexity analysis.

Open analysis.txt. This file contains three sections - one for each of the functions you implemented. You need to determine the worst case time complexities of each of these functions and enter them in the file under the corresponding section, along with an explanation of your answer.

Important: The Map ADT uses an inefficient binary search tree implementation, but you should assume that it uses an AVL tree for complexity analysis.

Optional Task

Implement the FbFriendRecs2() function in Fb.c, which takes the name of a person and generates and prints friend recommendations for them. Unlike FbFriendRecs1, this function can recommend all people who are reachable from the person via friendship links (not just people who share a mutual friend), and should recommend people who are "closer" to the person first. In other words, friends of friends of the person should be recommended first, then friends of friends of friends, and so on. Obviously, it should not recommend someone who is already the person's friend. If multiple people are the same "distance" from the person, they can be recommended in any order.

Limit the number of recommendations to 20 to avoid generating too many recommendations.

This recommendation method only needs to list people's names - no additional information is required.

For example, consider the same scenario as in Part 2:

If FbFriendRecs2() was called with the name "Luna", the following is one possible valid output:

Luna's friend recommendations
	Ron
	Hermione
	Draco
	Lavender
	Vincent
	Gregory

Explanation: Ron and Hermione are the closest people to Luna who are not also her friends, so they are recommended first. The example output recommends Ron first and then Hermione, but it would be equally valid to recommend Hermione first and then Ron. Draco and Lavender are the next furthest away, so they are recommended next. It would be valid to recommend Lavender before Draco. Vincent and Gregory are the next furthest away, so they are printed next. Once again, it would be valid to recommend Gregory before Vincent.

When you think you are done, use the make command to recompile the runFb program and then develop some scenarios to test your code.